terraform template to build dynamic SSM automation

49 views Asked by At

running Terraform 1.7.4

for each row in arg map my SSM automation needs

{
     "description": "Invoke Lambda Function ${key}",
     "name": "InvokeLambdaFunction${key}",
     "action": "aws:invokeLambdaFunction",
     "inputs": {
         "FunctionName": "arn:aws:lambda:us-east-1:${account_number}:function:hello",
         "InvocationType": "RequestResponse",
         "Payload": "{\"key1\": \"${account_number}\", \"key2\": \"<first value from arg_map>\", \"key3\": \"<second value from arg_map>\"}"
     }
}

main.tf

provider "aws" {
  region = "us-east-1"  # Set your desired region here
}
    
variable "account_number" {
  default = "123456789"
}
    
variable "arg_map" {
  type = map(list(string))
  default = {
    "key" = ["a", "b"]
    "key" = ["d", "e"]
    "key" = ["g", "h"]
    "key" = ["j", "k"]
  }
}
    
resource "aws_ssm_document" "sync_epv2asm" {
  name          = "sync_epv2asm"
  document_type = "Automation"
 
  content = templatefile("${path.module}/ssm_document_template.tftpl", {
    account_number = var.account_number
    arg_map        = var.arg_map
  })
}

ssm_document_template.tftpl

{
  "schemaVersion": "0.3",
  "description": "My description.",
  "mainSteps": [
    % for key, values in arg_map:
     {
        "description": "Invoke Lambda Function ${key}",
        "name": "InvokeLambdaFunction${key}",
        "action": "aws:invokeLambdaFunction",
        "inputs": {
          "FunctionName": "arn:aws:lambda:us-east-1:${account_number}:function:hello",
          "InvocationType": "RequestResponse",
          "Payload": "{\"key1\": \"${account_number}\", \"key2\": \"${values[0]}\", \"key3\": \"${values[1]}\"}"
        }
    }% if not loop.last %,
    % endif
    % endfor
  ]
}

running terraform apply

│ Error: Invalid function argument │ │ on main.tf line 23, in resource "aws_ssm_document" "sync_epv2asm": │ 23: content = templatefile("${path.module}/ssm_document_template.tftpl", { │ 24: account_number = var.account_number │ 25: arg_map = var.arg_map │ 26: }) │ ├──────────────── │ │ while calling templatefile(path, vars) │ │ var.arg_map is a map of list of string │ │ Invalid value for "vars" parameter: vars map does not contain key "key", referenced at ./ssm_document_template.tftpl:7,44-47.

googling around is not finding anything. I even tried some of the AI to see if they and identify my problem.

some of the things on the internet were showing "{}" the template around the "%". that did not change my error.

any thoughts on why I cannot get the template to work?

2

There are 2 answers

4
Marko E On BEST ANSWER

Since you are trying to create a JSON document, templatefile is almost never enough by itself to achieve that. Based on the documentation for templatefile, you can also use the built-in jsonencode function with the template. It should look something like the following:

${jsonencode({
  "schemaVersion": "0.3",
  "description": "My description.",
  "mainSteps": [
    for key, values in arg_map:
    {
      "description": "Invoke Lambda Function ${key}",
      "name": "InvokeLambdaFunction${key}",
      "action": "aws:invokeLambdaFunction",
      "inputs": {
        "FunctionName": "arn:aws:lambda:us-east-1:${account_number}:function:hello:$LATEST",
        "InvocationType": "RequestResponse",
        "Payload": "{\"key1\": \"${account_number}\", \"key2\": \"${values[0]}\", \"key3\": \"${values[1]}\"}"
      }
    }
  ]
})}

The plan output shows the result like this:

  # aws_ssm_document.sync_epv2asm will be created
  + resource "aws_ssm_document" "sync_epv2asm" {
      + arn              = (known after apply)
      + content          = jsonencode(
            {
              + description   = "My description."
              + mainSteps     = [
                  + {
                      + action      = "aws:invokeLambdaFunction"
                      + description = "Invoke Lambda Function key1"
                      + inputs      = {
                          + FunctionName   = "arn:aws:lambda:us-east-1:123456789:function:hello:$LATEST"
                          + InvocationType = "RequestResponse"
                          + Payload        = jsonencode(
                                {
                                  + key1 = "123456789"
                                  + key2 = "a"
                                  + key3 = "b"
                                }
                            )
                        }
                      + name        = "InvokeLambdaFunctionkey1"
                    },
                  + {
                      + action      = "aws:invokeLambdaFunction"
                      + description = "Invoke Lambda Function key2"
                      + inputs      = {
                          + FunctionName   = "arn:aws:lambda:us-east-1:123456789:function:hello:$LATEST"
                          + InvocationType = "RequestResponse"
                          + Payload        = jsonencode(
                                {
                                  + key1 = "123456789"
                                  + key2 = "d"
                                  + key3 = "e"
                                }
                            )
                        }
                      + name        = "InvokeLambdaFunctionkey2"
                    },
                ]
              + schemaVersion = "0.3"
            }
        )
      + created_date     = (known after apply)
      + default_version  = (known after apply)
      + description      = (known after apply)
      + document_format  = "JSON"
      + document_type    = "Automation"
      + document_version = (known after apply)
      + hash             = (known after apply)
      + hash_type        = (known after apply)
      + id               = (known after apply)
      + latest_version   = (known after apply)
      + name             = "sync_epv2asm"
      + owner            = (known after apply)
      + parameter        = (known after apply)
      + platform_types   = (known after apply)
      + schema_version   = (known after apply)
      + status           = (known after apply)
      + tags_all         = (known after apply)
    }

I've trimmed the example to use only two keys, but this should work for any number of keys.

NOTE: You also have to append the Lambda version to the Lambda ARN, either using $LATEST or a version number, otherwise, the SSM document will throw an error:

Error: creating SSM Document (sync_epv2asm): InvalidDocumentContent: Input arn:aws:lambda:us-east-1:123456789:function:hello failed to match regex defined in document: (arn:aws)?(-[a-z]+)?(-[a-z]+)?(:lambda:)?([a-z]{2}-[a-z]+(-[a-z]+)?-\d{1}:)?(\d{12}:)?(function:)?([a-zA-Z0-9-]+)(:($LATEST|[a-zA-Z0-9-]+))?.

Apply output:

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_ssm_document.sync_epv2asm will be created
  + resource "aws_ssm_document" "sync_epv2asm" {
      + arn              = (known after apply)
      + content          = jsonencode(
            {
              + description   = "My description."
              + mainSteps     = [
                  + {
                      + action      = "aws:invokeLambdaFunction"
                      + description = "Invoke Lambda Function key1"
                      + inputs      = {
                          + FunctionName   = "arn:aws:lambda:us-east-1:123456789:function:hello:$LATEST"
                          + InvocationType = "RequestResponse"
                          + Payload        = jsonencode(
                                {
                                  + key1 = "123456789"
                                  + key2 = "a"
                                  + key3 = "b"
                                }
                            )
                        }
                      + name        = "InvokeLambdaFunctionkey1"
                    },
                  + {
                      + action      = "aws:invokeLambdaFunction"
                      + description = "Invoke Lambda Function key2"
                      + inputs      = {
                          + FunctionName   = "arn:aws:lambda:us-east-1:123456789:function:hello:$LATEST"
                          + InvocationType = "RequestResponse"
                          + Payload        = jsonencode(
                                {
                                  + key1 = "123456789"
                                  + key2 = "d"
                                  + key3 = "e"
                                }
                            )
                        }
                      + name        = "InvokeLambdaFunctionkey2"
                    },
                ]
              + schemaVersion = "0.3"
            }
        )
      + created_date     = (known after apply)
      + default_version  = (known after apply)
      + description      = (known after apply)
      + document_format  = "JSON"
      + document_type    = "Automation"
      + document_version = (known after apply)
      + hash             = (known after apply)
      + hash_type        = (known after apply)
      + id               = (known after apply)
      + latest_version   = (known after apply)
      + name             = "sync_epv2asm"
      + owner            = (known after apply)
      + parameter        = (known after apply)
      + platform_types   = (known after apply)
      + schema_version   = (known after apply)
      + status           = (known after apply)
      + tags_all         = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_ssm_document.sync_epv2asm: Creating...
aws_ssm_document.sync_epv2asm: Creation complete after 0s [id=sync_epv2asm]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
0
peter cooke On

Apparently there was a problem with my terraform. for some reason the template would only use the last line of arg_map.
I had to uninstall Terraform and reinstall terraform.

It appears that the invoke lambda function is smart. it needed $LATEST or $, and the actual account number.

Main.tf

provider "aws" {
  region = "us-east-1" # Set your desired region here
}

variable "account_number" {
  default = "123456789"
}

variable "arg_map" {
  type = map(list(string))
  default = {
    "key1" = ["a", "b"]
    "key2" = ["d", "e"]
    "key3" = ["g", "h"]
    "key4" = ["j", "k"]
  }
}

resource "aws_ssm_document" "sync_epv2asm" {
  name          = "sync_epv2asm"
  document_type = "Automation"

  content = templatefile("${path.module}/ssm_document_template.tftpl", {
    account_number = var.account_number
    arg_map        = var.arg_map
  })
}

ssm_document_template.tftpl

${jsonencode({
  "schemaVersion": "0.3",
  "description": "My description.",
  "mainSteps": [
    for key, values in arg_map:
    {
      "description": "Invoke Lambda Function ${key}",
      "name": "InvokeLambdaFunction${key}",
      "action": "aws:invokeLambdaFunction",
      "inputs": {
        "FunctionName": "arn:aws:lambda:us-east-1:${account_number}:function:hello:$LATEST",
        "InvocationType": "RequestResponse",
        "Payload": "{\"key1\": \"${account_number}\", \"key2\": \"${values[0]}\", \"key3\": \"${values[1]}\"}"
      }
    }
  ]
})}