Reference module with multiple resources - amazon-web-services

I am trying to create an AWS route53 hosted zone and add records to it. I added the following resources to a module main.tf
resource "aws_route53_zone" "zone" {
name = var.name
}
data "aws_route53_zone" "zone_data" {
name = var.name
}
resource "aws_route53_record" "record" {
zone_id = data.aws_route53_zone.zone_data.zone_id
name = var.record_name
type = var.record_type
ttl = var.record_ttl
records = var.record_value
}
Then I reference that module in a stack main.py as follows:
module "route53" {
source = "../../modules/route53"
name = "website.com"
type = "NS"
ttl = "30"
}
My issue is that building the stack will use the same name variable for both zone and record resources. How do I add another name to the stack module route53 for the record resource that is different from the zone resource?

If all you're trying to do in the module is create a zone and a record, you could use split to get the zone from the record name given. Like this:
main.tf
module "route53" {
source = "./modules/route53"
name = "something.website.com"
type = "NS"
ttl = "30"
}
modules/route53/main.tf
variable "name" {}
variable "type" {}
variable "ttl" {}
resource "aws_route53_zone" "this" {
name = split(".", var.name)[0]
}
resource "aws_route53_record" "this" {
zone_id = aws_route53_zone.this.zone_id
name = var.name
type = var.type
ttl = var.ttl
records = [var.name]
}
If however, you want multiple records in that zone, you could consider something like this, but this will depend heavily on what record configuration you're after.
main.tf
module "route53" {
source = "./modules/route53"
name = "website.com"
record_configs = {
something = {
type = "A"
records = ["192.168.0.99"]
}
other = {
type = "CNAME"
records = ["something.website.com"]
}
}
}
modules/route53/main.tf
variable "name" {}
variable "record_configs" {}
resource "aws_route53_zone" "this" {
name = var.name
}
resource "aws_route53_record" "this" {
for_each = var.record_configs
zone_id = aws_route53_zone.this.zone_id
name = each.key
type = each.value.type
records = each.value.records
}

If you have multiple records and names, the best way is to use for_each. For example:
variable "names" {
default = ["website.com", "website1.com", "website2.com"]
}
then
module "route53" {
source = "../../modules/route53"
for_each = toset(var.name)
name = each.key
type = "NS"
ttl = "30"
}
This way you can have same module for multiple names.

Randomly experimenting with solutions got me to add the resource variables' names as arguments to the module. This seems to allow referring to arguments of a specific resource in the root module if its argument name is the same as other resources (e.g. record_name vs name).
module "route53" {
source = "../../modules/route53"
name = "website.com"
record_name = "_auth.website.com"
record_type = "NS"
record_ttl = "30"
}

Related

Terraform for_each iteration over object

I'm relatively new to Terraform and I'm looking to simplify some private r53 zones I need to create using for_each.
I have the following local which I want to use to create private zones and associated A records in those zones:
locals {
private_zones = [
{
name = "foo.com"
txt = [
"This is the txt record for foo.com"]
ttl = 300
records = {
"host1" = "192.168.0.1",
"host2" = "192.168.0.2"
}
},
{
name = "bar.com"
txt = [
"This is the txt record for bar.com"]
ttl = 300
records = {
"host1" = "192.168.0.3",
"host2" = "192.168.0.4"
}
}
]
}
I've found some code which will allow me to iterate over the local to create the zones
resource "aws_route53_zone" "zone" {
for_each = { for name in local.private_zones : name.name => name }
name = each.value.name
vpc {
vpc_id = <vpc_id>
}
}
but I've no idea how I can iterate and create A records in the respective zone using the records list in each local.
You would use aws_route53_record and flattened private_zones:
locals {
private_zones_flat = merge([
for zone in local.private_zones: {
for host, ip in zone.records:
"${zone.name}-${host}" => {
zone_name = zone.name
host = host
ip = ip
}
}
]...)
}
resource "aws_route53_record" "host" {
for_each = local.private_zones_flat
zone_id = aws_route53_zone.zone[each.value.zone_name].zone_id
name = each.value.host
type = "A"
ttl = "300"
records = [each.value.ip]
}

Terraform: Iterating list for AWS Certificate validation with Cloudflare DNS

I have a map in a tfvars file that contains, Cloudflare zone id, site address, and zone (domain), I am wanting to iterate through that map, generating an ACM certificate, with a certificate validation DNS record being created in Cloudflare.
My map looks like this;
my_domains = {
example1 = {
cloudflare_zone_id = "00000000000000000000000000001"
address = "dev.example1.com"
domain = "example1.com"
}
example2 = {
cloudflare_zone_id = "0000000000000000000000000000002"
address = "dev.example2.com"
domain = "example2.com"
}
example3 = {
cloudflare_zone_id = "0000000000000000000000000000003"
address = "dev.library.example3.com"
domain = "example3.com"
}
}
I then have the following code for the certificate creation and validation:
resource "aws_acm_certificate" "my_certs" {
for_each = var.my_domains
domain_name = each.value.address
validation_method = "DNS"
subject_alternative_names = [
"*.${each.value.address}"
]
lifecycle {
create_before_destroy = true
}
}
resource "cloudflare_zone" "my_zone" {
for_each = var.my_domains
zone = each.value.domain
type = "full"
}
resource "cloudflare_record" "my_certificate_validation" {
for_each = {
for dvo in aws_acm_certificate.my_certs.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
zone_id = cloudflare_zone.my_zone.id
name = each.value.name
value = trimsuffix(each.value.record, ".")
type = each.value.type
ttl = 1
proxied = false
}
When I run a plan, I get the following errors:
Error: Missing resource instance key
on cfcertvalidation.tf line 23, in resource "cloudflare_record" "my_certificate_validation":
23: for dvo in aws_acm_certificate.my_certs.domain_validation_options : dvo.domain_name => {
Because aws_acm_certificate.my_certs has "for_each" set, its attributes must be
accessed on specific instances.
For example, to correlate with indices of a referring resource, use:
aws_acm_certificate.my_certs[each.key]
Error: Missing resource instance key
on cfcertvalidation.tf line 30, in resource "cloudflare_record" "my_certificate_validation":
30: zone_id = cloudflare_zone.my_zone.id
Because cloudflare_zone.cdt has "for_each" set, its attributes must be
accessed on specific instances.
For example, to correlate with indices of a referring resource, use:
cloudflare_zone.my_zone[each.key]
Note: I added the cloudflare_zone resource rther than using the zone id already in the map as a way to simplify things in troubleshooting.
I am sure the answer is in the suggestion for using a [each.key], but I'm not sure how to implement it.
Any assistance would be greatly appreciated.
I have changed the map somewhat for my solution, so for completeness I have included the changed map here:
variable "my_domains" {
type = map(any)
default = {
example1 = {
cf_zone_id = "0000000000000000000000000000"
address = "example1.com"
zone = "example1.com"
}
example2 = {
cf_zone_id = "0000000000000000000000000000"
address = "example2.com"
zone = "example2.com"
}
example3 = {
cf_zone_id = "0000000000000000000000000000"
address = "library.example3.com"
zone = "example3.com"
}
}
}
What follows is the working solution, we start out by creating a local variable of type list, looping through the my_domains map to get the cert validation records we need. That then gets converted into a map, which is then used by the cloudflare_record resource to create the relevant DNS entries.
resource "aws_acm_certificate" "my_certs" {
for_each = var.my_domains
domain_name = "${var.env_url_prefix}${var.my_domains[each.key] ["address"]}"
validation_method = "DNS"
subject_alternative_names = ["*.${var.env_url_prefix}${var.my_domains[each.key]["address"]}"]
lifecycle {
create_before_destroy = true
}
}
locals {
validation = [
for certs in keys(var.my_domains) : {
for dvo in aws_acm_certificate.my_certs[certs].domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
value = trimsuffix(dvo.resource_record_value, ".")
type = dvo.resource_record_type
zone_id = var.my_domains[certs]["cf_zone_id"] # Get the zone id
}
}
]
# transform the list into a map
validation_map = { for item in local.validation : keys(item)[0] => values(item)[0]
}
}
resource "cloudflare_record" "my_cert_validations" {
for_each = local.validation_map
zone_id = local.validation_map[each.key]["zone_id"]
name = local.validation_map[each.key]["name"]
value = local.validation_map[each.key]["value"]
type = local.validation_map[each.key]["type"]
ttl = 1
proxied = false #important otherwise validation will fail
}

Terraform for loop inside for_each argument

Can someone please explain what a for loop inside a for_each argument does in Terraform? I am trying to create an AWS SSL certificate. I've seen other code like the below but I don't understand it:
resource "aws_acm_certificate" "nonprod_cert" {
domain_name = var.phz_domain_name
validation_method = "DNS"
}
resource "aws_route53_record" "nonprod_cert_record" {
for_each = {
for dvo in aws_acm_certificate.nonprod_cert.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
zone_id = var.phz_id
name = each.value.name
type = each.value.type
records = [each.value.record]
ttl = 60
}
resource "aws_acm_certificate_validation" "nonprod_cert_validated" {
certificate_arn = aws_acm_certificate.nonprod_cert.arn
validation_record_fqdns = [for record in aws_route53_record.nonprod_cert_record : record.fqdn]
depends_on = [
aws_acm_certificate.nonprod_cert,
aws_route53_record.nonprod_cert_record
]
}
The specific line that I don't understand is the one in the route53 record. I get that a for_each argument can be used to create multiple resources from a single block, but I can't find anywhere that explains what this for loop is doing inside of it. If someone could explain that would be great!
The inner for "loop" creates the data that the for_each then iterates over. Specifically the each.key will be the dvo.domain_name and the each.value will be the
{
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
You could simply move that into a locals block beforehand and not have it in a single line:
locals {
records = {
for dvo in aws_acm_certificate.nonprod_cert.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
}
resource "aws_route53_record" "nonprod_cert_record" {
for_each = local.records
zone_id = var.phz_id
name = each.value.name
type = each.value.type
records = [each.value.record]
ttl = 60
}
That will have the exact same effect.
First they are using a for expression to convert one type of object into another type. In this case they are converting the list of domain_validation_options into a list of objects that can be used for creating aws_route53_record resources.
Next they are using for_each to create a new aws_route53_record resource for each element of the list that was generated by the for expression.
The key things to be aware of here are:
for is used to convert a list of objects into a list of different objects.
for_each is used to create multiple resources from a list, set, or map of values.
I highly recommend spending the time to go through the Terraform learning site, or at least the Terraform documentation to learn the basic keywords and overall syntax.

Creating multiple Alias records in Terraform with for_each

I am trying to create multiple alias DNS records in terraform using for_each. I am getting an error when specifying the name and zone ID of the Alias name. My question is how do I read from a variable defined as a set of strings? Below is my code block and variables defined:
resource "aws_route53_record" "alias_records" {
for_each = var.alias_names
zone_id = aws_route53_zone.zone.zone_id
name = each.key
type = "A"
alias {
name = var.alias_dns_names[each.key]
zone_id = var.alias_zone_ids[each.key]
evaluate_target_health = false
}
}
variable "alias_names" {
type = set(string)
}
variable "alias_dns_names" {
type = set(string)
}
variable "alias_zone_ids" {
type = set(string)
}
Error: Invalid index
on alias.tf line 8, in resource "aws_route53_record" "alias_records":
8: name = var.alias_dns_names[each.key]
This value does not have any indices.
Error: Invalid index
on alias.tf line 9, in resource "aws_route53_record" "alias_records":
9: zone_id = var.alias_zone_ids[each.key]
This value does not have any indices.
alias_names = [
"alias1",
"alias2",
"alias3"
}
alias_dns_names = [
"alias_dns_1",
"alias_dns_2",
"alias_dns_3"
}
alias_zone_ids = [
"alias_zone_1",
"alias_zone_2",
"alias_zone_3"
}
A set data structure is not ordered and its elements don't have any identifiers other than their values, so the set type is not appropriate for your use-case here.
Since you seem to be describing three different aspects of the same underlying object here, I think the most convenient way to expose that from your module would be as a map of objects, like this:
variable "aliases" {
type = map(object({
zone_id = string
dns_name = string
}))
}
You can then use this variable alone to capture all three of the values you need for each alias record:
aliases = {
alias1 = {
zone_id = "alias_zone_1"
dns_name = "alias_dns_1"
}
alias2 = {
zone_id = "alias_zone_2"
dns_name = "alias_dns_2"
}
alias3 = {
zone_id = "alias_zone_3"
dns_name = "alias_dns_3"
}
}
A map of any data type can be used directly with resource for_each, with each.key representing the map key and each.value representing the value. So you can then change your resource block like this:
resource "aws_route53_record" "alias_records" {
for_each = var.aliases
zone_id = aws_route53_zone.zone.zone_id
name = each.key
type = "A"
alias {
zone_id = each.value.zone_id
name = each.value.dns_name
evaluate_target_health = false
}
}
Because the elements of the map are objects, you can access them from each.value to get the zone_id and dns_name values that correspond with each key in the original map.

Terraform - Conditional Data Source

In terraform, is there any way to conditionally use a data source? For example:
data "aws_ami" "application" {
most_recent = true
filter {
name = "tag:environment"
values = ["${var.environment}"]
}
owners = ["self"]
}
I'm hoping to be able to pass in an environment variable via the command line, and based on that, determine whether or not to fetch this data source.
I know with resources you can use the count property, but it doesn't seem you can use that with data sources.
I would consider tucking this code away in a module, but modules also can't use the count parameter.
Lastly, another option would be to provide a "Default" value for the data source, if it returned null, but I don't think that's doable either.
Are there any other potential solutions for this?
You can use a conditional on data sources the same as you can with resources and also from Terraform 0.13+ on modules as well:
variable "lookup_ami" {
default = true
}
data "aws_ami" "application" {
count = var.lookup_ami ? 1 : 0
most_recent = true
filter {
name = "tag:environment"
values = [var.environment]
}
owners = ["self"]
}
One use case for this in Terraform 0.12+ is to utilise the lazy evaluation of ternary statements like with the following:
variable "internal" {
default = true
}
data "aws_route53_zone" "private_zone" {
count = var.internal ? 1 : 0
name = var.domain
vpc_id = var.vpc_id
private_zone = var.internal
}
data "aws_route53_zone" "public_zone" {
count = var.internal ? 0 : 1
name = var.domain
private_zone = var.internal
}
resource "aws_route53_record" "www" {
zone_id = var.internal ? data.aws_route53_zone.private_zone.zone_id : data.aws_route53_zone.public_zone.zone_id
name = "www.${var.domain}"
type = "A"
alias {
name = aws_elb.lb.dns_name
zone_id = aws_elb.lb.zone_id
evaluate_target_health = false
}
}
This would create a record in the private zone when var.internal is true and instead create a record in the public zone when var.internal is false.
For this specific use case you could also use Terraform 0.12+'s null to rewrite this more simply:
variable "internal" {
default = true
}
data "aws_route53_zone" "zone" {
name = var.domain
vpc_id = var.internal ? var.vpc_id : null
private_zone = var.internal
}
resource "aws_route53_record" "www" {
zone_id = data.aws_route53_zone.zone.zone_id
name = "www.${data.aws_route53_zone.zone.name}"
type = "A"
alias {
name = aws_elb.lb.dns_name
zone_id = aws_elb.lb.zone_id
evaluate_target_health = false
}
}
This would only pass the vpc_id parameter to the aws_route53_zone data source if var.internal is set to true as you can't set vpc_id when private_zone is false.
Old Terraform 0.11 and earlier answer:
You can in fact use a conditional on the count of data sources but I've yet to manage to work out a good use case for it when I've tried.
As an example I successfully had this working:
data "aws_route53_zone" "private_zone" {
count = "${var.internal == "true" ? 1 : 0}"
name = "${var.domain}"
vpc_id = "${var.vpc_id}"
private_zone = "true"
}
data "aws_route53_zone" "public_zone" {
count = "${var.internal == "true" ? 0 : 1}"
name = "${var.domain}"
private_zone = "false"
}
But then had issues in how to then select the output of it because Terraform will evaluate any variables in the ternary conditional before deciding which side of the ternary to use (instead of lazy evaluation). So something like this doesn't work:
resource "aws_route53_record" "www" {
zone_id = "${var.internal ? data.aws_route53_zone.private_zone.zone_id : data.aws_route53_zone.public_zone.zone_id}"
name = "www.example.com"
type = "A"
alias {
name = "${aws_elb.lb.dns_name}"
zone_id = "${aws_elb.lb.zone_id }"
evaluate_target_health = "false"
}
}
Because if internal is true then you get the private_zone data source but not the public_zone data source and so the second half of the ternary fails to evaluate because data.aws_route53_zone.public_zone.zone_id isn't defined and equally with the other way around too.
In your case you probably just want to conditionally use the data source so might be able to do something like this:
variable "dynamic_ami" { default = "true" }
variable "default_ami" { default = "ami-123456" }
data "aws_ami" "application" {
most_recent = true
filter {
name = "tag:environment"
values = ["${var.environment}"]
}
owners = ["self"]
}
resource "aws_instance" "app" {
ami = "${var.dynamic_ami == "true" ? data.aws_ami.application.id : var.default_ami}"
instance_type = "t2.micro"
}