├── LICENSE ├── README.md ├── alb.tf ├── code-build.tf ├── code-deploy.tf ├── code-pipeline.tf ├── ecr.tf ├── ecs-service.tf ├── ecs-task-def.tf ├── ecs-with-codepipeline.png ├── ecs.tf ├── launch-instance.tf ├── var.tf └── vpc.tf /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 GNOKOHEAT 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ecs-with-codepipeline-example-by-terraform 2 | ![GitHub](https://img.shields.io/github/license/gnokoheat/ecs-with-codepipeline-example-by-terraform) ![GitHub top language](https://img.shields.io/github/languages/top/gnokoheat/ecs-with-codepipeline-example-by-terraform) ![GitHub last commit](https://img.shields.io/github/last-commit/gnokoheat/ecs-with-codepipeline-example-by-terraform) 3 | 4 | **ECS with Codepipeline example by Terraform** 5 | 6 | - Building AWS ECS Infrastructure with AWS Codepipeline for Blue/Green deployment by Terraform 7 | 8 | ![](https://github.com/gnokoheat/ecs-with-codepipeline-example-by-terraform/blob/master/ecs-with-codepipeline.png?raw=true) 9 | 10 | ## Include 11 | This terraform code include All-In-One for ECS & Codepipeline settings even VPC infra. 12 | 13 | - AWS ECS : ECS Cluster(EC2 type), ECS Service, ESC Task definition(Dynamic port mapping) 14 | - AWS Codepipeline : AWS Codebuild(Github), AWS Codedeploy(Blue/Green deployment) 15 | - AWS EC2 : ECS Container Instances, ALB, ALB Target groups, Auto scaling groups, Security groups 16 | - AWS VPC : VPC, Subnets, Routing tables, Internet gateway, Nat gateway 17 | 18 | ## Related 19 | 20 | App for this infra code : https://github.com/gnokoheat/ecs-nodejs-app-example 21 | -------------------------------------------------------------------------------- /alb.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | target_groups = ["primary", "secondary"] 3 | hosts_name = ["*.yourdomain.com"] #example : fill your information 4 | } 5 | 6 | resource "aws_security_group" "alb" { 7 | name = "${var.service_name}-allow-http" 8 | vpc_id = "${aws_vpc.this.id}" 9 | 10 | ingress { 11 | from_port = 80 12 | protocol = "tcp" 13 | to_port = 80 14 | cidr_blocks = ["0.0.0.0/0"] 15 | } 16 | 17 | egress { 18 | from_port = 0 19 | protocol = "-1" 20 | to_port = 0 21 | cidr_blocks = ["0.0.0.0/0"] 22 | } 23 | 24 | tags = { 25 | Name = "${var.service_name}-allow-http" 26 | } 27 | } 28 | 29 | resource "aws_lb" "this" { 30 | name = "${var.service_name}-service-alb" 31 | internal = false 32 | load_balancer_type = "application" 33 | security_groups = ["${aws_security_group.alb.id}"] 34 | subnets = "${aws_subnet.public.*.id}" 35 | 36 | tags = { 37 | Name = "${var.service_name}-service-alb" 38 | } 39 | } 40 | 41 | resource "aws_lb_target_group" "this" { 42 | count = "${length(local.target_groups)}" 43 | name = "${var.service_name}-tg-${element(local.target_groups, count.index)}" 44 | 45 | port = 80 46 | protocol = "HTTP" 47 | vpc_id = "${aws_vpc.this.id}" 48 | target_type = "instance" 49 | 50 | health_check { 51 | path = "/" 52 | } 53 | } 54 | 55 | resource "aws_lb_listener" "this" { 56 | load_balancer_arn = "${aws_lb.this.arn}" 57 | port = "80" 58 | protocol = "HTTP" 59 | 60 | default_action { 61 | type = "forward" 62 | target_group_arn = "${aws_lb_target_group.this.0.arn}" 63 | } 64 | } 65 | 66 | resource "aws_lb_listener_rule" "this" { 67 | count = 2 68 | listener_arn = "${aws_lb_listener.this.arn}" 69 | 70 | action { 71 | type = "forward" 72 | target_group_arn = "${aws_lb_target_group.this.*.arn[count.index]}" 73 | } 74 | 75 | condition { 76 | field = "host-header" 77 | values = "${local.hosts_name}" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /code-build.tf: -------------------------------------------------------------------------------- 1 | data "aws_iam_policy_document" "assume_by_codebuild" { 2 | statement { 3 | sid = "AllowAssumeByCodebuild" 4 | effect = "Allow" 5 | actions = ["sts:AssumeRole"] 6 | 7 | principals { 8 | type = "Service" 9 | identifiers = ["codebuild.amazonaws.com"] 10 | } 11 | } 12 | } 13 | 14 | resource "aws_iam_role" "codebuild" { 15 | name = "${var.service_name}-codebuild" 16 | assume_role_policy = "${data.aws_iam_policy_document.assume_by_codebuild.json}" 17 | } 18 | 19 | data "aws_iam_policy_document" "codebuild" { 20 | statement { 21 | sid = "AllowS3" 22 | effect = "Allow" 23 | 24 | actions = [ 25 | "s3:PutObject", 26 | "s3:GetObject", 27 | "s3:GetObjectVersion", 28 | "s3:GetBucketAcl", 29 | "s3:GetBucketLocation" 30 | ] 31 | 32 | resources = ["*"] 33 | } 34 | 35 | statement { 36 | sid = "AllowECR" 37 | effect = "Allow" 38 | 39 | actions = [ 40 | "ecr:*" 41 | ] 42 | 43 | resources = ["*"] 44 | } 45 | 46 | statement { 47 | sid = "AWSKMSUse" 48 | effect = "Allow" 49 | 50 | actions = [ 51 | "kms:DescribeKey", 52 | "kms:GenerateDataKey*", 53 | "kms:Encrypt", 54 | "kms:ReEncrypt*", 55 | "kms:Decrypt" 56 | ] 57 | 58 | resources = ["*"] 59 | } 60 | 61 | statement { 62 | sid = "AllowECSDescribeTaskDefinition" 63 | effect = "Allow" 64 | actions = ["ecs:DescribeTaskDefinition"] 65 | resources = ["*"] 66 | } 67 | 68 | statement { 69 | sid = "AllowLogging" 70 | effect = "Allow" 71 | 72 | actions = [ 73 | "logs:CreateLogGroup", 74 | "logs:CreateLogStream", 75 | "logs:PutLogEvents", 76 | ] 77 | 78 | resources = ["*"] 79 | } 80 | } 81 | 82 | resource "aws_iam_role_policy" "codebuild" { 83 | role = "${aws_iam_role.codebuild.name}" 84 | policy = "${data.aws_iam_policy_document.codebuild.json}" 85 | } 86 | 87 | resource "aws_codebuild_project" "this" { 88 | name = "${var.service_name}-codebuild" 89 | description = "Codebuild for the ECS Green/Blue ${var.service_name} app" 90 | service_role = "${aws_iam_role.codebuild.arn}" 91 | 92 | artifacts { 93 | type = "NO_ARTIFACTS" 94 | } 95 | 96 | environment { 97 | compute_type = "BUILD_GENERAL1_SMALL" 98 | image = "aws/codebuild/docker:18.09.0" 99 | type = "LINUX_CONTAINER" 100 | privileged_mode = true 101 | 102 | environment_variable { 103 | name = "IMAGE_REPO_NAME" 104 | value = "${var.service_name}" 105 | } 106 | 107 | environment_variable { 108 | name = "AWS_ACCOUNT_ID" 109 | value = "${var.aws_account_id}" 110 | } 111 | 112 | environment_variable { 113 | name = "AWS_DEFAULT_REGION" 114 | value = "${var.region}" 115 | } 116 | 117 | environment_variable { 118 | name = "IMAGE_TAG" 119 | value = "latest" 120 | } 121 | 122 | environment_variable { 123 | name = "SERVICE_PORT" 124 | value = "${var.container_port}" 125 | } 126 | 127 | environment_variable { 128 | name = "MEMORY_RESV" 129 | value = "${var.memory_reserv}" 130 | } 131 | } 132 | 133 | source { 134 | type = "GITHUB" 135 | location = "https://github.com/${local.github_owner}/${local.github_repo}.git" 136 | git_clone_depth = 1 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /code-deploy.tf: -------------------------------------------------------------------------------- 1 | data "aws_iam_policy_document" "assume_by_codedeploy" { 2 | statement { 3 | sid = "" 4 | effect = "Allow" 5 | actions = ["sts:AssumeRole"] 6 | 7 | principals { 8 | type = "Service" 9 | identifiers = ["codedeploy.amazonaws.com"] 10 | } 11 | } 12 | } 13 | 14 | resource "aws_iam_role" "codedeploy" { 15 | name = "${var.service_name}-codedeploy" 16 | assume_role_policy = "${data.aws_iam_policy_document.assume_by_codedeploy.json}" 17 | } 18 | 19 | data "aws_iam_policy_document" "codedeploy" { 20 | statement { 21 | sid = "AllowLoadBalancingAndECSModifications" 22 | effect = "Allow" 23 | 24 | actions = [ 25 | "ecs:CreateTaskSet", 26 | "ecs:DeleteTaskSet", 27 | "ecs:DescribeServices", 28 | "ecs:UpdateServicePrimaryTaskSet", 29 | "elasticloadbalancing:DescribeListeners", 30 | "elasticloadbalancing:DescribeRules", 31 | "elasticloadbalancing:DescribeTargetGroups", 32 | "elasticloadbalancing:ModifyListener", 33 | "elasticloadbalancing:ModifyRule", 34 | "lambda:InvokeFunction", 35 | "cloudwatch:DescribeAlarms", 36 | "sns:Publish", 37 | "s3:GetObject", 38 | "s3:GetObjectMetadata", 39 | "s3:GetObjectVersion" 40 | ] 41 | 42 | resources = ["*"] 43 | } 44 | 45 | statement { 46 | sid = "AllowPassRole" 47 | effect = "Allow" 48 | 49 | actions = ["iam:PassRole"] 50 | 51 | resources = [ 52 | "${aws_iam_role.execution_role.arn}", 53 | "${aws_iam_role.task_role.arn}", 54 | ] 55 | } 56 | } 57 | 58 | resource "aws_iam_role_policy" "codedeploy" { 59 | role = "${aws_iam_role.codedeploy.name}" 60 | policy = "${data.aws_iam_policy_document.codedeploy.json}" 61 | } 62 | 63 | resource "aws_codedeploy_app" "this" { 64 | compute_platform = "ECS" 65 | name = "${var.service_name}-service-deploy" 66 | } 67 | 68 | resource "aws_codedeploy_deployment_group" "this" { 69 | app_name = "${aws_codedeploy_app.this.name}" 70 | deployment_group_name = "${var.service_name}-service-deploy-group" 71 | deployment_config_name = "CodeDeployDefault.ECSAllAtOnce" 72 | service_role_arn = "${aws_iam_role.codedeploy.arn}" 73 | 74 | blue_green_deployment_config { 75 | deployment_ready_option { 76 | action_on_timeout = "CONTINUE_DEPLOYMENT" 77 | } 78 | 79 | terminate_blue_instances_on_deployment_success { 80 | action = "TERMINATE" 81 | termination_wait_time_in_minutes = 60 82 | } 83 | } 84 | 85 | ecs_service { 86 | cluster_name = "${aws_ecs_cluster.this.name}" 87 | service_name = "${aws_ecs_service.this.name}" 88 | } 89 | 90 | deployment_style { 91 | deployment_option = "WITH_TRAFFIC_CONTROL" 92 | deployment_type = "BLUE_GREEN" 93 | } 94 | 95 | load_balancer_info { 96 | target_group_pair_info { 97 | prod_traffic_route { 98 | listener_arns = ["${aws_lb_listener.this.arn}"] 99 | } 100 | 101 | target_group { 102 | name = "${aws_lb_target_group.this.*.name[0]}" 103 | } 104 | 105 | target_group { 106 | name = "${aws_lb_target_group.this.*.name[1]}" 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /code-pipeline.tf: -------------------------------------------------------------------------------- 1 | locals { #example : fill your information 2 | github_token = "" 3 | github_owner = "gnokoheat" 4 | github_repo = "ecs-nodejs-app-example" 5 | github_branch = "master" 6 | } 7 | 8 | resource "aws_s3_bucket" "pipeline" { 9 | bucket = "${var.service_name}-codepipeline-bucket" 10 | 11 | policy = <> /etc/ecs/ecs.config 39 | EOF 40 | } 41 | 42 | resource "aws_autoscaling_group" "this" { 43 | name = "${var.service_name}-ecs-autoscaling-group" 44 | max_size = 2 45 | min_size = 1 46 | desired_capacity = 1 47 | vpc_zone_identifier = "${aws_subnet.private.*.id}" 48 | launch_configuration = "${aws_launch_configuration.this.name}" 49 | health_check_type = "ELB" 50 | 51 | tag { 52 | key = "Name" 53 | value = "ECS-Instance-${var.service_name}-service" 54 | propagate_at_launch = true 55 | } 56 | } 57 | 58 | resource "aws_iam_role" "ecs-instance-role" { 59 | name = "${var.service_name}-ecs-instance-role" 60 | path = "/" 61 | assume_role_policy = "${data.aws_iam_policy_document.ecs-instance-policy.json}" 62 | } 63 | 64 | data "aws_iam_policy_document" "ecs-instance-policy" { 65 | statement { 66 | actions = ["sts:AssumeRole"] 67 | 68 | principals { 69 | type = "Service" 70 | identifiers = ["ec2.amazonaws.com"] 71 | } 72 | } 73 | } 74 | 75 | resource "aws_iam_role_policy_attachment" "ecs-instance-role-attachment" { 76 | role = "${aws_iam_role.ecs-instance-role.name}" 77 | policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role" 78 | } 79 | 80 | resource "aws_iam_instance_profile" "ecs-instance-profile" { 81 | name = "${var.service_name}-ecs-instance-profile" 82 | path = "/" 83 | role = "${aws_iam_role.ecs-instance-role.id}" 84 | } 85 | -------------------------------------------------------------------------------- /var.tf: -------------------------------------------------------------------------------- 1 | #example : fill your information 2 | variable "region" { 3 | default = "us-east-1" 4 | } 5 | 6 | provider "aws" { 7 | access_key = "" 8 | secret_key = "" 9 | region = "${var.region}" 10 | } 11 | 12 | variable "ecs_key_pair_name" { 13 | default = "" 14 | } 15 | 16 | variable "aws_account_id" { 17 | default = "" 18 | } 19 | 20 | variable "service_name" { 21 | default = "demo-service" 22 | } 23 | 24 | variable "container_port" { 25 | default = "8080" 26 | } 27 | 28 | variable "memory_reserv" { 29 | default = 100 30 | } -------------------------------------------------------------------------------- /vpc.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | public_subnets = { 3 | "${var.region}a" = "10.10.101.0/24" 4 | "${var.region}b" = "10.10.102.0/24" 5 | "${var.region}c" = "10.10.103.0/24" 6 | } 7 | private_subnets = { 8 | "${var.region}a" = "10.10.201.0/24" 9 | "${var.region}b" = "10.10.202.0/24" 10 | "${var.region}c" = "10.10.203.0/24" 11 | } 12 | } 13 | 14 | resource "aws_vpc" "this" { 15 | cidr_block = "10.10.0.0/16" 16 | 17 | enable_dns_support = true 18 | enable_dns_hostnames = true 19 | 20 | tags = { 21 | Name = "${var.service_name}-vpc" 22 | } 23 | } 24 | 25 | resource "aws_internet_gateway" "this" { 26 | vpc_id = "${aws_vpc.this.id}" 27 | 28 | tags = { 29 | Name = "${var.service_name}-internet-gateway" 30 | } 31 | } 32 | 33 | resource "aws_subnet" "public" { 34 | count = "${length(local.public_subnets)}" 35 | cidr_block = "${element(values(local.public_subnets), count.index)}" 36 | vpc_id = "${aws_vpc.this.id}" 37 | 38 | map_public_ip_on_launch = true 39 | availability_zone = "${element(keys(local.public_subnets), count.index)}" 40 | 41 | tags = { 42 | Name = "${var.service_name}-service-public" 43 | } 44 | } 45 | 46 | resource "aws_subnet" "private" { 47 | count = "${length(local.private_subnets)}" 48 | cidr_block = "${element(values(local.private_subnets), count.index)}" 49 | vpc_id = "${aws_vpc.this.id}" 50 | 51 | map_public_ip_on_launch = true 52 | availability_zone = "${element(keys(local.private_subnets), count.index)}" 53 | 54 | tags = { 55 | Name = "${var.service_name}-service-private" 56 | } 57 | } 58 | 59 | resource "aws_default_route_table" "public" { 60 | default_route_table_id = "${aws_vpc.this.main_route_table_id}" 61 | 62 | tags = { 63 | Name = "${var.service_name}-public" 64 | } 65 | } 66 | 67 | resource "aws_route" "public_internet_gateway" { 68 | count = "${length(local.public_subnets)}" 69 | route_table_id = "${aws_default_route_table.public.id}" 70 | destination_cidr_block = "0.0.0.0/0" 71 | gateway_id = "${aws_internet_gateway.this.id}" 72 | 73 | timeouts { 74 | create = "5m" 75 | } 76 | } 77 | 78 | resource "aws_route_table_association" "public" { 79 | count = "${length(local.public_subnets)}" 80 | subnet_id = "${element(aws_subnet.public.*.id, count.index)}" 81 | route_table_id = "${aws_default_route_table.public.id}" 82 | } 83 | 84 | resource "aws_route_table" "private" { 85 | vpc_id = "${aws_vpc.this.id}" 86 | 87 | tags = { 88 | Name = "${var.service_name}-private" 89 | } 90 | } 91 | 92 | resource "aws_route_table_association" "private" { 93 | count = "${length(local.private_subnets)}" 94 | subnet_id = "${element(aws_subnet.private.*.id, count.index)}" 95 | route_table_id = "${aws_route_table.private.id}" 96 | } 97 | 98 | resource "aws_eip" "nat" { 99 | vpc = true 100 | 101 | tags = { 102 | Name = "${var.service_name}-eip" 103 | } 104 | } 105 | 106 | resource "aws_nat_gateway" "this" { 107 | allocation_id = "${aws_eip.nat.id}" 108 | subnet_id = "${aws_subnet.public.0.id}" 109 | 110 | tags = { 111 | Name = "${var.service_name}-nat-gw" 112 | } 113 | } 114 | 115 | resource "aws_route" "private_nat_gateway" { 116 | route_table_id = "${aws_route_table.private.id}" 117 | destination_cidr_block = "0.0.0.0/0" 118 | nat_gateway_id = "${aws_nat_gateway.this.id}" 119 | 120 | timeouts { 121 | create = "5m" 122 | } 123 | } 124 | --------------------------------------------------------------------------------