Files
spicy-automation/docs/PERSISTENT_ALB.md
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

7.9 KiB

Persistent ALB Pattern (bg-common)

The persistent ALB pattern ensures that ALB DNS names remain constant even when service stacks are deleted and recreated. This matches the legacy bg-common pattern.

How It Works

The bg-common ALB stack is always created separately. Services reference it via albLoadBalancerArn:

  1. bg-common ALB stack is deployed separately ({serviceStackName}-alb or via spicyALB)
  2. The ALB stack persists across service deployments
  3. Service stacks reference the existing ALB via albLoadBalancerArn (maps to existingALB in construct)
  4. ALB DNS name stays constant even when service stacks are deleted/recreated

Stack Naming

  • ALB Stack: {serviceStackName}-alb (shared between blue/green)
  • Service Stack (Rolling): {serviceStackName}
  • Service Stack (Blue/Green): {serviceStackName}-blue and {serviceStackName}-green

For blue/green deployments, both blue and green services share the same ALB stack.

Usage

Rolling Deployment (Non-Blue/Green)

@Library(["spicy-automation@main"]) _

spicyECSService(
    jenkinsAwsCredentialsId: "aws-credentials",
    region: "ca-central-1",
    stackName: "my-api-dev",
    serviceName: "my-api",

    // ... cluster/VPC config ...

    // Container
    image: "nexus.kodeniks.com/docker-hosted/my-api:latest",
    containerPort: 3000,

    // Reference bg-common ALB (always created separately) - auto-imports from ALB stack
    albStackName: "my-api-dev-alb",  // Auto-imports ALB ARN and listener ARNs

    // Routing
    hostHeader: "api-dev.example.com",
    priority: 100,

    // Tags
    ownerTag: "MyTeam",
    productTag: "my-product",
    componentTag: "api",
    environment: "dev",
)

What happens:

  1. Pipeline deploys my-api-dev-alb stack (ALB)
  2. Pipeline gets ALB outputs (DNS, listener ARNs)
  3. Pipeline deploys my-api-dev stack (service) referencing the ALB
  4. ALB DNS name stays constant: my-api-dev-alb-123456789.ca-central-1.elb.amazonaws.com

Blue/Green Deployment

@Library(["spicy-automation@main"]) _

spicyECSService(
    jenkinsAwsCredentialsId: "aws-credentials",
    region: "ca-central-1",
    stackName: "my-api-prod",  // Base name (blue/green suffixes added automatically)
    serviceName: "my-api",

    // ... cluster/VPC config ...

    // Container
    image: "nexus.kodeniks.com/docker-hosted/my-api:latest",
    containerPort: 3000,

    // Blue/Green
    blueGreen: true,
    activeHostname: "api.example.com",
    inactiveHostname: "inactive-api.example.com",
    bgHostedZoneId: "Z1234567890",  // Route53 hosted zone

    // Reference bg-common ALB (always created separately) - auto-imports from ALB stack
    albStackName: "my-api-prod-alb",  // Auto-imports ALB ARN and listener ARNs
    healthCheckPath: "/health",

    // Tags
    ownerTag: "MyTeam",
    productTag: "my-product",
    componentTag: "api",
    environment: "prod",
)

What happens:

  1. Pipeline deploys my-api-prod-alb stack (ALB) - only once, shared by blue/green
  2. Pipeline gets ALB outputs (or uses albStackName for auto-import)
  3. Pipeline deploys my-api-prod-green stack (inactive service) referencing the ALB
  4. Service creates listener rules on both HTTP and HTTPS listeners (if both available)
  5. Pipeline swaps hostname routing via ALB listener rule priorities (no DNS changes)
  6. ALB DNS name stays constant across all deployments

How Blue/Green Routing Works:

  • DNS Records: Both api.example.com and inactive-api.example.com DNS records are created and always point to the same ALB
  • Traffic Routing: Controlled by ALB listener rule priorities, not DNS:
    • Active service: Listener rule with priority 100 for api.example.com
    • Inactive service: Listener rule with priority 200 for inactive-api.example.com
  • Swapping: When swapping, only the listener rule priorities are updated (via CDK deployment). DNS records never change.
  • Result: Zero-downtime swaps in ~30 seconds (just ALB rule updates, no DNS propagation delays)

Benefits

  1. Static DNS: ALB DNS name never changes (unless ALB stack is deleted)
  2. No DNS Updates: Cloudflare/Route53 records set once and never need updating
  3. Faster Deployments: ALB stack only updates when ALB config changes
  4. Cost Efficient: ALB persists, avoiding recreation costs

ALB Stack Lifecycle

  • Created: Deployed separately via spicyALB pipeline function or manually
  • Updated: When ALB configuration changes (certificate, scheme, etc.)
  • Deleted: Only when explicitly destroyed (not deleted with service stacks)

Stack Outputs

The ALB stack exports these outputs (used by service stacks via CloudFormation imports):

Output Key Description Export Name
LoadBalancerDNS ALB DNS name {stackName}-internet-facing-url
LoadBalancerArn ALB ARN {stackName}-internet-facing-arn
HTTPListenerArn HTTP listener ARN {stackName}-internet-facing-http-listener
HTTPSListenerArn HTTPS listener ARN {stackName}-internet-facing-https-listener

Minimal Props: When using albStackName in service stack, these are auto-imported via CloudFormation Fn::ImportValue. The service creates listener rules on both HTTP and HTTPS listeners (if both are available), matching the old template behavior. | SecurityGroupId | ALB security group ID | {stackName}-internet-facing-security-group |

Deploying the bg-common ALB Stack

The bg-common ALB stack must be deployed separately before deploying services:

@Library(["spicy-automation@main"]) _

// Deploy bg-common ALB stack
spicyALB(
    jenkinsAwsCredentialsId: "aws-credentials",
    region: "ca-central-1",
    stackName: "my-api-dev-alb",
    vpcId: "vpc-12345678",
    availabilityZones: "ca-central-1a,ca-central-1b",
    subnetIds: "subnet-xxx,subnet-yyy",
    scheme: "internet-facing",
    certificateArn: "arn:aws:acm:...",
    ownerTag: "MyTeam",
    productTag: "my-product",
    componentTag: "alb",
    environment: "dev",
)

// Then deploy service referencing the ALB
spicyECSService(
    // ... config ...
    albLoadBalancerArn: "arn:aws:elasticloadbalancing:...",  // From ALB stack outputs
    albHttpsListenerArn: "arn:aws:elasticloadbalancing:...",  // From ALB stack outputs
)

Comparison with Legacy bg-common

Feature Legacy bg-common New Persistent ALB
Stack Name {envName}-bg-common {serviceStackName}-alb
Deployment Separate pipeline step Separate pipeline step (via spicyALB)
Blue/Green Shared ALB Shared ALB
DNS Management Manual Automatic (Route53) or Manual (Cloudflare)
Stack Lifecycle Manual Manual (deployed separately)

Troubleshooting

ALB Stack Not Found

If the ALB stack doesn't exist, the pipeline will create it automatically. If you see errors:

  1. Check the ALB stack name: {serviceStackName}-alb
  2. Verify the stack exists: aws cloudformation describe-stacks --stack-name {stackName}-alb
  3. Check stack outputs: aws cloudformation describe-stacks --stack-name {stackName}-alb --query 'Stacks[0].Outputs'

ALB DNS Name Changed

This should never happen with the persistent ALB pattern. If it does:

  1. Check if the ALB stack was deleted/recreated
  2. Verify the ALB stack name is consistent
  3. Check CloudFormation stack events for ALB replacement

Service Can't Find ALB

If the service stack can't reference the ALB:

  1. Verify ALB stack outputs are available
  2. Check that albLoadBalancerArn is set in the service context
  3. Verify the ALB ARN is correct