Make terraform module creation fail based on a condition - terraform

I am trying to leverage precondition hook to check for an input to a module creation in terraform.
module "groups" {
source = "../path/to/groups"
for_each = var.groups.groups
name = each.key
type = each.value.type
policies = each.value.policies
depends_on = [
module.policies
]
lifecycle {
precondition {
condition = alltrue([ for item in self.policies :
alltrue([ for p in item : contains(locals.policies_list, p) ]) ] )
error_message = format("Attempt to create a group with a non existing policy")
}
}
}
However, although:
terraform --version --json
{
"terraform_version": "1.3.7",
"platform": "linux_amd64",
"provider_selections": {},
"terraform_outdated": false
}
This fails:
The block type name "lifecycle" is reserved for use by Terraform in a future version.
Is this because the specific functionality is not available in terraform for module creation? Is there a way around making my module creation fail based on the above condition?

Unfortunately the lifecycle block is not available for a module.
A way around this is the following
module "groups" {
source = "../path/to/groups"
for_each = var.groups.groups
name = each.key
type = each.value.type
policies = each.value.policies
depends_on = [
null_resource.group_check,
module.policies
]
}
resource "null_resource" "group_check" {
for_each = var.groups.groups
lifecycle {
precondition {
condition = alltrue([for p in each.value.policies : contains(local.policies_list, p)])
error_message = format("Attempt to create a group with a non existing policy")
}
}
depends_on = [
module.policies
]
}

Related

Terraform using depends_on with previous iteration of loop

I'm using a for_each to loop through the following variable:
networks = {
network1 = {
name = "NETWORK1"
},
network2 = {
name = "NETWORK2"
}
}
There is a dependency on the underlying API that a second network can only be created if the previous one has been created.
Therefore I wanted to use a depends_on in the below snippet:
resource "virtual_network" "network" {
depends_on = []
for_each = var.networks
parameters {
payload {
name = each.value.name
}
}
}
How can I make the network creation dependent on the previous iteration of the for_each loop?

DRY Solution For aws_iam_policy_document On Count Enabled Resources

Original Attempt
data "aws_iam_policy_document" "lambda_read_secrets" {
statement {
actions = [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret",
"secretsmanager:ListSecretVersionIds"
]
effect = "Allow"
resources = [
"${var.enable_test_users == true ? aws_secretsmanager_secret.test_user[0].arn : ""}",
"${var.enable_prod_users == true ? aws_secretsmanager_secret.prod_user[0].arn : ""}"
]
}
statement {
effect = "Allow"
actions = ["secretsmanager:ListSecrets"]
resources = ["*"]
}
}
The issue is that this runs into
Error: error creating IAM policy test-lambda-logging20211011172058509500000003: MalformedPolicyDocument: Resource must be in ARN format or "*".
status code: 400, request id: c5c62446-eba7-450d-b97d-505be530ba2d
on ../../../../module/lambda/iam.tf line 58, in resource "aws_iam_policy" "lambda_read_secrets":
58: resource "aws_iam_policy" "lambda_read_secrets" {
because of the empty string.
Current Solution
Create a data "aws_iam_policy_document" "dev_lambda_read_secrets" and data "aws_iam_policy_document" "prod-lambda_read_secrets" and do if statements on which environment we're deploying to.
My primary issue with this solution is that it requires me to essentially double declare the same policy with a tweaked set of resources. I would love to just be able to have a single policy declaration with only the resources changing.
Terraform has the compact function. This lets us declare
data "aws_iam_policy_document" "lambda_read_secrets" {
statement {
actions = [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret",
"secretsmanager:ListSecretVersionIds"
]
effect = "Allow"
resources = compact([
"${var.enable_test_users == true ? aws_secretsmanager_secret.test_user[0].arn : ""}",
"${var.enable_prod_users == true ? aws_secretsmanager_secret.prod_user[0].arn : ""}"
])
}
statement {
effect = "Allow"
actions = ["secretsmanager:ListSecrets"]
resources = ["*"]
}
}

Terraform AWS IAM Iterate Over Rendered JSON Policies

How can I iterate over the JSON rendered data.aws_iam_policy_document documents within an aws_iam_policy?
data "aws_iam_policy_document" "role_1" {
statement {
sid = "CloudFront1"
actions = [
"cloudfront:ListDistributions",
"cloudfront:ListStreamingDistributions"
]
resources = ["*"]
}
}
data "aws_iam_policy_document" "role_2" {
statement {
sid = "CloudFront2"
actions = [
"cloudfront:CreateInvalidation",
"cloudfront:GetDistribution",
"cloudfront:GetInvalidation",
"cloudfront:ListInvalidations"
]
resources = ["*"]
}
}
variable "role_policy_docs" {
type = list(string)
description = "Policies associated with Role"
default = [
"data.aws_iam_policy_document.role_1.json",
"data.aws_iam_policy_document.role_2.json",
]
}
locals {
role_policy_docs = { for s in var.role_policy_docs: index(var.role_policy_docs, s) => s}
}
resource "aws_iam_policy" "role" {
for_each = local.role_policy_docs
name = format("RolePolicy-%02d", each.key)
description = "Custom Policies for Role"
policy = each.value
}
resource "aws_iam_role_policy_attachment" "role" {
for_each = { for p in aws_iam_policy.role : p.name => p.arn }
role = aws_iam_role.role.name
policy_arn = each.value
}
This example has been reduced down to the very basics. The policy documents are dynamically generated with the source_json and override_json conventions. I cannot simply combine the statements into a single policy document.
Terraform Error:
Error: "policy" contains an invalid JSON policy
on role.tf line 35, in resource "aws_iam_policy" "role":
35: policy = each.value
This:
variable "role_policy_docs" {
type = list(string)
description = "Policies associated with Role"
default = [
"data.aws_iam_policy_document.role_1.json",
"data.aws_iam_policy_document.role_2.json",
]
}
Is literally defining those default values as strings, so what you're getting is this:
+ role_policy_docs = {
+ 0 = "data.aws_iam_policy_document.role_1.json"
+ 1 = "data.aws_iam_policy_document.role_2.json"
}
If you tried removing the quotations around the data blocks, it will not be valid because you cannot use variables in default definitions. Instead, assign your policy documents to a new local, and use that local in your for loop instead:
locals {
role_policies = [
data.aws_iam_policy_document.role_1.json,
data.aws_iam_policy_document.role_2.json,
]
role_policy_docs = {
for s in local.role_policies :
index(local.role_policies, s) => s
}
}

How to get Subnet list from VPC with terraform

I've tried to get all subnet ids to add aws batch with terraform with following code:
data "aws_subnet_ids" "test_subnet_ids" {
vpc_id = "default"
}
data "aws_subnet" "test_subnet" {
count = "${length(data.aws_subnet_ids.test_subnet_ids.ids)}"
id = "${tolist(data.aws_subnet_ids.test_subnet_ids.ids)[count.index]}"
}
output "subnet_cidr_blocks" {
value = ["${data.aws_subnet.test_subnet.*.id}"]
}
Fortunately, it was working fine when I've tested like that. But when I tried to integrate with batch terraform like:
resource "aws_batch_compute_environment" "test-qr-processor" {
compute_environment_name = "test-qr-processor-test"
compute_resources {
instance_role = "${aws_iam_instance_profile.test-ec2-role.arn}"
instance_type = [
"optimal"
]
max_vcpus = 256
min_vcpus = 0
security_group_ids = [
"${aws_security_group.test-processor-batch.id}"
]
subnets = ["${data.aws_subnet.test_subnet.*.id}"]
type = "EC2"
}
service_role = "${aws_iam_role.test-batch-service-role.arn}"
type = "MANAGED"
depends_on = [ "aws_iam_role_policy_attachment.test-batch-service-role" ]
}
I've encountered following error message,
Error: Incorrect attribute value type
on terraform.tf line 142, in resource
"aws_batch_compute_environment" "test-processor": 142: subnets =
["${data.aws_subnet.test_subnet.*.id}"]
Inappropriate value for attribute "subnets": element 0: string
required.
Please let me know why, thanks.
"${data.aws_subnet.test_subnet.*.id}" is already string array type.
you should input value without [ ]
write code like :
subnets = "${data.aws_subnet.test_subnet.*.id}"
See :
Here's A document about Resource: aws_batch_compute_environment

Terraform azure-remove subcription details from output

I declared security group in following way:
resource "azurerm_network_security_group" "wan" {
count = "${var.enable_wan_subnet ? 1 : 0}"
provider = "azurerm.base"
name = "${format("%s-%s", var.environment_name, "WAN-Subnet-Security-Group")}"
location = "${azurerm_resource_group.this.location}"
resource_group_name = "${azurerm_resource_group.this.name}"
tags = "${
merge(map("Name", format("%s-%s-%s",var.environment_name,"WAN-Subnets", "Security-Group")),
var.tags_global,
var.tags_module)
}"
}
and created output for that security group:
output "security_groups_id_wan" {
value = "${azurerm_network_security_group.wan.*.id}"
depends_on = [
"azurerm_subnet.wan",
]
}
In output i'm getting
Actual output
security_groups_id_wan = [
/subscriptions/111-222-333-4445/resourceGroups/default_resource_group/providers/Microsoft.Network/networkSecurityGroups/DF-DTAP-WAN-Subnet-Security-Group
]
How, from output, to remove all except resource name (DF-DTAP-WAN-Subnet-Security-Group)
Desired output:
security_groups_id_wan = [
DF-DTAP-WAN-Subnet-Security-Group
]
You can just use the Terraform functions and change the output value like this:
output "security_groups_id_wan" {
value = "${slice(split("/",azurerm_network_security_group.wan.*.id), length(split("/",azurerm_network_security_group.wan.*.id))-1, length(split("/",azurerm_network_security_group.wan.*.id)))}"
depends_on = [
"azurerm_subnet.wan",
]
}
With the functions, you can output every resource as you need. For more details, see Terraform Supported built-in functions.
Update
The test with an existing NSG through the Terraform data and the template here:
data "azurerm_network_security_group" "test" {
name = "azureUbuntu18-nsg"
resource_group_name = "charles"
}
output "substring" {
value = "${slice(split("/",data.azurerm_network_security_group.test.id), length(split("/",data.azurerm_network_security_group.test.id))-1, length(split("/",data.azurerm_network_security_group.test.id)))}"
}
The screenshot of the result here:
You built that name yourself with "${format("%s-%s", var.environment_name, "WAN-Subnet-Security-Group")}" so why not just output that?
To save repeating yourself you could put that in a local and refer to it in both the resource and the output:
locals {
security_group_name = "${format("%s-%s", var.environment_name, "WAN-Subnet-Security-Group")}"
}
resource "azurerm_network_security_group" "wan" {
count = "${var.enable_wan_subnet ? 1 : 0}"
provider = "azurerm.base"
name = "${local.security_group_name}"
# ...
}
output "security_groups_id_wan" {
value = "${local.security_group_name}"
}
Note that you also didn't need the depends_on because a) it's an output, it happens at the end of things anyway and b) you already have an implicit dependency on that resource because you used an interpolation that included the resource.
You can read more about Terraform dependencies via the Hashicorp Learn platform.
Addition to #Charles Xu's answer:Had to convert list to string first
output "subnets_id_wan" {
value = "${slice(split("/",join(",",azurerm_subnet.wan.*.id)), length(split("/",join(",",azurerm_subnet.wan.*.id)))-1, length(split("/",join(",",azurerm_subnet.wan.*.id))))}"
depends_on = [
"azurerm_subnet.wan",
]
}

Resources