Shared Resources for Your Terraformed Docker Environment on AWS

Written by: Phillip Shipley

In Part 1 of this series, we laid the groundwork for Terraforming infrastructure on Amazon. Today we’ll define our actual infrastructure consisting of networks, servers, etc. Have fun!

Shared Resources Declaration

Since our service depends on underlying resources like the VPC and EC2 instances, we’ll create the shared environment first. Make sure you’re in the my-terraform-environment/shared/ folder before proceeding.

We’ll start with core ECS-related resources. We need to look up the most recent ECS optimized AMI, create the ECS cluster, and create the necessary IAM Roles for services and instances to use to support ECS.

// shared/main.tf
/*
 * Determine most recent ECS optimized AMI
 */
data "aws_ami" "ecs_ami" {
 most_recent = true
 owners      = ["amazon"]
 filter {
   name   = "name"
   values = ["amzn-ami-*-amazon-ecs-optimized"]
 }
}
/*
 * Create ECS cluster
 */
resource "aws_ecs_cluster" "ecs_cluster" {
 name = "ecs-cluster"
}
/*
 * Create ECS IAM Instance Role and Policy
 * Use random id in naming of roles to prevent collisions
 * should other ECS clusters be created in same AWS account
 * using this same code.
 */
resource "random_id" "code" {
 byte_length = 4
}
resource "aws_iam_role" "ecsInstanceRole" {
 name               = "ecsInstanceRole-${random_id.code.hex}"
 assume_role_policy = <<EOF
{
 "Version": "2008-10-17",
 "Statement": [
   {
     "Sid": "",
     "Effect": "Allow",
     "Principal": {
       "Service": "ec2.amazonaws.com"
     },
     "Action": "sts:AssumeRole"
   }
 ]
}
EOF
}
resource "aws_iam_role_policy" "ecsInstanceRolePolicy" {
 name   = "ecsInstanceRolePolicy-${random_id.code.hex}"
 role   = "${aws_iam_role.ecsInstanceRole.id}"
 policy = <<EOF
{
 "Version": "2012-10-17",
 "Statement": [
   {
     "Effect": "Allow",
     "Action": [
       "ecs:CreateCluster",
       "ecs:DeregisterContainerInstance",
       "ecs:DiscoverPollEndpoint",
       "ecs:Poll",
       "ecs:RegisterContainerInstance",
       "ecs:StartTelemetrySession",
       "ecs:Submit*",
       "ecr:GetAuthorizationToken",
       "ecr:BatchCheckLayerAvailability",
       "ecr:GetDownloadUrlForLayer",
       "ecr:BatchGetImage",
       "logs:CreateLogStream",
       "logs:PutLogEvents"
     ],
     "Resource": "*"
   }
 ]
}
EOF
}
/*
 * Create ECS IAM Service Role and Policy
 */
resource "aws_iam_role" "ecsServiceRole" {
 name               = "ecsServiceRole-${random_id.code.hex}"
 assume_role_policy = <<EOF
{
 "Version": "2008-10-17",
 "Statement": [
   {
     "Sid": "",
     "Effect": "Allow",
     "Principal": {
       "Service": "ecs.amazonaws.com"
     },
     "Action": "sts:AssumeRole"
   }
 ]
}
EOF
}
resource "aws_iam_role_policy" "ecsServiceRolePolicy" {
 name   = "ecsServiceRolePolicy-${random_id.code.hex}"
 role   = "${aws_iam_role.ecsServiceRole.id}"
 policy = <<EOF
{
 "Version": "2012-10-17",
 "Statement": [
   {
     "Effect": "Allow",
     "Action": [
       "ec2:AuthorizeSecurityGroupIngress",
       "ec2:Describe*",
       "elasticloadbalancing:DeregisterInstancesFromLoadBalancer",
       "elasticloadbalancing:DeregisterTargets",
       "elasticloadbalancing:Describe*",
       "elasticloadbalancing:RegisterInstancesWithLoadBalancer",
       "elasticloadbalancing:RegisterTargets"
     ],
     "Resource": "*"
   }
 ]
}
EOF
}
resource "aws_iam_instance_profile" "ecsInstanceProfile" {
 name = "ecsInstanceProfile-${random_id.code.hex}"
 role = "${aws_iam_role.ecsInstanceRole.name}"
}

Dynamically looking up the current ECS-optimized AMI like this is a goldmine in itself. Before Terraform, I had to look this up on a webpage on a regular basis to keep my cluster instances current and it was a pain. With a dynamic lookup like this, running terraform plan every so often will identify if a new AMI is available and if any resources depending on it will need to be updated and/or replaced.

It is a good idea to run a terraform plan periodically to ensure there are no errors in your code. At this point, running terraform plan should have output similar to:

$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
data.aws_ami.ecs_ami: Refreshing state...
------------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create
Terraform will perform the following actions:
  + aws_ecs_cluster.ecs_cluster
      id:                    <computed>
      name:                  "ecs-cluster"
  + aws_iam_instance_profile.ecsInstanceProfile
      id:                    <computed>
      arn:                   <computed>
      create_date:           <computed>
      name:                  "ecsInstanceProfile-${random_id.code.hex}"
      path:                  "/"
      role:                  "${aws_iam_role.ecsInstanceRole.name}"
      roles.#:               <computed>
      unique_id:             <computed>
  + aws_iam_role.ecsInstanceRole
      id:                    <computed>
      arn:                   <computed>
      assume_role_policy:    "{\n \"Version\": \"2008-10-17\",\n \"Statement\": [\n   {\n     \"Sid\": \"\",\n     \"Effect\": \"Allow\",\n     \"Principal\": {\n       \"Service\": \"ec2.amazonaws.com\"\n     },\n     \"Action\": \"sts:AssumeRole\"\n   }\n ]\n}\n"
      create_date:           <computed>
      force_detach_policies: "false"
      name:                  "ecsInstanceRole-${random_id.code.hex}"
      path:                  "/"
      unique_id:             <computed>
  + aws_iam_role.ecsServiceRole
      id:                    <computed>
      arn:                   <computed>
      assume_role_policy:    "{\n \"Version\": \"2008-10-17\",\n \"Statement\": [\n   {\n     \"Sid\": \"\",\n     \"Effect\": \"Allow\",\n     \"Principal\": {\n       \"Service\": \"ecs.amazonaws.com\"\n     },\n     \"Action\": \"sts:AssumeRole\"\n   }\n ]\n}\n"
      create_date:           <computed>
      force_detach_policies: "false"
      name:                  "ecsServiceRole-${random_id.code.hex}"
      path:                  "/"
      unique_id:             <computed>
  + aws_iam_role_policy.ecsInstanceRolePolicy
      id:                    <computed>
      name:                  "ecsInstanceRolePolicy-${random_id.code.hex}"
      policy:                "{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n   {\n     \"Effect\": \"Allow\",\n     \"Action\": [\n       \"ecs:CreateCluster\",\n       \"ecs:DeregisterContainerInstance\",\n       \"ecs:DiscoverPollEndpoint\",\n       \"ecs:Poll\",\n       \"ecs:RegisterContainerInstance\",\n       \"ecs:StartTelemetrySession\",\n       \"ecs:Submit*\",\n       \"ecr:GetAuthorizationToken\",\n       \"ecr:BatchCheckLayerAvailability\",\n       \"ecr:GetDownloadUrlForLayer\",\n       \"ecr:BatchGetImage\",\n       \"logs:CreateLogStream\",\n       \"logs:PutLogEvents\"\n     ],\n     \"Resource\": \"*\"\n   }\n ]\n}\n"
      role:                  "${aws_iam_role.ecsInstanceRole.id}"
  + aws_iam_role_policy.ecsServiceRolePolicy
      id:                    <computed>
      name:                  "ecsServiceRolePolicy-${random_id.code.hex}"
      policy:                "{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n   {\n     \"Effect\": \"Allow\",\n     \"Action\": [\n       \"ec2:AuthorizeSecurityGroupIngress\",\n       \"ec2:Describe*\",\n       \"elasticloadbalancing:DeregisterInstancesFromLoadBalancer\",\n       \"elasticloadbalancing:DeregisterTargets\",\n       \"elasticloadbalancing:Describe*\",\n       \"elasticloadbalancing:RegisterInstancesWithLoadBalancer\",\n       \"elasticloadbalancing:RegisterTargets\"\n     ],\n     \"Resource\": \"*\"\n   }\n ]\n}\n"
      role:                  "${aws_iam_role.ecsServiceRole.id}"
  + random_id.code
      id:                    <computed>
      b64:                   <computed>
      b64_std:               <computed>
      b64_url:               <computed>
      byte_length:           "4"
      dec:                   <computed>
      hex:                   <computed>
Plan: 7 to add, 0 to change, 0 to destroy.
------------------------------------------------------------------------
Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.
Releasing state lock. This may take a few moments...

At this time, the plan is to create seven new resources. The output from Terraform is color coded to help recognize what will be created, changed, destroyed, or read. Green for created (or destroyed and created if needed), yellow if it will be changed in place, red if it will be deleted, and cyan for data resources that will be read.

Next up, we’ll create the VPC and look up the default security group that is created automatically by AWS so that we can assign other resources to it for intra-VPC communications.

// shared/main.tf
/*
 * Create VPC
 */
resource "aws_vpc" "vpc" {
 cidr_block = "10.0.0.0/16"
 tags = {
   Name = "vpc-terraform"
 }
}
/*
 * Get default security group for reference later
 */
data "aws_security_group" "vpc_default_sg" {
 name   = "default"
 vpc_id = "${aws_vpc.vpc.id}"
}

Now that we have a VPC, we need to set up public and private subnets in multiple availability zones. Terraform has an ability to loop over lists and create multiple instances of a resource. For most resources, you can set a count attribute to specify how many of that instance should be created.

If a single instance is normally referenced with the naming format of resource_type.resource_name.attribute, for references that are created with count > 1 you must also specify the index of the instance you want to reference, such as resource_type.resource_name.index.attribute (for example, aws_subnet.private_subnet.0.id).

When we create our subnets below, we’ll do a little Terraform-fu to generate subnets for as many availability zones as are specified in the var var.aws_zones.

// shared/main.tf
/*
 * Create public and private subnets for each availability zone
 */
resource "aws_subnet" "public_subnet" {
 count             = "${length(var.aws_zones)}"
 vpc_id            = "${aws_vpc.vpc.id}"
 availability_zone = "${element(var.aws_zones, count.index)}"
 cidr_block        = "10.0.${(count.index + 1) * 10}.0/24"
 tags {
   Name = "public-${element(var.aws_zones, count.index)}"
 }
}
resource "aws_subnet" "private_subnet" {
 count             = "${length(var.aws_zones)}"
 vpc_id            = "${aws_vpc.vpc.id}"
 availability_zone = "${element(var.aws_zones, count.index)}"
 cidr_block        = "10.0.${(count.index + 1) * 11}.0/24"
 tags {
   Name = "private-${element(var.aws_zones, count.index)}"
 }
}

Given var.aws_zones = [“us-east-1c”, “us-east-1d”, “us-east-1e”], this will create six subnets: three public and three private.

!Sign up for a free Codeship Account

A VPC requires an Internet Gateway for the public subnets to be able to route out to the internet and a NAT gateway for private subnets to be able to do the same. We need to create these resources and set up routing tables for each of the six subnets to properly use them.

We’ll also allocate an Elastic IP (static IP address) to assign to our NAT instance so that our private subnet traffic “comes” from the same IP address. While we’re at it, we’ll designate that any of our private subnets can be used for hosting RDS instances by creating a DB subnet group.

// shared/main.tf
/*
 * Create internet gateway for VPC
 */
resource "aws_internet_gateway" "internet_gateway" {
 vpc_id = "${aws_vpc.vpc.id}"
}
/*
 * Create NAT gateway and allocate Elastic IP for it
 */
resource "aws_eip" "gateway_eip" {}
resource "aws_nat_gateway" "nat_gateway" {
 allocation_id = "${aws_eip.gateway_eip.id}"
 subnet_id     = "${aws_subnet.public_subnet.0.id}"
 depends_on    = ["aws_internet_gateway.internet_gateway"]
}
/*
 * Routes for private subnets to use NAT gateway
 */
resource "aws_route_table" "nat_route_table" {
 vpc_id = "${aws_vpc.vpc.id}"
}
resource "aws_route" "nat_route" {
 route_table_id         = "${aws_route_table.nat_route_table.id}"
 destination_cidr_block = "0.0.0.0/0"
 nat_gateway_id         = "${aws_nat_gateway.nat_gateway.id}"
}
resource "aws_route_table_association" "private_route" {
 count          = "${length(var.aws_zones)}"
 subnet_id      = "${element(aws_subnet.private_subnet.*.id, count.index)}"
 route_table_id = "${aws_route_table.nat_route_table.id}"
}
/*
 * Routes for public subnets to use internet gateway
 */
resource "aws_route_table" "igw_route_table" {
 vpc_id = "${aws_vpc.vpc.id}"
}
resource "aws_route" "igw_route" {
 route_table_id         = "${aws_route_table.igw_route_table.id}"
 destination_cidr_block = "0.0.0.0/0"
 gateway_id             = "${aws_internet_gateway.internet_gateway.id}"
}
resource "aws_route_table_association" "public_route" {
 count          = "${length(var.aws_zones)}"
 subnet_id      = "${element(aws_subnet.public_subnet.*.id, count.index)}"
 route_table_id = "${aws_route_table.igw_route_table.id}"
}
/*
 * Create DB Subnet Group for private subnets
 */
resource "aws_db_subnet_group" "db_subnet_group" {
 name       = "db-subnet"
 subnet_ids = ["${aws_subnet.private_subnet.*.id}"]
}

With our ECS cluster and network in place, we can create an auto-scaling group to launch EC2 instances into the ECS cluster. The ECS-optimized AMI looks for the ECS cluster name in the file /etc/ecs/ecs.config, so we need to create our instances with a simple shell script in the User Data field to put the ECS cluster name in place on startup. For this, we’ll use a template file and a data resource to render it for use. Create a file named shared/user-data.sh with the following contents:

#!/bin/bash
echo ECS_CLUSTER=${ecs_cluster_name} >> /etc/ecs/ecs.config

Now add the following to the shared/main.tf file:

// shared/main.tf
/*
 * Generate user_data from template file
 */
data "template_file" "user_data" {
 template = "${file("${path.module}/user-data.sh")}"
 vars {
   ecs_cluster_name = "${aws_ecs_cluster.ecs_cluster.name}"
 }
}
/*
 * Create Launch Configuration
 */
resource "aws_launch_configuration" "as_conf" {
 image_id             = "${data.aws_ami.ecs_ami.id}"
 instance_type        = "t2.micro"
 security_groups      = ["${data.aws_security_group.vpc_default_sg.id}"]
 iam_instance_profile = "${aws_iam_instance_profile.ecsInstanceProfile.id}"
 root_block_device {
   volume_size = "8"
 }
 user_data = "${data.template_file.user_data.rendered}"
 lifecycle {
   create_before_destroy = true
 }
}
/*
 * Create Auto Scaling Group
 */
resource "aws_autoscaling_group" "asg" {
 name                      = "asg-ecs-enviroment"
 availability_zones        = "${var.aws_zones}"
 vpc_zone_identifier       = ["${aws_subnet.private_subnet.*.id}"]
 min_size                  = "3"
 max_size                  = "3"
 desired_capacity          = "3"
 launch_configuration      = "${aws_launch_configuration.as_conf.id}"
 health_check_type         = "EC2"
 health_check_grace_period = "120"
 default_cooldown          = "30"
 lifecycle {
   create_before_destroy = true
 }
}

The services Terraform environment will need several values from the shared environment so we need to define them as outputs. Edit the outputs.tf file and add the following:

// shared/outputs.tf
output "aws_zones" {
 value = ["${var.aws_zones}"]
}
output "db_subnet_group_name" {
 value = "${aws_db_subnet_group.db_subnet_group.name}"
}
output "ecs_cluster_name" {
 value = "${aws_ecs_cluster.ecs_cluster.name}"
}
output "ecsServiceRole_arn" {
 value = "${aws_iam_role.ecsServiceRole.arn}"
}
output "private_subnet_ids" {
 value = ["${aws_subnet.private_subnet.*.id}"]
}
output "public_subnet_ids" {
 value = ["${aws_subnet.public_subnet.*.id}"]
}
output "vpc_default_sg_id" {
 value = "${data.aws_security_group.vpc_default_sg.id}"
}
output "vpc_id" {
 value = "${aws_vpc.vpc.id}"
}

That concludes the shared resources definition, so let’s go ahead and run another terraform plan to check that everything looks okay, and then run terraform apply.

$ terraform plan
Refreshing Terraform state in-memory prior to plan...
……
Plan: 30 to add, 0 to change, 0 to destroy.
……
Releasing state lock. This may take a few moments…
$ terraform apply
data.aws_ami.ecs_ami: Refreshing state...
random_id.code: Creating...
  b64:         "" => "<computed>"
  b64_std:     "" => "<computed>"
  b64_url:     "" => "<computed>"
  byte_length: "" => "4"
  dec:         "" => "<computed>"
  hex:         "" => "<computed>"
random_id.code: Creation complete after 0s (ID: lbOvJw)
aws_ecs_cluster.ecs_cluster: Creating...
  name: "" => "ecs-cluster"
aws_iam_role.ecsServiceRole: Creating...
  arn:                   "" => "<computed>"
  assume_role_policy:    "" => "{\n \"Version\": \"2008-10-17\",\n \"Statement\": [\n   {\n     \"Sid\": \"\",\n     \"Effect\": \"Allow\",\n     \"Principal\": {\n       \"Service\": \"ecs.amazonaws.com\"\n     },\n     \"Action\": \"sts:AssumeRole\"\n   }\n ]\n}\n"
  create_date:           "" => "<computed>"
  force_detach_policies: "" => "false"
  name:                  "" => "ecsServiceRole-95b3af27"
  path:                  "" => "/"
  unique_id:             "" => "<computed>"
aws_eip.gateway_eip: Creating...
  allocation_id:     "" => "<computed>"
  association_id:    "" => "<computed>"
  domain:            "" => "<computed>"
  instance:          "" => "<computed>"
  network_interface: "" => "<computed>"
  private_ip:        "" => "<computed>"
  public_ip:         "" => "<computed>"
  vpc:               "" => "<computed>"
aws_iam_role.ecsInstanceRole: Creating...
  arn:                   "" => "<computed>"
  assume_role_policy:    "" => "{\n \"Version\": \"2008-10-17\",\n \"Statement\": [\n   {\n     \"Sid\": \"\",\n     \"Effect\": \"Allow\",\n     \"Principal\": {\n       \"Service\": \"ec2.amazonaws.com\"\n     },\n     \"Action\": \"sts:AssumeRole\"\n   }\n ]\n}\n"
  create_date:           "" => "<computed>"
  force_detach_policies: "" => "false"
  name:                  "" => "ecsInstanceRole-95b3af27"
  path:                  "" => "/"
  unique_id:             "" => "<computed>"
aws_vpc.vpc: Creating...
  assign_generated_ipv6_cidr_block: "" => "false"
  cidr_block:                       "" => "10.0.0.0/16"
  default_network_acl_id:           "" => "<computed>"
  default_route_table_id:           "" => "<computed>"
  default_security_group_id:        "" => "<computed>"
  dhcp_options_id:                  "" => "<computed>"
  enable_classiclink:               "" => "<computed>"
  enable_classiclink_dns_support:   "" => "<computed>"
  enable_dns_hostnames:             "" => "<computed>"
  enable_dns_support:               "" => "true"
  instance_tenancy:                 "" => "<computed>"
  ipv6_association_id:              "" => "<computed>"
  ipv6_cidr_block:                  "" => "<computed>"
  main_route_table_id:              "" => "<computed>"
  tags.%:                           "" => "1"
  tags.Name:                        "" => "vpc-terraform"
aws_ecs_cluster.ecs_cluster: Creation complete after 0s (ID: arn:aws:ecs:us-east-1:462818068088:cluster/ecs-cluster)
data.template_file.user_data: Refreshing state...
aws_iam_role.ecsInstanceRole: Creation complete after 0s (ID: ecsInstanceRole-95b3af27)
aws_iam_role_policy.ecsInstanceRolePolicy: Creating...
  name:   "" => "ecsInstanceRolePolicy-95b3af27"
  policy: "" => "{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n   {\n     \"Effect\": \"Allow\",\n     \"Action\": [\n       \"ecs:CreateCluster\",\n       \"ecs:DeregisterContainerInstance\",\n       \"ecs:DiscoverPollEndpoint\",\n       \"ecs:Poll\",\n       \"ecs:RegisterContainerInstance\",\n       \"ecs:StartTelemetrySession\",\n       \"ecs:Submit*\",\n       \"ecr:GetAuthorizationToken\",\n       \"ecr:BatchCheckLayerAvailability\",\n       \"ecr:GetDownloadUrlForLayer\",\n       \"ecr:BatchGetImage\",\n       \"logs:CreateLogStream\",\n       \"logs:PutLogEvents\"\n     ],\n     \"Resource\": \"*\"\n   }\n ]\n}\n"
  role:   "" => "ecsInstanceRole-95b3af27"
aws_iam_instance_profile.ecsInstanceProfile: Creating...
  arn:         "" => "<computed>"
  create_date: "" => "<computed>"
  name:        "" => "ecsInstanceProfile-95b3af27"
  path:        "" => "/"
  role:        "" => "ecsInstanceRole-95b3af27"
  roles.#:     "" => "<computed>"
  unique_id:   "" => "<computed>"
aws_iam_role.ecsServiceRole: Creation complete after 0s (ID: ecsServiceRole-95b3af27)
aws_iam_role_policy.ecsServiceRolePolicy: Creating...
  name:   "" => "ecsServiceRolePolicy-95b3af27"
  policy: "" => "{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n   {\n     \"Effect\": \"Allow\",\n     \"Action\": [\n       \"ec2:AuthorizeSecurityGroupIngress\",\n       \"ec2:Describe*\",\n       \"elasticloadbalancing:DeregisterInstancesFromLoadBalancer\",\n       \"elasticloadbalancing:DeregisterTargets\",\n       \"elasticloadbalancing:Describe*\",\n       \"elasticloadbalancing:RegisterInstancesWithLoadBalancer\",\n       \"elasticloadbalancing:RegisterTargets\"\n     ],\n     \"Resource\": \"*\"\n   }\n ]\n}\n"
  role:   "" => "ecsServiceRole-95b3af27"
aws_iam_role_policy.ecsServiceRolePolicy: Creation complete after 1s (ID: ecsServiceRole-95b3af27:ecsServiceRolePolicy-95b3af27)
aws_iam_role_policy.ecsInstanceRolePolicy: Creation complete after 1s (ID: ecsInstanceRole-95b3af27:ecsInstanceRolePolicy-95b3af27)
aws_eip.gateway_eip: Creation complete after 1s (ID: eipalloc-25f39d17)
aws_iam_instance_profile.ecsInstanceProfile: Creation complete after 1s (ID: ecsInstanceProfile-95b3af27)
aws_vpc.vpc: Creation complete after 3s (ID: vpc-d82a38a1)
aws_internet_gateway.internet_gateway: Creating...
  vpc_id: "" => "vpc-d82a38a1"
data.aws_security_group.vpc_default_sg: Refreshing state...
aws_subnet.public_subnet[0]: Creating...
  assign_ipv6_address_on_creation: "" => "false"
  availability_zone:               "" => "us-east-1c"
  cidr_block:                      "" => "10.0.10.0/24"
  ipv6_cidr_block:                 "" => "<computed>"
  ipv6_cidr_block_association_id:  "" => "<computed>"
  map_public_ip_on_launch:         "" => "false"
  tags.%:                          "" => "1"
  tags.Name:                       "" => "public-us-east-1c"
  vpc_id:                          "" => "vpc-d82a38a1"
aws_subnet.private_subnet[0]: Creating...
  assign_ipv6_address_on_creation: "" => "false"
  availability_zone:               "" => "us-east-1c"
  cidr_block:                      "" => "10.0.11.0/24"
  ipv6_cidr_block:                 "" => "<computed>"
  ipv6_cidr_block_association_id:  "" => "<computed>"
  map_public_ip_on_launch:         "" => "false"
  tags.%:                          "" => "1"
  tags.Name:                       "" => "private-us-east-1c"
  vpc_id:                          "" => "vpc-d82a38a1"
aws_subnet.private_subnet[1]: Creating...
  assign_ipv6_address_on_creation: "" => "false"
  availability_zone:               "" => "us-east-1d"
  cidr_block:                      "" => "10.0.22.0/24"
  ipv6_cidr_block:                 "" => "<computed>"
  ipv6_cidr_block_association_id:  "" => "<computed>"
  map_public_ip_on_launch:         "" => "false"
  tags.%:                          "" => "1"
  tags.Name:                       "" => "private-us-east-1d"
  vpc_id:                          "" => "vpc-d82a38a1"
aws_route_table.nat_route_table: Creating...
  propagating_vgws.#: "" => "<computed>"
  route.#:            "" => "<computed>"
  vpc_id:             "" => "vpc-d82a38a1"
aws_subnet.private_subnet[2]: Creating...
  assign_ipv6_address_on_creation: "" => "false"
  availability_zone:               "" => "us-east-1e"
  cidr_block:                      "" => "10.0.33.0/24"
  ipv6_cidr_block:                 "" => "<computed>"
  ipv6_cidr_block_association_id:  "" => "<computed>"
  map_public_ip_on_launch:         "" => "false"
  tags.%:                          "" => "1"
  tags.Name:                       "" => "private-us-east-1e"
  vpc_id:                          "" => "vpc-d82a38a1"
aws_subnet.public_subnet[1]: Creating...
  assign_ipv6_address_on_creation: "" => "false"
  availability_zone:               "" => "us-east-1d"
  cidr_block:                      "" => "10.0.20.0/24"
  ipv6_cidr_block:                 "" => "<computed>"
  ipv6_cidr_block_association_id:  "" => "<computed>"
  map_public_ip_on_launch:         "" => "false"
  tags.%:                          "" => "1"
  tags.Name:                       "" => "public-us-east-1d"
  vpc_id:                          "" => "vpc-d82a38a1"
aws_subnet.public_subnet[2]: Creating...
  assign_ipv6_address_on_creation: "" => "false"
  availability_zone:               "" => "us-east-1e"
  cidr_block:                      "" => "10.0.30.0/24"
  ipv6_cidr_block:                 "" => "<computed>"
  ipv6_cidr_block_association_id:  "" => "<computed>"
  map_public_ip_on_launch:         "" => "false"
  tags.%:                          "" => "1"
  tags.Name:                       "" => "public-us-east-1e"
  vpc_id:                          "" => "vpc-d82a38a1"
aws_route_table.igw_route_table: Creating...
  propagating_vgws.#: "" => "<computed>"
  route.#:            "" => "<computed>"
  vpc_id:             "" => "vpc-d82a38a1"
aws_launch_configuration.as_conf: Creating...
  associate_public_ip_address:               "" => "false"
  ebs_block_device.#:                        "" => "<computed>"
  ebs_optimized:                             "" => "<computed>"
  enable_monitoring:                         "" => "true"
  iam_instance_profile:                      "" => "ecsInstanceProfile-95b3af27"
  image_id:                                  "" => "ami-9eb4b1e5"
  instance_type:                             "" => "t2.micro"
  key_name:                                  "" => "<computed>"
  name:                                      "" => "<computed>"
  root_block_device.#:                       "" => "1"
  root_block_device.0.delete_on_termination: "" => "true"
  root_block_device.0.iops:                  "" => "<computed>"
  root_block_device.0.volume_size:           "" => "8"
  root_block_device.0.volume_type:           "" => "<computed>"
  security_groups.#:                         "" => "1"
  security_groups.3222359879:                "" => "sg-22259851"
  user_data:                                 "" => "6a9e54d9d2d8048547951c51c5adf62d234bc1a3"
aws_route_table.nat_route_table: Creation complete after 1s (ID: rtb-7a613801)
aws_route_table.igw_route_table: Creation complete after 1s (ID: rtb-e66d349d)
aws_internet_gateway.internet_gateway: Creation complete after 2s (ID: igw-e9817490)
aws_subnet.public_subnet[1]: Creation complete after 2s (ID: subnet-05a7514e)
aws_route.igw_route: Creating...
  destination_cidr_block:     "" => "0.0.0.0/0"
  destination_prefix_list_id: "" => "<computed>"
  egress_only_gateway_id:     "" => "<computed>"
  gateway_id:                 "" => "igw-e9817490"
  instance_id:                "" => "<computed>"
  instance_owner_id:          "" => "<computed>"
  nat_gateway_id:             "" => "<computed>"
  network_interface_id:       "" => "<computed>"
  origin:                     "" => "<computed>"
  route_table_id:             "" => "rtb-e66d349d"
  state:                      "" => "<computed>"
aws_subnet.private_subnet[0]: Creation complete after 2s (ID: subnet-77cff95b)
aws_subnet.public_subnet[2]: Creation complete after 2s (ID: subnet-ebac68d4)
aws_subnet.private_subnet[2]: Creation complete after 2s (ID: subnet-51b6726e)
aws_subnet.public_subnet[0]: Creation complete after 2s (ID: subnet-e8cff9c4)
aws_route_table_association.public_route[1]: Creating...
  route_table_id: "" => "rtb-e66d349d"
  subnet_id:      "" => "subnet-05a7514e"
aws_nat_gateway.nat_gateway: Creating...
  allocation_id:        "" => "eipalloc-25f39d17"
  network_interface_id: "" => "<computed>"
  private_ip:           "" => "<computed>"
  public_ip:            "" => "<computed>"
  subnet_id:            "" => "subnet-e8cff9c4"
aws_route_table_association.public_route[0]: Creating...
  route_table_id: "" => "rtb-e66d349d"
  subnet_id:      "" => "subnet-e8cff9c4"
aws_route_table_association.public_route[2]: Creating...
  route_table_id: "" => "rtb-e66d349d"
  subnet_id:      "" => "subnet-ebac68d4"
aws_subnet.private_subnet[1]: Creation complete after 2s (ID: subnet-2da45266)
aws_route_table_association.private_route[2]: Creating...
  route_table_id: "" => "rtb-7a613801"
  subnet_id:      "" => "subnet-51b6726e"
aws_route_table_association.private_route[1]: Creating...
  route_table_id: "" => "rtb-7a613801"
  subnet_id:      "" => "subnet-2da45266"
aws_db_subnet_group.db_subnet_group: Creating...
  arn:                   "" => "<computed>"
  description:           "" => "Managed by Terraform"
  name:                  "" => "db-subnet"
  name_prefix:           "" => "<computed>"
  subnet_ids.#:          "" => "3"
  subnet_ids.3804843734: "" => "subnet-77cff95b"
  subnet_ids.4208861625: "" => "subnet-51b6726e"
  subnet_ids.633931707:  "" => "subnet-2da45266"
aws_route_table_association.private_route[0]: Creating...
  route_table_id: "" => "rtb-7a613801"
  subnet_id:      "" => "subnet-77cff95b"
aws_route_table_association.public_route[0]: Creation complete after 0s (ID: rtbassoc-dc8eaca6)
aws_route_table_association.public_route[1]: Creation complete after 0s (ID: rtbassoc-2396b459)
aws_route_table_association.public_route[2]: Creation complete after 0s (ID: rtbassoc-448eac3e)
aws_route_table_association.private_route[2]: Creation complete after 0s (ID: rtbassoc-e991b393)
aws_route_table_association.private_route[0]: Creation complete after 0s (ID: rtbassoc-0e92b074)
aws_route_table_association.private_route[1]: Creation complete after 0s (ID: rtbassoc-578cae2d)
aws_route.igw_route: Creation complete after 0s (ID: r-rtb-e66d349d1080289494)
aws_db_subnet_group.db_subnet_group: Creation complete after 1s (ID: db-subnet)
aws_launch_configuration.as_conf: Creation complete after 10s (ID: terraform-006cb0894f43b7f99761fc5636)
aws_autoscaling_group.asg: Creating...
  arn:                            "" => "<computed>"
  default_cooldown:               "" => "30"
  desired_capacity:               "" => "3"
  force_delete:                   "" => "false"
  health_check_grace_period:      "" => "120"
  health_check_type:              "" => "EC2"
  launch_configuration:           "" => "terraform-006cb0894f43b7f99761fc5636"
  load_balancers.#:               "" => "<computed>"
  max_size:                       "" => "3"
  metrics_granularity:            "" => "1Minute"
  min_size:                       "" => "3"
  name:                           "" => "asg-terraform-006cb0894f43b7f99761fc5636"
  protect_from_scale_in:          "" => "false"
  target_group_arns.#:            "" => "<computed>"
  vpc_zone_identifier.#:          "" => "3"
  vpc_zone_identifier.3804843734: "" => "subnet-77cff95b"
  vpc_zone_identifier.4208861625: "" => "subnet-51b6726e"
  vpc_zone_identifier.633931707:  "" => "subnet-2da45266"
  wait_for_capacity_timeout:      "" => "10m"
aws_nat_gateway.nat_gateway: Still creating... (10s elapsed)
aws_autoscaling_group.asg: Still creating... (10s elapsed)
aws_nat_gateway.nat_gateway: Still creating... (20s elapsed)
aws_autoscaling_group.asg: Still creating... (20s elapsed)
aws_nat_gateway.nat_gateway: Still creating... (30s elapsed)
aws_autoscaling_group.asg: Still creating... (30s elapsed)
aws_nat_gateway.nat_gateway: Still creating... (40s elapsed)
aws_autoscaling_group.asg: Still creating... (40s elapsed)
aws_nat_gateway.nat_gateway: Still creating... (50s elapsed)
aws_autoscaling_group.asg: Still creating... (50s elapsed)
aws_nat_gateway.nat_gateway: Still creating... (1m0s elapsed)
aws_autoscaling_group.asg: Still creating... (1m0s elapsed)
aws_nat_gateway.nat_gateway: Still creating... (1m10s elapsed)
aws_autoscaling_group.asg: Still creating... (1m10s elapsed)
aws_nat_gateway.nat_gateway: Still creating... (1m20s elapsed)
aws_autoscaling_group.asg: Creation complete after 1m19s (ID: asg-terraform-006cb0894f43b7f99761fc5636)
aws_nat_gateway.nat_gateway: Still creating... (1m30s elapsed)
aws_nat_gateway.nat_gateway: Creation complete after 1m38s (ID: nat-0fad1218a735c4ded)
aws_route.nat_route: Creating...
  destination_cidr_block:     "" => "0.0.0.0/0"
  destination_prefix_list_id: "" => "<computed>"
  egress_only_gateway_id:     "" => "<computed>"
  gateway_id:                 "" => "<computed>"
  instance_id:                "" => "<computed>"
  instance_owner_id:          "" => "<computed>"
  nat_gateway_id:             "" => "nat-0fad1218a735c4ded"
  network_interface_id:       "" => "<computed>"
  origin:                     "" => "<computed>"
  route_table_id:             "" => "rtb-7a613801"
  state:                      "" => "<computed>"
aws_route.nat_route: Creation complete after 0s (ID: r-rtb-7a6138011080289494)
Apply complete! Resources: 30 added, 0 changed, 0 destroyed.
Releasing state lock. This may take a few moments...
Outputs:
aws_zones = [
    us-east-1c,
    us-east-1d,
    us-east-1e
]
db_subnet_group_name = db-subnet
ecsServiceRole_arn = arn:aws:iam::462818068088:role/ecsServiceRole-95b3af27
ecs_cluster_name = ecs-cluster
private_subnet_ids = [
    subnet-77cff95b,
    subnet-2da45266,
    subnet-51b6726e
]
public_subnet_ids = [
    subnet-e8cff9c4,
    subnet-05a7514e,
    subnet-ebac68d4
]
vpc_default_sg_id = sg-22259851
vpc_id = vpc-d82a38a1

And just like that, the ECS cluster is ready for services to be deployed. Taking a look in AWS web console we can see there are three instances available and zero services running:

Just for fun and also to show another very cool feature of Terraform, let’s completely destroy everything we just created and recreate it.

$ terraform destroy
……
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  - destroy
Terraform will perform the following actions:
  - aws_autoscaling_group.asg
  - aws_db_subnet_group.db_subnet_group
  - aws_ecs_cluster.ecs_cluster
  - aws_eip.gateway_eip
  - aws_iam_instance_profile.ecsInstanceProfile
  - aws_iam_role.ecsInstanceRole
  - aws_iam_role.ecsServiceRole
  - aws_iam_role_policy.ecsInstanceRolePolicy
  - aws_iam_role_policy.ecsServiceRolePolicy
  - aws_internet_gateway.internet_gateway
  - aws_launch_configuration.as_conf
  - aws_nat_gateway.nat_gateway
  - aws_route.igw_route
  - aws_route.nat_route
  - aws_route_table.igw_route_table
  - aws_route_table.nat_route_table
  - aws_route_table_association.private_route[0]
  - aws_route_table_association.private_route[1]
  - aws_route_table_association.private_route[2]
  - aws_route_table_association.public_route[0]
  - aws_route_table_association.public_route[1]
  - aws_route_table_association.public_route[2]
  - aws_subnet.private_subnet[0]
  - aws_subnet.private_subnet[1]
  - aws_subnet.private_subnet[2]
  - aws_subnet.public_subnet[0]
  - aws_subnet.public_subnet[1]
  - aws_subnet.public_subnet[2]
  - aws_vpc.vpc
  - random_id.code
Plan: 0 to add, 0 to change, 30 to destroy.
Do you really want to destroy?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.
  Enter a value: yes
……
Destroy complete! Resources: 30 destroyed.
$ terraform apply
data.aws_ami.ecs_ami: Refreshing state...
random_id.code: Creating...
....
Apply complete! Resources: 30 added, 0 changed, 0 destroyed.
....

Beautiful. With just two commands, we can destroy everything and recreate it.

Summing It Up

Today we did a lot of work. Well, to be honest we did a lot of configuring and let the robots in our computers do the work. And that is exactly how I like it. To have done all of that work manually in the AWS console would have taken hours.

In the next post, we’ll complete this tutorial by defining the actual Docker service to build on this shared environment and have something to play with.

Stay up to date

We'll never share your email address and you can opt out at any time, we promise.