CodeDeploy: The First In-Place Deploy to EC2
The code is built and tested. The final leg of the chain is putting it onto a running server β and this is the longest, most important section of the series. Part IV is dedicated to CodeDeploy, targeting EC2. This article lays the foundation: an EC2 instance with the agent, an application and a deployment group, an appspec.yml describing how to deploy, then runs the first in-place deploy and observes each step.
π° Cost
This article (and all of Part IV) runs one EC2 t3.micro. It is cheap (about USD 0.01/hour) and free-tier-eligible on many accounts, but it bills while running β remember to terminate it once you finish Part IV (cleanup section at the end). CodeDeploy itself is free for deployments to EC2.
Goal
Understand the components of CodeDeploy and how they fit together, then run a real in-place deploy to EC2 and read the lifecycle events.
The pieces of CodeDeploy
CodeDeploy has a few concepts to keep apart. An application is a logical container for one application. A deployment group is a set of target machines with the same deployment method (targeting machines by tag or by Auto Scaling group). A revision is the package of content to deploy (a zip bundle with appspec.yml at the root). The agent is a process running on each EC2 that continually asks CodeDeploy "anything to deploy?". appspec.yml is the file describing what to copy where and which scripts to run at each stage.
What the service role and EC2 need
CodeDeploy needs a service role to act on your behalf. A trust policy for codedeploy.amazonaws.com, and for EC2 attach the managed policy AWSCodeDeployRole:
$ aws iam create-role --role-name awscicd-codedeploy-role \
--assume-role-policy-document file://cd-trust.json
$ aws iam attach-role-policy --role-name awscicd-codedeploy-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole
The EC2 itself needs two things: an instance role so the agent can read the revision from S3, and the agent installed. The instance role (via an instance profile) grants s3:Get* on the artifact bucket; the agent is installed via user-data at startup:
#!/bin/bash
dnf install -y ruby wget httpd
systemctl enable --now httpd
cd /tmp
wget -q https://aws-codedeploy-ap-southeast-1.s3.ap-southeast-1.amazonaws.com/latest/install
chmod +x ./install && ./install auto
systemctl enable --now codedeploy-agent
Launch the instance with the instance profile, the user-data above, and a tag for the deployment group to target β here App=awscicd-demo:
$ aws ec2 run-instances --image-id <al2023-ami> --instance-type t3.micro \
--iam-instance-profile Name=awscicd-ec2-profile \
--subnet-id <subnet> --security-group-ids <sg-http> --associate-public-ip-address \
--user-data file://userdata.sh \
--tag-specifications 'ResourceType=instance,Tags=[{Key=App,Value=awscicd-demo}]'
The tag App=awscicd-demo is the thread connecting the instance to the deployment group: CodeDeploy finds target machines by tag, not by id, so adding/removing machines with the same tag joins them to the group automatically.
appspec.yml: the deploy script
This file lives at the root of the revision and describes which files to copy where and which hook to run at each stage:
version: 0.0
os: linux
files:
- source: index.html
destination: /var/www/html/
hooks:
ApplicationStop:
- location: scripts/stop_server.sh
runas: root
BeforeInstall:
- location: scripts/before_install.sh
runas: root
ApplicationStart:
- location: scripts/start_server.sh
runas: root
ValidateService:
- location: scripts/validate.sh
runas: root
The files block copies index.html into the web directory; the hooks run scripts at the right stage (stop the old app, install needed packages, start, validate). The validate.sh script curls localhost to confirm the app is alive β if it returns an error, the deploy is considered failed. These hooks are the deep-dive topic of Article 9; here we just need a minimal working set.
Application, deployment group, revision
Create the application and deployment group (targeting the tag, in-place type, one machine at a time):
$ aws deploy create-application --application-name awscicd-demo --compute-platform Server
$ aws deploy create-deployment-group --application-name awscicd-demo \
--deployment-group-name awscicd-demo-dg \
--service-role-arn arn:aws:iam::111122223333:role/awscicd-codedeploy-role \
--ec2-tag-filters Key=App,Value=awscicd-demo,Type=KEY_AND_VALUE \
--deployment-config-name CodeDeployDefault.OneAtATime
Package the revision and push it to S3 with aws deploy push (it zips the current directory, uploads, and prints the ready-to-run deploy command):
$ aws deploy push --application-name awscicd-demo \
--s3-location s3://awscicd-artifacts-111122223333-ap-southeast-1/revisions/awscicd-demo.zip \
--source .
To deploy with this revision, run:
aws deploy create-deployment ... key=revisions/awscicd-demo.zip,bundleType=zip ...
Run the deploy
$ DID=$(aws deploy create-deployment --application-name awscicd-demo \
--deployment-group-name awscicd-demo-dg \
--s3-location bucket=awscicd-artifacts-...,key=revisions/awscicd-demo.zip,bundleType=zip \
--query 'deploymentId' --output text)
$ aws deploy get-deployment --deployment-id $DID --query 'deploymentInfo.status'
InProgress
...
Succeeded
Inside a deploy
This is the part worth dissecting. When you create-deployment, CodeDeploy does not push anything down to the machine. It only marks that there is a new job for the deployment group. The agent on the EC2 β which polls continually β sees the job, then pulls the revision from S3 itself and runs through the chain of lifecycle events. View that chain on the instance:
$ aws deploy get-deployment-instance --deployment-id $DID --instance-id i-03a4... \
--query 'instanceSummary.lifecycleEvents[].[lifecycleEventName,status]' --output text
ApplicationStop Succeeded
DownloadBundle Succeeded
BeforeInstall Succeeded
Install Succeeded
AfterInstall Succeeded
ApplicationStart Succeeded
ValidateService Succeeded
Read this chain: ApplicationStop runs the hook that stops the old app, DownloadBundle is the agent pulling the zip from S3, BeforeInstall is the prepare hook, Install copies files per the appspec's files block, AfterInstall/ApplicationStart restart, ValidateService runs the validation hook. DownloadBundle and Install are events CodeDeploy does itself; the rest run your hooks. Because the agent pulls rather than CodeDeploy pushing, the instance needs no inbound port open for the deploy β which is also why this model is safe for machines deep in a private subnet.
create-deployment (revision = zip on S3)
β CodeDeploy marks a job for the deployment group (tag App=awscicd-demo)
βΌ
βββββββββββββ EC2 (agent polling) ββββββββββββββββ
β agent sees job β DownloadBundle (pull zip S3) β
β runs appspec by lifecycle event: β
β ApplicationStop β BeforeInstall β Install β
β β AfterInstall β ApplicationStart β Validate β
βββββββββββββββββββββββββ¬ββββββββββββββββββββββββββ
index.html β /var/www/html ; httpd serves
βΌ
curl http://<ip>/ β "Hello from the awscicd demo app β v2"
Verify the app actually serves:
$ curl http://<public-ip>/
... <h1>Hello from the awscicd demo app β v2</h1> ...
The app is now running on EC2, put there by CodeDeploy.
π§Ή Cleanup
Article 9 reuses this exact instance and deployment group, so keep them until the end of Article 9. When cleaning up (end of Part IV):
$ aws ec2 terminate-instances --instance-ids i-03a4...
$ aws deploy delete-deployment-group --application-name awscicd-demo --deployment-group-name awscicd-demo-dg
$ aws deploy delete-application --application-name awscicd-demo
$ aws ec2 delete-security-group --group-id <sg>
# keep the instance profile/role until the end of the series
EC2 bills while running, so do not forget to terminate it β this is the main paid resource of the series.
Wrap-up
CodeDeploy puts a revision (a bundle with appspec.yml) onto the machines in a deployment group, targeted by tag. EC2 needs an instance role (read S3) and the agent (installed via user-data). appspec.yml declares files to copy and hooks to run scripts at each lifecycle event. The core mechanism: the agent pulls the revision when it sees a job, then runs the chain ApplicationStop β DownloadBundle β BeforeInstall β Install β AfterInstall β ApplicationStart β ValidateService β the instance needs no inbound port for the deploy. The in-place deploy puts the app right onto the running instance.
The next article digs into the hooks: the exact run order, what each hook is for, the CodeDeploy environment variables passed into the scripts, and what happens when a hook fails.