Using Terraform

Problem

If we use Terraform to build our infrastructure in AWS, we can use its outputs to populate the relevant portions of our deployfish.yml file.

Setup

We’re going to presume a more sophisticated setup with an ECS cluster, an ELB, a task role to allow the container to have rights to other AWS services, an S3 bucket, and and RDS database.We’ll also use the terraform state file that has been uploaded to S3.

Configuration

The Terraform Section

Here’s the configuration file with terraform:

terraform:
  statefile: 's3://hello-world-remotestate-file/hello-world-terraform-state'
  lookups:
    cluster_name: 'test-cluster-name'
    load_balancer_name: 'test-elb-id'
    task_role_arn: 'iam-role-hello-world-test-task'
    rds_address: 'test-rds-address'
    app_bucket: 's3-hello-world-test-bucket'

services:
  - name: hello-world-test
    cluster: ${terraform.cluster_name}
    count: 1
    load_balancer:
      service_role_arn: arn:aws:iam::111122223333:role/ecsServiceRole
      load_balancer_name: ${terraform.load_balancer_name}
      container_name: hello-world
      container_port: 80
    family: hello-world
    task_role_arn: ${terraform.task_role_arn}
    containers:
      - name: hello-world
        image: tutum/hello-world
        cpu: 128
        memory: 256
        ports:
          - "80"
        command: /usr/bin/supervisord
    config:
      - DB_NAME=hello_world
      - DB_USER=hello_world_u
      - DB_PASSWORD:secure=${env.DB_PASSWORD}
      - DB_HOST=${terraform.rds_address}
      - AWS_BUCKET=${terraform.app_bucket}

We first declare a terraform: section in the top-level of your deployfish.yml file. The values we define in that section are then available as a variable in services: section definitions, in the form ${terraform.variable_name}. In the above config, we’ve defined cluster to be ${terraform.cluster_name}. When we deploy, this will be automatically converted to:

cluster: test-cluster-name

Defining an Environment

We can take this a step further, though. Typically, we will use terraform to define all of the various environments, like test and prod. We can define the environment in our service definition with the environment parameter:

services:
  - name: hello-world-test
    cluster: ${terraform.cluster_name}
    environment: test
    count: 1
    ...

We can then use this environmant value in our terraform: section:

terraform:
  statefile: 's3://hello-world-remotestate-file/hello-world-terraform-state'
  lookups:
    cluster_name: '{environment}-cluster-name'
    load_balancer_name: '{environment}-elb-id'
    task_role_arn: 'iam-role-hello-world-{environment}-task'
    rds_address: '{environment}-rds-address'
    app_bucket: 's3-hello-world-{environment}-bucket'
...

Multiple Environments

This section can then be used for multiple service definitions under services: based on the different environments:

terraform:
  statefile: 's3://hello-world-remotestate-file/hello-world-terraform-state'
  lookups:
    cluster_name: '{environment}-cluster-name'
    load_balancer_name: '{environment}-elb-id'
    task_role_arn: 'iam-role-hello-world-{environment}-task'
    rds_address: '{environment}-rds-address'
    app_bucket: 's3-hello-world-{environment}-bucket'

services:
  - name: hello-world-test
    cluster: ${terraform.cluster_name}
    environment: test
    count: 1
    load_balancer:
      service_role_arn: arn:aws:iam::111122223333:role/ecsServiceRole
      load_balancer_name: ${terraform.load_balancer_name}
      container_name: hello-world
      container_port: 80
    family: hello-world
    task_role_arn: ${terraform.task_role_arn}
    containers:
      - name: hello-world
        image: tutum/hello-world
        cpu: 128
        memory: 256
        ports:
          - "80"
        command: /usr/bin/supervisord
    config:
      - DB_NAME=hello_world
      - DB_USER=hello_world_u
      - DB_PASSWORD:secure=${env.DB_PASSWORD}
      - DB_HOST=${terraform.rds_address}
      - AWS_BUCKET=${terraform.app_bucket}

  - name: hello-world-prod
    cluster: ${terraform.cluster_name}
    environment: prod
    count: 1
    load_balancer:
      service_role_arn: arn:aws:iam::111122223333:role/ecsServiceRole
      load_balancer_name: ${terraform.load_balancer_name}
      container_name: hello-world
      container_port: 80
    family: hello-world
    task_role_arn: ${terraform.task_role_arn}
    containers:
      - name: hello-world
        image: tutum/hello-world
        cpu: 256
        memory: 512
        ports:
          - "80"
        command: /usr/bin/supervisord
    config:
      - DB_NAME=hello_world
      - DB_USER=hello_world_u
      - DB_PASSWORD:secure=${env.DB_PASSWORD}
      - DB_HOST=${terraform.rds_address}
      - AWS_BUCKET=${terraform.app_bucket}

Here we defined both a test and prod environment. When we deploy test we will use one environment file to set the config parameters that contains the test values, and a prod environment file to define its values.

Another advantage of specifying an envieronment, is that you can use this environment in place of the service name when calling deploy.

Terraform List and Map Outputs

Terraform supports outputting lists and maps, and you can use lookups of list and map values in your service definitions:

terraform:
  statefile: 's3://hello-world-remotestate-file/hello-world-terraform-state'
  lookups:
    cluster_name: '{environment}-cluster-name'
    security_groups: 'service-security-groups'
    load_balancer: 'load-balancer-config'

services:
  - name: hello-world
    cluster: ${terraform.cluster_name}
    environment: prod
    count: 1
    load_balancer: ${terraform.load_balancer}
    vpc_configuration:
      security_groups: ${terraform.security_groups}

Deploy

To set the AWS Parameter Store values for test:

deploy --env_file=test.env config write test

Then for prod:

deploy --env_file=prod.env config write prod

The services are then created with:

deploy create test

and:

deploy create prod