Terraform and GCS : how to create a multi zone LB - google-cloud-platform

I want to create a load balancer adressing two or more instances on several zones of the same region in GCP
I start like that :
- create a backend service adressing two instance groups :
resource "google_compute_backend_service" "www-service" {
name = "${var.environment}-www-service"
protocol = "HTTP"
port_name = "http"
backend {
group = "${google_compute_instance_group.instance-group-0.self_link}"
}
backend {
group = "${google_compute_instance_group.instance-group-1.self_link}"
}
health_checks = ["${google_compute_health_check.health-check1.self_link}"]
}
Then I have two instance groups, each one with a one instance with that syntax :
resource "google_compute_instance_group" "instance-group-0" {
count = "${var.web_count}"
name = "${var.environment}-instance-group-0"
instances = ["${google_compute_instance.www.self_link}"]
named_port {
name = "http"
port = "80"
}
network = "${google_compute_network.platform-network.self_link}"
}
I get an error :
google_compute_backend_service.www-service: Resource 'google_compute_instance_group.instance-group-0' not found for variable 'google_compute_instance_group.instance-group-0.self_link'
I see that switching the backend/group declarations in the backend_service moves the error to group-1, so I can guess that this is not the proper syntax, although you can create a backend_service with multiple instance groups in the Google GUI.
I have two questions :
Q1. How can I create backend_service with multiple instance groups ?
what is the right syntax ?
Q2. Is it possible to reference a compute_instance in a compute_instance_group via a syntax like :
instances = ["${google_compute_instance.www.[count.index].self_link}"]
(the above syntax does not work)

Thanks for your answer
Finally I found the syntax across multiple Github tickets :
resource "google_compute_instance_group" "instance-group-0" {
name = "${var.environment}-instance-group-0"
zone = "${data.google_compute_zones.available.names[0]}"
instances = ["${slice(google_compute_instance.www.*.self_link, 0, floor(var.web_count/2)-1)}"]
named_port {
name = "http"
port = "80"
}
network = "${google_compute_network.platform-network.self_link}"
}
same for instance group 1, but different slice
Then :
resource "google_compute_instance" "www" {
count = "${var.web_count}"
zone = "${data.google_compute_zones.available.names[floor((2*count.index)/var.web_count)]"}
I must say that different design decisions can be taken :
- use a region managed instance group is simpler, except the template is static
- use Kubernetes

A1 - each backend service can only use a single instance group. You need to create a new backend for each group. You can then assign multiple backends to your load balancer.
On another note, the error message is with regards to the resource type you are using. Here is the resource page for unmanaged Instance groups, Managed instance groups and for backend services. You can find a list of the different compute APIs here.
A2- you should use $(ref.name.selfLink) instead, I believe this syntax should work and you can still use your variable. Also, make sure to update the API call syntax for adding instances to the group

Related

Nesting for_each loops and configuring multiple regions in Python

We have a long(ish) list of ip addresses to trust, for services published by CloudFlare, and rather than asking each team that publishes a service from an account in aws to implement this in their security groups / acls etc. I thought a prefix list would be perfect. I would like to set this up in a central account, that is then shared to all the child accounts across the aws Organisation. Ideally, I would like to avoid declaring more resources than necessary, so I am using for_each loops and a dynamic entry. So far so good.
However, prefix lists are not a global object, meaning they need to be created per region, and Terraform requires that this is set on the Provider level.
Is there a way to have a single resource declaration work with a single local map to dynamically manage all these moving parts?
It seems the fewest steps I can do, is to have a resource declaration per region, and then a local map per provider, which is already making 6 "blocks" for only 3 regions, + 2 lists for ipv4 and ipv6...
Here's the code example:
locals {
# The two ip lists to add to appropriate prefix lists
cloudflare-ips = {
prefixlist_cloudflare_ipv4 = {
ips = [
"10.0.0.0/32",
"173.245.48.0/20",
...
],
type = "IPv4",
},
prefixlist_cloudflare_ipv6 = {
ips = [
"2400:cb00::/32",
"2606:4700::/32",
...
],
type = "IPv6",
}
}
}
# Generate multipel predix lists from the map of ipv4 and ipv6 addresses
resource "aws_ec2_managed_prefix_list" "cloudflare-ipv4" {
for_each = local.cloudflare-ips
name = "cloudflare_${each.value.type}"
# Here is where I have to add a provider for the region, and this cannot be done within any type of loop ?
provider = aws.ap-southeast-1
address_family = each.value.type
max_entries = 50
dynamic "entry" {
for_each = tolist( each.value.ips )
content {
cidr = entry.value
description = "CloudFlare ${entry.key}"
}
}
}
I've tried including a list of strings with region / provider names and calling these directly from inside the loop, but even though it can evaluate a string (e.g. provider = "aws.ap-southeast-1") it does not accept reading from the local map like so:
provider = each.value.region
with error:
│ The provider argument requires a provider type name, optionally followed by a period and then a configuration alias.
I guess the next step would be to make this into a module ? Any other suggestions... ?
this cannot be done within any type of loop
That's correct. You can't have dynamic provider, thus you can't use any loops and variables. Its value must be hardcoded.

Mapping multiple Security Groups into ELB

I'm trying to attach multiple security groups containing Cloudfront CIDRs to my AWS ALB.
locals {
chunks = chunklist(data.aws_ip_ranges.cloudfront.cidr_blocks, 60)
chunks_map = { for i in range(length(local.chunks)): i => local.chunks[i] }
}
resource "aws_security_group" "sg" {
for_each = local.chunks_map
name = "{each.key}"
egress {
....
}
}
resource "aws_elb" "load" {
name = "test"
security_groups = aws_security_group.sg.id // This is wrong
My error that I'm receiving is
Because aws_security_group.sg has for_each se, its attributes must be access on specific instances
Using for_each again doesn't make sense because i don't want to create multiple resources, I just want to ensure that all security groups created are attached to the load balancer. Any ideas?
Since you've used for_each there will be more than instance of aws_security_group.sg. To get id from all of them you can use splat operator:
security_groups = values(aws_security_group.sg)[*].id

Security groups should be able to communicate to other security groups

My company requires that I we expressly specify all allowed ports and protocols in security group ingress rules. I would like to have a long list of ports protocols and security groups to allow ingress/egress for
from_port, to_port, protocol, security_group_that_port_protocol_restriction_applies_to
The below example has the problem that the "master-sg-ingress-security-groups" variable needs to have the security groups to be defined.
resource "aws_security_group" "master_lb_sg" {
....
}
resource "aws_security_group" "worker_sg" {
......
}
########
####### list of port protocols and security groups to create ingress blocks for. Problem is that security groups to not exist at variable creation time.
########
variable "master-sg-ingress-security-groups" {
depends_on = [aws_security_group.master_lb_sg, aws_security_group.worker_sg]
description = "List of port numbers for specific security group. company bans allowing all ports and protocols. "
type = map(any)
default = {
"ingress1" = [80, 80, "TCP", aws_security_group.master_lb_sg],
"ingress2" = [443, 443, "TCP", aws_security_group.master_lb_sg],
"ingress3" = [3398,3398, "RDP", aws_security_group.bastion_host_sg],
....
"ingress4" = [1024, 1024, "UDP", aws_security_group.worker_sg]
}
}
#####
#### I want to iterate over the above list of security groups and create dynamic ingress rules but other security groups do not exist
####
resource "aws_security_group" "test" {
depends_on = [aws_security_group.master_lb_sg, aws_security_group.worker_sg]
provider = aws.region_master
name = "master-sg"
description = "security group for Jenkins master"
vpc_id = aws_vpc.vpc_master.id
dynamic "ingress" {
# this for_each is not identical to for_each in line 21
for_each = var.master-sg-ingress-security-groups
content {
from_port = ingress.value[0]
to_port = ingress.value[1]
protocol = ingress.value[2]
security_group = ingress[3]
}
}
}
I am think ing I have to just copy paste blocks of text for each ingress
Is there a way to get around the problem of aws_security_group.worker_sg in a variable
Sadly not from TF itself. Variables must be fully defined when you run your script. But you could maybe modify master-sg-ingress-security-groups into a local variable. This way you could construct your map which includes other variables.
So depending exactly on your use-case, you could maybe have a base variable called base-master-sg-ingress-security-groups, and then in locals construct a final map which would containe references to other existing SGs.
Alternatively, you could split your TF script into two parts. The first one would deploy core SGs and output their IDs. Then these IDs would be used as input variables for the second part which would deploy SGs that reference the core ones.

share multiple DNS domain with multiple aws accounts by terraform in AWS Resource Access Manager

I'm forwarding DNS requests sent to a list of internal domains (on premise) by using AWS Route53 resolver. By terraform, I want to share the rules I created to other accounts of the company, so I have the following:
# I create as much share endpoint as domain I have, so If I have 30 domains, I'll make 30 endpoint RAM:
resource "aws_ram_resource_share" "endpoint_share" {
count = length(var.forward_domain)
name = "route53-${var.forward_domain[count.index]}-share"
allow_external_principals = false
}
# Here I share every single endpoint with all the AWS ACcount we have
resource "aws_ram_principal_association" "endpoint_ram_principal" {
count = length(var.resource_share_accounts)
principal = var.resource_share_accounts[count.index]
resource_share_arn = {
for item in aws_ram_resource_share.endpoint_share[*]:
item.arn
}
}
The last block, calls the arn output of the first one which is a list.
Now, this last block doesn't work, I don't know how to use multiple counts, when I run this, I get the following error:
Error: Invalid 'for' expression
line 37: Key expression is required when building an object.
Any idea how to make this work?
Terraform version: 0.12.23
Use square brackets in resource_share_arn, like this:
resource_share_arn = [
for item in aws_ram_resource_share.endpoint_share[*]:
item.arn
]

Setting "count" based on the length of an attribute on another resource

I have a fairly simple Terraform configuration, which creates a Route53 zone and then creates NS records in Cloudflare to delegate the subdomain to that zone. At present, it assumes there's always exactly four authoritative DNS servers for every Route53 zone, and creates four separate cloudflare_record resources, but I'd like to generalise that, partially because who knows if AWS will start putting a fifth authoritative server out there in the future, but also as a "test case" for more complicated stuff in the future (like AWS AZs, which I know vary in count between regions).
What I've come up with so far is:
resource "cloudflare_record" "public-zone-ns" {
domain = "example.com"
name = "${terraform.env}"
type = "NS"
ttl = "120"
count = "${length(aws_route53_zone.public-zone.name_servers)}"
value = "${lookup(aws_route53_zone.public-zone.name_servers, count.index)}"
}
resource "aws_route53_zone" "public-zone" {
name = "${terraform.env}.example.com"
}
When I run terraform plan over this, though, I get this error:
Error running plan: 1 error(s) occurred:
* cloudflare_record.public-zone-ns: cloudflare_record.public-zone-ns: value of 'count' cannot be computed
I think what that means is that because the aws_route53_zone hasn't actually be created, terraform doesn't know what length(aws_route53_zone.public-zone.name_servers) is, and therefore the interpolation into cloudflare_record.public-zone-ns.count fails and I'm screwed.
However, it seems surprising to me that Terraform would be so inflexible; surely being able to create a variable number of resources like this would be meat-and-potatoes stuff. Hard-coding the length, or creating separate resources, just seems so... limiting.
So, what am I missing? How can I create a number of resources when I don't know in advance how many I need?
Currently count not being able to be calculated is an open issue in terraform https://github.com/hashicorp/terraform/issues/12570
You could move the name servers to a variable array and then get the length of that, all in one terraform script.
I got your point, this surprised me as well. Even I added depends_on to resource cloudflare_record, it is helpless.
What you can do to pass this issue is to split it into two stacks and make sure the route 53 record is created before cloudflare record.
Stack #1
resource "aws_route53_zone" "public-zone" {
name = "${terraform.env}.example.com"
}
output "name_servers" {
value = "${aws_route53_zone.public-zone.name_servers}"
}
Stack #2
data "terraform_remote_state" "route53" {
backend = "s3"
config {
bucket = "terraform-state-prod"
key = "network/terraform.tfstate"
region = "us-east-1"
}
}
resource "cloudflare_record" "public-zone-ns" {
domain = "example.com"
name = "${terraform.env}"
type = "NS"
ttl = "120"
count = "${length(data.terraform_remote_state.route53.name_servers)}"
value = "${element(data.terraform_remote_state.route53.name_servers, count.index)}"
}