induktiv
induktiv

Reputation: 150

How to avoid duplication in terraform when having multiple services in ECS which differ only in the command?

We create ECS services in Terraform by defining a template_file which populates a task definition JSON template with all needed variables. Then a aws_ecs_task_definition is created with the rendered template_file. With this task definition the aws_ecs_service is created:

data "template_file" "web" {
  template = "${file("${path.module}/tasks/web.json")}"

  vars {
    ...
  }
}

resource "aws_ecs_task_definition" "web" {
  container_definitions    = "${data.template_file.web.rendered}"
  requires_compatibilities = ["FARGATE"]
  ...
}

data "aws_ecs_task_definition" "web" {
  task_definition = "${aws_ecs_task_definition.web.family}"
}

resource "aws_ecs_service" "web" {
  name            = "web"
  task_definition = "${aws_ecs_task_definition.web.family}:${max("${aws_ecs_task_definition.web.revision}", "${data.aws_ecs_task_definition.web.revision}")}"
  ...
}

There are additional services with task definitions nearly identical to the first one, only having small differences like another command (for example for starting sidekiq instead of the web app).

Is there any other way of doing this other than duplicating everything (the JSON template, template_file with all defined variables, aws_ecs_task_definition and aws_ecs_service)?

Upvotes: 3

Views: 2025

Answers (2)

induktiv
induktiv

Reputation: 150

Expanding on the accepted answer to show how to also remove the duplication caused by the defined variables in the template_file vars block (which do not change and thus would have to be duplicated between invocations of the module). It is also not a solution to just inline these variables or use defaults because they still will change between projects, just not within services of the same project. We can use local variables for setting the defaults and overriding defaults with the merge function:

main.tf

locals {
  task_variables = {
    image = "..."
    # lots of other variables
    command = "[\"nginx\", \"-g\", \"daemon off; error_log /dev/stdout info;\"]"
  }
}

# first invocation of the module, overriding the command
module "sidekiq" {
  source = "ecs_service"
  ...
  task_variables = "${merge(
    local.task_variables,
    map(
      "command", "[\"bash\", \"-c\", \"exec bundle exec sidekiq\"]",
    )
  )}"
}

# second invocation of the module, no overrides
module "web" {
  source = "ecs_service"
  task_variables = "${local.task_variables}"
}

Module ecs_service

variable "task_variables" {
  type = "map"
}

data "template_file" "web_task" {
  template = "${file("${path.module}/tasks/task_definition.json")}"

  vars = "${var.task_variables}"
}

resource "aws_ecs_task_definition" "web" {
  container_definitions    = "${data.template_file.web_task.rendered}"
  ...
}

data "aws_ecs_task_definition" "web" {
  task_definition = "${aws_ecs_task_definition.web.family}"
  ...
}

resource "aws_ecs_service" "web" {
  task_definition = "${aws_ecs_task_definition.web.family}:${max("${aws_ecs_task_definition.web.revision}", "${data.aws_ecs_task_definition.web.revision}")}"
  ...
}

Upvotes: 0

ydaetskcoR
ydaetskcoR

Reputation: 56877

Modules are the main way to solve this in Terraform.

If you move your existing code into a single folder you can then define variables that allow you to customise that module such as the command to be passed to your ECS service.

So in your case you might have something like this:

modules/foo-service/main.tf

data "template_file" "web" {
  template = "${file("${path.module}/tasks/web.json")}"

  vars {
    # ...
    command = "${var.command}"
  }
}

resource "aws_ecs_task_definition" "web" {
  container_definitions    = "${data.template_file.web.rendered}"
  requires_compatibilities = ["FARGATE"]
  # ...
}

data "aws_ecs_task_definition" "web" {
  task_definition = "${aws_ecs_task_definition.web.family}"
}

resource "aws_ecs_service" "web" {
  name            = "web"
  task_definition = "${aws_ecs_task_definition.web.family}:${max("${aws_ecs_task_definition.web.revision}", "${data.aws_ecs_task_definition.web.revision}")}"
  # ...
}

modules/foo-service/variables.tf

variable "command" {}

staging/main.tf

module "foo_service_web" {
  source  = "../modules/foo-service"
  command = "bundle exec server"
}

module "foo_service_sidekiq" {
  source  = "../modules/foo-service"
  command = "bundle exec sidekiq"
}

Upvotes: 2

Related Questions