Guide · AWS CodeDeploy · Deployment Automation

AWS CodeDeploy for MCP Servers — AppSpec Lifecycle Hooks, Deployment Groups, Rollback

AWS CodeDeploy automates application deployments to EC2 instances, on-premises servers, Lambda functions, and ECS — replacing manual SSH-and-rsync deployments with controlled, auditable, and rollback-capable delivery. For MCP server developers running on EC2 or bare-metal servers, CodeDeploy handles the deployment lifecycle through appspec.yml hooks: stop the running MCP server, place new files, run any migration scripts, start the new version, and validate health. If validation fails, CodeDeploy rolls back automatically to the previous revision. The EC2 deployment model uses the CodeDeploy agent — a daemon process on each instance that polls CodeDeploy for pending deployments and executes hooks locally. Critical CodeDeploy decisions for EC2: choosing a deployment configuration (OneAtATime keeps at least N-1 instances serving; AllAtOnce is fastest but causes downtime), designing lifecycle hooks to drain in-flight MCP connections before replacing files, and configuring automatic rollback triggers on CloudWatch alarms rather than relying solely on hook exit codes.

TL;DR

Create a CodeDeploy application and deployment group pointing at your EC2 instances (via tag or Auto Scaling group). Write an appspec.yml at the root of your deployment bundle with files mappings and hooks referencing shell scripts at BeforeInstall, AfterInstall, ApplicationStart, and ValidateService. Use HalfAtATime deployment configuration for zero-downtime rolling deploys. Enable automatic rollback on deployment failure. Install the CodeDeploy agent on all target instances via Systems Manager Run Command at fleet scale. See CodeDeploy blue-green ECS deployments for containerized MCP servers and CodePipeline integration for triggering deployments from CI.

AppSpec file structure for EC2

The appspec.yml file lives at the root of your deployment bundle (the ZIP or tar.gz artifact). It defines which files CodeDeploy places on disk and which scripts run at each lifecycle event. The deployment bundle is the entire directory structure that the agent extracts on the instance — typically your compiled MCP server binary or Node.js application directory plus startup scripts.

# appspec.yml — root of deployment bundle
version: 0.0
os: linux
files:
  - source: /
    destination: /opt/mcp-server
    overwrite: yes
file_exists_behavior: OVERWRITE
permissions:
  - object: /opt/mcp-server
    pattern: "**"
    owner: mcp-server
    group: mcp-server
    mode: 755
    type:
      - directory
  - object: /opt/mcp-server/mcp-server
    owner: mcp-server
    group: mcp-server
    mode: 755
    type:
      - file
hooks:
  BeforeInstall:
    - location: scripts/stop_server.sh
      timeout: 30
      runas: root
  AfterInstall:
    - location: scripts/install_dependencies.sh
      timeout: 120
      runas: mcp-server
  ApplicationStart:
    - location: scripts/start_server.sh
      timeout: 60
      runas: root
  ValidateService:
    - location: scripts/validate_health.sh
      timeout: 30
      runas: mcp-server

The files section maps paths from the deployment bundle to destination paths on disk. source: / copies the entire bundle contents to destination: /opt/mcp-server. The file_exists_behavior: OVERWRITE directive replaces existing files — without it, CodeDeploy fails on files that already exist. The permissions section sets ownership and mode after placement, which prevents permission drift between deployments.

# scripts/stop_server.sh
#!/bin/bash
set -e
# Drain in-flight MCP connections by sending SIGTERM (graceful shutdown)
# MCP servers should handle SIGTERM by stopping new connections and
# finishing in-flight tool calls before exiting
if systemctl is-active --quiet mcp-server; then
  systemctl stop mcp-server
  # Wait up to 25 seconds for graceful shutdown (leave 5s buffer for hook timeout)
  for i in $(seq 1 25); do
    systemctl is-active --quiet mcp-server || exit 0
    sleep 1
  done
  # Force kill if still running
  systemctl kill --signal=SIGKILL mcp-server || true
fi
exit 0

# scripts/validate_health.sh
#!/bin/bash
set -e
# Health check via the MCP server's /health endpoint
for i in $(seq 1 10); do
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/health || echo "000")
  if [ "$STATUS" = "200" ]; then
    echo "Health check passed after $i attempts"
    exit 0
  fi
  echo "Health check attempt $i: HTTP $STATUS — waiting 2s"
  sleep 2
done
echo "Health check failed after 10 attempts"
exit 1

The ValidateService hook is the last line of defense before CodeDeploy marks a deployment successful. If this script exits with a non-zero code, CodeDeploy marks the instance deployment failed. If enough instances in the deployment group fail validation (based on the minimum healthy hosts threshold in your deployment configuration), the deployment is marked failed and — if automatic rollback is enabled — CodeDeploy redeploys the previous revision.

Deployment group configuration

A deployment group defines which instances receive the deployment and how the rollout proceeds. For EC2 fleets, you can target instances by tag key-value pairs or by Auto Scaling group name. Tag-based targeting is more flexible; ASG-based targeting is the right choice when you want new instances launched during an ASG scale-out event to automatically receive the latest revision.

# Create CodeDeploy application
aws deploy create-application \
  --application-name mcp-server-app \
  --compute-platform Server

# Create deployment group (EC2 tag-based)
aws deploy create-deployment-group \
  --application-name mcp-server-app \
  --deployment-group-name mcp-server-production \
  --deployment-config-name CodeDeployDefault.HalfAtATime \
  --ec2-tag-filters '[
    {"Key":"App","Type":"KEY_AND_VALUE","Value":"mcp-server"},
    {"Key":"Env","Type":"KEY_AND_VALUE","Value":"production"}
  ]' \
  --service-role-arn arn:aws:iam::123456789012:role/CodeDeployServiceRole \
  --auto-rollback-configuration '{
    "enabled": true,
    "events": ["DEPLOYMENT_FAILURE", "DEPLOYMENT_STOP_ON_ALARM"]
  }' \
  --alarm-configuration '{
    "enabled": true,
    "alarms": [{"name": "mcp-server-error-rate-high"}]
  }'

# Create deployment group (Auto Scaling group)
aws deploy create-deployment-group \
  --application-name mcp-server-app \
  --deployment-group-name mcp-server-asg \
  --deployment-config-name CodeDeployDefault.HalfAtATime \
  --auto-scaling-groups '["mcp-server-asg-production"]' \
  --service-role-arn arn:aws:iam::123456789012:role/CodeDeployServiceRole \
  --auto-rollback-configuration '{
    "enabled": true,
    "events": ["DEPLOYMENT_FAILURE"]
  }'

The CodeDeploy service role needs ec2:Describe*, autoscaling:*, elasticloadbalancing:*, and SNS publish permissions. Use the AWS managed policy AWSCodeDeployRole as a starting point — it covers the required EC2 and ELB permissions for in-place rolling deployments.

Deployment configurations

A deployment configuration specifies the minimum number of healthy hosts that must remain in service during a deployment. This controls both the pace of the rollout and the blast radius if a hook fails.

ConfigHealthy hosts minimumBehaviorUse for MCP servers
OneAtATimeAll minus 1Deploys to one instance, waits for success, then moves to nextVery safe; very slow for large fleets. Good for single-instance MCP servers where you can tolerate brief reduced capacity
HalfAtATime50%Deploys to half the fleet simultaneouslyGood balance: half the fleet serves traffic while the other half gets updated; two rounds total
AllAtOnce0Deploys to all instances simultaneouslyFastest but causes complete downtime if deploy fails; only use for non-production environments
CustomN or N%Specify exact minimum hosts or percentageFor fleets where you want to keep at least 3 instances serving (MinimumHealthyHosts: {type: HOST_COUNT, value: 3})
# Create custom deployment configuration
aws deploy create-deployment-config \
  --deployment-config-name mcp-server-keep-3-healthy \
  --minimum-healthy-hosts '{"type":"HOST_COUNT","value":3}' \
  --compute-platform Server

# Deploy using custom config
aws deploy create-deployment \
  --application-name mcp-server-app \
  --deployment-group-name mcp-server-production \
  --deployment-config-name mcp-server-keep-3-healthy \
  --s3-location bucket=my-artifacts,key=mcp-server/revision.zip,bundleType=zip \
  --description "Deploy v1.2.3 from CodePipeline execution abc123"

CodeDeploy agent installation

The CodeDeploy agent is a daemon that must run on every EC2 instance in a deployment group. It polls the CodeDeploy service for pending deployment instructions and executes hooks on the instance. Install it via AWS Systems Manager Run Command to avoid SSHing into each instance manually.

# Install CodeDeploy agent via SSM Run Command (all instances with tag App=mcp-server)
aws ssm send-command \
  --document-name "AWS-ConfigureAWSPackage" \
  --parameters '{"action":["Install"],"installationType":["Uninstall and reinstall"],"name":["AWSCodeDeployAgent"]}' \
  --targets '[{"Key":"tag:App","Values":["mcp-server"]}]' \
  --max-concurrency "50%" \
  --max-errors "10%"

# Check agent status on an instance
sudo service codedeploy-agent status

# The agent runs as root by default — required to execute hooks with runas directives
# Agent log location for troubleshooting failed hooks:
# /var/log/aws/codedeploy-agent/codedeploy-agent.log
# Individual deployment lifecycle event logs:
# /opt/codedeploy-agent/deployment-root///logs/

# Verify agent version (upgrade annually or on major CodeDeploy feature releases)
/opt/codedeploy-agent/bin/codedeploy-agent --version

For Amazon Linux 2 / AL2023 AMIs, the CodeDeploy agent requires Ruby. If your AMI does not have Ruby, install it before the agent: yum install ruby. The agent polls the CodeDeploy endpoint every 3 seconds by default — you can reduce polling interval in /etc/codedeploy-agent/conf/codedeployagent.yml but 3 seconds is typically fast enough for most deployment pipelines.

In Auto Scaling groups, install the agent in your AMI's user data or bake it into the AMI. New instances that launch without the agent will be tagged as failed deployment targets and skipped by CodeDeploy — the deployment appears to succeed but the new instance runs an older revision. Always verify agent health in your ASG launch template.

Failure modes reference

FailureSymptomFix
BeforeInstall hook timeoutDeployment fails on instance with timeout error in lifecycle event logsDefault hook timeout is 3600 seconds; if stop_server.sh hangs, the deployment waits then fails. Add a PID-based kill to the stop script as a fallback after the graceful shutdown window
ValidateService fails: port not listening yetHealth check returns HTTP 000 (connection refused)MCP server process started but not yet accepting connections — increase retry count and sleep in validate_health.sh; add a process-level check before the port check
file_exists_behavior not setDeployment fails: "The file already exists"Set file_exists_behavior: OVERWRITE in appspec.yml; without it, CodeDeploy refuses to overwrite existing files on the instance
Agent not polling after installDeployment stays "InProgress" indefinitely; no lifecycle events on instanceAgent may not have the correct IAM instance profile permissions — instance needs codedeploy:* and s3:GetObject on the artifact bucket; check agent log at /var/log/aws/codedeploy-agent/
Rollback fails: previous revision missingAuto-rollback triggered but CodeDeploy reports no previous revisionThis is the first deployment to the group — no previous revision exists to roll back to; ensure you test deployments to staging before production and keep at least one successful deployment in the deployment history
AllAtOnce in productionDeployment failure takes down entire fleet simultaneouslyNever use AllAtOnce for production — if ValidateService fails on all instances simultaneously, the entire MCP server fleet goes offline; use HalfAtATime or OneAtATime minimum