Terraform: Multiple ACM certificates - terraform

I'm trying to write some TF that, given a single FQDN for a site, will generate an ACM certificate, create the R53 records for validation and run the validation in a single TF pass.
I'm not using subdomains, and I got it working for a single FQDN, but as is the nature with TF, I want to be able to add another FQDN to the variable in the future to have multiple certs.
When I run the below code I get the error:
Error: Error running plan: 1 error occurred:
* aws_acm_certificate_validation.cert: 2 errors occurred:
* aws_acm_certificate_validation.cert[0]: Resource 'aws_route53_record.cert_validation' does not have attribute 'fqdn' for variable 'aws_route53_record.cert_validation.*.fqdn'
* aws_acm_certificate_validation.cert[1]: Resource 'aws_route53_record.cert_validation' does not have attribute 'fqdn' for variable 'aws_route53_record.cert_validation.*.fqdn'
But I know that the R53 record does export the fqdn attribute.
acm.tf:
resource "aws_acm_certificate" "cert" {
count = "${length(var.certificate_fqdns)}"
domain_name = "${element(var.certificate_fqdns, count.index)}"
validation_method = "DNS"
tags = "${local.all_tags}"
lifecycle {
create_before_destroy = true
}
}
resource "aws_route53_record" "cert_validation" {
count = "${length(var.certificate_fqdns)}"
name = "${lookup(local.domain_validation_options[count.index], "resource_record_name")}"
type = "${lookup(local.domain_validation_options[count.index], "resource_record_type")}"
zone_id = "${data.aws_route53_zone.cert_fqdn_zone.*.id}"
records = ["${lookup(local.domain_validation_options[count.index], "resource_record_value")}"]
ttl = 60
}
resource "aws_acm_certificate_validation" "cert" {
count = "${length(var.certificate_fqdns)}"
certificate_arn = "${element(aws_acm_certificate.cert.*.arn, count.index)}"
validation_record_fqdns = ["${aws_route53_record.cert_validation.*.fqdn}"]
}
variables.tf:
variable "certificate_fqdns" {
description = "The FQDNs to be used to create ACM certificates."
type = "list"
default = []
}
locals {
domain_validation_options = "${flatten(aws_acm_certificate.cert.*.domain_validation_options)}"
}
data "aws_route53_zone" "cert_fqdn_zone" {
name = "${element(var.certificate_fqdns, count.index)}"
}
and my vars file contains an entry like this:
"certificate_fqdns": [
"example.com"
]
EDIT: Added the data lookup for the Route53 Zones, which only seems to return the zone for the first domain provided in the variable even when there are multiple, different domains. i.e. example1.com and example2.com it will use the same zone ID for both sets of R53 records which will obviously fail

Related

Azure terraform module for container registry - dynamic block doesn’t remove IP addresses when emptying white listed IP list to complete zero

I have written a terraform module for Azure Container Registry (ACR). I would like to have the option to make ACR either publicly available or be available to selected networks only and switch between these two. By selected networks I mean specific subnets or IPs are whitelisted. If no subnet or IP list is provided, the ACR will be public. Otherwise, it will be available via selected networks.
This is how I have defined IP list and subnet list in variables.tf file:
variable "allowed_subnet_ids" {
type = list(string)
description = "List of subnet IDs to be allowed to access the ACR"
}
variable "allowed_ips" {
type = list(string)
description = "White list IP addresses"
}
variable "public_network_access_enabled"{
type = bool
description = "(Optional) Whether public network access is allowed for the container registry. Defaults to true."
}
I have made the network_rule_set property optional by using dynamic block in main.tf as follows:
resource "azurerm_container_registry" "this" {
name = local.acr_name
resource_group_name = var.resource_group_name
location = var.location
sku = var.sku
admin_enabled = var.admin_enabled
public_network_access_enabled = var.public_network_access_enabled
dynamic "network_rule_set" {
for_each = (length(var.allowed_ips) != 0 || length(var.allowed_subnet_ids) != 0) ? [1] : []
content {
default_action = "Deny"
dynamic "virtual_network" {
for_each = var.allowed_subnet_ids
content {
action = "Allow"
subnet_id = virtual_network.value
}
}
dynamic "ip_rule" {
for_each = var.allowed_ips
content {
action = "Allow"
ip_range = ip_rule.value
}
}
}
}
Network_rule_set allows white listing IPs and subnets in ACR and making it optionally public or private by using dynamic block as shown above.
To provide variable values, I have used a terraform.tfvars as follows:
env = "sdbx"
application_id = "appid"
resource_group_name = "rg-sbx"
role = "public"
location = "westeurope"
allowed_ips = [ "84.x.x.x", "51.x.x.x"]
# allowed_ips = []
allowed_subnet_ids = []
public_network_access_enabled = true
Here is the question: There is one serious issue though. If we have a list of IPs to be white listed, it will work. If we later decide to remove IPs from this list or change them, but the list is still not EMPTY, it will work as expected. But if you initiate the ACR with a list of IPs (non-empty list) and later you decide to empty it like
allowed_ips = []
It will skip the block and will not remove those IPs! Does anyone have any solutions for it? I want the block to be able to switch between public and selected networks as shown in Azure portal. In other words I want the dynamic block be able to shrink the IP list to zero when I replace the allowed_ips to an empty list and this way make my ACR public.
Here you can see in the images below how the network should toggle between two states which is my end goal:
ACR available by selected networks and white listed IPs:
ACR available publicly when no allopwed_ips or allowed_subnet_ids are provided should make it a public ACR:
For completeness here is my terraform provider and specifications:
terraform {
required_version = ">= 1.0.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">=3.0.0"
}
}
}
provider "azurerm" {
features {}
}
In order to switch between networks when ACR is either publicly available or available to selected networks only, we need to keep a condition for network_rule_set Detailed code as below.
Created a ACR by allowing all Networks
Step1:
Here is the latest code which i have added
**main.tf file as follows: **
provider "azurerm" {
features {}
}
resource "azurerm_container_registry" "acr_name" {
name = "acrswarna"
resource_group_name = var.resource_group_name
location = var.location
sku = var.sku
admin_enabled = var.admin_enabled
// Disable this code block for allowed ip network - Begin
network_rule_set {
default_action = "Allow"
}
// Disable this code block for allowed ip network - End
dynamic "network_rule_set" {
for_each = (length(var.allowed_ips) != 0 || length(var.allowed_subnet_ids) != 0) ? [1] : []
content {
default_action = "Deny"
dynamic "virtual_network" {
for_each = var.allowed_subnet_ids
content {
action = "Allow"
subnet_id = virtual_network.value
}
}
dynamic "ip_rule" {
for_each = var.allowed_ips
content {
action = "Allow"
ip_range = ip_rule.value
}}}}}
variable tf file as
variable "allowed_subnet_ids" {
type = list(string)
description = "List of subnet IDs to be allowed to access the ACR"
}
variable "allowed_ips" {
type = list(string)
description = "White list IP addresses"
}
variable "public_network_access_enabled"{
type = bool
description = "(Optional) Whether public network access is allowed for the container registry. Defaults to true."
}
variable "admin_enabled"{
type = bool
description = "(Optional) Whether public network access is allowed for the container registry. Defaults to true."
}
variable "sku" {
type = string
description = "SKU"
}
variable "resource_group_name" {
type = string
description = "resource_group_name"
}
variable "location" {
type = string
description = "location"
}
Terraform.tfvar file code
env = "sdbx"
application_id = "appid"
resource_group_name = "rg-swarna"
role = "public"
location = "westeurope"
//Disable/ Enable the allowed ips when ever need if it was empty All Network will allow and if any ips its allowed only required range
//allowed_ips = ["84.1.2.3", "51.3.4.2"]
allowed_ips = []
allowed_subnet_ids = []
admin_enabled = true
sku="Premium"
public_network_access_enabled = true
Step2:
Run below commands
terraform plan -var-file .\terraform.tfvars
terraform apply -var-file .\terraform.tfvars -auto-approve
Step3:
After Terraform apply on azure portal we can see the ACR,
Step4:
Updated the code with selected range of IPs and re-run the terraform code
Changes to be done on
Replace below code in terraform.tfvars
//Disable/ Enable the allowed ips when ever need if it was empty All Network will allow and if any ips its allowed only required range
allowed_ips = ["84.1.2.3", "51.3.4.2"]
//allowed_ips = []
main tf file - replace this below code
# // Disable this code block for allowed ip network - Begin
# network_rule_set {
# default_action = "Allow"
# }
# // Disable this code block for allowed ip network - End
Repeat Step2
Here is the output from the portal once after implementation. with selected IP ranges
Roll-back process to keep All Network as default and removed above IP Ranges on portal.
Changes to be done on
Replace below code in terraform.tfvars
//Disable/ Enable the allowed ips when ever need if it was empty All Network will allow and if any ips its allowed only required range
//allowed_ips = ["84.1.2.3", "51.3.4.2"]
allowed_ips = []
main.tf file - replace this below code
// Disable this code block for allowed ip network - Begin
network_rule_set {
default_action = "Allow"
}
// Disable this code block for allowed ip network - End
Repeat Step2
OUTPUT
After terraform apply you can be able to see the traffic route to All Network and removed respective IP address on portal.

Default DNS records in every zone managed via terraform (eg. MX records)

I'm looking for a way to manage cloudflare zones and records with terraform and create some default records (eg. MX) in every zone that is managed via terraform, something like this:
resource "cloudflare_zone" "example_net" {
type = "full"
zone = "example.net"
}
resource "cloudflare_zone" "example_com" {
type = "full"
zone = "example.com"
}
resource "cloudflare_record" "mxrecord"{
for_each=cloudflare_zone.*
name = "${each.value.zone}"
priority = "1"
proxied = "false"
ttl = "1"
type = "MX"
value = "mail.foo.bar"
zone_id = each.value.id
}
Does anyone have a clue for me how to achieve this (and if this is even possible...)?
Thanks a lot!
You could create a module responsible for the zone resource, e.g.:
# modules/cf_zone/main.tf
resource "cloudflare_zone" "cf_zone" {
type = "full"
zone = var.zone_name
}
resource "cloudflare_record" "mxrecord"{
name = "${cloudflare_zone.cf_zone.name}"
priority = "1"
proxied = "false"
ttl = "1"
type = "MX"
value = "mail.foo.bar"
zone_id = "${cloudflare_zone.cf_zone.id}"
}
# main.tf
module "example_net" {
source = "./modules/cf_zone"
zone_name = "example_net"
}
module "example_com" {
source = "./modules/cf_zone"
zone_name = "example_com"
}
This would give you an advantage on creation of default resources and settings per zone (DNS entries, security settings, page rules, etc.). It is also a good way to keep all the default values in a single place for review.
You can ready more about terraform modules here.
This is easy to do if you use a module, as was correctly noted in the other answer, but you don't have to create one, you can use this module.
Then your configuration will look like this:
terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
}
}
}
variable "cloudflare_api_token" {
type = string
sensitive = true
description = "The Cloudflare API token."
}
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
locals {
domains = [
"example.com",
"example.net"
]
mx = "mail.foo.bar"
}
module "domains" {
source = "registry.terraform.io/alex-feel/zone/cloudflare"
version = "1.8.0"
for_each = toset(local.domains)
zone = each.value
records = [
{
record_name = "mx_1"
type = "MX"
value = local.mx
priority = 1
}
]
}
You can find an example of using this module that matches your question here.

How do you assign the output value of one resource as input to another?

I have two .tf files in my root module:
the first one is called api-gateway.tf which provision an API Gateway in AWS:
resource "aws_apigatewayv2_api" "apiGateway" {
name = "some_Name"
protocol_type = "HTTP"
}
output "api_gateway_endpoint" {
value = "${aws_apigatewayv2_api.apiGateway.api_endpoint}"
}
output "api_gateway_endpoint_id" {
value = "${aws_apigatewayv2_api.apiGateway.id}"
}
I have got another .tf file called route53.tf which creates a Route53 record :
resource "aws_route53_record" "www" {
zone_id = "xxxxx"
name = "someurl.com"
type = "A"
alias {
name = "${output.api_gateway_endpoint}"
zone_id = "${output.api_gateway_endpoint_id}"
evaluate_target_health = false
}
}
I need to pass the api_endpoint and id of the apigateway to route53, but I don't know how?
I have tried returning these two values using output and reference that inside the route53 resource, however it doesn't work. It gives me an undeclared resource error.
How do you assign the output value of one resource as input to another?
Assuming that you're processing both TF files together, there is need to use output vars, that is simply a reference.
zone_id = "${aws_apigatewayv2_api.apiGateway.regional_zone_id}"
As an example, my api gateway block (in my api_gateway.tf) is
resource "aws_api_gateway_domain_name" "devapi"
{
domain_name = "${var.appLower}.${local.domain}"
regional_certificate_arn = "${data.aws_acm_certificate.mts.arn}"
endpoint_configuration {
types = ["REGIONAL"]
}
}
whereas my route53 block (in my route53.tf) looks like this
resource "aws_route53_record" "devapi"
{
name = "${aws_api_gateway_domain_name.devapi.domain_name}"
type = "A"
zone_id = "${data.aws_route53_zone.mts.id}"
alias {
evaluate_target_health = true
name = "${aws_api_gateway_domain_name.devapi.regional_domain_name}"
zone_id = "${aws_api_gateway_domain_name.devapi.regional_zone_id}"
}
}

How do I make terraform skip that block while creating multiple resources in loop from a CSV file?

Hi I am trying to create a Terraform script which will take inputs from the user in the form of a CSV file and create multiple Azure resources.
For example if the user wants to create: ResourceGroup>Vnet>Subnet in bulk, he will provide input in CSV format as below:
resourcegroup,RG_location,RG_tag,domainname,DNS_Zone_tag,virtualnetwork,VNET_location,addressspace
csvrg1,eastus2,Terraform RG,test.sd,Terraform RG,csvvnet1,eastus2,10.0.0.0/16,Terraform VNET,subnet1,10.0.0.0/24
csvrg2,westus,Terraform RG2,test2.sd,Terraform RG2,csvvnet2,westus,172.0.0.0/8,Terraform VNET2,subnet1,171.0.0.0/24
I have written the following working main.tf file:
# Configure the Microsoft Azure Provider
provider "azurerm" {
version = "=1.43.0"
subscription_id = var.subscription
tenant_id = var.tenant
client_id = var.client
client_secret = var.secret
}
#Decoding the csv file
locals {
vmcsv = csvdecode(file("${path.module}/computelanding.csv"))
}
# Create a resource group if it doesn’t exist
resource "azurerm_resource_group" "myterraformgroup" {
count = length(local.vmcsv)
name = local.vmcsv[count.index].resourcegroup
location = local.vmcsv[count.index].RG_location
tags = {
environment = local.vmcsv[count.index].RG_tag
}
}
# Create a DNS Zone
resource "azurerm_dns_zone" "dnsp-private" {
count = 1
name = local.vmcsv[count.index].domainname
resource_group_name = local.vmcsv[count.index].resourcegroup
depends_on = [azurerm_resource_group.myterraformgroup]
tags = {
environment = local.vmcsv[count.index].DNS_Zone_tag
}
}
To be continued....
The issue I am facing here what is in the second resource group, the user don't want a resource type, suppose the user want to skip the DNS zone in the resource group csvrg2. How do I make terraform skip that block ?
Edit: What I am trying to achieve is "based on some condition in the CSV file, not to create azurerm_dns_zone resource for the resource group csvrg2"
I have provided an example of the CSV file, how it may look like below:
resourcegroup,RG_location,RG_tag,DNS_required,domainname,DNS_Zone_tag,virtualnetwork,VNET_location,addressspace
csvrg1,eastus2,Terraform RG,1,test.sd,Terraform RG,csvvnet1,eastus2,10.0.0.0/16,Terraform VNET,subnet1,10.0.0.0/24
csvrg2,westus,Terraform RG2,0,test2.sd,Terraform RG2,csvvnet2,westus,172.0.0.0/8,Terraform VNET2,subnet1,171.0.0.0/24
you had already the right thought in your mind using the depends_on function. Although, you're using a count inside, which causes from my understanding, that once the first resource[0] is created, Terraform sees the dependency as solved and goes ahead as well.
I found this post with a workaround which you might be able to try:
https://github.com/hashicorp/terraform/issues/15285#issuecomment-447971852
That basically tells us to create a null_resource like in that example:
variable "instance_count" {
default = 0
}
resource "null_resource" "a" {
count = var.instance_count
}
resource "null_resource" "b" {
depends_on = [null_resource.a]
}
In your example, it might look like this:
# Create a resource group if it doesn’t exist
resource "azurerm_resource_group" "myterraformgroup" {
count = length(local.vmcsv)
name = local.vmcsv[count.index].resourcegroup
location = local.vmcsv[count.index].RG_location
tags = {
environment = local.vmcsv[count.index].RG_tag
}
}
# Create a DNS Zone
resource "azurerm_dns_zone" "dnsp-private" {
count = 1
name = local.vmcsv[count.index].domainname
resource_group_name = local.vmcsv[count.index].resourcegroup
depends_on = null_resource.example
tags = {
environment = local.vmcsv[count.index].DNS_Zone_tag
}
}
resource "null_resource" "example" {
...
depends_on = [azurerm_resource_group.myterraformgroup[length(local.vmcsv)]]
}
or depending on your Terraform version (0.12+ which you're using guessing your syntax)
# Create a resource group if it doesn’t exist
resource "azurerm_resource_group" "myterraformgroup" {
count = length(local.vmcsv)
name = local.vmcsv[count.index].resourcegroup
location = local.vmcsv[count.index].RG_location
tags = {
environment = local.vmcsv[count.index].RG_tag
}
}
# Create a DNS Zone
resource "azurerm_dns_zone" "dnsp-private" {
count = 1
name = local.vmcsv[count.index].domainname
resource_group_name = local.vmcsv[count.index].resourcegroup
depends_on = [azurerm_resource_group.myterraformgroup[length(local.vmcsv)]]
tags = {
environment = local.vmcsv[count.index].DNS_Zone_tag
}
}
I hope that helps.
Greetings

terraform add route53 record to multiple zones

I want to add the same route53 record into multiple zones, and I can't figure it out.
I'm trying this
resource "aws_route53_zone" "internal-common-domain" {
name = "${var.internal-common-domain}"
vpc_id = "${aws_vpc.VPC.id}"
}
resource "aws_route53_zone" "internal-office-domain" {
name = "${var.env}-${var.internal-common-domain}"
vpc_id = "${aws_vpc.VPC.id}"
}
resource "aws_route53_record" "middle-tier-lb" {
zone_id = "${aws_route53_zone.*.zone_id}"
name = "middle-tier-elb.${var.internal-common-domain}"
type = "CNAME"
ttl = "300"
records = ["${aws_alb.MiddleTierLoadBalancer.dns_name}"]
}
So rather than defining a record resource for each zone I want to add it to I just want to add this record to all the defined zones.
Note the splat in the record for zone_id, doesn't work, Can anyone tell me the most efficient way to do this in terraform ?

Resources