diff --git a/examples/basic/main.tf b/examples/basic/main.tf index 58a4373..c35fbb6 100644 --- a/examples/basic/main.tf +++ b/examples/basic/main.tf @@ -27,4 +27,5 @@ module "this" { security_group_type = "egress" load_balancer_name = "${local.project_name}-lb" domain = local.domain + domain_zone = local.zone } diff --git a/examples/cert/main.tf b/examples/cert/main.tf index baf0909..d8399ac 100644 --- a/examples/cert/main.tf +++ b/examples/cert/main.tf @@ -27,5 +27,6 @@ module "this" { security_group_type = "project" load_balancer_name = "${local.project_name}-lb" domain = local.domain + domain_zone = local.zone cert_use_strategy = "create" } diff --git a/examples/domain/main.tf b/examples/domain/main.tf index 3a1eaf5..9f6487d 100644 --- a/examples/domain/main.tf +++ b/examples/domain/main.tf @@ -27,4 +27,5 @@ module "this" { security_group_type = "project" load_balancer_name = "${local.project_name}-lb" domain = local.domain + domain_zone = local.zone } diff --git a/examples/dualstack/main.tf b/examples/dualstack/main.tf new file mode 100644 index 0000000..0484771 --- /dev/null +++ b/examples/dualstack/main.tf @@ -0,0 +1,32 @@ + +provider "aws" { + default_tags { + tags = { + Id = local.identifier + Owner = "terraform-ci@suse.com" + } + } +} +provider "acme" { + server_url = "https://acme-staging-v02.api.letsencrypt.org/directory" +} +locals { + identifier = var.identifier + example = "dualstack" + project_name = "tf-${substr(md5(join("-", [local.example, md5(local.identifier)])), 0, 5)}-${local.identifier}" + zone = var.zone + domain = "${local.identifier}.${local.zone}" +} + +# AWS reserves the first four IP addresses and the last IP address in any CIDR block for its own use (cumulatively) +# gives 256 usable addresses from .1 to .254, but AWS reserves .1 to .4 and .255, leaving .5 to .254 +module "this" { + source = "../../" + vpc_name = "${local.project_name}-vpc" + vpc_type = "dualstack" + security_group_name = "${local.project_name}-sg" + security_group_type = "egress" + load_balancer_name = "${local.project_name}-lb" + domain = local.domain + domain_zone = local.zone +} diff --git a/examples/dualstack/outputs.tf b/examples/dualstack/outputs.tf new file mode 100644 index 0000000..9c7a45e --- /dev/null +++ b/examples/dualstack/outputs.tf @@ -0,0 +1,18 @@ +output "vpc" { + value = module.this.vpc +} +output "subnets" { + value = module.this.subnets +} +output "security_group" { + value = module.this.security_group +} +output "load_balancer" { + value = module.this.load_balancer +} +output "domain" { + value = module.this.domain +} +output "certificate" { + value = module.this.certificate +} diff --git a/examples/dualstack/variables.tf b/examples/dualstack/variables.tf new file mode 100644 index 0000000..cf5f8cf --- /dev/null +++ b/examples/dualstack/variables.tf @@ -0,0 +1,6 @@ +variable "identifier" { + type = string +} +variable "zone" { + type = string +} diff --git a/examples/dualstack/versions.tf b/examples/dualstack/versions.tf new file mode 100644 index 0000000..ee03d75 --- /dev/null +++ b/examples/dualstack/versions.tf @@ -0,0 +1,21 @@ +terraform { + required_version = ">= 1.5.0, < 1.6" + required_providers { + local = { + source = "hashicorp/local" + version = ">= 2.4" + } + aws = { + source = "hashicorp/aws" + version = ">= 5.11" + } + random = { + source = "hashicorp/random" + version = ">= 3.1" + } + acme = { + source = "vancluever/acme" + version = ">= 2.0" + } + } +} diff --git a/examples/ingress/main.tf b/examples/ingress/main.tf index 29b9292..af73df0 100644 --- a/examples/ingress/main.tf +++ b/examples/ingress/main.tf @@ -26,6 +26,7 @@ module "this" { security_group_name = "${local.project_name}-sg" security_group_type = "egress" domain = local.domain + domain_zone = local.zone load_balancer_name = "${local.project_name}-lb" load_balancer_access_cidrs = { application = { diff --git a/examples/ipv6/main.tf b/examples/ipv6/main.tf new file mode 100644 index 0000000..ad888d5 --- /dev/null +++ b/examples/ipv6/main.tf @@ -0,0 +1,33 @@ + +provider "aws" { + default_tags { + tags = { + Id = local.identifier + Owner = "terraform-ci@suse.com" + } + } +} +provider "acme" { + server_url = "https://acme-staging-v02.api.letsencrypt.org/directory" +} +locals { + identifier = var.identifier + example = "ipv6" + project_name = "tf-${substr(md5(join("-", [local.example, md5(local.identifier)])), 0, 5)}-${local.identifier}" + zone = var.zone + domain = "${local.identifier}.${local.zone}" +} + +# AWS reserves the first four IP addresses and the last IP address in any CIDR block for its own use (cumulatively) +# gives 256 usable addresses from .1 to .254, but AWS reserves .1 to .4 and .255, leaving .5 to .254 +module "this" { + source = "../../" + vpc_name = "${local.project_name}-vpc" + vpc_type = "ipv6" + subnet_use_strategy = "create" + security_group_name = "${local.project_name}-sg" + security_group_type = "egress" + load_balancer_name = "${local.project_name}-lb" + domain = local.domain + domain_zone = local.zone +} diff --git a/examples/ipv6/outputs.tf b/examples/ipv6/outputs.tf new file mode 100644 index 0000000..58ae582 --- /dev/null +++ b/examples/ipv6/outputs.tf @@ -0,0 +1,21 @@ +output "vpc" { + value = module.this.vpc +} +output "subnets" { + value = module.this.subnets +} +output "security_group" { + value = module.this.security_group +} +output "load_balancer" { + value = module.this.load_balancer +} +output "domain" { + value = module.this.domain +} +output "certificate" { + value = module.this.certificate +} +output "subnet_map" { + value = module.this.subnet_map +} diff --git a/examples/ipv6/variables.tf b/examples/ipv6/variables.tf new file mode 100644 index 0000000..cf5f8cf --- /dev/null +++ b/examples/ipv6/variables.tf @@ -0,0 +1,6 @@ +variable "identifier" { + type = string +} +variable "zone" { + type = string +} diff --git a/examples/ipv6/versions.tf b/examples/ipv6/versions.tf new file mode 100644 index 0000000..ee03d75 --- /dev/null +++ b/examples/ipv6/versions.tf @@ -0,0 +1,21 @@ +terraform { + required_version = ">= 1.5.0, < 1.6" + required_providers { + local = { + source = "hashicorp/local" + version = ">= 2.4" + } + aws = { + source = "hashicorp/aws" + version = ">= 5.11" + } + random = { + source = "hashicorp/random" + version = ">= 3.1" + } + acme = { + source = "vancluever/acme" + version = ">= 2.0" + } + } +} diff --git a/examples/selectsubnets/main.tf b/examples/selectsubnets/main.tf new file mode 100644 index 0000000..69e95df --- /dev/null +++ b/examples/selectsubnets/main.tf @@ -0,0 +1,44 @@ + +provider "aws" { + default_tags { + tags = { + Id = local.identifier + Owner = "terraform-ci@suse.com" + } + } +} +provider "acme" { + server_url = "https://acme-staging-v02.api.letsencrypt.org/directory" +} +locals { + identifier = var.identifier + example = "selectsubnets" + project_name = "tf-${substr(md5(join("-", [local.example, md5(local.identifier)])), 0, 5)}-${local.identifier}" + zone = var.zone + domain = "${local.identifier}.${local.zone}" +} + +module "setup" { + source = "../../" + vpc_name = local.project_name + subnet_names = [local.project_name] + security_group_use_strategy = "skip" +} + +# AWS reserves the first four IP addresses and the last IP address in any CIDR block for its own use (cumulatively) +# gives 256 usable addresses from .1 to .254, but AWS reserves .1 to .4 and .255, leaving .5 to .254 +module "this" { + depends_on = [ + module.setup, + ] + source = "../../" + vpc_use_strategy = "select" + vpc_name = local.project_name + subnet_use_strategy = "select" + subnet_names = [local.project_name] + security_group_name = "${local.project_name}-sg" + security_group_type = "egress" + load_balancer_name = "${local.project_name}-lb" + domain = local.domain + domain_zone = local.zone +} diff --git a/examples/selectsubnets/outputs.tf b/examples/selectsubnets/outputs.tf new file mode 100644 index 0000000..9c7a45e --- /dev/null +++ b/examples/selectsubnets/outputs.tf @@ -0,0 +1,18 @@ +output "vpc" { + value = module.this.vpc +} +output "subnets" { + value = module.this.subnets +} +output "security_group" { + value = module.this.security_group +} +output "load_balancer" { + value = module.this.load_balancer +} +output "domain" { + value = module.this.domain +} +output "certificate" { + value = module.this.certificate +} diff --git a/examples/selectsubnets/variables.tf b/examples/selectsubnets/variables.tf new file mode 100644 index 0000000..cf5f8cf --- /dev/null +++ b/examples/selectsubnets/variables.tf @@ -0,0 +1,6 @@ +variable "identifier" { + type = string +} +variable "zone" { + type = string +} diff --git a/examples/selectsubnets/versions.tf b/examples/selectsubnets/versions.tf new file mode 100644 index 0000000..ee03d75 --- /dev/null +++ b/examples/selectsubnets/versions.tf @@ -0,0 +1,21 @@ +terraform { + required_version = ">= 1.5.0, < 1.6" + required_providers { + local = { + source = "hashicorp/local" + version = ">= 2.4" + } + aws = { + source = "hashicorp/aws" + version = ">= 5.11" + } + random = { + source = "hashicorp/random" + version = ">= 3.1" + } + acme = { + source = "vancluever/acme" + version = ">= 2.0" + } + } +} diff --git a/examples/selectvpc/main.tf b/examples/selectvpc/main.tf index a5079c3..7b97bb6 100644 --- a/examples/selectvpc/main.tf +++ b/examples/selectvpc/main.tf @@ -36,4 +36,5 @@ module "this" { security_group_type = "egress" load_balancer_name = "${local.project_name}-lb" domain = local.domain + domain_zone = local.zone } diff --git a/flake.lock b/flake.lock index 69a75c3..e63ddcf 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1718870667, - "narHash": "sha256-jab3Kpc8O1z3qxwVsCMHL4+18n5Wy/HHKyu1fcsF7gs=", + "lastModified": 1718983919, + "narHash": "sha256-+1xgeIow4gJeiwo4ETvMRvWoircnvb0JOt7NS9kUhoM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9b10b8f00cb5494795e5f51b39210fed4d2b0748", + "rev": "90338afd6177fc683a04d934199d693708c85a3b", "type": "github" }, "original": { diff --git a/main.tf b/main.tf index ee5f5ba..1d8ee3b 100644 --- a/main.tf +++ b/main.tf @@ -45,27 +45,78 @@ locals { # vpc vpc_name = var.vpc_name vpc_type = var.vpc_type - vpc_zones = var.vpc_zones vpc_public = var.vpc_public - vpc_cidr = var.vpc_cidr - - # subnet + vpc_zones = var.vpc_zones availability_zones = (length(local.vpc_zones) > 0 ? ({ for i in length(local.vpc_zones) : tostring(i) => local.vpc_zones[i] }) : ({ "0" = data.aws_availability_zones.available.names[0] }) ) + vpc_ipv4 = (local.vpc_mod > 0 ? module.vpc[0].ipv4 : null) + vpc_ipv6 = (local.vpc_mod > 0 ? module.vpc[0].ipv6 : null) + + # tflint-ignore: terraform_unused_declarations + fail_ipv6_missing = ( + ( + local.vpc_mod == 1 && + (local.vpc_type == "ipv6" || local.vpc_type == "dualstack") && + local.vpc_ipv6 == "" + ) ? + one([local.vpc_ipv6, "missing_ipv6_address"]) : + false + ) + # tflint-ignore: terraform_unused_declarations + fail_ipv4_missing = ( + ( + local.vpc_mod == 1 && + local.vpc_ipv4 == "" + ) ? + one([local.vpc_ipv4, "missing_ipv4_address"]) : + false + ) + + + # subnet + subnet_names = var.subnet_names + subnet_map = (length(local.subnet_names) > 0 ? + { for i in range((length(local.subnet_names) * local.subnet_mod)) : + tostring(i) => { + name = local.subnet_names[i] + ipv4_cidr = cidrsubnet(local.vpc_ipv4, 1, i) + ipv6_cidr = cidrsubnet(local.vpc_ipv6, 8, i) + az = local.availability_zones[i] + } + } : + { for i in range((length(local.availability_zones) * local.subnet_mod)) : + tostring(i) => { + name = "${local.vpc_name}-${local.availability_zones[i]}" + ipv4_cidr = cidrsubnet(local.vpc_ipv4, 1, i) + ipv6_cidr = cidrsubnet(local.vpc_ipv6, 8, i) + az = local.availability_zones[i] + } + } + ) + # tflint-ignore: terraform_unused_declarations + fail_subnet_map_length = ((local.subnet_mod == 1 && local.subnet_use_strategy == "create" && (length(local.subnet_map) != length(local.availability_zones))) ? one([jsonencode(local.subnet_names), "length_subnet_names_must_match_availability_zones"]) : false) + # tflint-ignore: terraform_unused_declarations + fail_subnet_map_empty = ((local.subnet_mod == 1 && local.subnet_use_strategy == "create" && (length(local.subnet_map) < 1)) ? one([jsonencode(local.subnet_map), "subnet_map_empty"]) : false) + # tflint-ignore: terraform_unused_declarations + fail_subnets_not_created = ((local.subnet_mod == 1 && length(module.subnet) < 1) ? one([local.subnet_mod, "subnets_not_created"]) : false) + # security group security_group_name = var.security_group_name security_group_type = var.security_group_type - # domain - domain = var.domain - cert_use_strategy = var.cert_use_strategy - # load balancer load_balancer_name = var.load_balancer_name load_balancer_access_cidrs = var.load_balancer_access_cidrs + + # domain + domain = var.domain + cert_use_strategy = var.cert_use_strategy + domain_zone = var.domain_zone + # tflint-ignore: terraform_unused_declarations + fail_domain_zone = ((local.domain_mod == 1 && local.domain_use_strategy != "skip" && local.domain_zone == "") ? one([local.domain_zone, "domain_zone_missing"]) : false) } data "aws_availability_zones" "available" { @@ -84,14 +135,15 @@ module "subnet" { depends_on = [ module.vpc, ] - for_each = (local.subnet_mod == 1 ? local.availability_zones : tomap({})) + for_each = (local.subnet_mod == 1 ? local.subnet_map : tomap({})) source = "./modules/subnet" use = local.subnet_use_strategy type = local.vpc_type vpc_id = module.vpc[0].id - vpc_cidr = local.vpc_cidr - name = "${local.vpc_name}-${each.value}" - availability_zone = each.value + ipv4_cidr = each.value.ipv4_cidr + ipv6_cidr = each.value.ipv6_cidr + name = each.value.name + availability_zone = each.value.az public = local.vpc_public } @@ -143,4 +195,6 @@ module "domain" { cert_use_strategy = local.cert_use_strategy content = lower(local.domain) ips = module.network_load_balancer[0].public_ips + domain_zone = local.domain_zone + vpc_type = local.vpc_type } diff --git a/modules/domain/main.tf b/modules/domain/main.tf index 3ce3a05..a2a9336 100644 --- a/modules/domain/main.tf +++ b/modules/domain/main.tf @@ -1,28 +1,21 @@ locals { - use = var.use - cert_use = var.cert_use_strategy - content = lower(var.content) - ips = var.ips - - content_parts = split(".", local.content) - top_level_domain = join(".", [ - local.content_parts[(length(local.content_parts) - 2)], - local.content_parts[(length(local.content_parts) - 1)], - ]) - subdomain = local.content_parts[0] - found_zone = join(".", [ - for part in local.content_parts : part if part != local.subdomain - ]) + use = var.use + cert_use = var.cert_use_strategy + content = lower(var.content) + ips = var.ips + vpc_type = var.vpc_type + domain_zone = var.domain_zone # zone - zone_id = data.aws_route53_zone.select[0].id - zone = local.found_zone - zone_select = 1 - zone_resource = data.aws_route53_zone.select[0] + zone_id = data.aws_route53_zone.select.id + zone = data.aws_route53_zone.select.name + zone_resource = data.aws_route53_zone.select # domain record create = (local.use == "create" ? 1 : 0) select = (local.use == "select" ? 1 : 0) + ipv6 = (local.vpc_type == "ipv6" ? local.create : 0) + ipv4ds = ((local.vpc_type == "ipv4" || local.vpc_type == "dualstack") ? local.create : 0) # cert create_cert = (local.cert_use == "create" ? 1 : 0) @@ -30,8 +23,7 @@ locals { } data "aws_route53_zone" "select" { - count = local.zone_select - name = local.zone + name = local.domain_zone } resource "aws_route53domains_registered_domain" "select" { @@ -39,11 +31,11 @@ resource "aws_route53domains_registered_domain" "select" { domain_name = local.content } -resource "aws_route53_record" "new" { +resource "aws_route53_record" "ipv4" { depends_on = [ data.aws_route53_zone.select, ] - count = local.create + count = local.ipv4ds zone_id = local.zone_id name = local.content type = "A" @@ -51,6 +43,18 @@ resource "aws_route53_record" "new" { records = local.ips } +resource "aws_route53_record" "ipv6" { + depends_on = [ + data.aws_route53_zone.select, + ] + count = local.ipv6 + zone_id = local.zone_id + name = local.content + type = "AAAA" + ttl = 30 + records = local.ips +} + # cert generation resource "tls_private_key" "private_key" { count = local.create_cert @@ -60,9 +64,12 @@ resource "tls_private_key" "private_key" { # Warning, this can lead to rate limiting if you are not careful # make sure you are not creating a new acme_registration for every certificate resource "acme_registration" "reg" { + depends_on = [ + data.aws_route53_zone.select, + ] count = local.create_cert account_key_pem = tls_private_key.private_key[0].private_key_pem - email_address = "${local.zone_id}@${local.top_level_domain}" + email_address = "${local.zone_id}@${local.zone}" } resource "tls_private_key" "cert_private_key" { @@ -80,7 +87,8 @@ resource "tls_cert_request" "req" { resource "acme_certificate" "new" { depends_on = [ data.aws_route53_zone.select, - aws_route53_record.new, + aws_route53_record.ipv4, + aws_route53_record.ipv6, acme_registration.reg, tls_private_key.private_key, tls_private_key.cert_private_key, @@ -106,7 +114,8 @@ resource "acme_certificate" "new" { resource "aws_iam_server_certificate" "new" { depends_on = [ data.aws_route53_zone.select, - aws_route53_record.new, + aws_route53_record.ipv4, + aws_route53_record.ipv6, acme_registration.reg, tls_private_key.private_key, tls_private_key.cert_private_key, @@ -125,7 +134,8 @@ resource "aws_iam_server_certificate" "new" { data "aws_iam_server_certificate" "select" { depends_on = [ data.aws_route53_zone.select, - aws_route53_record.new, + aws_route53_record.ipv4, + aws_route53_record.ipv6, acme_registration.reg, tls_private_key.private_key, tls_private_key.cert_private_key, diff --git a/modules/domain/outputs.tf b/modules/domain/outputs.tf index 5a8462b..68af270 100644 --- a/modules/domain/outputs.tf +++ b/modules/domain/outputs.tf @@ -1,5 +1,20 @@ output "id" { - value = (local.select == 1 ? aws_route53domains_registered_domain.select[0].id : aws_route53_record.new[0].id) + value = ( + local.select == 1 ? + aws_route53domains_registered_domain.select[0].id : + local.ipv4ds == 1 ? + aws_route53_record.ipv4[0].id : + aws_route53_record.ipv6[0] + ) +} +output "domain" { + value = ( + local.select == 1 ? + aws_route53domains_registered_domain.select[0] : + local.ipv4ds == 1 ? + aws_route53_record.ipv4[0] : + aws_route53_record.ipv6[0] + ) } output "zone_id" { value = local.zone_id @@ -7,9 +22,6 @@ output "zone_id" { output "zone" { value = local.zone_resource } -output "domain" { - value = (local.select == 1 ? aws_route53domains_registered_domain.select[0] : aws_route53_record.new[0]) -} output "certificate" { value = (local.cert_use != "skip" ? (local.select_cert == 1 ? { id = data.aws_iam_server_certificate.select[0].id diff --git a/modules/domain/variables.tf b/modules/domain/variables.tf index 36f044d..266cd23 100644 --- a/modules/domain/variables.tf +++ b/modules/domain/variables.tf @@ -38,3 +38,17 @@ variable "ips" { EOT default = [] } + +variable "vpc_type" { + type = string + description = <<-EOT + The vpc type helps generate the proper domains. + EOT +} + +variable "domain_zone" { + type = string + description = <<-EOT + The zone to use for the domain. + EOT +} diff --git a/modules/network_load_balancer/main.tf b/modules/network_load_balancer/main.tf index ad88db7..fe51a3c 100644 --- a/modules/network_load_balancer/main.tf +++ b/modules/network_load_balancer/main.tf @@ -2,14 +2,19 @@ locals { use = var.use name = var.name vpc_id = var.vpc_id - vpc_type = var.vpc_type + type = var.vpc_type security_group_id = var.security_group_id subnets = var.subnets access_info = (var.access_info == null ? {} : var.access_info) create = (local.use == "create" ? 1 : 0) select = (local.use == "select" ? 1 : 0) - eips = (local.select == 1 ? data.aws_eip.selected : aws_eip.created) - public_ips = (local.select == 1 ? [for e in data.aws_eip.selected : e.public_ip if can(e.public_ip)] : [for e in aws_eip.created : e.public_ip if can(e.public_ip)]) + ipv4ds = ((local.type == "ipv4" || local.type == "dualstack") ? local.create : 0) + ipv6ds = ((local.type == "ipv6" || local.type == "dualstack") ? local.create : 0) + public_ips = (local.ipv4ds == 1 ? + [for e in aws_eip.created : e.public_ip if can(e.public_ip)] : + [for s in local.subnets : cidrhost(s.cidrs.ipv6, -2) if can(cidrhost(s.cidrs.ipv6, -2))] + ) + } data "aws_lb" "selected" { @@ -19,21 +24,10 @@ data "aws_lb" "selected" { } } -data "aws_eip" "selected" { - for_each = (local.select == 1 ? local.subnets : {}) - filter { - name = "name" - values = [each.value.name] - } -} - resource "aws_eip" "created" { - for_each = (local.create == 1 ? local.subnets : {}) - domain = "vpc" - associate_with_private_ip = ((local.vpc_type == "ipv4" || local.vpc_type == "dualstack") ? - (each.value.cidrs.ipv4 != null ? cidrhost(each.value.cidrs.ipv4, -2) : "") : - (each.value.cidrs.ipv6 != null ? cidrhost(each.value.cidrs.ipv6, -2) : "") - ) + for_each = (local.ipv4ds == 1 ? local.subnets : {}) + domain = "vpc" + associate_with_private_ip = cidrhost(each.value.cidrs.ipv4, -2) tags = { Name = each.value.name } @@ -65,13 +59,21 @@ resource "aws_lb" "new" { internal = false load_balancer_type = "network" security_groups = [aws_security_group.load_balancer[0].id, local.security_group_id] - enable_cross_zone_load_balancing = true # cross zone load balancing is necessary for HA - ip_address_type = local.vpc_type # ipv4, ipv6, or dualstack + enable_cross_zone_load_balancing = true # cross zone load balancing is necessary for HA dynamic "subnet_mapping" { for_each = local.subnets content { - subnet_id = subnet_mapping.value.id - allocation_id = local.eips[subnet_mapping.key].id + subnet_id = subnet_mapping.value.id + allocation_id = ( + local.ipv4ds == 1 && can(aws_eip.created[subnet_mapping.key].id) ? + aws_eip.created[subnet_mapping.key].id : # map EIP with attached ipv4 private address + null # don't use allocation_id when ipv6 only + ) + ipv6_address = ( + local.ipv6ds == 1 && can(cidrhost(subnet_mapping.value.cidrs.ipv6, -2)) ? # map ipv6 address directly + cidrhost(subnet_mapping.value.cidrs.ipv6, -2) : + null # don't use ipv6_address unless ipv6 is enabled + ) } } tags = { diff --git a/modules/security_group/main.tf b/modules/security_group/main.tf index 3af79b7..455fbb7 100644 --- a/modules/security_group/main.tf +++ b/modules/security_group/main.tf @@ -8,8 +8,8 @@ locals { vpc_id = var.vpc_id vpc_type = var.vpc_type vpc_cidr = var.vpc_cidr - ipv4 = (local.vpc_type == "dualstack" || local.vpc_type == "ipv4" ? true : false) - ipv6 = (local.vpc_type == "dualstack" || local.vpc_type == "ipv6" ? true : false) + ipv4ds = (local.vpc_type == "dualstack" || local.vpc_type == "ipv4" ? 1 : 0) + ipv6ds = (local.vpc_type == "dualstack" || local.vpc_type == "ipv6" ? 1 : 0) } data "aws_security_group" "selected" { @@ -36,44 +36,44 @@ resource "aws_security_group" "new" { } } -# this allows egress between servers on the VPC +# these allow servers within the VPC to establish connections with other servers in the VPC resource "aws_vpc_security_group_egress_rule" "project_egress_ipv4" { - count = (local.ipv4 ? (local.type.project_egress ? 1 : 0) : 0) + count = (local.type.project_egress ? local.ipv4ds : 0) ip_protocol = "-1" cidr_ipv4 = local.vpc_cidr.ipv4 security_group_id = aws_security_group.new[0].id } resource "aws_vpc_security_group_egress_rule" "project_egress_ipv6" { - count = (local.ipv6 ? (local.type.project_egress ? 1 : 0) : 0) + count = (local.type.project_egress ? local.ipv6ds : 0) ip_protocol = "-1" cidr_ipv6 = local.vpc_cidr.ipv6 security_group_id = aws_security_group.new[0].id } -# this allows ingress between servers on the VPC +# these allow servers within the VPC to accept inbound connections from other servers in the VPC resource "aws_vpc_security_group_ingress_rule" "project_ingress_ipv4" { - count = (local.ipv4 ? (local.type.project_ingress ? 1 : 0) : 0) + count = (local.type.project_ingress ? local.ipv4ds : 0) ip_protocol = "-1" cidr_ipv4 = local.vpc_cidr.ipv4 security_group_id = aws_security_group.new[0].id } resource "aws_vpc_security_group_ingress_rule" "project_ingress_ipv6" { - count = (local.ipv6 ? (local.type.project_ingress ? 1 : 0) : 0) + count = (local.type.project_ingress ? local.ipv6ds : 0) ip_protocol = "-1" cidr_ipv6 = local.vpc_cidr.ipv6 security_group_id = aws_security_group.new[0].id } # this is necessary if you want to update or install anything from the internet -# allows servers to initiate connections to the public internet +# allows servers to initiate connections to public internet servers resource "aws_vpc_security_group_egress_rule" "external_egress_ipv4" { - count = (local.ipv4 ? (local.type.public_egress ? 1 : 0) : 0) + count = (local.type.public_egress ? local.ipv4ds : 0) ip_protocol = "-1" cidr_ipv4 = "0.0.0.0/0" security_group_id = aws_security_group.new[0].id } resource "aws_vpc_security_group_egress_rule" "external_egress_ipv6" { - count = (local.ipv6 ? (local.type.public_egress ? 1 : 0) : 0) + count = (local.type.public_egress ? local.ipv6ds : 0) ip_protocol = "-1" cidr_ipv6 = "::/0" security_group_id = aws_security_group.new[0].id @@ -83,13 +83,13 @@ resource "aws_vpc_security_group_egress_rule" "external_egress_ipv6" { # allows the public internet to initiate connections to the server # WARNING! this exposes your entire project to the public internet resource "aws_vpc_security_group_ingress_rule" "external_ingress_ipv4" { - count = (local.ipv4 ? (local.type.public_ingress ? 1 : 0) : 0) + count = (local.type.public_ingress ? local.ipv4ds : 0) ip_protocol = "-1" cidr_ipv4 = "0.0.0.0/0" security_group_id = aws_security_group.new[0].id } resource "aws_vpc_security_group_ingress_rule" "external_ingress_ipv6" { - count = (local.ipv6 ? (local.type.public_ingress ? 1 : 0) : 0) + count = (local.type.public_ingress ? local.ipv6ds : 0) ip_protocol = "-1" cidr_ipv6 = "::/0" security_group_id = aws_security_group.new[0].id diff --git a/modules/subnet/main.tf b/modules/subnet/main.tf index 7dadb90..9a3edd0 100644 --- a/modules/subnet/main.tf +++ b/modules/subnet/main.tf @@ -1,16 +1,17 @@ locals { use = var.use select = (local.use == "select" ? 1 : 0) - create = (local.use != "select" ? 1 : 0) + create = (local.use == "create" ? 1 : 0) vpc_id = var.vpc_id - vpc_cidr = var.vpc_cidr + ipv6_cidr = var.ipv6_cidr + ipv4_cidr = var.ipv4_cidr type = var.type - ipv4 = ((local.type == "dualstack" || local.type == "ipv4") ? local.create : 0) - ipv6 = (local.type == "ipv6" ? local.create : 0) + ipv6ds = ((local.type == "ipv6" || local.type == "dualstack") ? local.create : 0) availability_zone = var.availability_zone public = var.public name = var.name } + data "aws_subnet" "selected" { count = local.select filter { @@ -19,25 +20,12 @@ data "aws_subnet" "selected" { } } -resource "aws_subnet" "ipv6" { - count = local.ipv6 - vpc_id = local.vpc_id - availability_zone = local.availability_zone - map_public_ip_on_launch = local.public - ipv6_cidr_block = (can(local.vpc_cidr.ipv6) ? local.vpc_cidr.ipv6 : "") - ipv6_native = true - assign_ipv6_address_on_creation = true - enable_resource_name_dns_aaaa_record_on_launch = true - tags = { - Name = local.name - } -} - -resource "aws_subnet" "ipv4" { - count = local.ipv4 +resource "aws_subnet" "created" { + count = local.create vpc_id = local.vpc_id - cidr_block = (can(local.vpc_cidr.ipv4) ? local.vpc_cidr.ipv4 : "") - assign_ipv6_address_on_creation = (local.type == "dualstack" ? true : false) + cidr_block = local.ipv4_cidr + ipv6_cidr_block = local.ipv6_cidr + assign_ipv6_address_on_creation = (local.ipv6ds == 1 ? true : false) availability_zone = local.availability_zone map_public_ip_on_launch = local.public tags = { diff --git a/modules/subnet/outputs.tf b/modules/subnet/outputs.tf index 20af827..897b87c 100644 --- a/modules/subnet/outputs.tf +++ b/modules/subnet/outputs.tf @@ -1,49 +1,67 @@ +output "name" { + value = local.name +} +output "type" { + value = local.type +} output "id" { value = ( - local.select == 1 ? data.aws_subnet.selected[0].id : - local.ipv4 == 1 ? aws_subnet.ipv4[0].id : can(aws_subnet.ipv6[0].id) ? aws_subnet.ipv6[0].id : "" + local.select == 1 ? + data.aws_subnet.selected[0].id : + can(aws_subnet.created[0].id) ? + aws_subnet.created[0].id : + "" ) } output "arn" { value = ( - local.select == 1 ? data.aws_subnet.selected[0].arn : - local.ipv4 == 1 ? aws_subnet.ipv4[0].arn : can(aws_subnet.ipv6[0].arn) ? aws_subnet.ipv6[0].arn : "" + local.select == 1 ? + data.aws_subnet.selected[0].arn : + can(aws_subnet.created[0].arn) ? + aws_subnet.created[0].arn : + "" ) } -output "name" { - value = local.name -} -output "type" { - value = local.type -} output "availability_zone" { value = ( - local.select == 1 ? data.aws_subnet.selected[0].availability_zone : - local.ipv4 == 1 ? aws_subnet.ipv4[0].availability_zone : can(aws_subnet.ipv6[0].availability_zone) ? aws_subnet.ipv6[0].availability_zone : "" + local.select == 1 ? + data.aws_subnet.selected[0].availability_zone : + can(aws_subnet.created[0].availability_zone) ? + aws_subnet.created[0].availability_zone : + "" ) } output "availability_zone_id" { value = ( - local.select == 1 ? data.aws_subnet.selected[0].availability_zone_id : - local.ipv4 == 1 ? aws_subnet.ipv4[0].availability_zone_id : can(aws_subnet.ipv6[0].availability_zone_id) ? aws_subnet.ipv6[0].availability_zone_id : "" + local.select == 1 ? + data.aws_subnet.selected[0].availability_zone_id : + can(aws_subnet.created[0].availability_zone_id) ? + aws_subnet.created[0].availability_zone_id : + "" + ) +} +output "tags" { + value = ( + local.select == 1 ? + data.aws_subnet.selected[0].tags : + can(aws_subnet.created[0].tags) ? + aws_subnet.created[0].tags : + tomap({ "" = "" }) ) } output "cidrs" { value = ( local.select == 1 ? { - ipv4 = (can(data.aws_subnet.selected[0].cidr_block) ? data.aws_subnet.selected[0].cidr_block : ""), - ipv6 = (can(data.aws_subnet.selected[0].ipv6_cidr_block) ? data.aws_subnet.selected[0].ipv6_cidr_block : "") + ipv4 = data.aws_subnet.selected[0].cidr_block + ipv6 = data.aws_subnet.selected[0].ipv6_cidr_block } : local.create == 1 ? { - ipv4 = (can(aws_subnet.ipv4[0].cidr_block) ? aws_subnet.ipv4[0].cidr_block : ""), - ipv6 = (can(aws_subnet.ipv6[0].ipv6_cidr_block) ? aws_subnet.ipv6[0].ipv6_cidr_block : "") + ipv4 = aws_subnet.created[0].cidr_block + ipv6 = aws_subnet.created[0].ipv6_cidr_block } : - {} # default - ) -} -output "tags" { - value = ( - local.select == 1 ? data.aws_subnet.selected[0].tags : - local.ipv4 == 1 ? aws_subnet.ipv4[0].tags : can(aws_subnet.ipv6[0].tags) ? aws_subnet.ipv6[0].tags : tomap({ "" = "" }) + { + ipv4 = "" + ipv6 = "" + } ) } diff --git a/modules/subnet/variables.tf b/modules/subnet/variables.tf index df524bf..6c9d440 100644 --- a/modules/subnet/variables.tf +++ b/modules/subnet/variables.tf @@ -13,14 +13,18 @@ variable "vpc_id" { The AWS unique id for the VPC which this subnet will be created in. EOT } -variable "vpc_cidr" { - type = object({ - ipv4 = string - ipv6 = string - }) - description = <<-EOT - An object with the CIDR blocks for the VPC. - When not dualstack it is expected that one of these will be "". +variable "ipv4_cidr" { + type = string + description = <<-EOT + The CIDR for the subnet to create. + Ignored when selecting. + EOT +} +variable "ipv6_cidr" { + type = string + description = <<-EOT + The CIDR for the subnet to create. + Ignored when selecting. EOT } variable "name" { diff --git a/modules/vpc/main.tf b/modules/vpc/main.tf index 631e0f7..550d381 100644 --- a/modules/vpc/main.tf +++ b/modules/vpc/main.tf @@ -4,8 +4,8 @@ locals { type = var.type select = (local.use == "select" ? 1 : 0) create = (local.use == "create" ? 1 : 0) - ipv4 = (local.type == "ipv4" ? local.create : 0) - ipv6 = ((local.type == "ipv6" || local.type == "dualstack") ? local.create : 0) + ipv6ds = ((local.type == "dualstack" || local.type == "ipv6") ? local.create : 0) + ipv4ds = ((local.type == "dualstack" || local.type == "ipv4") ? local.create : 0) } data "aws_vpc" "selected" { @@ -18,8 +18,8 @@ data "aws_vpc" "selected" { resource "aws_vpc" "new" { count = local.create - cidr_block = (local.ipv4 == 1 ? "10.0.0.0/16" : "") - assign_generated_ipv6_cidr_block = (local.ipv6 == 1 ? true : false) + cidr_block = "10.0.0.0/16" + assign_generated_ipv6_cidr_block = true tags = { Name = local.name } @@ -34,7 +34,7 @@ resource "aws_internet_gateway" "new" { } resource "aws_route" "public_ipv4" { - count = local.ipv4 + count = local.ipv4ds depends_on = [ aws_internet_gateway.new, aws_vpc.new, @@ -45,7 +45,7 @@ resource "aws_route" "public_ipv4" { } resource "aws_route" "public_ipv6" { - count = local.ipv6 + count = local.ipv6ds depends_on = [ aws_internet_gateway.new, aws_vpc.new, diff --git a/modules/vpc/outputs.tf b/modules/vpc/outputs.tf index 9c205a4..c3d82bd 100644 --- a/modules/vpc/outputs.tf +++ b/modules/vpc/outputs.tf @@ -1,18 +1,54 @@ output "id" { - value = (local.select == 1 ? data.aws_vpc.selected[0].id : (can(aws_vpc.new[0].id) ? aws_vpc.new[0].id : "")) + value = ( + local.select == 1 ? + data.aws_vpc.selected[0].id : + local.create == 1 ? + aws_vpc.new[0].id : + "" + ) } output "arn" { - value = (local.select == 1 ? data.aws_vpc.selected[0].arn : (can(aws_vpc.new[0].arn) ? aws_vpc.new[0].arn : "")) + value = ( + local.select == 1 ? + data.aws_vpc.selected[0].arn : + local.create == 1 ? + aws_vpc.new[0].arn : + "" + ) } output "ipv4" { - value = (local.select == 1 ? data.aws_vpc.selected[0].cidr_block : (can(aws_vpc.new[0].cidr_block) ? aws_vpc.new[0].cidr_block : "")) + value = ( + local.select == 1 ? + data.aws_vpc.selected[0].cidr_block : + local.create == 1 ? + aws_vpc.new[0].cidr_block : + "" + ) } output "ipv6" { - value = (local.select == 1 ? data.aws_vpc.selected[0].ipv6_cidr_block : (can(aws_vpc.new[0].ipv6_cidr_block) ? aws_vpc.new[0].ipv6_cidr_block : "")) + value = ( + local.select == 1 ? + data.aws_vpc.selected[0].ipv6_cidr_block : + local.create == 1 ? + aws_vpc.new[0].ipv6_cidr_block : + "" + ) } output "main_route_table_id" { - value = (local.select == 1 ? data.aws_vpc.selected[0].main_route_table_id : (can(aws_vpc.new[0].main_route_table_id) ? aws_vpc.new[0].main_route_table_id : "")) + value = ( + local.select == 1 ? + data.aws_vpc.selected[0].main_route_table_id : + local.create == 1 ? + aws_vpc.new[0].main_route_table_id : + "" + ) } output "tags" { - value = (local.select == 1 ? data.aws_vpc.selected[0].tags : (can(aws_vpc.new[0].tags) ? aws_vpc.new[0].tags : tomap({ "" = "" }))) + value = ( + local.select == 1 ? + data.aws_vpc.selected[0].tags : + local.create == 1 ? + aws_vpc.new[0].tags : + tomap({ "" = "" }) + ) } diff --git a/outputs.tf b/outputs.tf index 5e4c7d0..81e7227 100644 --- a/outputs.tf +++ b/outputs.tf @@ -2,8 +2,8 @@ output "vpc" { value = { id = can(module.vpc[0].id) ? module.vpc[0].id : "" arn = can(module.vpc[0].arn) ? module.vpc[0].arn : "" - cidr_block = can(module.vpc[0].cidr_block) ? module.vpc[0].cidr_block : "" - ipv6_cidr_block = can(module.vpc[0].ipv6_cidr_block) ? module.vpc[0].ipv6_cidr_block : "" + ipv4_cidr = can(module.vpc[0].ipv4) ? module.vpc[0].ipv4 : "" + ipv6_cidr = can(module.vpc[0].ipv6) ? module.vpc[0].ipv6 : "" main_route_table_id = can(module.vpc[0].main_route_table_id) ? module.vpc[0].main_route_table_id : "" tags = can(module.vpc[0].tags) ? module.vpc[0].tags : tomap({ "" = "" }) } @@ -13,15 +13,18 @@ output "vpc" { } output "subnets" { - value = { for subnet in module.subnet : - subnet.name => { - id = (can(module.subnet[subnet.name].id) ? module.subnet[subnet.name].id : "") - arn = (can(module.subnet[subnet.name].arn) ? module.subnet[subnet.name].arn : "") - availability_zone = (can(module.subnet[subnet.name].availability_zone) ? module.subnet[subnet.name].availability_zone : "") - availability_zone_id = (can(module.subnet[subnet.name].availability_zone_id) ? module.subnet[subnet.name].availability_zone_id : "") - cidr = (can(module.subnet[subnet.name].cidr) ? module.subnet[subnet.name].cidr : "") - vpc_id = (can(module.vpc[0].id) ? module.vpc[0].id : "") - tags = (can(module.subnet[subnet.name].tags) ? module.subnet[subnet.name].tags : tomap({ "" = "" })) + value = { for i in range(length(module.subnet)) : + module.subnet[keys(module.subnet)[i]].name => { + id = module.subnet[keys(module.subnet)[i]].id + arn = module.subnet[keys(module.subnet)[i]].arn + availability_zone = module.subnet[keys(module.subnet)[i]].availability_zone + availability_zone_id = module.subnet[keys(module.subnet)[i]].availability_zone_id + cidrs = { + ipv4 = module.subnet[keys(module.subnet)[i]].cidrs.ipv4 + ipv6 = module.subnet[keys(module.subnet)[i]].cidrs.ipv6 + } + vpc_id = module.vpc[0].id + tags = module.subnet[keys(module.subnet)[i]].tags } } description = <<-EOT @@ -120,3 +123,7 @@ output "certificate" { This is helpful for servers and applications to import for securing transfer. EOT } + +output "subnet_map" { + value = local.subnet_map +} diff --git a/run_tests.sh b/run_tests.sh index efa8d24..b1fae6f 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -22,7 +22,7 @@ EOF --jsonfile /tmp/test.log \ --post-run-command "bash /tmp/test-processor" \ -- \ - -parallel=20 \ + -parallel=10 \ -timeout=80m \ "$@" } diff --git a/tests/dualstack_test.go b/tests/dualstack_test.go new file mode 100644 index 0000000..96e5f24 --- /dev/null +++ b/tests/dualstack_test.go @@ -0,0 +1,30 @@ +package test + +import ( + "os" + "testing" + + "github.com/gruntwork-io/terratest/modules/random" + "github.com/gruntwork-io/terratest/modules/terraform" +) + +// this test generates all objects, no overrides +func TestDualstack(t *testing.T) { + t.Parallel() + zone := os.Getenv("ZONE") + uniqueID := os.Getenv("IDENTIFIER") + if uniqueID == "" { + uniqueID = random.UniqueId() + } + directory := "dualstack" + region := "us-west-2" + + terraformVars := map[string]interface{}{ + "identifier": uniqueID, + "zone": zone, + } + terraformOptions := setup(t, directory, region, terraformVars) + defer teardown(t, directory) + defer terraform.Destroy(t, terraformOptions) + terraform.InitAndApply(t, terraformOptions) +} diff --git a/tests/ipv6_test.go b/tests/ipv6_test.go new file mode 100644 index 0000000..f237269 --- /dev/null +++ b/tests/ipv6_test.go @@ -0,0 +1,30 @@ +package test + +import ( + "os" + "testing" + + "github.com/gruntwork-io/terratest/modules/random" + "github.com/gruntwork-io/terratest/modules/terraform" +) + +// this test generates all objects, no overrides +func TestIpv6(t *testing.T) { + t.Parallel() + zone := os.Getenv("ZONE") + uniqueID := os.Getenv("IDENTIFIER") + if uniqueID == "" { + uniqueID = random.UniqueId() + } + directory := "ipv6" + region := "us-west-2" + + terraformVars := map[string]interface{}{ + "identifier": uniqueID, + "zone": zone, + } + terraformOptions := setup(t, directory, region, terraformVars) + defer teardown(t, directory) + defer terraform.Destroy(t, terraformOptions) + terraform.InitAndApply(t, terraformOptions) +} diff --git a/tests/selectsubnets_test.go b/tests/selectsubnets_test.go new file mode 100644 index 0000000..d4d29e7 --- /dev/null +++ b/tests/selectsubnets_test.go @@ -0,0 +1,30 @@ +package test + +import ( + "os" + "testing" + + "github.com/gruntwork-io/terratest/modules/random" + "github.com/gruntwork-io/terratest/modules/terraform" +) + +// this test generates all objects, no overrides +func TestSelectSubnets(t *testing.T) { + t.Parallel() + zone := os.Getenv("ZONE") + uniqueID := os.Getenv("IDENTIFIER") + if uniqueID == "" { + uniqueID = random.UniqueId() + } + directory := "selectsubnets" + region := "us-west-2" + + terraformVars := map[string]interface{}{ + "identifier": uniqueID, + "zone": zone, + } + terraformOptions := setup(t, directory, region, terraformVars) + defer teardown(t, directory) + defer terraform.Destroy(t, terraformOptions) + terraform.InitAndApply(t, terraformOptions) +} diff --git a/variables.tf b/variables.tf index 4440d0f..c0b614f 100644 --- a/variables.tf +++ b/variables.tf @@ -55,19 +55,6 @@ variable "vpc_public" { EOT default = false } -variable "vpc_cidr" { - type = object({ - ipv4 = string - ipv6 = string - }) - description = <<-EOT - The CIDR block to use for the VPC. - EOT - default = { - ipv4 = "10.0.0.0/16" # must be a /16 block or smaller - ipv6 = "fc00:0:0:/56" # must be a /56 block - } -} # subnet variable "subnet_use_strategy" { @@ -77,7 +64,7 @@ variable "subnet_use_strategy" { 'skip' to disable, 'select' to use existing, or 'create' to generate new subnet resources. - The default is 'create', which requires a subnet_name and subnet_cidr to be provided. + The default is 'create', which requires a subnet_name to be provided. When selecting a subnet, the subnet_name must be provided and a subnet with the tag "Name" with the given name must exist. When skipping a subnet, the security group and load balancer will also be skipped (automatically). EOT @@ -88,6 +75,17 @@ variable "subnet_use_strategy" { } } +variable "subnet_names" { + type = list(string) + description = <<-EOT + The names to use for the subnets to select or create. + Required when not skipping subnets. + When creating, the number of subnet_names must match the number of vpc_zones. + Only one subnet can be provisioned per zone, this is to align with load balancer mappings. + EOT + default = [] +} + # security group variable "security_group_use_strategy" { type = string @@ -209,6 +207,17 @@ variable "domain_use_strategy" { error_message = "The domain_use_strategy value must be one of 'skip', 'select', or 'create'." } } +variable "domain_zone" { + type = string + description = <<-EOT + The domain zone to use for generating the domain. + This is only required when using the 'create' domain_use_strategy. + The domain zone must already exist in AWS. + WARNING! Domain zones can take up to 24 hours to propagate, this is why we don't include them. + Required when not using 'skip' as the domain_use_strategy. + EOT + default = "" +} variable "domain" { type = string description = <<-EOT