'how to use aws secret manager values of a postgres database in terraform ecs task definition

I have the settings.py file below of a django application using terraform , docker compose and im trying to get the value of the database stored in aws secret manager in ecs task definition

settings.py

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql_psycopg2",
        "NAME": os.environ.get("POSTGRES_DB"),
        "USER": os.environ.get("POSTGRES_USER"),
        "PASSWORD": os.environ.get("POSTGRES_PASSWORD"),
        "HOST": os.environ.get("POSTGRES_HOST"),
        "PORT": 5432,
    }
}

task definition

resource "aws_ecs_task_definition" "ecs_task_definition" {
  family                   = "ecs_container"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = 256                      
  memory                   = 512
  execution_role_arn       = data.aws_iam_role.fargate.arn
  task_role_arn            = data.aws_iam_role.fargate.arn
  container_definitions = jsonencode([
    {
      "name"         : "ecs_test_backend",
      "image"        : "${aws_ecr_repository.ecr_repository.repository_url}:latest",
      "cpu"          : 256,
      "memory"       : 512,
      "essential"    : true,
      "portMappings" : [
        {
          containerPort = 8000
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-region": "us-east-1",
          "awslogs-group": "/ecs/ecs_test_backend",
          "awslogs-stream-prefix": "ecs"
        }
      }
      "environment"   : [
        {
          "name": "POSTGRES_DB",
          "value": "${var.POSTGRES_NAME}" <=== HERE
        },
        {
          "name": "POSTGRES_PASSWORD",
          "value": "${var.POSTGRES_PASSWORD}"  <=== HERE
        },
        {
          "name": "POSTGRES_USERNAME",
          "value": "${var.POSTGRES_USERNAME}"  <=== HERE
        },
        {
          "name": "POSTGRES_PORT",
          "value": "${var.POSTGRES_PORT}"  <=== HERE
        },
        {
          "name": "POSTGRES_HOST",
          "value": "${var.POSTGRES_HOST}"  <=== HERE
        },
      ]
    }
  ])
}

variables

variable "POSTGRES_PASSWORD" {
    type = string
    default = "somepassword"
}

The variables above is the same used while creating the postgres rds instance.

The configuration below var.XXX does not seem to work as the task logs return psycopg2.OperationalError: FATAL: password authentication failed for user "root"

It probably because its not able to read the value.

Is the above the correct way to grab value from AWS Secret Manager using Terraform and ECS?



Solution 1:[1]

You should use the sensitive data handling feature of ECS tasks, documented here.

You would move the environment variables from the environment block of your task definition to the secrets block, and give the ARN of the secret instead of the value. The ECS container will then read those secrets when it starts your container, and set them in the container's environment.

Solution 2:[2]

I have been looking and I couldn't find any good examples to achieve this in the proper way, and while the response from @Mark B adds some context is true that some code snippets can be helpful since sometimes is more complicated to code it (with all the requirements) than the theoretical explanation. With that said I will split into pieces what you need to complete:

The parameter store

You need to save your secrets somewhere and the parameters store is a good place for it according AWS best practices:

resource "aws_ssm_parameter" "main" {
  for_each    = var.parameters
  name        = "/path/${var.app_name}/${each.key}"
  description = "Secrets for application ${var.app_name}"
  type        = "SecureString"
  value       = each.value
}

variable "parameters" {
  type = map(any)
}

variable "app_name" {
  type = string
}

output "arns" {
  value = [for k, v in var.parameters : { name = k, valueFrom = aws_ssm_parameter.main[k].arn }]
}

(The code above creates several parameters)

Be sure of adding the correct permissions to your task execution role:

resource "aws_iam_role_policy" "parameter_policy" {
  name = "mypolicy"
  role = aws_iam_role.your_role.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "ssm:GetParameters",
        ]
        Effect   = "Allow"
        Resource = ["*"]
      },
    ]
  })
}

Be aware that the above is just an example, to follow best practices be sure to limit the resources to the arn(s) of the parameters created above.

Finally how to use it

container_definitions = <<TASK_DEFINITION
[
  {
    "name": "hello-world",
    "image": "nginxdemos/hello",
    "cpu": 1024,
    "memory": 2048,
    "essential": true,
    "secrets": ${jsonencode(module.hello-world-secrets.arns)},
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-region": "your_region",
        "awslogs-group": "hello_world",
        "awslogs-stream-prefix": "hello_world"
      }
    }
  }
]
TASK_DEFINITION

module "hello-world-secrets" {
  source   = "./modules/secrets"
  app_name = "hello-world"
  parameters = {
    hello = "world"
  }
}

You need to decide in which point create the secret / store the secret, e.g. on your CI/CD inject your secret and create the parameter store resource.

Hopefully, this help someone else looking for a way of achieving this.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Mark B
Solution 2