Skip to content

Repository files navigation

🟧 AWS CodeDeploy Terraform Module

Secure-by-default AWS CodeDeploy application + deployment group(s) + custom deployment config(s) — Server (EC2/On-Premises), Lambda, and ECS compute platforms, with alarm-triggered and failure-triggered auto-rollback ON by default. Built for the AWS provider v6.x.

Terraformawsmoduletyperesources


🧩 Overview

  • 🚀 Provisions the aws_codedeploy_app keystone plus any number of deployment groups and custom deployment configs from a single composite call.
  • 🖥️ Supports all three CodeDeploy compute platformsServer (EC2/On-Premises), Lambda, and ECS — set once on the application and gating which deployment-group / deployment-config arguments are valid.
  • 🔁 Auto-rollback ON by default — every deployment group rolls back automatically on DEPLOYMENT_FAILURE; supplying CloudWatch alarm names auto-enables alarm monitoring and appends DEPLOYMENT_STOP_ON_ALARM.
  • 🛑 Fail-closed alarm pollingignore_poll_alarm_failure defaults to false: a deployment stops (does not proceed blind) if CloudWatch alarm state can't be retrieved.
  • 🔵🟢 First-class blue/green support — deployment_style, blue_green_deployment_config, and ECS/ALB target_group_pair_info traffic shifting.
  • 🔑 iam:PassRole called out prominently — this module never creates the CodeDeploy service role; it only consumes service_role_arn by reference and documents the caller-principal trust boundary required to pass it.
  • 🏷️ Universal tagging on the application and every deployment group; aws_codedeploy_deployment_config has no tags argument — deliberately excluded, and documented as an exception (the CodeDeploy analog of the ASG tag-block exception).
  • 🧱 Deployment groups and custom deployment configs are map(object(...)) keyed by their own name — clean for_each, no count.

💡 Why it matters: A regulated FI's release pipeline is only as safe as its rollback discipline. Bundling the application, deployment groups, and any custom rollout curve behind one secure-by-default module means every deployment target — EC2 fleet, Lambda alias, or ECS service — inherits the same fail-closed rollback posture without each team having to remember to wire it themselves.


❤️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!


🗺️ Where this fits in the family

terraform-aws-codedeploy is a Phase 5 (Developer Tools) composite. It deliberately creates none of its own compute, networking, messaging, or IAM resources — every deployment target is wired in by reference from a sibling module, and the service role that CodeDeploy assumes is consumed, never created.

flowchart LR
iam["terraform-aws-iam-role"]
asg["terraform-aws-autoscaling-group"]
ecsc["terraform-aws-ecs-cluster"]
ecss["terraform-aws-ecs-service"]
lb["terraform-aws-lb"]
sns["terraform-aws-sns"]
cwa["terraform-aws-cloudwatch-alarm"]
cd["terraform-aws-codedeploy"]
cp["terraform-aws-codepipeline"]
iam -- "service_role_arn (iam:PassRole)" --> cd
asg -- "autoscaling_groups (names)" --> cd
ecsc -- "cluster_name" --> ecss
ecss -- "service_name" --> cd
lb -- "target_group / listener_arns" --> cd
sns -- "trigger_target_arn" --> cd
cwa -- "alarm name (NOT arn)" --> cd
cd -- "application_name / deployment_group_name" --> cp
style cd fill:#FF9900,color:#fff,stroke:#cc7a00,stroke-width:2px
Loading

ℹ️ terraform-aws-iam-role, terraform-aws-autoscaling-group, terraform-aws-ecs-cluster, terraform-aws-ecs-service, terraform-aws-lb, and terraform-aws-sns are Phase 1 / Phase 2 modules; terraform-aws-cloudwatch-alarm is Phase 4; terraform-aws-codepipeline is a Phase 5 sibling that consumes this module's name and deployment_group_names outputs for its deploy action.


🧬 What this module builds

flowchart TB
subgraph CDMOD["terraform-aws-codedeploy"]
app["aws_codedeploy_app.this<br/>(keystone)<br/>name, compute_platform, tags"]
subgraph DG["aws_codedeploy_deployment_group.this<br/>(for_each: map keyed by deployment_group_name)"]
dg1["service_role_arn (var, shared)<br/>ec2_tag_filter / ec2_tag_set / autoscaling_groups<br/>ecs_service / load_balancer_info<br/>deployment_style / blue_green_deployment_config<br/>alarm_configuration / auto_rollback_configuration<br/>trigger_configuration / tags"]
end
subgraph DC["aws_codedeploy_deployment_config.this<br/>(for_each: map keyed by deployment_config_name)"]
dc1["compute_platform<br/>minimum_healthy_hosts (Server)<br/>traffic_routing_config (ECS/Lambda)<br/>zonal_config (Server)<br/>NO tags argument"]
end
end
app -- "app_name" --> DG
DG -. "deployment_config_name (by name, optional)".-> DC
style app fill:#FF9900,color:#fff,stroke:#cc7a00,stroke-width:2px
Loading
ResourceRoleCardinality
aws_codedeploy_app.thisKeystone application; owns compute_platform1
aws_codedeploy_deployment_group.thisDeployment target(s) — in-place or blue/greenper deployment_groups entry
aws_codedeploy_deployment_config.thisCustom rollout curve(s) beyond the AWS-managed presetsper deployment_configs entry (0-N)

✅ Provider / Versions

RequirementVersion
Terraform>= 1.12.0
hashicorp/aws>= 6.0, < 7.0

No provider {} block is declared inside the module — the caller's configured provider (region, credentials, default_tags) is inherited. No region variable — CodeDeploy is a standard regional service.


🔑 Required IAM Permissions

⚠️iam:PassRole is mandatory and must be scoped tightly. Every aws_codedeploy_deployment_group this module creates requires the Terraform identity running this module to pass var.service_role_arn to the codedeploy.amazonaws.com service principal. Grant iam:PassRoleconditioned on iam:PassedToService = codedeploy.amazonaws.com and scoped to the exact role ARN(s) this module is expected to pass — never a wildcard Resource: "*" grant. Without this permission, aws_codedeploy_deployment_group creation fails with an access-denied / InvalidRoleException error even though the role itself exists and is correctly configured.

Least-privilege actions the Terraform identity needs:

ActionRequired forNotes
codedeploy:CreateApplication, codedeploy:DeleteApplication, codedeploy:GetApplication, codedeploy:UpdateApplicationApplication lifecycle
codedeploy:CreateDeploymentGroup, codedeploy:DeleteDeploymentGroup, codedeploy:GetDeploymentGroup, codedeploy:UpdateDeploymentGroupDeployment group lifecycle
codedeploy:CreateDeploymentConfig, codedeploy:DeleteDeploymentConfig, codedeploy:GetDeploymentConfigCustom deployment config lifecycleOnly when deployment_configs is non-empty; AWS-managed presets need no create permission
codedeploy:ListApplications, codedeploy:ListDeploymentGroups, codedeploy:ListDeploymentConfigs, codedeploy:ListTagsForResourceDrift detection / terraform plan reads
codedeploy:TagResource, codedeploy:UntagResourceTagging (application + deployment groups)
iam:PassRolePassing service_role_arn to CodeDeployMANDATORY — condition on iam:PassedToService = codedeploy.amazonaws.com; see callout above
iam:GetRoleValidating the supplied service_role_arn existsRead-only

The CodeDeploy service role itself (created by terraform-aws-iam-role, never by this module) additionally needs one AWS-managed policy, selected by compute_platform:

compute_platformAWS-managed policyGrants
ServerAWSCodeDeployRoleRead EC2/ASG tags; manage ASG lifecycle hooks/scaling; publish to SNS; read CloudWatch alarms; read/update Classic ELB / target groups. Launch-template ASGs additionally need ec2:RunInstances, ec2:CreateTags, and iam:PassRole for the instance profile.
ECS (full access)AWSCodeDeployRoleForECSManage ECS task sets; update ELB target groups/listeners/rules; invoke Lambda hooks; read S3 revision artifacts; read CloudWatch alarms; publish SNS
ECS (limited)AWSCodeDeployRoleForECSLimitedManage ECS task sets; read CloudWatch alarms; publish SNS (no ELB/S3/Lambda-hook access)
Lambda (full access)AWSCodeDeployRoleForLambdaRead/update/invoke Lambda functions and aliases; read S3 revision artifacts; read CloudWatch alarms; publish SNS
Lambda (limited)AWSCodeDeployRoleForLambdaLimitedRead/update/invoke Lambda functions and aliases; read S3 revision artifacts; read CloudWatch alarms (no SNS publish)

📋 AWS Prerequisites

  • No service-linked role. CodeDeploy relies entirely on the caller-supplied service_role_arn (a standard IAM role, not a service-linked role) — the role and its trust policy (trusting codedeploy.amazonaws.com) must exist before any deployment group is created.
  • compute_platform is set once, at the application level, and drives which deployment-group and deployment-config arguments are valid — see the compatibility matrix below.
  • Region: No global-service constraint; CodeDeploy is regional and the module inherits the caller's provider region.
  • Quotas (soft, raisable via Service Quotas): 100 applications per account/Region (default) · 100 deployment groups per application · 20 concurrent deployments per account/Region · 1,500 instances per deployment group (Server).
  • Blue/green with COPY_AUTO_SCALING_GROUP: CodeDeploy provisions a new, Terraform-unmanaged ASG (named CodeDeploy_<deployment-group-name>_<deployment-id>) outside Terraform state. Prefer green_fleet_provisioning_option.action = "DISCOVER_EXISTING" with separately Terraform-managed blue/green ASGs to avoid drift and orphaned infrastructure on destroy.
  • SNS trigger permission: if trigger_configuration is set, the target SNS topic's resource policy (or the service role) must allow codedeploy.amazonaws.com / the service role to sns:Publish — granted on the topic (terraform-aws-sns), not by this module.

compute_platform compatibility matrix

Deployment-group argumentServerECSLambda
ec2_tag_filter / ec2_tag_set
on_premises_instance_tag_filter
autoscaling_groups
ecs_service✅ Required
deployment_style.deployment_type = IN_PLACE❌ (BLUE_GREEN only)N/A (block not used)
deployment_style.deployment_type = BLUE_GREEN✅ (optional)✅ RequiredN/A
load_balancer_info.elb_info (Classic ELB)
load_balancer_info.target_group_info
load_balancer_info.target_group_pair_info✅ Required for blue/green
blue_green_deployment_config✅ (blue/green only)✅ Required
termination_hook_enabled
alarm_configuration
auto_rollback_configuration
trigger_configuration
Deployment-config argumentServerECSLambda
minimum_healthy_hosts✅ Required
traffic_routing_config (TimeBasedCanary / TimeBasedLinear / AllAtOnce)
zonal_config

ℹ️ This module's variables.tf enforces the ❌/✅ Required cells above with validation {} blocks against var.compute_platform, so a mismatch fails at terraform plan with a named error instead of a server-side CodeDeploy API rejection.


📁 Module Structure

terraform-aws-codedeploy/
├── providers.tf # terraform{} + required_providers (aws >= 6.0, < 7.0); no provider block
├── variables.tf # name, service_role_arn, compute_platform, deployment_groups, deployment_configs, tags
├── main.tf # locals (secure-default computation) + the three resources
├── outputs.tf # id + arn + name, deployment_group/config maps, tags_all
├── README.md # this file
└── SCOPE.md # in/out-of-scope, IAM, prerequisites, compatibility matrix, gotchas

⚙️ Quick Start

module"deploy_role" {
source="git::https://github.com/microsoftexpert/terraform-aws-iam-role?ref=v1.0.0"name="casey-codedeploy-service-role"assume_role_policy=jsonencode({
Version ="2012-10-17"
Statement = [{
Effect ="Allow"
Principal = { Service ="codedeploy.amazonaws.com" }
Action ="sts:AssumeRole"
}]
})
managed_policy_arns=["arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole"]
}
module"codedeploy" {
source="git::https://github.com/microsoftexpert/terraform-aws-codedeploy?ref=v1.0.0"name="casey-billing-service"service_role_arn=module.deploy_role.arn# iam:PassRole required on the caller — see abovedeployment_groups={
production = {
autoscaling_groups = ["casey-billing-prod-asg"] # from terraform-aws-autoscaling-group
}
}
tags={
Environment ="prod"
App ="billing-service"
}
}

⚠️ Pin the source with ?ref=v1.0.0, never a branch. The Terraform identity applying this module must be separately granted iam:PassRole on module.deploy_role.arn, conditioned on iam:PassedToService = codedeploy.amazonaws.com — this module cannot grant that permission to itself.


🔌 Cross-Module Contract

Consumes

InputTypeSource module
service_role_arnstring (ARN)terraform-aws-iam-roleREQUIRED, see IAM section above
deployment_groups[*].autoscaling_groupslist(string) (ASG names)terraform-aws-autoscaling-group
deployment_groups[*].ecs_service.cluster_name / .service_namestringterraform-aws-ecs-cluster / terraform-aws-ecs-service
deployment_groups[*].load_balancer_info names / listener ARNsstring / list(string)terraform-aws-lb
deployment_groups[*].trigger_configuration[*].trigger_target_arnstring (ARN)terraform-aws-sns
deployment_groups[*].alarm_configuration.alarmslist(string) (alarm names, not ARNs)terraform-aws-cloudwatch-alarm (pass the alarm's name output)

Emits

OutputDescriptionConsumed by
idAWS-assigned application idReference/lookup
arnApplication ARN — arn:aws:codedeploy:<region>:<account>:application:<name>terraform-aws-iam-policy (scoping policies), audit
nameApplication nameterraform-aws-codepipeline deploy-action application_name; deployment group app_name
deployment_group_ids / deployment_group_arnsMaps keyed by deployment group nameterraform-aws-codepipeline deploy-action wiring; audit
deployment_group_namesMap of deployment group names (echoes the map key)CLI/console cross-reference; CodePipeline deployment_group_name
deployment_config_ids / deployment_config_arnsMaps of any custom deployment configs createddeployment_groups[*].deployment_config_name wiring, including cross-module reuse
tags_allAll tags on the application, incl. provider default_tagsGovernance/audit

📚 Example Library

1 · Minimal — Server-platform application, no deployment groups yet
module"codedeploy" {
source="git::https://github.com/microsoftexpert/terraform-aws-codedeploy?ref=v1.0.0"name="casey-minimal-app"service_role_arn=module.deploy_role.arn# compute_platform defaults to "Server"; deployment_groups defaults to {}
}
2 · EC2/ASG deployment group — wiring terraform-aws-autoscaling-group + terraform-aws-iam-role
module"deploy_role" {
source="git::https://github.com/microsoftexpert/terraform-aws-iam-role?ref=v1.0.0"name="casey-app-codedeploy-role"assume_role_policy=jsonencode({
Version ="2012-10-17"
Statement = [{
Effect ="Allow", Principal = { Service ="codedeploy.amazonaws.com" }, Action ="sts:AssumeRole"
}]
})
managed_policy_arns=["arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole"]
}
module"asg" {
source="git::https://github.com/microsoftexpert/terraform-aws-autoscaling-group?ref=v1.0.0"name="casey-app-prod"#... launch_template, vpc_zone_identifier, min/max/desired...
}
module"codedeploy" {
source="git::https://github.com/microsoftexpert/terraform-aws-codedeploy?ref=v1.0.0"name="casey-app"service_role_arn=module.deploy_role.arndeployment_groups={
production = {
autoscaling_groups = [module.asg.name]
}
}
}
3 · Blue/green EC2 with a Classic ELB
module"codedeploy" {
source="git::https://github.com/microsoftexpert/terraform-aws-codedeploy?ref=v1.0.0"name="casey-legacy-app"service_role_arn=module.deploy_role.arndeployment_groups={
production = {
deployment_style = {
deployment_type ="BLUE_GREEN"
deployment_option ="WITH_TRAFFIC_CONTROL"
}
load_balancer_info = {
elb_info = { name ="casey-legacy-classic-elb" } # from a Classic ELB, wired by name
}
blue_green_deployment_config = {
deployment_ready_option = {
action_on_timeout ="STOP_DEPLOYMENT"
wait_time_in_minutes =60
}
green_fleet_provisioning_option = {
action ="DISCOVER_EXISTING"# never COPY_AUTO_SCALING_GROUP — see SCOPE.md gotchas
}
terminate_blue_instances_on_deployment_success = {
action ="KEEP_ALIVE"
}
}
}
}
}
4 · ECS deployment group — wiring terraform-aws-ecs-cluster/terraform-aws-ecs-service + ALB target-group pair
module"codedeploy" {
source="git::https://github.com/microsoftexpert/terraform-aws-codedeploy?ref=v1.0.0"name="casey-orders-service"compute_platform="ECS"service_role_arn=module.ecs_deploy_role.arn# attached to AWSCodeDeployRoleForECSdeployment_groups={
production = {
ecs_service = {
cluster_name = module.ecs_cluster.name # terraform-aws-ecs-cluster
service_name = module.ecs_service.name # terraform-aws-ecs-service
}
deployment_style = {
deployment_type ="BLUE_GREEN"
deployment_option ="WITH_TRAFFIC_CONTROL"
}
load_balancer_info = {
target_group_pair_info = {
prod_traffic_route = { listener_arns = [module.alb.https_listener_arn] } # terraform-aws-lb
target_group = [
{ name = module.tg_blue.name },
{ name = module.tg_green.name },
]
}
}
blue_green_deployment_config = {
deployment_ready_option = { action_on_timeout ="CONTINUE_DEPLOYMENT" }
terminate_blue_instances_on_deployment_success = {
action ="TERMINATE"
termination_wait_time_in_minutes =5
}
}
}
}
}
5 · Lambda deployment group with a custom TimeBasedLinear deployment config
module"codedeploy" {
source="git::https://github.com/microsoftexpert/terraform-aws-codedeploy?ref=v1.0.0"name="casey-invoice-processor"compute_platform="Lambda"service_role_arn=module.lambda_deploy_role.arn# attached to AWSCodeDeployRoleForLambdadeployment_configs={
"casey-invoice-linear-10-5min"= {
compute_platform ="Lambda"
traffic_routing_config = {
type ="TimeBasedLinear"
time_based_linear = { interval =5, percentage =10 }
}
}
}
deployment_groups={
production = {
deployment_config_name ="casey-invoice-linear-10-5min"# deployment_style / load_balancer_info are NOT used for Lambda — traffic# shifting comes entirely from the deployment config above.
}
}
}
6 · Alarm-based rollback — wiring terraform-aws-cloudwatch-alarm
module"high_error_rate" {
source="git::https://github.com/microsoftexpert/terraform-aws-cloudwatch-alarm?ref=v1.0.0"name="casey-app-5xx-high"#... metric, threshold, comparison_operator...
}
module"codedeploy" {
source="git::https://github.com/microsoftexpert/terraform-aws-codedeploy?ref=v1.0.0"name="casey-app"service_role_arn=module.deploy_role.arndeployment_groups={
production = {
autoscaling_groups = [module.asg.name]
alarm_configuration = {
alarms = [module.high_error_rate.name] # NAME, not arn — see Architecture Notes# ignore_poll_alarm_failure defaults to false (fail closed)
}
# DEPLOYMENT_STOP_ON_ALARM is appended to auto_rollback automatically# because alarm_configuration.alarms is non-empty.
}
}
}
7 · SNS trigger configuration — wiring terraform-aws-sns
module"deploy_notifications" {
source="git::https://github.com/microsoftexpert/terraform-aws-sns?ref=v1.0.0"name="casey-app-deploy-events"
}
module"codedeploy" {
source="git::https://github.com/microsoftexpert/terraform-aws-codedeploy?ref=v1.0.0"name="casey-app"service_role_arn=module.deploy_role.arndeployment_groups={
production = {
autoscaling_groups = [module.asg.name]
trigger_configuration = [
{
trigger_name ="deploy-failures"
trigger_events = ["DeploymentFailure", "DeploymentRollback"]
trigger_target_arn = module.deploy_notifications.arn
}
]
}
}
}
# NOTE: the SNS topic's resource policy (or the service role) must separately# allow codedeploy.amazonaws.com / the service role to sns:Publish — grant that# on module.deploy_notifications, not here.
8 · Custom Server deployment config with minimum_healthy_hosts and zonal_config
module"codedeploy" {
source="git::https://github.com/microsoftexpert/terraform-aws-codedeploy?ref=v1.0.0"name="casey-app"service_role_arn=module.deploy_role.arndeployment_configs={
"casey-zonal-90pct"= {
compute_platform ="Server"
minimum_healthy_hosts = { type ="FLEET_PERCENT", value =90 } # REQUIRED for Server
zonal_config = {
monitor_duration_in_seconds =300
first_zone_monitor_duration_in_seconds =600
minimum_healthy_hosts_per_zone = { type ="FLEET_PERCENT", value =50 }
}
}
}
deployment_groups={
production = {
autoscaling_groups = [module.asg.name]
deployment_config_name ="casey-zonal-90pct"
}
}
}
9 · Tags — merge with provider default_tags
# Caller's provider block owns default_tags; resource tags win on key conflict.provider"aws" {
region="us-east-1"default_tags {
tags={ Owner ="platform", ManagedBy ="terraform" }
}
}
module"codedeploy" {
source="git::https://github.com/microsoftexpert/terraform-aws-codedeploy?ref=v1.0.0"name="casey-app"service_role_arn=module.deploy_role.arntags={
Environment ="prod"
DataClass ="internal"
}
deployment_groups={
production = { autoscaling_groups = [module.asg.name] }
}
}
# module.codedeploy.tags_all == { Owner, ManagedBy, Environment, DataClass }# aws_codedeploy_deployment_config (if any) receives NO tags — no tags argument exists.
10 · Secure-by-default opt-out — outdated_instances_strategy = IGNORE (documented exception)
module"codedeploy" {
source="git::https://github.com/microsoftexpert/terraform-aws-codedeploy?ref=v1.0.0"name="casey-legacy-batch"service_role_arn=module.deploy_role.arndeployment_groups={
production = {
autoscaling_groups = [module.asg.name]
outdated_instances_strategy ="IGNORE"# opt-out — mid-deployment new instances# keep the OLD revision instead of the# current one. EXCEPTION: only for legacy# batch fleets where mixed-revision# instances are tolerated; document why in# the calling root module.# auto_rollback_enabled is intentionally left at its secure default (true) —# this opt-out targets outdated-instance handling only, never rollback safety.
}
}
}
11 · for_each multi-environment deployment-group pattern
locals {
envs={
dev = { asg = module.asg_dev.name, min_healthy =0 }
qa = { asg = module.asg_qa.name, min_healthy =50 }
prod = { asg = module.asg_prod.name, min_healthy =90 }
}
}
module"codedeploy" {
source="git::https://github.com/microsoftexpert/terraform-aws-codedeploy?ref=v1.0.0"name="casey-app"service_role_arn=module.deploy_role.arndeployment_configs={
fork, vinlocal.envs:"casey-app-${k}-minhealthy"=> {
compute_platform ="Server"
minimum_healthy_hosts = { type ="FLEET_PERCENT", value = v.min_healthy }
}
}
deployment_groups={
fork, vinlocal.envs:k=> {
autoscaling_groups = [v.asg]
deployment_config_name ="casey-app-${k}-minhealthy"
}
}
}
12 · Import block — adopting an existing application into Terraform state
import {
to=module.codedeploy.aws_codedeploy_app.thisid="casey-app-existing"
}
import {
to=module.codedeploy.aws_codedeploy_deployment_group.this["production"]
id="casey-app-existing:production"# composite id — app_name:deployment_group_name
}
module"codedeploy" {
source="git::https://github.com/microsoftexpert/terraform-aws-codedeploy?ref=v1.0.0"name="casey-app-existing"service_role_arn=module.deploy_role.arndeployment_groups={
production = { autoscaling_groups = [module.asg.name] }
}
}
13 · End-to-end composition — IAM role + ECS + ALB + SNS + CloudWatch alarm (finale)
module"ecs_deploy_role" {
source="git::https://github.com/microsoftexpert/terraform-aws-iam-role?ref=v1.0.0"name="casey-orders-codedeploy-role"assume_role_policy=jsonencode({
Version ="2012-10-17"
Statement = [{
Effect ="Allow", Principal = { Service ="codedeploy.amazonaws.com" }, Action ="sts:AssumeRole"
}]
})
managed_policy_arns=["arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForECS"]
}
module"ecs_cluster" {
source="git::https://github.com/microsoftexpert/terraform-aws-ecs-cluster?ref=v1.0.0"name="casey-orders"
}
module"ecs_service" {
source="git::https://github.com/microsoftexpert/terraform-aws-ecs-service?ref=v1.0.0"name="orders-api"cluster_arn=module.ecs_cluster.arn# deployment_controller.type = "CODE_DEPLOY" on the service, per AWS blue/green requirements
}
module"alb" {
source="git::https://github.com/microsoftexpert/terraform-aws-lb?ref=v1.0.0"name="casey-orders"vpc_id=module.vpc.vpc_idsubnet_ids=values(module.vpc.public_subnet_ids)
}
module"deploy_notifications" {
source="git::https://github.com/microsoftexpert/terraform-aws-sns?ref=v1.0.0"name="casey-orders-deploy-events"
}
module"high_error_rate" {
source="git::https://github.com/microsoftexpert/terraform-aws-cloudwatch-alarm?ref=v1.0.0"name="casey-orders-5xx-high"
}
module"codedeploy" {
source="git::https://github.com/microsoftexpert/terraform-aws-codedeploy?ref=v1.0.0"name="casey-orders-service"compute_platform="ECS"service_role_arn=module.ecs_deploy_role.arndeployment_groups={
production = {
ecs_service = {
cluster_name = module.ecs_cluster.name
service_name = module.ecs_service.name
}
deployment_style = {
deployment_type ="BLUE_GREEN"
deployment_option ="WITH_TRAFFIC_CONTROL"
}
load_balancer_info = {
target_group_pair_info = {
prod_traffic_route = { listener_arns = [module.alb.https_listener_arn] }
target_group = [
{ name = module.tg_blue.name },
{ name = module.tg_green.name },
]
}
}
blue_green_deployment_config = {
terminate_blue_instances_on_deployment_success = {
action ="TERMINATE"
termination_wait_time_in_minutes =5
}
}
trigger_configuration = [{
trigger_name ="deploy-failures"
trigger_events = ["DeploymentFailure", "DeploymentRollback"]
trigger_target_arn = module.deploy_notifications.arn
}]
alarm_configuration = {
alarms = [module.high_error_rate.name]
}
}
}
tags={ Environment ="prod", App ="orders-service" }
}

📥 Inputs

  • Core:name (FORCE-NEW), service_role_arn (REQUIRED — iam:PassRole on the caller), compute_platform (Server | Lambda | ECS, default Server)
  • Deployment groups:deployment_groupsmap(object({ deployment_config_name?, ec2_tag_filter?, ec2_tag_set?, on_premises_instance_tag_filter?, autoscaling_groups?, ecs_service?, load_balancer_info?, trigger_configuration?, alarm_configuration?, auto_rollback_enabled?, auto_rollback_events?, deployment_style?, blue_green_deployment_config?, outdated_instances_strategy?, termination_hook_enabled? })), default {}
  • Custom deployment configs:deployment_configsmap(object({ compute_platform?, minimum_healthy_hosts?, traffic_routing_config?, zonal_config? })), default {}
  • Universal:tags (flows to the application and every deployment group; NOT to deployment configs)

🧾 Outputs

  • Primary:id (AWS-assigned application id), arn (application ARN), name, application_id (alias of id)
  • Application attributes:compute_platform, linked_to_github, github_account_name
  • Deployment groups:deployment_group_ids (composite app:group strings), deployment_group_arns, deployment_group_names, deployment_group_unique_ids (AWS-assigned unique id, distinct from the composite id)
  • Deployment configs:deployment_config_ids, deployment_config_arns (empty maps when deployment_configs = {})
  • Tags:tags_all (application only — deployment groups carry their own individually; deployment configs have none)

ℹ️ No outputs are marked sensitive — this module emits no secrets.


🧠 Architecture Notes

  • ARN / ID formats:
  • Application arnarn:aws:codedeploy:<region>:<account>:application:<name>
  • Application id → an AWS-assigned opaque id string — not the application name (use the name output for that).
  • Deployment group id → the composite string<app_name>:<deployment_group_name> — not an opaque AWS-assigned id. This affects terraform import syntax (colon-separated, see Example 12) and any consumer expecting a UUID-style id; use deployment_group_unique_ids for the true AWS-assigned identifier.
  • Deployment group arnarn:aws:codedeploy:<region>:<account>:deploymentgroup:<application>/<deployment_group_name> (note the slash, not a second colon, before the group name).
  • Deployment config arnarn:aws:codedeploy:<region>:<account>:deploymentconfig:<deployment_config_name>; id equals deployment_config_name itself.
  • Force-new / immutable fields:name is FORCE-NEW — renaming destroys and recreates the application and, because deployment-group ids embed app_name, implicitly forces replacement of every attached deployment group. compute_platform is effectively force-new: the provider doesn't mark it explicitly, but changing it after deployment groups exist breaks their argument compatibility and requires destroy/recreate of the app and its children in practice.
  • tagstags_alldefault_tags:var.tags flows to the application and every deployment group; tags_all on the application reflects the computed merge of resource tags over provider default_tags (resource tags win on key conflict). default_tags remains the caller's provider-block concern. aws_codedeploy_deployment_config has no tags / tags_all at all — deployment configs are not a taggable CodeDeploy resource type; var.tags is deliberately not wired to aws_codedeploy_deployment_config.this (see main.tf).
  • alarm_configuration.alarms is a list of CloudWatch alarm NAMEs, not ARNs. Unlike most cross-resource wiring in this library, passing an ARN here is a silent no-op / invalid reference at the CodeDeploy API level, not a Terraform-time type error — pass module.alarm.name, never module.alarm.arn.
  • Eventual consistency on freshly created IAM roles. A service_role_arn created in the same apply (via terraform-aws-iam-role) can cause aws_codedeploy_deployment_group creation to fail intermittently if CodeDeploy's assume-role check races IAM's eventual-consistency window. Add an explicit depends_on in the calling root module from module.codedeploy to the role/policy-attachment resources; this module cannot add that dependency internally since it does not create the role.
  • Destroy ordering. Deployment groups are removed before (or alongside, via Terraform's automatic dependency graph on app_name) the parent application. An in-flight deployment blocks deletion of its deployment group until it reaches a terminal state (Succeeded, Failed, or Stopped) — a terraform destroy mid-deployment will hang or error.
  • Blue/green COPY_AUTO_SCALING_GROUP creates unmanaged infrastructure. See AWS Prerequisites above — prefer DISCOVER_EXISTING.
  • No us-east-1 constraint. CodeDeploy is a standard regional service; no global-resource gotcha applies here.

🧱 Design Principles

Secure-by-default posture and the explicit opt-out for each:

Hardened defaultBehaviorOpt-out / control
Automatic rollback on deployment failureauto_rollback_configuration { enabled = true, events = ["DEPLOYMENT_FAILURE",...] } on every deployment groupdeployment_groups[*].auto_rollback_enabled = false — requires a documented exception; regulated-FI deployments should not disable failure rollback
Alarm-triggered rollbackEnabled automatically (alarm_configuration.enabled = true, DEPLOYMENT_STOP_ON_ALARM appended to rollback events) whenever alarm_configuration.alarms is non-emptyOmit / empty the alarms list to skip alarm monitoring entirely
ignore_poll_alarm_failurefalse (fail closed — the deployment stops if CloudWatch alarm status can't be retrieved)alarm_configuration.ignore_poll_alarm_failure = true — NOT recommended; document the exception
outdated_instances_strategy"UPDATE" (mid-deployment new EC2 instances receive the current revision, avoiding drift)deployment_groups[*].outdated_instances_strategy = "IGNORE" — see Example 10
deployment_option traffic controlDefaults to "WITH_TRAFFIC_CONTROL" automatically whenever load_balancer_info is wired, so instances drain before receiving new codeOnly relevant when deployment_style is used; omitting load_balancer_info naturally yields "WITHOUT_TRAFFIC_CONTROL"
IAM PassRole scopeDocumented requirement that the caller's principal scope iam:PassRole to the specific service_role_arn, never Resource: "*"Not a module opt-out — an IAM-policy authoring requirement on the caller (see Required IAM Permissions)

Other principles: one application, N deployment groups, N custom deployment configs from a single module call; deployment groups and deployment configs are for_each over map(object(...)) keyed by their own name (no count); IAM role creation, ASG/ECS/LB/SNS/CloudWatch-alarm resources are deliberately out of scope — this module's blast radius is limited to the CodeDeploy application / deployment-group / deployment-config layer itself; AWS-managed deployment config presets are never created as resources, only referenced by name.


🚀 Runbook

terraform init -backend=false
terraform validate
terraform fmt -check
terraform plan # requires valid AWS credentials (profile / SSO / OIDC) + a region
terraform apply
terraform output

⚠️plan / apply require valid AWS credentials and a configured region (provider block / AWS_PROFILE / SSO / OIDC). Always pin the module source with ?ref=v1.0.0, never a branch.


🧪 Testing

  • terraform init -backend=false && terraform validate — schema + reference integrity.
  • terraform fmt -check — formatting.
  • terraform plan against a sandbox account — confirm the compute-platform validation blocks catch a deliberately-wrong field (e.g. set ecs_service with compute_platform = "Server" and confirm plan-time failure, not an API error).
  • Confirm deployment_group_arns uses the deploymentgroup:<app>/<group> ARN shape (slash, not colon) after apply.
  • Verify rollback: trigger a deliberately failing deployment revision in a sandbox and confirm the deployment group rolls back automatically per auto_rollback_configuration.
  • Destroy test in a throwaway account — confirm an in-flight deployment blocks deletion as documented, then retry after it reaches a terminal state.

💬 Example Output

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
arn = "arn:aws:codedeploy:us-east-1:123456789012:application:casey-app"
id = "d-ABCDEFGHI"
name = "casey-app"
compute_platform = "Server"
deployment_group_ids = { "production" = "casey-app:production" }
deployment_group_arns = { "production" = "arn:aws:codedeploy:us-east-1:123456789012:deploymentgroup:casey-app/production" }
deployment_group_names = { "production" = "production" }
deployment_config_ids = {}
deployment_config_arns = {}
tags_all = { "Environment" = "prod", "App" = "billing-service" }

🔍 Troubleshooting

  • AccessDeniedException / InvalidRoleException on deployment group create: The Terraform identity is missing iam:PassRole on service_role_arn, or the condition iam:PassedToService = codedeploy.amazonaws.com doesn't match. Grant it scoped to the exact role ARN — never Resource: "*". See the Required IAM Permissions callout.
  • Tag drift / unexpected tags on the application: Caused by default_tags overlap. tags_all merges resource tags over provider default_tags with resource tags winning — set the value explicitly in var.tags if it differs from expectation. Remember aws_codedeploy_deployment_config has no tags at all, so it can never show tag drift.
  • Credential-chain failures (NoCredentialProviders / ExpiredToken): No valid credentials resolved. Set AWS_PROFILE, refresh SSO, or confirm OIDC role assumption in CI — this module never takes credentials as variables.
  • Deployment group creation intermittently fails right after the IAM role is created in the same apply: IAM eventual consistency — CodeDeploy's assume-role check can race a freshly created role/policy attachment. Add depends_on from module.codedeploy to the IAM role/policy-attachment resources in your root module, or re-apply.
  • Alarm-based rollback never triggers: Confirm you passed the alarm's name, not its arn, to alarm_configuration.alarms — the CodeDeploy API silently treats an ARN as an unmatched name. Also confirm ignore_poll_alarm_failure is false (the default) if you expect a stop-on-unknown-state behavior.
  • A blue/green deployment leaves behind an Auto Scaling group after destroy:green_fleet_provisioning_option.action = "COPY_AUTO_SCALING_GROUP" was used — CodeDeploy creates that ASG outside Terraform state, so terraform destroy never sees it. Switch to "DISCOVER_EXISTING" with a separately Terraform-managed blue/green ASG pair, and manually clean up any already-created CodeDeploy_* ASGs.
  • DependencyViolation / hang on destroy: An in-flight deployment is blocking deletion of its deployment group. Stop or wait out the deployment (Succeeded / Failed / Stopped) before retrying destroy.
  • Plan-time validation error naming a compute-platform mismatch (e.g. "ecs_service is only valid when compute_platform = ECS"): Working as intended — this module's variables.tf enforces the compute_platform compatibility matrix at plan time rather than letting it surface as a CodeDeploy API error during apply. Fix the field/platform mismatch per the error message.
  • terraform import fails on a deployment group with an "invalid id" error: The import id must be the colon-separated composite string app_name:deployment_group_name (see Example 12), not the AWS-assigned deployment_group_id (use deployment_group_unique_ids if you need that value for reference).

🔗 Related Docs

  • Terraform Registry — hashicorp/aws provider: aws_codedeploy_app, aws_codedeploy_deployment_group, aws_codedeploy_deployment_config
  • AWS — AWS CodeDeploy User Guide: primary components, IAM service role setup, deployment configurations, blue/green deployments
  • AWS — How AWS CodeDeploy works with IAM (resource ARN formats, managed policies)
  • AWS — Integrating CodeDeploy with Amazon EC2 Auto Scaling (blue/green ASG provisioning behavior)
  • terraform-aws-iam-role, terraform-aws-autoscaling-group, terraform-aws-ecs-cluster, terraform-aws-ecs-service, terraform-aws-lb, terraform-aws-sns, terraform-aws-cloudwatch-alarm, terraform-aws-codepipeline (sibling modules)

🧡 "Infrastructure as Code should be standardized, consistent, and secure."

Releases

Packages

Contributors

Languages