Pass command as variable to ECS task definition - amazon-web-services

Is there a way to pass a Docker command as a Terraform variable to the ECS task definition that is defined in Terraform?

According to the aws_ecs_task_definition documentation, the container_definitions property is an unparsed JSON object that's an array of container definitions as you'd pass directly to the AWS APIs. One of the properties of that object is a command.
Paraphrasing the documentation somewhat, you'd come up with a sample task definition like:
resource "aws_ecs_task_definition" "service" {
family = "service"
container_definitions = <<DEFINITIONS
[
{
"name": "first",
"image": "service-first",
"command": ["httpd", "-f", "-p", "8080"],
"cpu": 10,
"memory": 512,
"essential": true
}
]
DEFINITIONS
}

You can try the below method to take the command as a variable with template condition if nothing is passed from the root module.
service.json
[
{
...
],
%{ if command != "" }
"command" : [${command}],
%{ endif ~}
...
}
]
container.tf
data "template_file" "container_def" {
count = 1
template = file("${path.module}/service.json")
vars = {
command = var.command != "" ? join(",", formatlist("\"%s\"", var.command)) : ""
}
}
main.tf
module "example" {
...
command = ["httpd", "-f", "-p", "8080"]
...
}
variables.tf
variable "command" {
default = ""
}

Related

Dynamic Task Definition in Terraform

At the moment I have multiple aws_ecs_task_definition that are blueprints for my ECS tasks. Rather than having a separate module to store each of these unique definitions, I would like to somehow structure my task definition to be agnostic so that it can be used for each of my containers. The main challenge I am facing is that the environment variables differ between task definitions. For example, one task definition might have 4 environment variables and another might have 12. Also, some of the values these variables have are provided by other modules by way of using Outputs (e.g. databases endpoints). Below is an example of an existing task definition.
resource "aws_ecs_task_definition" "service" {
execution_role_arn = var.ecsTaskExecutionRole
task_role_arn = var.ecsTaskExecutionRole
requires_compatibilities = ["FARGATE"]
family = "${var.name}-definition"
container_definitions = jsonencode([
{
"environment" : [
{ "name" : "database_endpoint",
"value" : "${var.database_endpoint}"
},
{ "name" : "database_password",
"value" : "admin"
}
],
"healthCheck" : {
"command" : [
"CMD-SHELL",
"echo \"hello\""
],
"interval" : 10,
"timeout" : 60,
"retries" : 10,
"startPeriod" : 60
},
command = ["start", "--auto-build"]
name = "${var.name}"
image = "${var.image}"
cpu = 1024
memory = 2048
essential = true
portMappings = [
{
containerPort = 7278
hostPort = 7278
}
]
},
])
network_mode = "awsvpc"
cpu = 1024
memory = 2048
runtime_platform {
operating_system_family = "LINUX"
}
}
In essence, I want to be able to dynamically build the environment list based on an object that is provided as a variable when making the module call. For example, that variable might look like this and the values that are empty string are updated at a later point:
containers = {
image1 = {
image = "imageUrl"
}
image2 = {
image = "imageUrl"
documentdb_endpoint = ""
}
image3 = {
image = "imageUrl"
redis_endpoint = ""
}
image4 = {
image = "imageUrl"
}
}
Would appreciate any advice to help make the code more concise.

Terraform Error: Provider produced inconsistent final plan - value "known after apply" causes empty list on plan

The following contrived example causes "Error: Provider produced inconsistent final plan" because of the locals.project_id used in the list of rrdatas on the google_dns_record_set.cdn_dns_txt_record_firebase resource. The project_id value is known only after apply and I do not know how to manage this for the rrdatas list. When I come to apply the plan, the value changes and causes the error mentioned. Your help would be really appreciated.
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = ">= 4.24.0"
}
random = {
version = ">= 3.3.2"
}
}
}
locals {
project_id = random_id.project_id.hex
}
resource "random_id" "project_id" {
keepers = {
project_id = "my-project-id"
}
byte_length = 8
prefix = "project-"
}
resource "google_project" "my_project" {
name = "A Great Project"
project_id = random_id.project_id.hex
}
resource "google_dns_record_set" "cdn_dns_txt_record_firebase" {
name = "www.bob.com"
project = google_project.my_project.project_id
managed_zone = "bob.com."
type = "TXT"
ttl = 300
rrdatas = [
"\"v=spf1 include:_spf.firebasemail.com ~all\"",
"firebase=${local.project_id}"
]
}
The plan for the google_dns_record_set.cdn_dns_txt_record_firebase resource looks like this:
# google_dns_record_set.cdn_dns_txt_record_firebase will be created
+ resource "google_dns_record_set" "cdn_dns_txt_record_firebase" {
+ id = (known after apply)
+ managed_zone = "bob.com."
+ name = "www.bob.com"
+ project = (known after apply)
+ ttl = 300
+ type = "TXT"
}
But I would expect something more like:
# google_dns_record_set.cdn_dns_txt_record_firebase will be created
+ resource "google_dns_record_set" "cdn_dns_txt_record_firebase" {
+ id = (known after apply)
+ managed_zone = "bob.com."
+ name = "www.bob.com"
+ project = (known after apply)
+ rrdatas = [
+ "\"v=spf1 include:_spf.firebasemail.com ~all\"",
+ "firebase=(known after apply)",
]
+ ttl = 300
+ type = "TXT"
}
Ran into this issue with the AWS Provider. I know it's not quite the same but my solution was to modify the infrastructure via AWS CLI directly. From there I removed the state record of the specific resource in our state store (Terraform Cloud) and then did an import of the resource from AWS.
If you are managing remote state yourself you could likely run the terraform plan and then discard the run, but modify the remote state directly with the changes that Terraform detected. It's definitely a provider bug but that might be a workaround.
We can see from the plan information that the provider has indeed generated an invalid plan in this case, for the reason you observed: you set rrdatas in the configuration, and so the provider ought to have generated an initial plan to set those values.
As is being discussed over in the bug report you filed about this, the provider seems to be mishandling the unknown value you passed here, and returning a plan that has it set to null instead of to unknown as expected.
Until that bug is fixed in the provider, I think the main workaround would be to find some way to ensure that Terraform Core already knows the value of local.project_id before asking the provider to plan resource "google_dns_record_set" "cdn_dns_txt_record_firebase".
One way to achieve that would be to create a targeted plan that tells Terraform to focus only on generating that random ID in its first operation, and then once that's succeeded you can use normal Terraform applies moving forward as long as you avoid regenerating that random ID:
terraform apply -target=random_id.project_id to just generate that random ID, without planning anything else.
terraform apply to converge everything else. This should succeed because random_id.project_id.hex should already be known from the previous run, and so local.project_id will be known too. The provider then won't run into this buggy behavior, because it will see rrdatas as being a known list of strings rather than as an unknown value.
You have to check the tfstate file in order to find the incoherence between your terraform code and tfstate attribute.
It helped me to fix issue:
tfstate
{
"module": "module.gitlab_cloud_sql",
"mode": "managed",
"type": "random_string",
"name": "random",
"provider": "provider[\"registry.terraform.io/hashicorp/random\"]",
"instances": [
{
"schema_version": 2,
"attributes": {
"id": "pemz",
"keepers": null,
"length": null,
"lower": null,
"min_lower": null
"min_numeric": null,
"min_special": null,
"min_upper": null,
"number": null,
"numeric": null,
"override_special": null,
"result": "pemz",
"special": null,
"upper": null
},
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjIifQ=="
}
]
},
Terraform code:
resource "random_string" "random" {
length = 4
special = false
lower = true
upper = false
numeric = false
min_upper = 0
lifecycle {
ignore_changes = all
}
}
After fixing the tfstate, it works
{
"module": "module.gitlab_cloud_sql",
"mode": "managed",
"type": "random_string",
"name": "random",
"provider": "provider[\"registry.terraform.io/hashicorp/random\"]",
"instances": [
{
"schema_version": 2,
"attributes": {
"id": "pemz",
"keepers": null,
"length": 4,
"lower": true,
"min_lower": 0,
"min_numeric": 0,
"min_special": 0,
"min_upper": 0,
"number": false,
"numeric": false,
"override_special": null,
"result": "pemz",
"special": false,
"upper": false
},
}
]
},

How can we enable Amazon S3 replication modification sync in terraform?

I am working on an Amazon S3 replication using terraform . I want to enable rule "Repilcate modification sync" but I don't think so it is defined in terraform .
Right now my code looks :
replication_configuration {
role = "${aws_iam_role.source_replication.arn}"
rules {
id = "${local.replication_name}"
status = "Enabled"
prefix = "${var.replicate_prefix}"
destination {
bucket = "${local.dest_bucket_arn}"
storage_class = "STANDARD"
access_control_translation = {
owner = "Destination"
}
account_id = "${data.aws_caller_identity.dest.account_id}"
}
source_selection_criteria {
replica_modifications {
Status = "Enabled"
}
}
}
}
It gives an error :
Error: Unsupported block type
on s3_bucket.tf line 61, in resource "aws_s3_bucket" "bucket":
61: replica_modifications {
Blocks of type "replica_modifications" are not expected here.
The rules which I have to enable looks like this in console.
With AWS CLI in terraform , I am not sure how can I use variables like destination ${local.dest_bucket_arn} and ${aws_iam_role.source_replication.arn} in my son file which I am calling.
resource "null_resource" "awsrepl" {
# ...
provisioner "local-exec" {
command = "aws s3api put-bucket-replication --replication-configuration templatefile://replication_source.json --bucket ${var.bucket_name}"
}
}
replication_source.json looks like :
{
"Rules": [
{
"Status": "Enabled",
"DeleteMarkerReplication": { "Status": "Enabled" },
"SourceSelectionCriteria": {
"ReplicaModifications":{
"Status": "Enabled"
}
},
"Destination": {
"Bucket": "${local.dest_bucket_arn}"
},
"Priority": 1
}
],
"Role": "${aws_iam_role.source_replication.arn}"
}
You are correct. It is not yet supported, but there is a GitHub issue for that already:
Amazon S3 Two-way Replication via Replica Modification Sync
By the way, Delete marker replication is also not supported.
Your options are to either do it manually after you deploy your bucket, or use local-exec to run AWS CLI to do it, or aws_lambda_invocation.
Was able to achieve this using local-exec and temmplate_file in terraform :
data "template_file" "replication_dest" {
template = "${file("replication_dest.json")}"
vars = {
srcarn = "${aws_s3_bucket.bucket.arn}"
destrolearn = "${aws_iam_role.dest_replication.arn}"
kmskey = "${data.aws_caller_identity.current.account_id}"
keyalias = "${data.aws_kms_key.s3.key_id}"
srcregion = "${data.aws_region.active.name}"
}
}
resource "null_resource" "awsdestrepl" {
# ...
provisioner "local-exec" {
command = "aws s3api put-bucket-replication --bucket ${aws_s3_bucket.dest.bucket} --replication-configuration ${data.template_file.replication_dest.rendered}"
}
depends_on = [aws_s3_bucket.dest]
}
And replication_dest.json looks like this :
"{
\"Rules\": [
{
\"Status\": \"Enabled\",
\"DeleteMarkerReplication\": { \"Status\": \"Enabled\" },
\"Filter\": {\"Prefix\": \"\"},
\"SourceSelectionCriteria\": {
\"ReplicaModifications\":{
\"Status\": \"Enabled\"
},
\"SseKmsEncryptedObjects\":{
\"Status\": \"Enabled\"
}
},
\"Destination\": {
\"Bucket\": \"${bucketarn}\",
\"EncryptionConfiguration\": {
\"ReplicaKmsKeyID\": \"arn:aws:kms:${destregion}:${kmskey}:${keyalias}\"
}
},
\"Priority\": 1
}
],
\"Role\": \"${rolearn}\"
}"
And you are good to go . :)

How to iterate list of objects in terraform locals

Basically, we are trying to create cloudwatch dashboards using terraform 0.13.5 and our requirement is to pass the 2 variable to widget block i.e. ${function_name} and ${title}.This will be passed as object variable.
Error : Invalid template interpolation value
Cannot include the given value in a string template: string required.
here is the code:
locals{
lambda = [
{
function_name = "lambda1"
title = "Error"
},
{
function_name = "lambda1"
title = "Error1"
}
]
widget_defination = <<EOT
%{ for function_name , title in local.lambda}
[
{
"type": "metric",
"x": 0,
"y": 0,
"width": 12,
"height": 6,
"properties": {
"metrics": [
[
"AWS/EC2",
"CPUUtilization",
"FunctionName",
"${funtion_name}"
]
],
"period": 300,
"stat": "Average",
"region": "us-east-1",
"title": "${title}"
}
}
]
}
%{endfor }
EOT
}
Gotcha.
We need to call objects in widgets like -
${function_name.function_name} and
${function_name.title}
As far as I know that is not the way to work with variables in terraform.
You have to declare the variables and type on its own file and assign their values in a different file or as a result of a resource creation.
You are talking about widgets so I'm not sure if you already know that because I've never used widgets before. But if you need some help ASAP I don't mind to try..
variables.tf
variable "project_name" {
type = string
}
variable "vpc_id" {}
...
terraform.tfvars
project_name = "my-project"
vpc_id = "vpc-10101010"
...
The way you put that in a template is up to you.
I would suggest a simple approach like bash but IDK maybe the widgets are fun
And a little EDIT here because I just saw your "Gotcha" late.. yes, do not mix variables and strings.. you should vote for your own answer :D

terraform keeps forcing new resource/force replacement for container definition with default parameters

I am bringing up aws_ecs_task_defintion with following terraform configuration.
I pass local.image_tag as variable to control the deployment of our ecr image through terraform.
I am able to bring up the ecs_cluster on initial terraform plan/apply cycle just fine.
However, on the subsequent terraform plan/apply cycle, terraform is forcing the new container definition and thats why redeploying the entire task definition even though our ecr image local.image_tag remains just same
This behaviour, is causing the unintended task definition recycle without any changes to the ecr image and just terraform forcing values with defaults.
TF Config
resource "aws_ecs_task_definition" "this_task" {
family = "this-service"
execution_role_arn = var.this_role
task_role_arn = var.this_role
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = 256
memory = var.env != "prod" ? 512 : 1024
tags = local.common_tags
# Log the to datadog if it's running in the prod account.
container_definitions = (
<<TASK_DEFINITION
[
{
"essential": true,
"image": "AWS_ACCOUNT_ID.dkr.ecr.us-west-2.amazonaws.com/thisisservice:${local.image_tag}",
"environment" :[
{"name":"ID", "value":"${jsondecode(data.aws_secretsmanager_secret_version.this_decrypt.secret_string)["id"]}"},
{"name":"SECRET","value":"${jsondecode(data.aws_secretsmanager_secret_version.this_decrypt.secret_string)["secret"]}"},
{"name":"THIS_SOCKET_URL","value":"${local.websocket_url}"},
{"name":"THIS_PLATFORM_API","value":"${local.platform_api}"},
{"name":"REDISURL","value":"${var.redis_url}"},
{"name":"BASE_S3","value":"${aws_s3_bucket.ec2_vp.id}"}
],
"name": "ec2-vp",
"logConfiguration": {
"logDriver": "awsfirelens",
"options": {
"Name": "datadog",
"apikey": "${jsondecode(data.aws_secretsmanager_secret_version.datadog_api_key[0].secret_string)["api_key"]}",
"Host": "http-intake.logs.datadoghq.com",
"dd_service": "this",
"dd_source": "this",
"dd_message_key": "log",
"dd_tags": "cluster:${var.cluster_id},Env:${var.env}",
"TLS": "on",
"provider": "ecs"
}
},
"portMappings": [
{
"containerPort": 443,
"hostPort": 443
}
]
},
{
"essential": true,
"image": "amazon/aws-for-fluent-bit:latest",
"name": "log_router",
"firelensConfiguration": {
"type": "fluentbit",
"options": { "enable-ecs-log-metadata": "true" }
}
}
]
TASK_DEFINITION
)
}
-/+ resource "aws_ecs_task_definition" "this_task" {
~ arn = "arn:aws:ecs:ca-central-1:AWS_ACCOUNT_ID:task-definition/this:4" -> (known after apply)
~ container_definitions = jsonencode(
~ [ # forces replacement
~ {
- cpu = 0 -> null
environment = [
{
name = "BASE_S3"
value = "thisisthevalue"
},
{
name = "THIS_PLATFORM_API"
value = "thisisthevlaue"
},
{
name = "SECRET"
value = "thisisthesecret"
},
{
name = "ID"
value = "thisistheid"
},
{
name = "THIS_SOCKET_URL"
value = "thisisthevalue"
},
{
name = "REDISURL"
value = "thisisthevalue"
},
]
essential = true
image = "AWS_ACCOUNT_ID.dkr.ecr.us-west-2.amazonaws.com/this:v1.0.0-develop.6"
logConfiguration = {
logDriver = "awsfirelens"
options = {
Host = "http-intake.logs.datadoghq.com"
Name = "datadog"
TLS = "on"
apikey = "thisisthekey"
dd_message_key = "log"
dd_service = "this"
dd_source = "this"
dd_tags = "thisisthetags"
provider = "ecs"
}
}
- mountPoints = [] -> null
name = "ec2-vp"
~ portMappings = [
~ {
containerPort = 443
hostPort = 443
- protocol = "tcp" -> null
},
]
- volumesFrom = [] -> null
} # forces replacement,
~ {
- cpu = 0 -> null
- environment = [] -> null
essential = true
firelensConfiguration = {
options = {
enable-ecs-log-metadata = "true"
}
type = "fluentbit"
}
image = "amazon/aws-for-fluent-bit:latest"
- mountPoints = [] -> null
name = "log_router"
- portMappings = [] -> null
- user = "0" -> null
- volumesFrom = [] -> null
} # forces replacement,
]
)
cpu = "256"
execution_role_arn = "arn:aws:iam::AWS_ACCOUNTID:role/thisistherole"
family = "this"
~ id = "this-service" -> (known after apply)
memory = "512"
network_mode = "awsvpc"
requires_compatibilities = [
"FARGATE",
]
~ revision = 4 -> (known after apply)
tags = {
"Cluster" = "this"
"Env" = "this"
"Name" = "this"
"Owner" = "this"
"Proj" = "this"
"SuperCluster" = "this"
"Terraform" = "true"
}
task_role_arn = "arn:aws:iam::AWS_ACCOUNT+ID:role/thisistherole"
}
Above is the terraform plan that is forcing new task definition/container definition.
As you can see , terraform is replacing all default values with null or empty. I have double check the terraform.tfstate file it already generated from the previous run and those values are exactly the same as its showing on the above plan.
I am not sure why this unintended behaviour is happening and want to have some clues on how to fix this.
I am using terraform 0.12.25 and latest terraform aws provider.
There is a known terraform aws provider bug for this issue.
In order to make terraform not replace the running task / container definition, I have to fill out all the default values that its showing on terraform plan with either null or empty sets of configuration.
Once all the parameters are filled out, I ran the terafform plan/apply cycle again to ensure its not replacing the container definition like it was doing it before.
I got the same issue when I have the aws-for-fluent-bit as a sidecar container. Adding "user": "0" in this container definition is the least thing that can prevent the task definition from being force recreated.
{
"name": "log_router",
"image": "public.ecr.aws/aws-observability/aws-for-fluent-bit:latest",
"logConfiguration": null,
"firelensConfiguration": {
"type": "fluentbit",
"options": {
"enable-ecs-log-metadata": "true"
}
},
"user": "0"
}