├── terraform ├── versions.tf ├── outputs.tf ├── provider.tf ├── logs.tf ├── templates │ └── ecs │ │ └── cb_app.json.tpl ├── roles.tf ├── alb.tf ├── security.tf ├── variables.tf ├── ecs.tf ├── network.tf └── auto_scaling.tf ├── .gitignore ├── README.md ├── LICENSE └── CODE_OF_CONDUCT.md /terraform/versions.tf: -------------------------------------------------------------------------------- 1 | 2 | terraform { 3 | required_version = ">= 0.12" 4 | } 5 | -------------------------------------------------------------------------------- /terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | # outputs.tf 2 | 3 | output "alb_hostname" { 4 | value = aws_alb.main.dns_name 5 | } 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled files 2 | *.tfstate 3 | *.tfstate.backup 4 | .terraform.tfstate.lock.info 5 | 6 | # Module directory 7 | .terraform/ -------------------------------------------------------------------------------- /terraform/provider.tf: -------------------------------------------------------------------------------- 1 | # provider.tf 2 | 3 | # Specify the provider and access details 4 | provider "aws" { 5 | shared_credentials_file = "$HOME/.aws/credentials" 6 | profile = "default" 7 | region = var.aws_region 8 | } 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Source code for a [tutorial on Medium](https://medium.com/@bradford_hamilton/deploying-containers-on-amazons-ecs-using-fargate-and-terraform-part-2-2e6f6a3a957f) I recently published. 2 | 3 | Terraform config for deploying docker containers to ECS using Fargate launch type. Currently set up for my [Crystal Blockchain Application](https://github.com/bradford-hamilton/crystal-blockchain). 4 | 5 | ### Show your support 6 | 7 | Give a ⭐ if this project was helpful in any way! -------------------------------------------------------------------------------- /terraform/logs.tf: -------------------------------------------------------------------------------- 1 | # logs.tf 2 | 3 | # Set up CloudWatch group and log stream and retain logs for 30 days 4 | resource "aws_cloudwatch_log_group" "cb_log_group" { 5 | name = "/ecs/cb-app" 6 | retention_in_days = 30 7 | 8 | tags = { 9 | Name = "cb-log-group" 10 | } 11 | } 12 | 13 | resource "aws_cloudwatch_log_stream" "cb_log_stream" { 14 | name = "cb-log-stream" 15 | log_group_name = aws_cloudwatch_log_group.cb_log_group.name 16 | } 17 | 18 | -------------------------------------------------------------------------------- /terraform/templates/ecs/cb_app.json.tpl: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "cb-app", 4 | "image": "${app_image}", 5 | "cpu": ${fargate_cpu}, 6 | "memory": ${fargate_memory}, 7 | "networkMode": "awsvpc", 8 | "logConfiguration": { 9 | "logDriver": "awslogs", 10 | "options": { 11 | "awslogs-group": "/ecs/cb-app", 12 | "awslogs-region": "${aws_region}", 13 | "awslogs-stream-prefix": "ecs" 14 | } 15 | }, 16 | "portMappings": [ 17 | { 18 | "containerPort": ${app_port}, 19 | "hostPort": ${app_port} 20 | } 21 | ] 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /terraform/roles.tf: -------------------------------------------------------------------------------- 1 | # ECS task execution role data 2 | data "aws_iam_policy_document" "ecs_task_execution_role" { 3 | version = "2012-10-17" 4 | statement { 5 | sid = "" 6 | effect = "Allow" 7 | actions = ["sts:AssumeRole"] 8 | 9 | principals { 10 | type = "Service" 11 | identifiers = ["ecs-tasks.amazonaws.com"] 12 | } 13 | } 14 | } 15 | 16 | # ECS task execution role 17 | resource "aws_iam_role" "ecs_task_execution_role" { 18 | name = var.ecs_task_execution_role_name 19 | assume_role_policy = data.aws_iam_policy_document.ecs_task_execution_role.json 20 | } 21 | 22 | # ECS task execution role policy attachment 23 | resource "aws_iam_role_policy_attachment" "ecs_task_execution_role" { 24 | role = aws_iam_role.ecs_task_execution_role.name 25 | policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" 26 | } -------------------------------------------------------------------------------- /terraform/alb.tf: -------------------------------------------------------------------------------- 1 | # alb.tf 2 | 3 | resource "aws_alb" "main" { 4 | name = "cb-load-balancer" 5 | subnets = aws_subnet.public.*.id 6 | security_groups = [aws_security_group.lb.id] 7 | } 8 | 9 | resource "aws_alb_target_group" "app" { 10 | name = "cb-target-group" 11 | port = 80 12 | protocol = "HTTP" 13 | vpc_id = aws_vpc.main.id 14 | target_type = "ip" 15 | 16 | health_check { 17 | healthy_threshold = "3" 18 | interval = "30" 19 | protocol = "HTTP" 20 | matcher = "200" 21 | timeout = "3" 22 | path = var.health_check_path 23 | unhealthy_threshold = "2" 24 | } 25 | } 26 | 27 | # Redirect all traffic from the ALB to the target group 28 | resource "aws_alb_listener" "front_end" { 29 | load_balancer_arn = aws_alb.main.id 30 | port = var.app_port 31 | protocol = "HTTP" 32 | 33 | default_action { 34 | target_group_arn = aws_alb_target_group.app.id 35 | type = "forward" 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Bradford Lamson-Scribner 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 | -------------------------------------------------------------------------------- /terraform/security.tf: -------------------------------------------------------------------------------- 1 | # security.tf 2 | 3 | # ALB Security Group: Edit to restrict access to the application 4 | resource "aws_security_group" "lb" { 5 | name = "cb-load-balancer-security-group" 6 | description = "controls access to the ALB" 7 | vpc_id = aws_vpc.main.id 8 | 9 | ingress { 10 | protocol = "tcp" 11 | from_port = var.app_port 12 | to_port = var.app_port 13 | cidr_blocks = ["0.0.0.0/0"] 14 | } 15 | 16 | egress { 17 | protocol = "-1" 18 | from_port = 0 19 | to_port = 0 20 | cidr_blocks = ["0.0.0.0/0"] 21 | } 22 | } 23 | 24 | # Traffic to the ECS cluster should only come from the ALB 25 | resource "aws_security_group" "ecs_tasks" { 26 | name = "cb-ecs-tasks-security-group" 27 | description = "allow inbound access from the ALB only" 28 | vpc_id = aws_vpc.main.id 29 | 30 | ingress { 31 | protocol = "tcp" 32 | from_port = var.app_port 33 | to_port = var.app_port 34 | security_groups = [aws_security_group.lb.id] 35 | } 36 | 37 | egress { 38 | protocol = "-1" 39 | from_port = 0 40 | to_port = 0 41 | cidr_blocks = ["0.0.0.0/0"] 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /terraform/variables.tf: -------------------------------------------------------------------------------- 1 | # variables.tf 2 | 3 | variable "aws_region" { 4 | description = "The AWS region things are created in" 5 | default = "us-west-2" 6 | } 7 | 8 | variable "ecs_task_execution_role_name" { 9 | description = "ECS task execution role name" 10 | default = "myEcsTaskExecutionRole" 11 | } 12 | 13 | variable "az_count" { 14 | description = "Number of AZs to cover in a given region" 15 | default = "2" 16 | } 17 | 18 | variable "app_image" { 19 | description = "Docker image to run in the ECS cluster" 20 | default = "bradfordhamilton/crystal_blockchain:latest" 21 | } 22 | 23 | variable "app_port" { 24 | description = "Port exposed by the docker image to redirect traffic to" 25 | default = 3000 26 | } 27 | 28 | variable "app_count" { 29 | description = "Number of docker containers to run" 30 | default = 3 31 | } 32 | 33 | variable "health_check_path" { 34 | default = "/" 35 | } 36 | 37 | variable "fargate_cpu" { 38 | description = "Fargate instance CPU units to provision (1 vCPU = 1024 CPU units)" 39 | default = "1024" 40 | } 41 | 42 | variable "fargate_memory" { 43 | description = "Fargate instance memory to provision (in MiB)" 44 | default = "2048" 45 | } 46 | 47 | -------------------------------------------------------------------------------- /terraform/ecs.tf: -------------------------------------------------------------------------------- 1 | # ecs.tf 2 | 3 | resource "aws_ecs_cluster" "main" { 4 | name = "cb-cluster" 5 | } 6 | 7 | data "template_file" "cb_app" { 8 | template = file("./templates/ecs/cb_app.json.tpl") 9 | 10 | vars = { 11 | app_image = var.app_image 12 | app_port = var.app_port 13 | fargate_cpu = var.fargate_cpu 14 | fargate_memory = var.fargate_memory 15 | aws_region = var.aws_region 16 | } 17 | } 18 | 19 | resource "aws_ecs_task_definition" "app" { 20 | family = "cb-app-task" 21 | execution_role_arn = aws_iam_role.ecs_task_execution_role.arn 22 | network_mode = "awsvpc" 23 | requires_compatibilities = ["FARGATE"] 24 | cpu = var.fargate_cpu 25 | memory = var.fargate_memory 26 | container_definitions = data.template_file.cb_app.rendered 27 | } 28 | 29 | resource "aws_ecs_service" "main" { 30 | name = "cb-service" 31 | cluster = aws_ecs_cluster.main.id 32 | task_definition = aws_ecs_task_definition.app.arn 33 | desired_count = var.app_count 34 | launch_type = "FARGATE" 35 | 36 | network_configuration { 37 | security_groups = [aws_security_group.ecs_tasks.id] 38 | subnets = aws_subnet.private.*.id 39 | assign_public_ip = true 40 | } 41 | 42 | load_balancer { 43 | target_group_arn = aws_alb_target_group.app.id 44 | container_name = "cb-app" 45 | container_port = var.app_port 46 | } 47 | 48 | depends_on = [aws_alb_listener.front_end, aws_iam_role_policy_attachment.ecs_task_execution_role] 49 | } 50 | 51 | -------------------------------------------------------------------------------- /terraform/network.tf: -------------------------------------------------------------------------------- 1 | # network.tf 2 | 3 | # Fetch AZs in the current region 4 | data "aws_availability_zones" "available" { 5 | } 6 | 7 | resource "aws_vpc" "main" { 8 | cidr_block = "172.17.0.0/16" 9 | } 10 | 11 | # Create var.az_count private subnets, each in a different AZ 12 | resource "aws_subnet" "private" { 13 | count = var.az_count 14 | cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index) 15 | availability_zone = data.aws_availability_zones.available.names[count.index] 16 | vpc_id = aws_vpc.main.id 17 | } 18 | 19 | # Create var.az_count public subnets, each in a different AZ 20 | resource "aws_subnet" "public" { 21 | count = var.az_count 22 | cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, var.az_count + count.index) 23 | availability_zone = data.aws_availability_zones.available.names[count.index] 24 | vpc_id = aws_vpc.main.id 25 | map_public_ip_on_launch = true 26 | } 27 | 28 | # Internet Gateway for the public subnet 29 | resource "aws_internet_gateway" "gw" { 30 | vpc_id = aws_vpc.main.id 31 | } 32 | 33 | # Route the public subnet traffic through the IGW 34 | resource "aws_route" "internet_access" { 35 | route_table_id = aws_vpc.main.main_route_table_id 36 | destination_cidr_block = "0.0.0.0/0" 37 | gateway_id = aws_internet_gateway.gw.id 38 | } 39 | 40 | # Create a NAT gateway with an Elastic IP for each private subnet to get internet connectivity 41 | resource "aws_eip" "gw" { 42 | count = var.az_count 43 | vpc = true 44 | depends_on = [aws_internet_gateway.gw] 45 | } 46 | 47 | resource "aws_nat_gateway" "gw" { 48 | count = var.az_count 49 | subnet_id = element(aws_subnet.public.*.id, count.index) 50 | allocation_id = element(aws_eip.gw.*.id, count.index) 51 | } 52 | 53 | # Create a new route table for the private subnets, make it route non-local traffic through the NAT gateway to the internet 54 | resource "aws_route_table" "private" { 55 | count = var.az_count 56 | vpc_id = aws_vpc.main.id 57 | 58 | route { 59 | cidr_block = "0.0.0.0/0" 60 | nat_gateway_id = element(aws_nat_gateway.gw.*.id, count.index) 61 | } 62 | } 63 | 64 | # Explicitly associate the newly created route tables to the private subnets (so they don't default to the main route table) 65 | resource "aws_route_table_association" "private" { 66 | count = var.az_count 67 | subnet_id = element(aws_subnet.private.*.id, count.index) 68 | route_table_id = element(aws_route_table.private.*.id, count.index) 69 | } 70 | 71 | -------------------------------------------------------------------------------- /terraform/auto_scaling.tf: -------------------------------------------------------------------------------- 1 | # auto_scaling.tf 2 | 3 | resource "aws_appautoscaling_target" "target" { 4 | service_namespace = "ecs" 5 | resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.main.name}" 6 | scalable_dimension = "ecs:service:DesiredCount" 7 | min_capacity = 3 8 | max_capacity = 6 9 | } 10 | 11 | # Automatically scale capacity up by one 12 | resource "aws_appautoscaling_policy" "up" { 13 | name = "cb_scale_up" 14 | service_namespace = "ecs" 15 | resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.main.name}" 16 | scalable_dimension = "ecs:service:DesiredCount" 17 | 18 | step_scaling_policy_configuration { 19 | adjustment_type = "ChangeInCapacity" 20 | cooldown = 60 21 | metric_aggregation_type = "Maximum" 22 | 23 | step_adjustment { 24 | metric_interval_lower_bound = 0 25 | scaling_adjustment = 1 26 | } 27 | } 28 | 29 | depends_on = [aws_appautoscaling_target.target] 30 | } 31 | 32 | # Automatically scale capacity down by one 33 | resource "aws_appautoscaling_policy" "down" { 34 | name = "cb_scale_down" 35 | service_namespace = "ecs" 36 | resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.main.name}" 37 | scalable_dimension = "ecs:service:DesiredCount" 38 | 39 | step_scaling_policy_configuration { 40 | adjustment_type = "ChangeInCapacity" 41 | cooldown = 60 42 | metric_aggregation_type = "Maximum" 43 | 44 | step_adjustment { 45 | metric_interval_upper_bound = 0 46 | scaling_adjustment = -1 47 | } 48 | } 49 | 50 | depends_on = [aws_appautoscaling_target.target] 51 | } 52 | 53 | # CloudWatch alarm that triggers the autoscaling up policy 54 | resource "aws_cloudwatch_metric_alarm" "service_cpu_high" { 55 | alarm_name = "cb_cpu_utilization_high" 56 | comparison_operator = "GreaterThanOrEqualToThreshold" 57 | evaluation_periods = "2" 58 | metric_name = "CPUUtilization" 59 | namespace = "AWS/ECS" 60 | period = "60" 61 | statistic = "Average" 62 | threshold = "85" 63 | 64 | dimensions = { 65 | ClusterName = aws_ecs_cluster.main.name 66 | ServiceName = aws_ecs_service.main.name 67 | } 68 | 69 | alarm_actions = [aws_appautoscaling_policy.up.arn] 70 | } 71 | 72 | # CloudWatch alarm that triggers the autoscaling down policy 73 | resource "aws_cloudwatch_metric_alarm" "service_cpu_low" { 74 | alarm_name = "cb_cpu_utilization_low" 75 | comparison_operator = "LessThanOrEqualToThreshold" 76 | evaluation_periods = "2" 77 | metric_name = "CPUUtilization" 78 | namespace = "AWS/ECS" 79 | period = "60" 80 | statistic = "Average" 81 | threshold = "10" 82 | 83 | dimensions = { 84 | ClusterName = aws_ecs_cluster.main.name 85 | ServiceName = aws_ecs_service.main.name 86 | } 87 | 88 | alarm_actions = [aws_appautoscaling_policy.down.arn] 89 | } 90 | 91 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at cipherbinservice@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | --------------------------------------------------------------------------------