Files
spicy-automation/vars/spicyECSCluster.groovy
Ryan Wilson 68684df471 Initial commit: Spicy CDK automation framework
Jenkins shared library and CDK constructs for AWS infrastructure.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-11-18 22:21:00 -08:00

200 lines
6.5 KiB
Groovy

/**
* 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