How do I add environment variables and control the container CMD in ECS?

This message is extracted from a ticket originally emailed to support@gruntwork.io. Names and URLs have been removed where appropriate.

I’m using Gruntwork’s Reference Architecture and am adding a new app in infrastructure-live/dev/eu-west-1/dev/services/my-app. I saw in sample app (dev/eu-west-1/dev/services/sample-app-frontend) ECS task parameters like image,cpu,memory…

I need to add more environment variables and also to control the container CMD - where i have to put them?

This is a common question, so I’ll walk through here how to find this sort of answer yourself, and how to think about your Terraform modules in general.

If you look at the source parameter in your terraform.tfvars file, you’ll see that the module we use to deploy the sample apps is ecs-service-with-alb in the infrastructure-modules repo. This module contains the ECS container definition in a JSON file under infrastructure-modules/services/ecs-service-with-alb/container-definition/container-definition.json, which includes settings such as CPU, memory, environment variables, etc:

[
  {
    "name": "${container_name}",
    "image": "${image}:${version}",
    "cpu": ${cpu},
    "memory": ${memory},
    "essential": true,
    "portMappings": ${port_mappings},
    "environment": ${env_vars},
    "logConfiguration": {
      "logDriver": "syslog",
      "options": {
        "tag": "${container_name} ({{.ID}})"
      }
    }
  }
]

You could add command to this file or any other of the ECS task definition parameters:

[
  {
    "name": "${container_name}",
    "image": "${image}:${version}",
    "cpu": ${cpu},
    "memory": ${memory},
    "essential": true,
    "portMappings": ${port_mappings},
    "environment": ${env_vars},
    "logConfiguration": {
      "logDriver": "syslog",
      "options": {
        "tag": "${container_name} ({{.ID}})"
      }
    },
    "command": ${command}
  }
]

The interpolated values (${...}) in container-definition.json are dynamically filled in from the Terraform code in infrastructure-modules/services/ecs-service-with-alb/main.tf:

data "template_file" "ecs_task_container_definitions" {
  template = "${file("${path.module}/container-definition/container-definition.json")}"

  vars {
    container_name = "${var.service_name}"

    image         = "${var.image}"
    version       = "${var.version}"
    cpu           = "${var.cpu}"
    memory        = "${var.memory}"
    port_mappings = "[${join(",", data.template_file.port_mappings.*.rendered)}]"
    env_vars      = "[${join(",",  data.template_file.all_env_vars.*.rendered)}]"
  }
}

To fill in the command dynamically, you’d probably want to add an input variable called command:

variable "command" {
  description = "Custom command to run in the Docker container"
  type = "list"
}

And add it to the list of interpolated variables in ecs_task_container_definitions:

data "template_file" "ecs_task_container_definitions" {
  template = "${file("${path.module}/container-definition/container-definition.json")}"

  vars {
    container_name = "${var.service_name}"

    image         = "${var.image}"
    version       = "${var.version}"
    cpu           = "${var.cpu}"
    memory        = "${var.memory}"
    port_mappings = "[${join(",", data.template_file.port_mappings.*.rendered)}]"
    env_vars      = "[${join(",",  data.template_file.all_env_vars.*.rendered)}]"

    command = "${jsonencode(var.command)}"
  }
}

Now you can specify this command value in terraform.tfvars:

command = ["node", "/app/server.js", "--foo", "--bar"]

As for environment variables, you can see in the ecs_task_container_definitions that env_vars is filled in from data.template_file.all_env_vars, which in turn looks up values from module.all_env_vars:

module "all_env_vars" {
  source    = "git::git@github.com:gruntwork-io/package-terraform-utilities.git//modules/intermediate-variable?ref=v0.0.1"
  map_value = "${merge(module.default_env_vars.map_value, var.extra_env_vars)}"
}

This combines two maps: one with a bunch of default environment variables, and one from an input variable called extra_env_vars. You can set this input variable in your terraform.tfvars file to provide custom environment variables:

command = ["node", "/app/server.js", "--foo", "--bar"]

extra_env_vars = {
  foo = "bar"
}

If the command and extra_env_vars are different in each environment (dev, stage, prod), this approach should be all you need.

However, note that ecs-service-with-alb is a fairly “generic” module. If certain services need custom settings in all environments, instead of copying those settings into each terraform.tfvars file, a common pattern is to create wrappers for the ecs-serviec-with-alb module that are customized to specific services.

For example, if you needed to set those environment variables and the custom command in all environments, then you could create a my-app module in the infrastructure-modules repo, have it “wrap” the generic ecs-service-with-alb​ module and add your custom settings.

For example, you could add infrastructure-modules/services/my-app/main.tf with the following rough structure:

module "service" {
  # Use the ecs-service-with-alb module under the hood
  source = "../ecs-service-with-alb"
  
  # Pass through most params...
  aws_account_id = "${var.aws_account_id}"
  aws_region = "${var.aws_region}"

  # You could enforce a naming convention for this service
  service_name = "my-app-${var.vpc_name}"

  # Set the command you need for this service
  command = ["node", "/app/server.js", "--foo", "--bar"]

  # Set the env vars you need for this service
  extra_env_vars = {
    foo = "bar"
  }

  # ... (other params omitted) ...
}

And now, in your terraform.tfvars file, instead of pointing source to ecs-service-with-alb, you should point it to your custom my-app module.

Great explanation. Many thx!