The most surprising thing about building an ECS CI/CD pipeline is how much of the "magic" is actually just well-defined, explicit configuration.
Let’s see it in action. Imagine we have a simple Node.js web application that we want to deploy to ECS.
First, we need our application code in a repository. For this example, let’s say it’s in AWS CodeCommit, but GitHub or Bitbucket work just as well.
# Sample app.js
const express = require('express');
const app = express();
const port = 8080; // Standard for many containerized apps
app.get('/', (req, res) => {
res.send('Hello from ECS!');
});
app.listen(port, () => {
console.log(`App listening on port ${port}`);
});
We also need a Dockerfile to build our container image:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 8080
CMD ["node", "app.js"]
Now, the CI/CD pipeline itself. We’ll use AWS CodePipeline to orchestrate the process, CodeBuild for building the Docker image, and CodeDeploy for deploying to ECS.
1. Source Stage: This stage pulls the latest code from our repository.
- Provider: AWS CodeCommit (or GitHub, Bitbucket)
- Repository:
my-ecs-app - Branch:
main
2. Build Stage:
This is where CodeBuild kicks in. It needs a buildspec.yml file in our repository to tell it what to do.
# buildspec.yml
version: 0.2
phases:
pre_build:
commands:
- echo Logging in to Amazon ECR...
- aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com # Replace with your AWS Account ID and Region
- REPOSITORY_URI=123456789012.dkr.ecr.us-east-1.amazonaws.com/my-ecs-app # Replace
- IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION) # Use Git commit hash for tag
build:
commands:
- echo Build started on `date`
- echo Building the Docker image...
- docker build -t $REPOSITORY_URI:latest .
- docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
post_build:
commands:
- echo Pushing the Docker image...
- docker push $REPOSITORY_URI:latest
- docker push $REPOSITORY_URI:$IMAGE_TAG
- echo Writing image definitions file...
- printf '{"name":"my-ecs-app-container","imageUri":"%s"}' $REPOSITORY_URI:$IMAGE_TAG > imagedefinitions.json # Crucial for CodeDeploy
- cat imagedefinitions.json
artifacts:
files:
- imagedefinitions.json
- Environment: Managed image
aws/codebuild/standard:6.0with Docker enabled. - Buildspec:
buildspec.yml(from our repo) - Output Artifacts:
imagedefinitions.json(this file is key for CodeDeploy)
The imagedefinitions.json file is a small JSON object that tells ECS which Docker image to use for a specific task definition. CodeDeploy uses this to update your ECS service.
3. Deploy Stage:
This is where CodeDeploy takes over. It needs an appspec.yml file in your repository.
# appspec.yml
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: arn:aws:ecs:us-east-1:123456789012:task-definition/my-ecs-app:1 # Replace with your Task Definition ARN
LoadBalancerInfo:
ContainerName: my-ecs-app-container # Must match the name in imagedefinitions.json
ContainerPort: 8080 # The port your container listens on
Hooks:
BeforeInstall:
- Location: scripts/before_install.sh
Timeout: 300
RunAsUser: root
AfterInstall:
- Location: scripts/after_install.sh
Timeout: 300
RunAsUser: root
ApplicationStart:
- Location: scripts/application_start.sh
Timeout: 300
RunAsUser: root
ApplicationStop:
- Location: scripts/application_stop.sh
Timeout: 300
RunAsUser: root
- Provider: AWS CodeDeploy
- Application Name:
my-ecs-app - Deployment Group:
my-ecs-app-deployment-group(this group points to your ECS cluster, service, and specifies deployment strategy like Blue/Green or Rolling)
CodeDeploy will take the imagedefinitions.json artifact from CodeBuild, find the specified ECS task definition, create a new revision of that task definition with the updated image URI, and then update the ECS service to use that new revision.
The Hooks in appspec.yml allow you to run scripts at various points in the deployment lifecycle. For a simple web app, ApplicationStart might just involve health checks, while ApplicationStop could gracefully shut down old tasks.
The core idea is that CodePipeline orchestrates the flow, CodeBuild creates the artifact (the Docker image and imagedefinitions.json), and CodeDeploy uses that artifact to update your running ECS service with minimal downtime.
The most commonly overlooked part of this setup is ensuring your ECS service’s IAM role has permissions to ecs:RegisterTaskDefinition and ecs:UpdateService. Without these, CodeDeploy will fail to update your service.
Once this pipeline is running, the next logical step is to implement traffic shifting with Blue/Green deployments using CodeDeploy’s integrated features.