Skip to content

GitLab CI + AWS ALB

This tutorial takes you from zero to a fully deployed multi-service ALB on AWS, using the working examples in the chant repo. You’ll use Claude Code to build the templates, set up GitLab, and deploy — all by telling your agent what to do.

The examples are already written. Your job is to build them, configure your credentials, and push.

Three CloudFormation stacks, deployed by three GitLab CI pipelines:

┌─────────────────────────────────────────────────────────────────┐
│ GitLab │
│ │
│ gitlab-aws-alb-infra ──► shared-alb stack │
│ (VPC, ALB, ECS cluster, ECR repos) │
│ │
│ gitlab-aws-alb-api ──► shared-alb-api stack │
│ (docker build → ECR push → Fargate service at /api/*) │
│ │
│ gitlab-aws-alb-ui ──► shared-alb-ui stack │
│ (docker build → ECR push → Fargate service at /*) │
│ │
└─────────────────────────────────────────────────────────────────┘
┌──────────────┐
│ ALB │
│ (port 80) │
└──────┬───────┘
┌─────────┴─────────┐
│ │
/api/* /* (default)
│ │
┌─────┴─────┐ ┌─────┴─────┐
│ API svc │ │ UI svc │
│ go-httpbin│ │ nginx │
└───────────┘ └───────────┘

The source lives in two places:

ExampleLocationWhat it defines
Infra stack (AWS)lexicons/aws/examples/shared-alb/VPC, ALB, ECS cluster, ECR repos, CF outputs
API service stack (AWS)lexicons/aws/examples/shared-alb-api/Fargate service at /api/* with go-httpbin
UI service stack (AWS)lexicons/aws/examples/shared-alb-ui/Fargate service at /* with nginx
Infra pipeline (GitLab)examples/gitlab-aws-alb-infra/CF deploy job
API pipeline (GitLab)examples/gitlab-aws-alb-api/Docker build → ECR push → CF deploy
UI pipeline (GitLab)examples/gitlab-aws-alb-ui/Docker build → ECR push → CF deploy
  • AWS account with IAM credentials (access key + secret key). You need broad permissions — CloudFormation, ECS, EC2, ECR, IAM, ELB, and CloudWatch Logs. Admin or power-user access works best.
  • GitLab account — gitlab.com (free tier) or self-hosted.
  • glab CLI installed and authenticated:
    Terminal window
    brew install glab
    glab auth login
    For self-hosted GitLab: glab auth login --hostname gitlab.mycompany.com
  • chant and bun installed (Installation guide)
  • Claude Code with chant MCP configured (Agent Integration guide)

The AWS and GitLab examples are already in the repo. You just need to build them.

“Build the CloudFormation templates for the shared-alb, shared-alb-api, and shared-alb-ui examples. Then build the GitLab CI pipelines for all three gitlab-aws-alb examples.”

The agent runs chant build in each example directory:

Terminal window
# AWS stacks → template.json
cd lexicons/aws/examples/shared-alb && chant build src --lexicon aws -o template.json
cd lexicons/aws/examples/shared-alb-api && chant build src --lexicon aws -o template.json
cd lexicons/aws/examples/shared-alb-ui && chant build src --lexicon aws -o template.json
# GitLab pipelines → .gitlab-ci.yml
cd examples/gitlab-aws-alb-infra && chant build src --lexicon gitlab -o .gitlab-ci.yml
cd examples/gitlab-aws-alb-api && chant build src --lexicon gitlab -o .gitlab-ci.yml
cd examples/gitlab-aws-alb-ui && chant build src --lexicon gitlab -o .gitlab-ci.yml

This gives you three CF templates and three .gitlab-ci.yml files, all generated from the TypeScript source.

Infra stack — three files do all the work:

// shared-alb/src/network.ts — entire VPC in one line
export const network = VpcDefault({});
// shared-alb/src/alb.ts — ALB + ECS cluster + execution role
export const shared = AlbShared({
vpcId: network.vpc.VpcId,
publicSubnetIds: [network.publicSubnet1.SubnetId, network.publicSubnet2.SubnetId],
});
// shared-alb/src/ecr.ts — container registries for each service
export const apiRepo = new ECRRepository({ RepositoryName: "alb-api", ... });
export const uiRepo = new ECRRepository({ RepositoryName: "alb-ui", ... });

The infra stack exports everything service stacks need — cluster ARN, listener ARN, VPC ID, subnet IDs, ECR repo URIs — via CloudFormation outputs in outputs.ts.

Service stacks — each receives shared infra as parameters and creates a single Fargate service:

shared-alb-api/src/service.ts
export const api = FargateService({
clusterArn: Ref(clusterArn),
listenerArn: Ref(listenerArn),
image: Ref(image), // injected by CI/CD pipeline
containerPort: 8080,
pathPatterns: ["/api", "/api/*"],
healthCheckPath: "/api/get",
});

GitLab pipelines — the infra pipeline runs a single CF deploy job. The service pipelines have two stages: build a Docker image and push it to ECR, then deploy the CF stack with cross-stack parameter passing:

gitlab-aws-alb-infra/src/pipeline.ts
const awsImage = new Image({ name: "amazon/aws-cli:latest", entrypoint: [""] });
export const deployInfra = new Job({
stage: "deploy",
image: awsImage,
script: [
"aws cloudformation deploy --template-file templates/template.json " +
"--stack-name shared-alb --capabilities CAPABILITY_IAM --no-fail-on-empty-changeset",
],
rules: [new Rule({ if: "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH" })],
});

Step 2: Create GitLab projects and set credentials

Section titled “Step 2: Create GitLab projects and set credentials”

“Create three private GitLab projects: gitlab-aws-alb-infra, gitlab-aws-alb-api, gitlab-aws-alb-ui. My AWS credentials are in ~/.aws/credentials, region is us-east-1, account ID is 123456789012.”

Tell the agent where your credentials are — ~/.aws/credentials, a specific profile, a file, or just paste them. It reads the values and sets everything up.

The agent:

  1. Creates three projects:

    Terminal window
    glab project create gitlab-aws-alb-infra --private
    glab project create gitlab-aws-alb-api --private
    glab project create gitlab-aws-alb-ui --private
  2. Sets CI/CD variables on each project:

VariableDescriptionSet on
AWS_ACCESS_KEY_IDIAM access keyAll 3 projects
AWS_SECRET_ACCESS_KEYIAM secret key (masked)All 3 projects
AWS_DEFAULT_REGIONe.g. us-east-1All 3 projects
AWS_ACCOUNT_IDe.g. 123456789012API + UI only

“Push to GitLab and deploy — infra first, then the services.”

The agent handles the ordering:

  1. Assembles each GitLab repo — copies the built CF template and .gitlab-ci.yml into a fresh git repo for each project. The service repos also get a Dockerfile (the API uses go-httpbin, the UI uses nginx).

  2. Pushes the infra repo first and monitors the pipeline:

    Terminal window
    glab ci list --project user/gitlab-aws-alb-infra
    glab ci trace --project user/gitlab-aws-alb-infra
  3. Waits for the infra pipeline to succeed — VPC, ALB, ECS cluster, and ECR repos are now live.

  4. Pushes API and UI repos — both can deploy in parallel now that ECR repos exist.

  5. Monitors service pipelines — watches Docker build + CF deploy until both succeed.

“Check that everything deployed correctly.”

The agent verifies all three stacks and tests the endpoints:

Terminal window
# Check stack status
aws cloudformation describe-stacks --stack-name shared-alb --query 'Stacks[0].StackStatus'
aws cloudformation describe-stacks --stack-name shared-alb-api --query 'Stacks[0].StackStatus'
aws cloudformation describe-stacks --stack-name shared-alb-ui --query 'Stacks[0].StackStatus'
# Get the ALB DNS name
ALB_DNS=$(aws cloudformation describe-stacks --stack-name shared-alb \
--query 'Stacks[0].Outputs[?OutputKey==`AlbDnsName`].OutputValue' --output text)
# Test the services
curl http://$ALB_DNS/api/get # → go-httpbin JSON response
curl http://$ALB_DNS/ # → nginx welcome page

These are already handled in the examples, but knowing them helps if you customize the pipelines.

The amazon/aws-cli Docker image sets aws as its entrypoint. GitLab CI prepends the entrypoint to every script command, so aws cloudformation deploy ... becomes aws aws cloudformation deploy .... The examples fix this with entrypoint: [""]:

const awsImage = new Image({
name: "amazon/aws-cli:latest",
entrypoint: [""], // ← critical
});

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

Terminal window
# Wrong — variable inside single quotes, not expanded
IMAGE_URI=$(echo "$OUTPUTS" | jq -r '... + ":$CI_COMMIT_REF_SLUG"')
# Right — what the examples do: expand outside jq
IMAGE_URI=$(echo "$OUTPUTS" | jq -r '...'):$CI_COMMIT_REF_SLUG

Service pipelines push Docker images to ECR. If the infra stack hasn’t created the repos yet, docker push fails with “repository does not exist.” That’s why infra deploys first.

The docker:27-cli image is Alpine-based. The examples install AWS CLI in before_script to authenticate with ECR:

Terminal window
apk add --no-cache aws-cli

After the initial deployment:

  • Code changes to API or UI — push to the respective GitLab repo. The pipeline rebuilds the Docker image, pushes to ECR, and redeploys the Fargate service. No need to touch the infra stack.
  • Infrastructure changes — push to the infra repo. Only re-run when you need to modify VPC, ALB, or ECS cluster settings.
  • Adding a new service — add an ECR repo to the infra stack, create a new service stack and pipeline following the same pattern, redeploy infra first, then push the new service.

“Tear down everything.”

The agent handles the ordering:

Terminal window
# Delete service stacks first
aws cloudformation delete-stack --stack-name shared-alb-api
aws cloudformation delete-stack --stack-name shared-alb-ui
aws cloudformation wait stack-delete-complete --stack-name shared-alb-api
aws cloudformation wait stack-delete-complete --stack-name shared-alb-ui
# Then delete infra
aws cloudformation delete-stack --stack-name shared-alb
aws cloudformation wait stack-delete-complete --stack-name shared-alb
# Optionally delete GitLab projects
glab project delete user/gitlab-aws-alb-infra
glab project delete user/gitlab-aws-alb-api
glab project delete user/gitlab-aws-alb-ui