Terraform nested loop expression - terraform

I'm trying to construct a collection to loop over based on nested data. The example below shows the code I am trying to use:
repos.tfvars
repositories = {
workflows = {
description = "Reusable workflow repository"
topics = ["github-actions", "reusable-workflows"]
},
aws-network-speciality = {
description = "AWS Network Speciality study repository"
topics = ["amazon-web-services", "aws", "terraform", "iac"]
}
}
main.tf
locals {
terraform_repos = { for each, repo in var.repositories : each => repo if contains(["terraform"], repo)}
resource "github_repository_file" "tflint" {
for_each = local.terraform_repos
repository = each.key
file = ".tflint"
content = file("${path.module}/github_repository_files/.tflint")
commit_message = "chore(managed-by-terraform): tflint configuration file"
}
The desired outcome is that tflint is uploaded to each repository with the terraform topic.
Currently local.terraform_repos returns an empty map - I'm just unsure how to drill down into var.repositories to query the topics[] and then return the name of the repo(s) for the loop.

contains should be used for topics and also you have wrong arguments. It should be:
terraform_repos = { for each, repo in var.repositories : each => repo if contains(repo.topics, "terraform")}

Related

Using Terraform to load a single file from git repo

We want to load a file from a git repo and place it into a parameter store. The file contains configuration data that is custom to each of several organizational-accounts, which are being constructed with Terraform and are otherwise identical. The data will be stored in AWS SM Parameter Store.
For example the Terraform code to store a string as a parameter is:
resource "aws_ssm_parameter" "parameter_config" {
name = "config_object"
type = "String"
value = "long json string with configuration data"
}
I know there is a file() operator (reference) from Terraform and I know that TF can load files from remote git repos, but I'm not sure if I can bring all this together.
There are a few ways that you can do this.
The first would be to use the github provider with the github_repository_file data source:
terraform {
required_providers {
github = {
source = "integrations/github"
version = "5.12.0"
}
}
}
provider "github" {
token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
owner = "org-name"
}
data "github_repository_file" "config" {
repository = "my-repo"
branch = "master"
file = "config-file"
}
resource "aws_ssm_parameter" "parameter_config" {
name = "config_object"
type = "String"
value = data.github_repository_file.config.content
}
You could also do this with the http provider:
data "http" "config" {
url = "https://raw.githubusercontent.com/my-org/my-repo/master/config-file"
}
resource "aws_ssm_parameter" "parameter_config" {
name = "config_object"
type = "String"
value = data.http.config.response_body
}
Keep in mind that you may get multiline string delimiters when using the http data source method. (e.g.: <<EOT ... EOT)

Conditional attributes of github repository in Terraform

I want to conditionally set arguments to resources when managing Github repositories with Terraform.
I would like to have different pages blocks depending on the value of the has_pages parameter, as in the following example
Example:
resource "github_repository" "example" {
name = "example"
description = "My awesome web page"
private = false
if ${var.has_page} == true:
pages {
source {
branch = "master"
path = "/docs"
}
}
end
}
I'm having trouble using dynamic as well!
Does anyone know how to make this happen?
For this you can use a combination of dynamic and for_each:
resource "github_repository" "example" {
name = "example"
description = "My awesome web page"
private = false
dynamic "pages" {
for_each = var.has_page ? [1] : []
content {
source {
branch = "master"
path = "/docs"
}
}
}
}
More information about how to use dynamic is in [1] and about for_each is in [2].
[1] https://www.terraform.io/language/expressions/dynamic-blocks
[2] https://www.terraform.io/language/meta-arguments/for_each

Whats the right way to create multiple elements using terraform variables?

I am creating AWS SQS queues using Terraform. for each service, i need to create two queues, one normal queue and one error queue. The settings for each are mostly the same, but i need to create the error queue first so i can pass its ARN to the normal queue as part of its redrive policy. Instead of creating 10 modules there has to be a better way to loop through replacing just the names. So programming logic... foreach queue in queue_prefixes, create error module, then regular module. Im sure im just not searching right or asking the right question.
sandbox/main.tf
provider "aws" {
region = "us-west-2"
}
module "hfd_sqs_error_sandbox" {
source = "../"
for_each = var.queue_prefixes
name= each.key+"_Error"
}
module "hfd_sqs_sandbox" {
source = "../"
name=hfd_sqs_error_sandbox.name
redrive_policy = jsonencode({
deadLetterTargetArn = hfd_sqs_error_sandbox_this_sqs_queue_arn,
maxReceiveCount = 3
})
}
variables.tf
variable "queue_prefixes" {
description = "Create these queues with the enviroment prefixed"
type = list(string)
default = [
"Clops",
"Document",
"Ledger",
"Log",
"Underwriting",
"Wallet",
]
}
You may want to consider adding a wrapper module that creates both Normal Queue and Dead-Letter Queue. That would make creating resources in order much easier.
Consider this example (with null resources for easy testing):
Root module creating all queues:
# ./main.tf
locals {
queue_prefixes = [
"Queue_Prefix_1",
"Queue_Prefix_2",
]
}
module queue_set {
source = "./modules/queue_set"
for_each = toset(local.queue_prefixes)
name = each.key
}
Wrapper module creating a set of 2 queues: normal + dlq:
# ./modules/queue_set/main.tf
variable "name" {
type = string
}
module dlq {
source = "../queue"
name = "${var.name}_Error"
}
module queue {
source = "../queue"
name = var.name
redrive_policy = module.dlq.id
}
Individual queue resource suitable to create both types of queues:
# ./modules/queue/main.tf
variable "name" {
type = string
}
variable "redrive_policy" {
type = string
default = ""
}
resource "null_resource" "queue" {
provisioner "local-exec" {
command = "echo \"Created queue ${var.name}, redrive policy: ${var.redrive_policy}\""
}
# this is irrelevant to the question, it's just to make null resource change every time
triggers = {
always_run = timestamp()
}
}
output "id" {
value = null_resource.queue.id
}
Now if we run this stack, we can see the resources created in the correct order:

How to use shared_image plan data like publisher, offer and sku in virtual_machine resource in Terraform

Using Terraform v0.12.9, azurerm provider v1.36.1
Scenario:
You have created an image based on a third-party marketplace image.
You have published this image to a shared image gallery.
You have been able to do so in an automated fashion using Terraform and Azure Devops pipelines. (ex. CIS hardened images)
For each of these previous steps you have provided plan information data. if not, you would not be able to create a VM from this image, because there is an extra cost associated with using the marketplace image.
Now you want other teams be able to use your image from the Shared Image Gallery through Terraform Automation.
How to get the plan information from the shared image without hard-coding it?
Well, I thought to just retreive the data using a data source using:
data "azurerm_shared_image" "image" {
name = var.image_name
gallery_name = var.gallery_name
resource_group_name = var.rsg_name
}
and using the necessary blocks inside the virtual_machine resource like so:
storage_image_reference {
id = data.azurerm_shared_image.image.id
}
plan {
name = data.azurerm_shared_image.image.sku
publisher = data.azurerm_shared_image.image.publisher
product = data.azurerm_shared_image.image.offer
}
However, I get an error stating:
Error: Unsupported attribute
This object has no argument, nested block, or exported attribute named "sku".
for each sku, publisher and offer.
We can get a better picture of what we are dealing with to add an output for testing purposes, like so:
output "imagedata" {
value = data.azurerm_shared_image.image
}
We get an important insight:
imagedata = {
"description" = ""
"eula" = ""
"gallery_name" = [removed]
"id" = [removed]
"identifier" = [
{
"offer" = "cis-centos-7-l1"
"publisher" = "center-for-internet-security-inc"
"sku" = "cis-centos75-l1"
},
]
"location" = [removed]
"name" = [removed]
"os_type" = "Linux"
"privacy_statement_uri" = ""
"release_note_uri" = ""
"resource_group_name" = [removed]
"tags" = {}
}
Ah now using data.azurerm_shared_image.image.identifier.sku won't just work here either. As far as Terraform is concerned, the identifier block is only one element in an array.
Solution:
We retrieve the data still the same way:
data "azurerm_shared_image" "image" {
name = var.image_name
gallery_name = var.gallery_name
resource_group_name = var.rsg_name
}
And we reference it in our virtual_machine resource like so:
storage_image_reference {
id = data.azurerm_shared_image.image.id
}
plan {
name = data.azurerm_shared_image.image.identifier[0].sku
publisher = data.azurerm_shared_image.image.identifier[0].publisher
product = data.azurerm_shared_image.image.identifier[0].offer
}

AWS Codepipeline with Terraform - How to dynamically create stages

I designed an AWS codepipeline module using terraform module, I have multiple actual codepipelines using the codepipeline module. I use module as design pattern because all the codepipelines look similar, except that some of the codepipelines need approval stages, some do not need. How do I design the codepipeline module approval stages so that the actual codepipelines can be created based on different needs?
I tried to use count = 0 or 1 to control the stage but it does not work because the stage is not resource-level. Is there any tricky way or workaround?
I feel this link asked the similar question but I cannot figure out what is the answer:
Terraform & AWS CodePipeline - Dynamically define actions on a stage
Here is my codepipeline terraform module:
resource "aws_codepipeline" "dev" {
name = "my_codepipeline"
role_arn = ...
...
stage {
name = "Source"
...
}
stage {
name = "test"
...
}
stage {
# count = 0 # or 1. it does not work
name = "Approval"
action {
name = "Approval"
owner = "AWS"
category = "Approval"
provider = "Manual"
version = "1"
configuration {
NotificationArn = "..."
CustomData = "..."
ExternalEntityLink = "..."
}
}
}
stage {
name = "prod"
...
}
}
To dynamically add a stage (and not just an action) you could do the following:
dynamic "stage" {
for_each = var.production_approval ? [1] : []
content {
name = "Approve"
action {
configuration = {
NotificationArn = var.approve_sns_arn
CustomData = var.approve_comment
}
name = "Production-Approval"
category = "Approval"
owner = "AWS"
provider = "Manual"
version = "1"
}
}
}
When going through your use case, I have the feeling that it is very suitable with new terraform feature in v0.12.x
Below is a sample on how to use for_each to set dynamic target regions, you should be fine to do the same for stages.
dynamic "target_region" {
for_each = var.target_image_regions
content {
name = target_region.value
regional_replica_count = 1
}
}
let me know if this works for you or not.
Reference: https://www.hashicorp.com/blog/announcing-terraform-0-12
I figured that you can get this working in Terraform 0.12+ as BMW said, but only if you have number of block greater than 0.
At least 1 "action" blocks are required.
Unfortunately, my (and your) use case required 0/1 actions depending on the environment so we have to manage it manually for a while.
Cheers.
dynamic "action" {
for_each = local.production_approval # e.g. [] || [true]
content {
category = "Approval"
configuration = {}
input_artifacts = []
name = "Production-Approval"
output_artifacts = []
owner = "AWS"
provider = "Manual"
run_order = 1
version = "1"
}
}

Resources