Skip to content

GitLab CI + AWS ALB

Two TypeScript source directories produce two CloudFormation stacks and two GitLab CI pipelines. One shared ALB fans out to two Fargate services based on path rules. The split is by deploy unit: the infrastructure everything else depends on is its own stack, and the two services that ride on it are declared together in one project because they deploy together.

┌─────────────────────────────────────────────────────────────────┐
│ shared-alb stack (gitlab-aws-alb-infra) │
│ VPC + ALB + ECS cluster + ECR repos │
└──────────────────────────────┬──────────────────────────────────┘
│ ClusterArn, ListenerArn, ECR URIs
┌─────────────────────────────────────────────────────────────────┐
│ shared-alb-services stack (gitlab-aws-alb-services) │
│ Fargate at /api/* (priority 100) │
│ Fargate at /* (priority 200, catch-all) │
└─────────────────────────────────────────────────────────────────┘

The two projects:

ProjectStackWhat it declares
examples/gitlab-aws-alb-infra/shared-albVPC, ALB, ECS cluster, ECR repos, the stack outputs the services consume, the deploy pipeline, and the alb-deploy Op in ops/
examples/gitlab-aws-alb-services/shared-alb-servicesBoth FargateService composites, their image parameters, and the pipeline that builds both images and deploys the stack

lexicons/aws/examples/shared-alb/, shared-alb-api/ and shared-alb-ui/ hold the AWS-only halves of the same declarations, without the GitLab pipeline; lexicons/aws/examples/multi-service-alb/ puts all of it in one stack.

  • How FargateService composite wires a Fargate task to an ALB listener rule — path pattern, priority, health check path, and security group all in one call
  • Why infra must deploy before services: the ECR repos live in the infra stack; the service pipeline pushes images to them
  • How two services share one stack: two composite calls in one src/, two image parameters, one aws cloudformation deploy
  • Two pitfalls already solved in the examples: amazon/aws-cli entrypoint override and $CI_COMMIT_REF_SLUG in jq expressions
Build the CloudFormation templates and the GitLab CI pipelines for the
gitlab-aws-alb-infra and gitlab-aws-alb-services examples.
My AWS credentials are in ~/.aws/credentials, region is us-east-1,
account ID is 123456789012.

See the example READMEs for full instructions:

FargateService: one composite, seven resources

Section titled “FargateService: one composite, seven resources”

Each service is a single composite call. It expands to seven CloudFormation resources: a Fargate task definition, ECS service, ALB listener rule, target group, task role, security group, and CloudWatch log group. Two calls in one src/ is why the merged stack has 14:

examples/gitlab-aws-alb-services/src/services.ts
export const api = FargateService({
clusterArn: Ref(clusterArn), // from infra stack parameter
listenerArn: Ref(listenerArn), // from infra stack parameter
image: Ref(apiImage), // injected by CI: ECR_URI:$CI_COMMIT_REF_SLUG
containerPort: 8080,
pathPatterns: ["/api", "/api/*"],
healthCheckPath: "/api/get",
priority: 100, // UI is 200 (evaluated later, catch-all)
});

Both services sit in one src/, so one chant build produces one template with both. They share the ALB parameters and differ only where they must: the path pattern and priority that route traffic to them, and which image parameter each one reads (apiImage or uiImage).

The stack receives infra stack outputs as CloudFormation parameters — clusterArn, listenerArn, vpcId, subnet IDs, execution role ARN. The pipeline fetches them with aws cloudformation describe-stacks and passes them as --parameter-overrides.

The amazon/aws-cli Docker image sets aws as its entrypoint. GitLab CI prepends the image entrypoint to every script command, turning aws cloudformation deploy into aws aws cloudformation deploy. Fix: entrypoint: [""].

examples/gitlab-aws-alb-infra/src/pipeline.ts
const awsImage = new Image({
name: "amazon/aws-cli:latest",
entrypoint: [""], // ← critical — overrides the image's aws entrypoint
});

Shell variables inside single-quoted jq expressions are not expanded. The examples build the ECR image URI by appending the tag outside jq:

Terminal window
# Right — expand outside jq
IMAGE_URI=$(echo "$OUTPUTS" | jq -r '.Stacks[0].Outputs[] | select(.OutputKey=="ApiRepoUri") | .OutputValue'):$CI_COMMIT_REF_SLUG
1. Push infra repo → infra pipeline → shared-alb stack created (ECR repos exist)
2. Push services repo → services pipeline → two parallel docker builds → ECR push
→ one CF deploy of shared-alb-services

If the services pipeline runs before infra, docker push fails with “repository does not exist.”

examples/gitlab-aws-alb-infra/ops/alb-deploy.op.ts declares that order as a single named workflow: a parallel Build phase over both projects, a sequential Deploy phase that applies shared-alb and then shared-alb-services, and a Verify phase that snapshots. chant run alb-deploy from the infra directory runs it without GitLab at all.