CodeDeploy Lifecycle Hooks: Order, Variables, and When a Hook Fails

K
Kai··5 min read

In the previous article the appspec ran through a chain of lifecycle events and the app came up on EC2. But "ran through" hides a few important details: the exact order, which hook runs from which revision, and what happens when a hook breaks. Misunderstanding these spots leads to deploy failures that are hard to debug. This article dissects the hook layer, with two real experiments: reading the environment variables CodeDeploy passes into a hook, and deliberately failing a hook to see how the deploy reacts.

Goal

Get exactly right the lifecycle event order and where hooks run, which variables get passed into the script, and the behavior when a hook fails.

Lifecycle event order

An in-place deploy goes through a fixed chain of events. Not every event lets you attach a hook — CodeDeploy handles some itself:

ApplicationStop    ← your hook (runs from the OLD revision)
DownloadBundle     ← CodeDeploy does this (the agent pulls the zip from S3)
BeforeInstall      ← your hook
Install            ← CodeDeploy does this (copies files per the `files` block)
AfterInstall       ← your hook
ApplicationStart   ← your hook
ValidateService    ← your hook

The five bolded events (ApplicationStop, BeforeInstall, AfterInstall, ApplicationStart, ValidateService) are where you attach scripts in hooks. The two events DownloadBundle and Install are performed by CodeDeploy and can't take a hook.

There's one extremely important and easy-to-get-wrong detail: ApplicationStop runs scripts from the revision deployed previously, not the new revision. The reason makes sense — to stop the running app, CodeDeploy needs to use the stop procedure of the version currently running, and that version is the old revision. The practical consequence: if you write stop_server.sh wrong in the new revision, that error only shows up on the deploy after the next one, not this one. And on the first deploy (no old revision exists yet), ApplicationStop is simply skipped.

Which hook for which job

Each event suits a kind of work: ApplicationStop cleanly stops the running app; BeforeInstall prepares (install system packages, back up, clean directories); AfterInstall configures after files are copied (change permissions, fix config, build assets in place); ApplicationStart restarts the service; ValidateService checks the app actually works (health check). Putting work in the right event lets the deploy stop early when something's wrong, before a broken app gets exposed to users.

Environment variables CodeDeploy passes into hooks

Each hook script receives a set of environment variables that tell it the context it's running in. To see them firsthand, add an AfterInstall hook that writes those variables to a file the web server serves:

#!/bin/bash
# scripts/write_info.sh — AfterInstall hook
cat > /var/www/html/deploy-info.txt <<INFO
APPLICATION_NAME=${APPLICATION_NAME}
DEPLOYMENT_GROUP_NAME=${DEPLOYMENT_GROUP_NAME}
DEPLOYMENT_ID=${DEPLOYMENT_ID}
LIFECYCLE_EVENT=${LIFECYCLE_EVENT}
INFO

After deploying, read the file with curl:

$ curl http://<public-ip>/deploy-info.txt
APPLICATION_NAME=awscicd-demo
DEPLOYMENT_GROUP_NAME=awscicd-demo-dg
DEPLOYMENT_ID=d-XXXXXXXXX
LIFECYCLE_EVENT=AfterInstall

LIFECYCLE_EVENT tells the script which event it's running in — useful when one script is shared across several hooks and you want to branch by event. DEPLOYMENT_ID, APPLICATION_NAME, DEPLOYMENT_GROUP_NAME give context for logging or calling APIs. These variables are available in every hook script, no extra declaration needed.

When a hook fails

This is the most important part. Deliberately make validate.sh fail:

#!/bin/bash
echo "Simulating a failed validation"
exit 1

Deploy again and look at the result:

$ aws deploy get-deployment --deployment-id $DID --query 'deploymentInfo.status'
Failed

$ 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   Failed

The deploy stops right at ValidateService — the events before it ran to completion, and (if any) events after it don't run. A hook returning a non-zero exit code makes the whole deploy fail. Check the diagnostics to find out exactly what went wrong:

$ aws deploy get-deployment-instance --deployment-id $DID --instance-id i-03a4... \
    --query 'instanceSummary.lifecycleEvents[?status==`Failed`].diagnostics.[scriptName,message,errorCode]' --output text
scripts/validate.sh   Script at specified location: scripts/validate.sh run as user root failed with exit code 1   ScriptFailed

CodeDeploy points directly at which script (scripts/validate.sh), the error code (ScriptFailed), and the exit code (1). This is why ValidateService is valuable: it's the last gate on the target machine — if the just-deployed app can't pass the health check, the deploy is marked failed instead of quietly serving a broken build. Combined with auto-rollback (Article 10), a failed validate will automatically pull back the previous good build.

   timeline of one deploy (in-place):

   [OLD revision]       [NEW revision]
   ApplicationStop  ──▶ DownloadBundle ─▶ BeforeInstall ─▶ Install ─▶ AfterInstall
        (stop app)       (pull zip S3)     (prepare)       (copy)     (configure)
                                                                          │
                                              ApplicationStart ◀──────────┘
                                                    │
                                              ValidateService ──┬─ Succeeded → deploy OK
                                                                └─ exit≠0   → deploy FAILED, stops here

runas and timeout

Each hook also declares runas (which user the script runs as — here root to install packages and modify /var/www) and timeout (max seconds; exceeding it counts as a hook failure). Set a reasonable timeout for slow hooks (e.g. waiting for a service to be ready) so the deploy doesn't hang indefinitely.

🧹 Cleanup

Article 10 moves to an Auto Scaling Group, so the end of this article is the time to terminate the single instance:

$ aws ec2 terminate-instances --instance-ids i-03a4...

(Keep the application and deployment group — Article 10 attaches the ASG to those same ones.)

Wrap-up

An in-place deploy goes through a fixed chain of events: ApplicationStop → DownloadBundle → BeforeInstall → Install → AfterInstallApplicationStartValidateService, where you attach hooks to five events and CodeDeploy handles two itself. ApplicationStop runs scripts from the old revision (to correctly stop the running app), so an error in it shows up on a later deploy. Every hook receives environment variables (LIFECYCLE_EVENT, DEPLOYMENT_ID...) that tell it the context. A hook returning a non-zero code fails the deploy right at that event, with diagnostics pointing at the exact script and error — making ValidateService the last gate on the target machine.

The next article scales from one instance to many: deploy to an Auto Scaling Group, and pick a deployment config (OneAtATime, HalfAtATime, AllAtOnce) to control whether the deploy goes machine by machine or all at once — the tradeoff between speed and safety.