├── .codeac.yml ├── .gitignore ├── README.md ├── backend.tf ├── cloudwatch.tf ├── data.tf ├── lb.tf ├── main.tf ├── route53.tf └── variables.tf /.codeac.yml: -------------------------------------------------------------------------------- 1 | version: '1' 2 | 3 | tools: 4 | tflint: 5 | files: 6 | - . 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .terraform/* 2 | *.tfstate* 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terraform module to deploy an APP into ECS Fargate cluster. 2 | 3 | This module: 4 | * Create tasks. 5 | * Create service. 6 | * Zero downtime deploy. 7 | * Create monitoring using CloudWatch. 8 | * Receive an json with containers definitions. 9 | 10 | 11 | ## Usage 12 | 13 | ```terraform 14 | module "app-deploy" { 15 | source = "git@github.com:gomex/terraform-module-fargate-deploy.git?ref=v0.1" 16 | containers_definitions = data.template_file.containers_definitions_json.rendered 17 | environment = "development" 18 | subdomain_name = "app" 19 | app_name = "app" 20 | hosted_zone_id = "Z09847195EBZJRXWQDI" 21 | app_port = "80" 22 | cloudwatch_group_name = "development-app" 23 | } 24 | 25 | ################ DATA ################ 26 | 27 | data "template_file" "containers_definitions_json" { 28 | template = file("./containers_definitions.json") 29 | 30 | vars = { 31 | APP_VERSION = var.APP_VERSION 32 | APP_IMAGE = var.APP_IMAGE 33 | ENVIRONMENT = "development" 34 | AWS_REGION = var.aws_region 35 | } 36 | } 37 | 38 | ################ VARIABLES ################ 39 | variable "APP_VERSION" { 40 | } 41 | 42 | variable "APP_IMAGE" { 43 | default = "app" 44 | } 45 | 46 | variable "aws_region" { 47 | default = "us-east-1" 48 | } 49 | ``` 50 | 51 | ## Container Definition Sample 52 | 53 | Create a folder called `templates` and a file called `containers_definitions.json` with the content bellow: 54 | 55 | ```json 56 | [ 57 | { 58 | "cpu": 1024, 59 | "image": "670631891947.dkr.ecr.us-east-1.amazonaws.com/my_app:${APP_VERSION}", 60 | "memory": 1024, 61 | "name": "myawesomeapp", 62 | "networkMode": "awsvpc", 63 | "portMappings": [ 64 | { 65 | "containerPort": 3000, 66 | "hostPort": 3000 67 | } 68 | ], 69 | "environment": [ 70 | { 71 | "name": "AWESOME_ENV_VAR", 72 | "value": "${AWESOME_ENV_VAR}" 73 | } 74 | ], 75 | "logConfiguration": { 76 | "logDriver": "awslogs", 77 | "options": { 78 | "awslogs-group": "awesomeapp-gp", 79 | "awslogs-region": "us-east-1", 80 | "awslogs-stream-prefix": "myawesomeapp-${APP_VERSION}" 81 | } 82 | } 83 | } 84 | ] 85 | ``` 86 | 87 | * The `name` of one container created on `containers_definitions.json` should be the same as `app_name` passed to module. 88 | * You can configure almost everything as variable, and you probably should do this. 89 | 90 | ## Multiple Container Definition Sample 91 | 92 | ```json 93 | [ 94 | { 95 | "cpu": 1024, 96 | "image": "670631891947.dkr.ecr.us-east-1.amazonaws.com/my_app:${APP_VERSION}", 97 | "memory": 1024, 98 | "name": "myawesomeapp", 99 | "networkMode": "awsvpc", 100 | "portMappings": [ 101 | { 102 | "containerPort": 3000, 103 | "hostPort": 3000 104 | } 105 | ] 106 | }, 107 | { 108 | "cpu": 1024, 109 | "image": "670631891947.dkr.ecr.us-east-1.amazonaws.com/my_worker:${APP_VERSION}", 110 | "memory": 1024, 111 | "name": "myawesomeworker", 112 | "networkMode": "awsvpc", 113 | "portMappings": [ 114 | { 115 | "containerPort": 3000, 116 | "hostPort": 3000 117 | } 118 | ] 119 | } 120 | ] 121 | ``` 122 | 123 | ## Inputs 124 | 125 | | Name | Description | Type | Default | Required | 126 | |------|-------------|:----:|:-----:|:-----:| 127 | | app\_count | Number of tasks that will be deployed for this app. | string | `"1"` | no | 128 | | app\_name | How your app will be called. | string | n/a | yes | 129 | | app\_port | The PORT that will be used to communication between load balancer and container. | string | `"3000"` | no | 130 | | aws\_region | The AWS region to create things in. | string | `"us-east-1"` | no | 131 | | cloudwatch\_group\_name | CloudWatch group name where to send the logs. | string | `"sample-group-name"` | no | 132 | | containers\_definitions | A JSON with all container definitions that should be run on the task. For more http://bit.do/eKzfH | string | n/a | yes | 133 | | environment | The enviroment name where that app will be deployed. | string | `"development"` | no | 134 | | fargate\_cpu | The maximum of CPU that the task can use. | string | `"1024"` | no | 135 | | fargate\_memory | The maximum of memory that the task can use. | string | `"1024"` | no | 136 | | fargate\_version | The fargate version used to deploy inside ECS cluster. | string | `"1.3.0"` | no | 137 | | subdomain\_name | The subdomain that will be create for the app. | string | n/a | yes | 138 | 139 | # Wiki 140 | 141 | Want to know more? -------------------------------------------------------------------------------- /backend.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 0.12.0, <= 0.13.0" 3 | } 4 | 5 | -------------------------------------------------------------------------------- /cloudwatch.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudwatch_log_group" "monit" { 2 | name = var.cloudwatch_group_name 3 | retention_in_days = "14" 4 | 5 | tags = { 6 | Environment = var.environment 7 | Application = var.app_name 8 | } 9 | } -------------------------------------------------------------------------------- /data.tf: -------------------------------------------------------------------------------- 1 | data "aws_vpc" "main" { 2 | tags = { 3 | Name = var.environment 4 | } 5 | } 6 | 7 | data "aws_subnet_ids" "private" { 8 | vpc_id = data.aws_vpc.main.id 9 | 10 | tags = { 11 | Name = "Private" 12 | } 13 | } 14 | 15 | data "aws_subnet_ids" "public" { 16 | vpc_id = data.aws_vpc.main.id 17 | 18 | tags = { 19 | Name = "Public" 20 | } 21 | } 22 | 23 | data "aws_ecs_cluster" "main" { 24 | cluster_name = var.environment 25 | } 26 | 27 | data "aws_caller_identity" "current" { 28 | } 29 | 30 | -------------------------------------------------------------------------------- /lb.tf: -------------------------------------------------------------------------------- 1 | # ALB Security group 2 | # This is the group you need to edit if you want to restrict access to your application 3 | resource "aws_security_group" "lb" { 4 | name = "${var.app_name}-${var.environment}" 5 | description = "controls access to the ALB" 6 | vpc_id = data.aws_vpc.main.id 7 | 8 | egress { 9 | from_port = 0 10 | to_port = 0 11 | protocol = "-1" 12 | cidr_blocks = ["0.0.0.0/0"] 13 | } 14 | } 15 | 16 | resource "aws_security_group_rule" "allow_http" { 17 | type = "ingress" 18 | protocol = "tcp" 19 | from_port = "80" 20 | to_port = "80" 21 | cidr_blocks = ["0.0.0.0/0"] 22 | security_group_id = aws_security_group.lb.id 23 | } 24 | 25 | resource "aws_security_group_rule" "allow_https" { 26 | type = "ingress" 27 | protocol = "tcp" 28 | from_port = "443" 29 | to_port = "443" 30 | cidr_blocks = ["0.0.0.0/0"] 31 | security_group_id = aws_security_group.lb.id 32 | } 33 | 34 | resource "aws_alb" "main" { 35 | name = "${var.app_name}-alb" 36 | subnets = data.aws_subnet_ids.public.ids 37 | security_groups = [aws_security_group.lb.id] 38 | } 39 | 40 | resource "aws_alb_target_group" "app" { 41 | name = "${var.app_name}-alb" 42 | port = "80" 43 | protocol = "HTTP" 44 | vpc_id = data.aws_vpc.main.id 45 | target_type = "ip" 46 | 47 | health_check { 48 | path = var.health_check_path 49 | interval = 50 50 | timeout = 30 51 | } 52 | } 53 | 54 | # Redirect all traffic from the ALB to the target group 55 | 56 | resource "aws_alb_listener" "front_end" { 57 | load_balancer_arn = aws_alb.main.arn 58 | port = "80" 59 | protocol = "HTTP" 60 | 61 | default_action { 62 | target_group_arn = aws_alb_target_group.app.arn 63 | type = "forward" 64 | } 65 | } -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ecs_task_definition" "app" { 2 | family = var.app_name 3 | network_mode = "awsvpc" 4 | requires_compatibilities = ["FARGATE"] 5 | cpu = var.fargate_cpu 6 | memory = var.fargate_memory 7 | 8 | execution_role_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/ecsTaskExecutionRole" 9 | task_role_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/ecsTaskExecutionRole" 10 | 11 | container_definitions = var.containers_definitions 12 | 13 | tags = { 14 | FargateApp = var.app_name 15 | } 16 | } 17 | 18 | resource "aws_ecs_service" "main" { 19 | name = "${var.app_name}-svc" 20 | cluster = data.aws_ecs_cluster.main.id 21 | 22 | task_definition = aws_ecs_task_definition.app.arn 23 | desired_count = var.app_count 24 | launch_type = "FARGATE" 25 | 26 | platform_version = var.fargate_version 27 | 28 | network_configuration { 29 | security_groups = [aws_security_group.ecs_tasks.id] 30 | subnets = split(",", join(",", data.aws_subnet_ids.private.ids)) 31 | } 32 | 33 | load_balancer { 34 | target_group_arn = aws_alb_target_group.app.id 35 | container_name = var.app_name 36 | container_port = var.app_port 37 | } 38 | 39 | depends_on = [aws_alb_listener.front_end] 40 | 41 | } 42 | 43 | # Traffic to the ECS Cluster should only come from the ALB 44 | 45 | resource "aws_security_group" "ecs_tasks" { 46 | name = "tf-ecs-${var.app_name}" 47 | description = "allow inbound access from the ALB only" 48 | vpc_id = data.aws_vpc.main.id 49 | 50 | ingress { 51 | protocol = "tcp" 52 | from_port = var.app_port 53 | to_port = var.app_port 54 | security_groups = [aws_security_group.lb.id] 55 | } 56 | 57 | egress { 58 | protocol = "-1" 59 | from_port = 0 60 | to_port = 0 61 | cidr_blocks = ["0.0.0.0/0"] 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /route53.tf: -------------------------------------------------------------------------------- 1 | resource "aws_route53_record" "subdomain-dns" { 2 | zone_id = var.hosted_zone_id 3 | name = var.subdomain_name 4 | type = "CNAME" 5 | ttl = "5" 6 | records = [aws_alb.main.dns_name] 7 | } -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | variable "fargate_version" { 2 | default = "1.3.0" 3 | description = "The fargate version used to deploy inside ECS cluster." 4 | } 5 | 6 | variable "fargate_cpu" { 7 | type = number 8 | default = "1024" 9 | description = "The maximum of CPU that the task can use." 10 | } 11 | 12 | variable "fargate_memory" { 13 | type = number 14 | default = "2048" 15 | description = "The maximum of memory that the task can use." 16 | } 17 | 18 | variable "hosted_zone_id" { 19 | type = string 20 | description = "Hosted Zone ID" 21 | } 22 | 23 | variable "app_name" { 24 | type = string 25 | description = "How your app will be called." 26 | } 27 | 28 | variable "app_port" { 29 | type = number 30 | default = "3000" 31 | description = "The PORT that will be used to communication between load balancer and container." 32 | } 33 | 34 | variable "app_count" { 35 | type = number 36 | default = "1" 37 | description = "Number of tasks that will be deployed for this app." 38 | } 39 | 40 | variable "environment" { 41 | type = string 42 | default = "development" 43 | description = "The enviroment name where that app will be deployed." 44 | } 45 | 46 | variable "cloudwatch_group_name" { 47 | type = string 48 | default = "sample-group-name" 49 | description = "CloudWatch group name where to send the logs." 50 | } 51 | 52 | variable "containers_definitions" { 53 | type = string 54 | description = "A JSON with all container definitions that should be run on the task. For more http://bit.do/eKzfH" 55 | } 56 | 57 | variable "subdomain_name" { 58 | type = string 59 | description = "The subdomain that will be create for the app." 60 | } 61 | 62 | variable "health_check_path" { 63 | type = string 64 | default = "/" 65 | description = "Default health check path" 66 | } 67 | --------------------------------------------------------------------------------