Skip to content

microsoftexpert/tf-mod-aws-codedeploy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

2 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

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.

Terraform aws module type resources


🧩 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 platforms β€” Server (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 polling β€” ignore_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

tf-mod-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["tf-mod-aws-iam-role"]
 asg["tf-mod-aws-autoscaling-group"]
 ecsc["tf-mod-aws-ecs-cluster"]
 ecss["tf-mod-aws-ecs-service"]
 lb["tf-mod-aws-lb"]
 sns["tf-mod-aws-sns"]
 cwa["tf-mod-aws-cloudwatch-alarm"]
 cd["tf-mod-aws-codedeploy"]
 cp["tf-mod-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

ℹ️ tf-mod-aws-iam-role, tf-mod-aws-autoscaling-group, tf-mod-aws-ecs-cluster, tf-mod-aws-ecs-service, tf-mod-aws-lb, and tf-mod-aws-sns are Phase 1 / Phase 2 modules; tf-mod-aws-cloudwatch-alarm is Phase 4; tf-mod-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["tf-mod-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
Resource Role Cardinality
aws_codedeploy_app.this Keystone application; owns compute_platform 1
aws_codedeploy_deployment_group.this Deployment target(s) β€” in-place or blue/green per deployment_groups entry
aws_codedeploy_deployment_config.this Custom rollout curve(s) beyond the AWS-managed presets per deployment_configs entry (0-N)

βœ… Provider / Versions

Requirement Version
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:PassRole conditioned 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:

Action Required for Notes
codedeploy:CreateApplication, codedeploy:DeleteApplication, codedeploy:GetApplication, codedeploy:UpdateApplication Application lifecycle β€”
codedeploy:CreateDeploymentGroup, codedeploy:DeleteDeploymentGroup, codedeploy:GetDeploymentGroup, codedeploy:UpdateDeploymentGroup Deployment group lifecycle β€”
codedeploy:CreateDeploymentConfig, codedeploy:DeleteDeploymentConfig, codedeploy:GetDeploymentConfig Custom deployment config lifecycle Only when deployment_configs is non-empty; AWS-managed presets need no create permission
codedeploy:ListApplications, codedeploy:ListDeploymentGroups, codedeploy:ListDeploymentConfigs, codedeploy:ListTagsForResource Drift detection / terraform plan reads β€”
codedeploy:TagResource, codedeploy:UntagResource Tagging (application + deployment groups) β€”
iam:PassRole Passing service_role_arn to CodeDeploy MANDATORY β€” condition on iam:PassedToService = codedeploy.amazonaws.com; see callout above
iam:GetRole Validating the supplied service_role_arn exists Read-only

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

compute_platform AWS-managed policy Grants
Server AWSCodeDeployRole Read 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) AWSCodeDeployRoleForECS Manage ECS task sets; update ELB target groups/listeners/rules; invoke Lambda hooks; read S3 revision artifacts; read CloudWatch alarms; publish SNS
ECS (limited) AWSCodeDeployRoleForECSLimited Manage ECS task sets; read CloudWatch alarms; publish SNS (no ELB/S3/Lambda-hook access)
Lambda (full access) AWSCodeDeployRoleForLambda Read/update/invoke Lambda functions and aliases; read S3 revision artifacts; read CloudWatch alarms; publish SNS
Lambda (limited) AWSCodeDeployRoleForLambdaLimited Read/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 (tf-mod-aws-sns), not by this module.

compute_platform compatibility matrix

Deployment-group argument Server ECS Lambda
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) βœ… Required N/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 argument Server ECS Lambda
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

tf-mod-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/tf-mod-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/tf-mod-aws-codedeploy?ref=v1.0.0"

  name             = "casey-billing-service"
  service_role_arn = module.deploy_role.arn # iam:PassRole required on the caller β€” see above

  deployment_groups = {
    production = {
      autoscaling_groups = ["casey-billing-prod-asg"] # from tf-mod-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

Input Type Source module
service_role_arn string (ARN) tf-mod-aws-iam-role β€” REQUIRED, see IAM section above
deployment_groups[*].autoscaling_groups list(string) (ASG names) tf-mod-aws-autoscaling-group
deployment_groups[*].ecs_service.cluster_name / .service_name string tf-mod-aws-ecs-cluster / tf-mod-aws-ecs-service
deployment_groups[*].load_balancer_info names / listener ARNs string / list(string) tf-mod-aws-lb
deployment_groups[*].trigger_configuration[*].trigger_target_arn string (ARN) tf-mod-aws-sns
deployment_groups[*].alarm_configuration.alarms list(string) (alarm names, not ARNs) tf-mod-aws-cloudwatch-alarm (pass the alarm's name output)

Emits

Output Description Consumed by
id AWS-assigned application id Reference/lookup
arn Application ARN β€” arn:aws:codedeploy:<region>:<account>:application:<name> tf-mod-aws-iam-policy (scoping policies), audit
name Application name tf-mod-aws-codepipeline deploy-action application_name; deployment group app_name
deployment_group_ids / deployment_group_arns Maps keyed by deployment group name tf-mod-aws-codepipeline deploy-action wiring; audit
deployment_group_names Map of deployment group names (echoes the map key) CLI/console cross-reference; CodePipeline deployment_group_name
deployment_config_ids / deployment_config_arns Maps of any custom deployment configs created deployment_groups[*].deployment_config_name wiring, including cross-module reuse
tags_all All tags on the application, incl. provider default_tags Governance/audit

πŸ“š Example Library

1 Β· Minimal β€” Server-platform application, no deployment groups yet
module "codedeploy" {
  source = "git::https://github.com/microsoftexpert/tf-mod-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 tf-mod-aws-autoscaling-group + tf-mod-aws-iam-role
module "deploy_role" {
  source = "git::https://github.com/microsoftexpert/tf-mod-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/tf-mod-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/tf-mod-aws-codedeploy?ref=v1.0.0"

  name             = "casey-app"
  service_role_arn = module.deploy_role.arn

  deployment_groups = {
    production = {
      autoscaling_groups = [module.asg.name]
    }
  }
}
3 Β· Blue/green EC2 with a Classic ELB
module "codedeploy" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-codedeploy?ref=v1.0.0"

  name             = "casey-legacy-app"
  service_role_arn = module.deploy_role.arn

  deployment_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 tf-mod-aws-ecs-cluster/tf-mod-aws-ecs-service + ALB target-group pair
module "codedeploy" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-codedeploy?ref=v1.0.0"

  name             = "casey-orders-service"
  compute_platform = "ECS"
  service_role_arn = module.ecs_deploy_role.arn # attached to AWSCodeDeployRoleForECS

  deployment_groups = {
    production = {
      ecs_service = {
        cluster_name = module.ecs_cluster.name # tf-mod-aws-ecs-cluster
        service_name = module.ecs_service.name # tf-mod-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] } # tf-mod-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/tf-mod-aws-codedeploy?ref=v1.0.0"

  name             = "casey-invoice-processor"
  compute_platform = "Lambda"
  service_role_arn = module.lambda_deploy_role.arn # attached to AWSCodeDeployRoleForLambda

  deployment_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 tf-mod-aws-cloudwatch-alarm
module "high_error_rate" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-cloudwatch-alarm?ref=v1.0.0"
  name   = "casey-app-5xx-high"
  #... metric, threshold, comparison_operator...
}

module "codedeploy" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-codedeploy?ref=v1.0.0"

  name             = "casey-app"
  service_role_arn = module.deploy_role.arn

  deployment_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 tf-mod-aws-sns
module "deploy_notifications" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-sns?ref=v1.0.0"
  name   = "casey-app-deploy-events"
}

module "codedeploy" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-codedeploy?ref=v1.0.0"

  name             = "casey-app"
  service_role_arn = module.deploy_role.arn

  deployment_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/tf-mod-aws-codedeploy?ref=v1.0.0"

  name             = "casey-app"
  service_role_arn = module.deploy_role.arn

  deployment_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/tf-mod-aws-codedeploy?ref=v1.0.0"

  name             = "casey-app"
  service_role_arn = module.deploy_role.arn

  tags = {
    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/tf-mod-aws-codedeploy?ref=v1.0.0"

  name             = "casey-legacy-batch"
  service_role_arn = module.deploy_role.arn

  deployment_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/tf-mod-aws-codedeploy?ref=v1.0.0"

  name             = "casey-app"
  service_role_arn = module.deploy_role.arn

  deployment_configs = {
    for k, v in local.envs : "casey-app-${k}-minhealthy" => {
      compute_platform      = "Server"
      minimum_healthy_hosts = { type = "FLEET_PERCENT", value = v.min_healthy }
    }
  }

  deployment_groups = {
    for k, v in local.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.this
  id = "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/tf-mod-aws-codedeploy?ref=v1.0.0"

  name             = "casey-app-existing"
  service_role_arn = module.deploy_role.arn

  deployment_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/tf-mod-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/tf-mod-aws-ecs-cluster?ref=v1.0.0"
  name   = "casey-orders"
}

module "ecs_service" {
  source      = "git::https://github.com/microsoftexpert/tf-mod-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/tf-mod-aws-lb?ref=v1.0.0"
  name       = "casey-orders"
  vpc_id     = module.vpc.vpc_id
  subnet_ids = values(module.vpc.public_subnet_ids)
}

module "deploy_notifications" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-sns?ref=v1.0.0"
  name   = "casey-orders-deploy-events"
}

module "high_error_rate" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-cloudwatch-alarm?ref=v1.0.0"
  name   = "casey-orders-5xx-high"
}

module "codedeploy" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-codedeploy?ref=v1.0.0"

  name             = "casey-orders-service"
  compute_platform = "ECS"
  service_role_arn = module.ecs_deploy_role.arn

  deployment_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_groups β€” map(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_configs β€” map(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 arn β†’ arn: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 arn β†’ arn:aws:codedeploy:<region>:<account>:deploymentgroup:<application>/<deployment_group_name> (note the slash, not a second colon, before the group name).
  • Deployment config arn β†’ arn: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.
  • tags ↔ tags_all ↔ default_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 tf-mod-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 default Behavior Opt-out / control
Automatic rollback on deployment failure auto_rollback_configuration { enabled = true, events = ["DEPLOYMENT_FAILURE",...] } on every deployment group deployment_groups[*].auto_rollback_enabled = false β€” requires a documented exception; regulated-FI deployments should not disable failure rollback
Alarm-triggered rollback Enabled automatically (alarm_configuration.enabled = true, DEPLOYMENT_STOP_ON_ALARM appended to rollback events) whenever alarm_configuration.alarms is non-empty Omit / empty the alarms list to skip alarm monitoring entirely
ignore_poll_alarm_failure false (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 control Defaults to "WITH_TRAFFIC_CONTROL" automatically whenever load_balancer_info is wired, so instances drain before receiving new code Only relevant when deployment_style is used; omitting load_balancer_info naturally yields "WITHOUT_TRAFFIC_CONTROL"
IAM PassRole scope Documented 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)
  • β€” tf-mod-aws-iam-role, tf-mod-aws-autoscaling-group, tf-mod-aws-ecs-cluster, tf-mod-aws-ecs-service, tf-mod-aws-lb, tf-mod-aws-sns, tf-mod-aws-cloudwatch-alarm, tf-mod-aws-codepipeline (sibling modules)

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

About

No description or website provided.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages