/** * Spicy ECS Cluster Pipeline * * Deploys an ECS cluster using AWS CDK with: * - EC2 Capacity Provider with managed scaling * - Optional Fargate capacity providers (FARGATE + FARGATE_SPOT) * - Mixed instances policy for Spot support * - Instance draining on termination * - Optional internal/external ALBs * * Usage in Jenkinsfile: * ```groovy * @Library(["spicy-automation@main"]) _ * * spicyECSCluster( * jenkinsAwsCredentialsId: "aws-credentials", * region: "ca-central-1", * stackName: "spicy-ecs-cluster", * vpcStackName: "production-vpc", * ownerTag: "SpicyTeam", * productTag: "spicy", * componentTag: "ecs-cluster", * environment: "dev", * instanceType: "m5a.large", * minClusterSize: 2, * maxClusterSize: 4, * spotEnabled: true, * onDemandPercentage: 20, * createExternalLoadBalancer: true, * createInternalLoadBalancer: true, * certificateArn: "arn:aws:acm:ca-central-1:123456789:certificate/xxx", * ) * ``` */ def call(Map args) { args = spicyDefaults(args) timeout(time: 1, unit: "DAYS") { timestamps { node("docker") { properties(args.pipelineProperties) ansiColor("xterm") { stage("Checkout") { checkout scm giteaUtils.setSuccess("checkout") } stage("Setup") { cdkUtils.install() giteaUtils.setSuccess("setup") } if (args.onPreDeploy) { spicyUtils.stageWithFailure("PreDeploy") { args.onPreDeploy.call(args, [:]) } } stage("Deploy ECS Cluster") { if (gitUtils.isMain()) { try { def context = buildEcsClusterContext(args) def account = awsUtils.buildAccountConfig(args) // Show diff first if (args.showDiff != false) { echo "Showing CDK diff..." cdkUtils.diff( account: account, stackName: args.stackName, stackType: "ecs-cluster", context: context ) } // Manual approval for production if (args.environment == 'prod' || args.environment == 'production') { manualApproval( message: "Deploy ECS cluster ${args.stackName} to production?", submitter: args.approvers ?: '' ) } // Deploy the stack echo "Deploying ECS cluster stack: ${args.stackName}" cdkUtils.deploy( account: account, stackName: args.stackName, stackType: "ecs-cluster", context: context ) giteaUtils.setSuccess("deploy") if (args.onPostDeploy) { spicyUtils.stageWithFailure("PostDeploy") { args.onPostDeploy.call(args, [ stackName: args.stackName ]) } } } catch (err) { giteaUtils.setFailed("deploy") throw err } } else { echo "Skipping deployment - not on main branch" // Show diff on non-main branches for PR review if (args.showDiffOnPR != false) { def context = buildEcsClusterContext(args) def account = awsUtils.buildAccountConfig(args) echo "Showing CDK diff for PR review..." try { cdkUtils.diff( account: account, stackName: args.stackName, stackType: "ecs-cluster", context: context ) } catch (err) { echo "Diff failed (stack may not exist yet): ${err.message}" } } } } giteaUtils.setSuccess("pipeline") } } } } } /** * Build CDK context map from pipeline arguments */ def buildEcsClusterContext(Map args) { def context = [:] // Required: VPC stack name (all VPC details imported from VPC stack exports) context.vpcStackName = args.vpcStackName if (!args.numberOfAzs) { error("numberOfAzs is required for ECS cluster; pass the same value used for the VPC stack (2-4).") } context.numberOfAzs = args.numberOfAzs.toString() // Required tags context.ownerTag = args.ownerTag context.productTag = args.productTag context.componentTag = args.componentTag ?: 'ecs-cluster' context.environment = args.environment ?: 'dev' context.build = args.build ?: gitUtils.getShortSHA() // Instance configuration if (args.instanceType) context.instanceType = args.instanceType if (args.additionalInstanceTypes) context.additionalInstanceTypes = args.additionalInstanceTypes if (args.keyName) context.keyName = args.keyName if (args.ebsVolumeSize) context.ebsVolumeSize = args.ebsVolumeSize.toString() // Container Insights if (args.containerInsights != null) context.containerInsights = args.containerInsights.toString() // Scaling configuration if (args.minClusterSize) context.minClusterSize = args.minClusterSize.toString() if (args.maxClusterSize) context.maxClusterSize = args.maxClusterSize.toString() if (args.targetCapacityPercent) context.targetCapacityPercent = args.targetCapacityPercent.toString() // Spot configuration if (args.spotEnabled != null) context.spotEnabled = args.spotEnabled.toString() if (args.onDemandPercentage != null) context.onDemandPercentage = args.onDemandPercentage.toString() if (args.spotAllocationStrategy) context.spotAllocationStrategy = args.spotAllocationStrategy // Load balancer configuration if (args.createExternalLoadBalancer != null) { context.createExternalLoadBalancer = args.createExternalLoadBalancer.toString() } if (args.createInternalLoadBalancer != null) { context.createInternalLoadBalancer = args.createInternalLoadBalancer.toString() } if (args.certificateArn) context.certificateArn = args.certificateArn // Fargate configuration (enables both FARGATE and FARGATE_SPOT capacity providers) if (args.enableFargate != null) context.enableFargate = args.enableFargate.toString() // Timeouts if (args.drainingTimeout) context.drainingTimeout = args.drainingTimeout.toString() if (args.maxInstanceLifetime) context.maxInstanceLifetime = args.maxInstanceLifetime.toString() return context } return this