├── .gitignore ├── .terraform.lock.hcl ├── README.md ├── alb.tf ├── deploy.tf ├── event_rules.tf ├── main.tf ├── sg.tf ├── terraform.tf └── variables.tf /.gitignore: -------------------------------------------------------------------------------- 1 | **/.terraform/* 2 | 3 | # .tfstate files 4 | *.tfstate 5 | *.tfstate.* 6 | 7 | # Exclude all .tfvars files, which are likely to contain sensitive data, such as 8 | # password, private keys, and other secrets. These should not be part of version 9 | # control as they are data points which are potentially sensitive and subject 10 | # to change depending on the environment. 11 | *.tfvars 12 | *.tfvars.json -------------------------------------------------------------------------------- /.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "4.60.0" 6 | constraints = "~> 4.60.0" 7 | hashes = [ 8 | "h1:b2U4EncUaHCsQuiePo/yHZiH5ib0rx+P/qG4LC8pGlA=", 9 | "zh:1853d6bc89e289ac36c13485e8ff877c1be8485e22f545bb32c7a30f1d1856e8", 10 | "zh:4321d145969e3b7ede62fe51bee248a15fe398643f21df9541eef85526bf3641", 11 | "zh:4c01189cc6963abfe724e6b289a7c06d2de9c395011d8d54efa8fe1aac444e2e", 12 | "zh:5934db7baa2eec0f9acb9c7f1c3dd3b3fe1e67e23dd4a49e9fe327832967b32b", 13 | "zh:5fbedf5d55c6e04e34c32b744151e514a80308e7dec633a56b852829b41e4b5a", 14 | "zh:651558e1446cc05061b75e6f5cc6e2959feb17615cd0ace6ec7a2bcc846321c0", 15 | "zh:76875eb697916475e554af080f9d4d3cd1f7d5d58ecdd3317a844a30980f4eec", 16 | "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", 17 | "zh:a52528e6d6c945a6ac45b89e9a70a5435148e4c151241e04c231dd2acc4a8c80", 18 | "zh:af5f94c69025f1c2466a3cf970d1e9bed72938ec33b976c8c067468b6707bb57", 19 | "zh:b6692fad956c9d4ef4266519d9ac2ee9f699f8f2c21627625c9ed63814d41590", 20 | "zh:b74311af5fa5ac6e4eb159c12cfb380dfe2f5cd8685da2eac8073475f398ae60", 21 | "zh:cc5aa6f738baa42edacba5ef1ca0969e5a959422e4491607255f3f6142ba90ed", 22 | "zh:dd1a7ff1b22f0036a76bc905a8229ce7ed0a7eb5a783d3a2586fb1bd920515c3", 23 | "zh:e5ab40c4ad0f1c7bd4d5d834d1aa144e690d1a93329d73b3d37512715a638de9", 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Continuous Deployment pipeline for ECS using ECR, CodePipeline and CodeDeploy 2 | 3 | Make sure to fill every variable from __variables.tf__ according to your environment. 4 | 5 | Note that the repository [martinKindall/aws_code_deploy_example](https://github.com/martinKindall/aws_code_deploy_example) is already part of this pipeline: it contains the necessary files for CodeDeploy and ECS to deploy a new task. You can use another repository with the proper _appspec.yml_ and _taskdef.json_ files. -------------------------------------------------------------------------------- /alb.tf: -------------------------------------------------------------------------------- 1 | 2 | resource "aws_default_vpc" "default" { 3 | tags = { 4 | Name = "Default VPC" 5 | } 6 | } 7 | 8 | data "aws_subnets" "mySubnets" { 9 | filter { 10 | name = "vpc-id" 11 | values = [aws_default_vpc.default.id] 12 | } 13 | } 14 | 15 | resource "aws_lb_target_group" "my_Tg" { 16 | name = "tf-example-lb-tg" 17 | port = "80" 18 | protocol = "HTTP" 19 | target_type = "ip" 20 | vpc_id = aws_default_vpc.default.id 21 | } 22 | 23 | resource "aws_lb_target_group" "my_Tg_2" { 24 | name = "tf-example-lb-tg-2" 25 | port = "80" 26 | protocol = "HTTP" 27 | target_type = "ip" 28 | vpc_id = aws_default_vpc.default.id 29 | } 30 | 31 | resource "aws_lb" "myAlb" { 32 | name = "test-lb-tf" 33 | internal = false 34 | load_balancer_type = "application" 35 | security_groups = [aws_security_group.spring_app.id] 36 | subnets = data.aws_subnets.mySubnets.ids 37 | } 38 | 39 | resource "aws_lb_listener" "front_end" { 40 | load_balancer_arn = aws_lb.myAlb.arn 41 | port = "80" 42 | protocol = "HTTP" 43 | 44 | default_action { 45 | type = "forward" 46 | target_group_arn = aws_lb_target_group.my_Tg.arn 47 | } 48 | 49 | lifecycle { 50 | ignore_changes = [ 51 | default_action[0].target_group_arn 52 | ] 53 | } 54 | } -------------------------------------------------------------------------------- /deploy.tf: -------------------------------------------------------------------------------- 1 | 2 | resource "aws_codepipeline" "codepipeline" { 3 | name = "tf-my-pipeline" 4 | role_arn = aws_iam_role.codepipeline_role.arn 5 | 6 | artifact_store { 7 | location = var.codepipeline_bucket 8 | type = "S3" 9 | 10 | encryption_key { 11 | id = var.codepipeline_bucket_encryption_key_arn 12 | type = "KMS" 13 | } 14 | } 15 | 16 | stage { 17 | name = "Source" 18 | 19 | action { 20 | name = "Source" 21 | category = "Source" 22 | owner = "AWS" 23 | provider = "CodeStarSourceConnection" 24 | version = "1" 25 | output_artifacts = ["SourceArtifact"] 26 | namespace = "SourceVariables" 27 | 28 | configuration = { 29 | ConnectionArn = var.codestar_connection_arn 30 | FullRepositoryId = var.git_repository_name 31 | BranchName = "main" 32 | OutputArtifactFormat = "CODE_ZIP" 33 | DetectChanges = true 34 | } 35 | } 36 | 37 | action { 38 | name = "SpringImage" 39 | category = "Source" 40 | owner = "AWS" 41 | provider = "ECR" 42 | version = "1" 43 | output_artifacts = ["MyImage"] 44 | 45 | configuration = { 46 | RepositoryName = var.repository_name 47 | ImageTag = "latest" 48 | } 49 | } 50 | } 51 | 52 | stage { 53 | name = "Deploy" 54 | 55 | action { 56 | name = "Deploy" 57 | category = "Deploy" 58 | owner = "AWS" 59 | provider = "CodeDeployToECS" 60 | input_artifacts = ["SourceArtifact", "MyImage"] 61 | version = "1" 62 | 63 | configuration = { 64 | ApplicationName = aws_codedeploy_app.my_spring_app.name 65 | DeploymentGroupName = aws_codedeploy_deployment_group.spring_group.deployment_group_name 66 | TaskDefinitionTemplateArtifact = "SourceArtifact" 67 | AppSpecTemplateArtifact = "SourceArtifact" 68 | AppSpecTemplatePath = "appspec.yml" 69 | TaskDefinitionTemplatePath = "taskdef.json" 70 | Image1ArtifactName = "MyImage" 71 | Image1ContainerName = "IMAGE1_NAME" 72 | } 73 | } 74 | } 75 | } 76 | 77 | resource "aws_iam_role" "codepipeline_role" { 78 | name = "tf-codepipeline-role" 79 | assume_role_policy = data.aws_iam_policy_document.assume_role_pipeline.json 80 | } 81 | 82 | data "aws_iam_policy_document" "assume_role_pipeline" { 83 | statement { 84 | effect = "Allow" 85 | 86 | principals { 87 | type = "Service" 88 | identifiers = ["codepipeline.amazonaws.com"] 89 | } 90 | 91 | actions = ["sts:AssumeRole"] 92 | } 93 | } 94 | 95 | resource "aws_iam_role_policy" "codepipeline_policy" { 96 | name = "codepipeline_policy" 97 | role = aws_iam_role.codepipeline_role.id 98 | policy = data.aws_iam_policy_document.codepipeline_policy.json 99 | } 100 | 101 | data "aws_iam_policy_document" "codepipeline_policy" { 102 | statement { 103 | effect = "Allow" 104 | actions = [ 105 | "iam:PassRole" 106 | ] 107 | resources = ["*"] 108 | 109 | condition { 110 | test = "StringEqualsIfExists" 111 | variable = "iam:PassedToService" 112 | values = ["ecs-tasks.amazonaws.com"] 113 | } 114 | } 115 | 116 | statement { 117 | effect = "Allow" 118 | actions = [ 119 | "codedeploy:CreateDeployment", 120 | "codedeploy:GetApplication", 121 | "codedeploy:GetApplicationRevision", 122 | "codedeploy:GetDeployment", 123 | "codedeploy:GetDeploymentConfig", 124 | "codedeploy:RegisterApplicationRevision" 125 | ] 126 | resources = ["*"] 127 | } 128 | 129 | statement { 130 | effect = "Allow" 131 | actions = [ 132 | "s3:*", 133 | "elasticloadbalancing:*", 134 | "ecs:*", 135 | "cloudwatch:*", 136 | "ecr:DescribeImages", 137 | "codestar-connections:UseConnection" 138 | ] 139 | resources = ["*"] 140 | } 141 | } 142 | 143 | resource "aws_codedeploy_app" "my_spring_app" { 144 | compute_platform = "ECS" 145 | name = "spring_app" 146 | } 147 | 148 | resource "aws_codedeploy_deployment_group" "spring_group" { 149 | app_name = aws_codedeploy_app.my_spring_app.name 150 | deployment_group_name = "ECSDeployment" 151 | deployment_config_name = "CodeDeployDefault.ECSAllAtOnce" 152 | service_role_arn = aws_iam_role.codeDeploy.arn 153 | 154 | auto_rollback_configuration { 155 | enabled = true 156 | events = ["DEPLOYMENT_FAILURE"] 157 | } 158 | 159 | blue_green_deployment_config { 160 | deployment_ready_option { 161 | action_on_timeout = "CONTINUE_DEPLOYMENT" 162 | } 163 | 164 | terminate_blue_instances_on_deployment_success { 165 | action = "TERMINATE" 166 | termination_wait_time_in_minutes = 1 167 | } 168 | } 169 | 170 | deployment_style { 171 | deployment_option = "WITH_TRAFFIC_CONTROL" 172 | deployment_type = "BLUE_GREEN" 173 | } 174 | 175 | ecs_service { 176 | cluster_name = aws_ecs_cluster.my_cluster.name 177 | service_name = aws_ecs_service.my_service.name 178 | } 179 | 180 | load_balancer_info { 181 | target_group_pair_info { 182 | prod_traffic_route { 183 | listener_arns = [aws_lb_listener.front_end.arn] 184 | } 185 | 186 | target_group { 187 | name = aws_lb_target_group.my_Tg.name 188 | } 189 | 190 | target_group { 191 | name = aws_lb_target_group.my_Tg_2.name 192 | } 193 | } 194 | } 195 | } 196 | 197 | resource "aws_iam_role" "codeDeploy" { 198 | name = "codeDeployEcs" 199 | assume_role_policy = data.aws_iam_policy_document.assume_role_codedeploy.json 200 | 201 | managed_policy_arns = ["arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS"] 202 | } 203 | 204 | data "aws_iam_policy_document" "assume_role_codedeploy" { 205 | statement { 206 | effect = "Allow" 207 | 208 | principals { 209 | type = "Service" 210 | identifiers = ["codedeploy.amazonaws.com"] 211 | } 212 | 213 | actions = ["sts:AssumeRole"] 214 | } 215 | } -------------------------------------------------------------------------------- /event_rules.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudwatch_event_rule" "ecr" { 2 | name = "trigger-ecr-event-codepipeline" 3 | description = "Triggers Codepipeline" 4 | role_arn = aws_iam_role.events_role.arn 5 | 6 | event_pattern = jsonencode({ 7 | detail-type = [ 8 | "ECR Image Action" 9 | ] 10 | detail = { 11 | action-type = ["PUSH"] 12 | image-tag = ["latest"] 13 | repository-name = [var.repository_name] 14 | result = ["SUCCESS"] 15 | } 16 | }) 17 | } 18 | 19 | resource "aws_cloudwatch_event_target" "codepipeline" { 20 | rule = aws_cloudwatch_event_rule.ecr.name 21 | target_id = "SendToCodePipelineSpringApp" 22 | arn = aws_codepipeline.codepipeline.arn 23 | role_arn = aws_iam_role.events_role.arn 24 | } 25 | 26 | resource "aws_iam_role" "events_role" { 27 | name = "tf-events-role" 28 | assume_role_policy = data.aws_iam_policy_document.assume_role_events.json 29 | } 30 | 31 | data "aws_iam_policy_document" "assume_role_events" { 32 | statement { 33 | effect = "Allow" 34 | 35 | principals { 36 | type = "Service" 37 | identifiers = ["events.amazonaws.com"] 38 | } 39 | 40 | actions = ["sts:AssumeRole"] 41 | } 42 | } 43 | 44 | resource "aws_iam_role_policy" "cloudwatch_event_policy" { 45 | name = "cloudwatch_event_policy" 46 | role = aws_iam_role.events_role.id 47 | policy = data.aws_iam_policy_document.cloudwatch_event_policy.json 48 | } 49 | 50 | data "aws_iam_policy_document" "cloudwatch_event_policy" { 51 | statement { 52 | effect = "Allow" 53 | actions = [ 54 | "codepipeline:StartPipelineExecution" 55 | ] 56 | resources = [aws_codepipeline.codepipeline.arn] 57 | } 58 | } -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | 2 | provider "aws" { 3 | region = var.aws_region 4 | } 5 | 6 | data "aws_caller_identity" "current" {} 7 | 8 | resource "aws_ecs_cluster" "my_cluster" { 9 | name = "exampleClusterTf" 10 | } 11 | 12 | resource "aws_ecs_service" "my_service" { 13 | name = "springApp" 14 | cluster = aws_ecs_cluster.my_cluster.id 15 | task_definition = format("arn:aws:ecs:%s:%s:task-definition/spring_app", var.aws_region, data.aws_caller_identity.current.account_id) 16 | desired_count = 0 17 | 18 | scheduling_strategy = "REPLICA" 19 | launch_type = "FARGATE" 20 | deployment_controller { 21 | type = "CODE_DEPLOY" 22 | } 23 | 24 | load_balancer { 25 | target_group_arn = aws_lb_target_group.my_Tg.arn 26 | container_name = "spring_ex" 27 | container_port = 80 28 | } 29 | 30 | network_configuration { 31 | subnets = data.aws_subnets.mySubnets.ids 32 | security_groups = [aws_security_group.spring_app.id] 33 | assign_public_ip = true 34 | } 35 | 36 | lifecycle { 37 | ignore_changes = [ 38 | task_definition, 39 | load_balancer 40 | ] 41 | } 42 | } -------------------------------------------------------------------------------- /sg.tf: -------------------------------------------------------------------------------- 1 | 2 | resource "aws_security_group" "spring_app" { 3 | name = "spring_app" 4 | description = "Allows HTTP to web server from anywhere" 5 | } 6 | 7 | resource "aws_vpc_security_group_ingress_rule" "http" { 8 | security_group_id = aws_security_group.spring_app.id 9 | 10 | cidr_ipv4 = "0.0.0.0/0" 11 | from_port = 80 12 | ip_protocol = "tcp" 13 | to_port = 80 14 | } 15 | 16 | resource "aws_vpc_security_group_egress_rule" "http" { 17 | security_group_id = aws_security_group.spring_app.id 18 | 19 | cidr_ipv4 = "0.0.0.0/0" 20 | ip_protocol = -1 21 | } 22 | -------------------------------------------------------------------------------- /terraform.tf: -------------------------------------------------------------------------------- 1 | 2 | terraform { 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 4.60.0" 8 | } 9 | } 10 | 11 | required_version = "~> 1.4.0" 12 | } -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | 2 | variable "aws_region" { 3 | description = "AWS region" 4 | type = string 5 | default = "eu-central-1" 6 | } 7 | 8 | variable "repository_name" { 9 | description = "Name of the ECR repository for the spring app" 10 | type = string 11 | default = "spring_example" 12 | } 13 | 14 | variable "codestar_connection_arn" { 15 | description = "ARN of codestar connection with privileges to the FullRepositoryId specified in codepipeline. The arn looks like this arn:aws:codestar-connections:eu-central-1::connection/" 16 | type = string 17 | } 18 | 19 | variable "git_repository_name" { 20 | description = "Repository name which contains the appspec.yml and taskdef.json. Example: martinKindall/aws_code_deploy_example" 21 | type = string 22 | } 23 | 24 | variable "codepipeline_bucket" { 25 | description = "S3 bucket name for codepipeline" 26 | type = string 27 | } 28 | 29 | variable "codepipeline_bucket_encryption_key_arn" { 30 | description = "ARN of the encryption key associated with the buckte. Example: arn:aws:kms:eu-central-1::key/" 31 | type = string 32 | } --------------------------------------------------------------------------------