diff --git a/examples/basic/main.tf b/examples/basic/main.tf index df81d2c..0a5b89a 100644 --- a/examples/basic/main.tf +++ b/examples/basic/main.tf @@ -13,7 +13,8 @@ provider "acme" { locals { identifier = var.identifier name = "tf-${local.identifier}" - domain = "${local.identifier}-${var.domain}" + 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) module "this" { diff --git a/examples/basic/variables.tf b/examples/basic/variables.tf index d7fa9b1..cf5f8cf 100644 --- a/examples/basic/variables.tf +++ b/examples/basic/variables.tf @@ -1,6 +1,6 @@ variable "identifier" { type = string } -variable "domain" { +variable "zone" { type = string } diff --git a/examples/domain/main.tf b/examples/domain/main.tf index 8820ec5..2d04be1 100644 --- a/examples/domain/main.tf +++ b/examples/domain/main.tf @@ -14,7 +14,8 @@ locals { identifier = var.identifier name = "tf-${local.identifier}" owner = "terraform-ci@suse.com" - domain = "${local.identifier}-${var.domain}" + zone = var.zone + domain = "${local.identifier}.${local.zone}" #zone = var.domain_zone } # AWS reserves the first four IP addresses and the last IP address in any CIDR block for its own use (cumulatively) diff --git a/examples/domain/variables.tf b/examples/domain/variables.tf index 7742e67..3af8793 100644 --- a/examples/domain/variables.tf +++ b/examples/domain/variables.tf @@ -5,10 +5,10 @@ variable "identifier" { # type = string # description = "The domain zone to use for the domain record. eg. example.com for domain 'test.example.com'" # } -variable "domain" { +variable "zone" { type = string description = <<-EOT - The domain to use for the domain record. eg. 'test.example.com'. - This example assumes that the zone already exists. + The domain to use as the zone for a generated domain name. + This must already exist in route53 and be globally populated. EOT } \ No newline at end of file diff --git a/examples/ingress/main.tf b/examples/ingress/main.tf new file mode 100644 index 0000000..007c1e3 --- /dev/null +++ b/examples/ingress/main.tf @@ -0,0 +1,41 @@ + +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" # use this url in test + #server_url = "https://acme-v02.api.letsencrypt.org/directory" # use this url in production +} +locals { + identifier = var.identifier + name = "tf-${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) +module "this" { + source = "../../" + vpc_name = local.name + vpc_cidr = "10.0.255.0/24" # gives 256 usable addresses from .1 to .254, but AWS reserves .1 to .4 and .255, leaving .5 to .254 + security_group_name = local.name + security_group_type = "egress" + domain = local.domain + load_balancer_name = local.name + load_balancer_access_cidrs = { + application = { + port = 443 + protocol = "tcp" + cidrs = ["1.1.1.1/32"] + } + platform = { + port = 6443 + protocol = "tcp" + cidrs = ["2.2.2.2/32"] + } + } +} diff --git a/examples/ingress/outputs.tf b/examples/ingress/outputs.tf new file mode 100644 index 0000000..9c7a45e --- /dev/null +++ b/examples/ingress/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/ingress/variables.tf b/examples/ingress/variables.tf new file mode 100644 index 0000000..cf5f8cf --- /dev/null +++ b/examples/ingress/variables.tf @@ -0,0 +1,6 @@ +variable "identifier" { + type = string +} +variable "zone" { + type = string +} diff --git a/examples/ingress/versions.tf b/examples/ingress/versions.tf new file mode 100644 index 0000000..1399985 --- /dev/null +++ b/examples/ingress/versions.tf @@ -0,0 +1,17 @@ +terraform { + required_version = ">= 1.5.0, < 1.6" + required_providers { + local = { + source = "hashicorp/local" + version = ">= 2.4" + } + aws = { + source = "hashicorp/aws" + version = ">= 5.11" + } + acme = { + source = "vancluever/acme" + version = ">= 2.0" + } + } +} \ No newline at end of file diff --git a/examples/selectvpc/main.tf b/examples/selectvpc/main.tf index 9e261db..7b9cbd1 100644 --- a/examples/selectvpc/main.tf +++ b/examples/selectvpc/main.tf @@ -14,7 +14,8 @@ provider "acme" { locals { identifier = var.identifier name = "tf-${local.identifier}" - domain = "${local.identifier}-${var.domain}" + zone = var.zone + domain = "${local.identifier}.${local.zone}" } module "setup" { diff --git a/examples/selectvpc/variables.tf b/examples/selectvpc/variables.tf index d7fa9b1..cf5f8cf 100644 --- a/examples/selectvpc/variables.tf +++ b/examples/selectvpc/variables.tf @@ -1,6 +1,6 @@ variable "identifier" { type = string } -variable "domain" { +variable "zone" { type = string } diff --git a/flake.lock b/flake.lock index c8aedb6..cc2e8ff 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1712192574, - "narHash": "sha256-LbbVOliJKTF4Zl2b9salumvdMXuQBr2kuKP5+ZwbYq4=", + "lastModified": 1713254108, + "narHash": "sha256-0TZIsfDbHG5zibtlw6x0yOp3jkInIGaJ35B7Y4G8Pec=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "f480f9d09e4b4cf87ee6151eba068197125714de", + "rev": "2fd19c8be2551a61c1ddc3d9f86d748f4db94f00", "type": "github" }, "original": { diff --git a/main.tf b/main.tf index 568aa42..9d4eaea 100644 --- a/main.tf +++ b/main.tf @@ -123,7 +123,7 @@ module "network_load_balancer" { vpc_id = module.vpc[0].id security_group_id = module.security_group[0].id subnet_ids = [for subnet in module.subnet : subnet.id] - access_cidrs = local.load_balancer_access_cidrs + access_info = local.load_balancer_access_cidrs } module "domain" { diff --git a/modules/domain/main.tf b/modules/domain/main.tf index 294c8fe..b4d3855 100644 --- a/modules/domain/main.tf +++ b/modules/domain/main.tf @@ -1,5 +1,4 @@ locals { - use = var.use content = lower(var.content) ip = var.ip @@ -133,7 +132,7 @@ resource "aws_iam_server_certificate" "new" { acme_certificate.new, ] count = local.create - name_prefix = local.content + name_prefix = "${local.content}-" certificate_body = acme_certificate.new[0].certificate_pem private_key = tls_private_key.cert_private_key[0].private_key_pem lifecycle { @@ -151,9 +150,8 @@ data "aws_iam_server_certificate" "select" { tls_private_key.cert_private_key, tls_cert_request.req, acme_certificate.new, - ] count = local.select - name_prefix = local.content + name_prefix = "${local.content}-" latest = true } diff --git a/modules/network_load_balancer/main.tf b/modules/network_load_balancer/main.tf index de2e875..fe510e5 100644 --- a/modules/network_load_balancer/main.tf +++ b/modules/network_load_balancer/main.tf @@ -4,11 +4,11 @@ locals { vpc_id = var.vpc_id security_group_id = var.security_group_id subnet_ids = var.subnet_ids - access_cidrs = var.access_cidrs + access_info = (var.access_info == null ? {} : var.access_info) create = (local.use == "create" ? 1 : 0) select = (local.use == "select" ? 1 : 0) - - public_ip = (local.select == 1 ? data.aws_eip.selected[0].public_ip : aws_eip.created[0].public_ip) + eip = (local.select == 1 ? data.aws_eip.selected[0] : aws_eip.created[0]) + public_ip = (local.select == 1 ? data.aws_eip.selected[0].public_ip : aws_eip.created[0].public_ip) } data "aws_lb" "selected" { @@ -42,13 +42,13 @@ resource "aws_security_group" "load_balancer" { } resource "aws_security_group_rule" "external_ingress" { - for_each = (local.create == 1 ? local.access_cidrs : {}) + for_each = (local.create == 1 ? local.access_info : {}) security_group_id = aws_security_group.load_balancer[0].id type = "ingress" - from_port = each.key - to_port = each.key - protocol = "-1" - cidr_blocks = each.value + from_port = each.value.port + to_port = each.value.port + protocol = each.value.protocol + cidr_blocks = each.value.cidrs } resource "aws_lb" "new" { @@ -57,9 +57,36 @@ resource "aws_lb" "new" { internal = false load_balancer_type = "network" security_groups = [local.security_group_id] - subnets = local.subnet_ids - + dynamic "subnet_mapping" { + for_each = toset(local.subnet_ids) + content { + subnet_id = subnet_mapping.key + allocation_id = local.eip.id + } + } tags = { Name = local.name } } + +resource "aws_lb_target_group" "created" { + for_each = (local.create == 1 ? local.access_info : {}) + name_prefix = "${substr(md5("${local.name}-${each.key}"), 0, 5)}-" + port = each.value.port + protocol = upper(each.value.protocol) + vpc_id = local.vpc_id + tags = { + Name = "${local.name}-${each.key}" + } +} + +resource "aws_lb_listener" "created" { + for_each = (local.create == 1 ? local.access_info : {}) + load_balancer_arn = aws_lb.new[0].arn + port = each.value.port + protocol = upper(each.value.protocol) + default_action { + type = "forward" + target_group_arn = aws_lb_target_group.created[each.key].arn + } +} diff --git a/modules/network_load_balancer/outputs.tf b/modules/network_load_balancer/outputs.tf index f7c329d..0fa3feb 100644 --- a/modules/network_load_balancer/outputs.tf +++ b/modules/network_load_balancer/outputs.tf @@ -10,3 +10,6 @@ output "load_balancer" { output "public_ip" { value = local.public_ip } +output "listeners" { + value = (local.create == 1 ? aws_lb_listener.created : {}) +} \ No newline at end of file diff --git a/modules/network_load_balancer/variables.tf b/modules/network_load_balancer/variables.tf index 855d41b..ab26ee9 100644 --- a/modules/network_load_balancer/variables.tf +++ b/modules/network_load_balancer/variables.tf @@ -37,12 +37,25 @@ variable "subnet_ids" { EOT default = [] } -variable "access_cidrs" { - type = map(list(string)) +variable "access_info" { + type = map(object({ + port = number + cidrs = list(string) + protocol = string + })) description = <<-EOT - A list of maps relating a port to a list of CIDRs that are allowed to access the load balancer external to the VPC. - If this is not provided, no IP addresses will be allowed to access the load balancer externally. - example: {"443" = ["1.1.1.1/32"]} would allow IP address 1.1.1.1 to access the load balancer on port 443. + A map of access information objects. + The port is the port to expose on the load balancer. + The cidrs is a list of external cidr blocks to allow access to the load balancer. + The protocol is the network protocol to expose on, this can be 'udp' or 'tcp'. + Example: + { + test = { + port = 443 + cidrs = ["1.1.1.1/32"] + protocol = "tcp" + } + } EOT - default = {} + default = null } diff --git a/outputs.tf b/outputs.tf index fc61e5b..f5e5c10 100644 --- a/outputs.tf +++ b/outputs.tf @@ -43,7 +43,8 @@ output "subnets" { } }) description = <<-EOT - The subnet object from AWS. + The subnet objects from AWS. + This can be used to provision ec2 instances. EOT } @@ -64,6 +65,9 @@ output "security_group" { }) description = <<-EOT The security group object from AWS. + This is the project level security group, + this should be common among all servers and objects in the project. + This can be helpful to make sure that all servers in the same vpc can talk to each other. EOT } @@ -88,6 +92,9 @@ output "load_balancer" { }) description = <<-EOT The load balancer object from AWS. + When generated, this can be helpful to set up indirect access to servers. + This is a network load balancer with either UDP or TCP protocol. + As such, it doesn't encrypt or decrypt data and TLS must be handled at the server level. EOT } @@ -108,6 +115,8 @@ output "domain" { }) description = <<-EOT The domain object from AWS. + When generated, the domain is applied to the EIP created with the load balancer. + This is helpful when you want to expose an application indirectly. EOT } @@ -130,5 +139,7 @@ output "certificate" { }) description = <<-EOT The certificate object from AWS. + When generating a domain, a valid TLS certificate is also generated. + This is helpful for servers and applications to import for securing transfer. EOT } diff --git a/tests/basic_test.go b/tests/basic_test.go index b72ec71..7faf690 100644 --- a/tests/basic_test.go +++ b/tests/basic_test.go @@ -11,7 +11,7 @@ import ( // this test generates all objects, no overrides func TestBasic(t *testing.T) { t.Parallel() - domain := os.Getenv("DOMAIN") + zone := os.Getenv("ZONE") uniqueID := os.Getenv("IDENTIFIER") if uniqueID == "" { uniqueID = random.UniqueId() @@ -21,7 +21,7 @@ func TestBasic(t *testing.T) { terraformVars := map[string]interface{}{ "identifier": uniqueID, - "domain": domain, + "zone": zone, } terraformOptions := setup(t, directory, region, terraformVars) defer teardown(t, directory) diff --git a/tests/domain_test.go b/tests/domain_test.go index 1b86086..27b992e 100644 --- a/tests/domain_test.go +++ b/tests/domain_test.go @@ -15,13 +15,13 @@ func TestDomain(t *testing.T) { if uniqueID == "" { uniqueID = random.UniqueId() } - domain := os.Getenv("DOMAIN") + zone := os.Getenv("ZONE") directory := "domain" region := "us-west-1" terraformVars := map[string]interface{}{ "identifier": uniqueID, - "domain": domain, + "zone": zone, } terraformOptions := setup(t, directory, region, terraformVars) diff --git a/tests/ingress_test.go b/tests/ingress_test.go new file mode 100644 index 0000000..6ce66db --- /dev/null +++ b/tests/ingress_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 improves on the basic by adding ingress for the load balancer +func TestIngress(t *testing.T) { + t.Parallel() + zone := os.Getenv("ZONE") + uniqueID := os.Getenv("IDENTIFIER") + if uniqueID == "" { + uniqueID = random.UniqueId() + } + directory := "ingress" + region := "us-west-1" + + 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/selectvpc_test.go b/tests/selectvpc_test.go index 492928f..2ac1800 100644 --- a/tests/selectvpc_test.go +++ b/tests/selectvpc_test.go @@ -9,9 +9,9 @@ import ( ) // this test generates all objects, no overrides -func TestSelectvpc(t *testing.T) { +func TestSelectVpc(t *testing.T) { t.Parallel() - domain := os.Getenv("DOMAIN") + zone := os.Getenv("ZONE") uniqueID := os.Getenv("IDENTIFIER") if uniqueID == "" { uniqueID = random.UniqueId() @@ -21,7 +21,7 @@ func TestSelectvpc(t *testing.T) { terraformVars := map[string]interface{}{ "identifier": uniqueID, - "domain": domain, + "zone": zone, } terraformOptions := setup(t, directory, region, terraformVars) defer teardown(t, directory) diff --git a/variables.tf b/variables.tf index 94f7595..ec60d98 100644 --- a/variables.tf +++ b/variables.tf @@ -7,7 +7,7 @@ variable "vpc_use_strategy" { 'select' to use existing, or 'create' to generate new vpc resources. The default is 'create', which requires a vpc_name and vpc_cidr to be provided. - When selecting a vpc, the vpc_name must be provided and a vpc that has a tag "Name with the given name must exist. + When selecting a vpc, the vpc_name must be provided and a vpc that has a tag "Name" with the given name must exist. When skipping a vpc, the subnet, security group, and load balancer will also be skipped (automatically). EOT default = "create" @@ -109,8 +109,10 @@ variable "security_group_name" { type = string description = <<-EOT The name of the ec2 security group to create or select. - This is required. - If you would like to create a security group please specify the type of security group you would like to create. + When choosing the "create" or "select" strategy, this is required. + When choosing the "skip" strategy, this is ignored. + When selecting a security group, the security_group_name must be provided and a security group with the given name must exist. + When creating a security group, the name will be used to tag the resource, and security_group_type is required. The types are located in modules/security_group/types.tf. EOT default = "" @@ -122,7 +124,7 @@ variable "security_group_type" { We provide opinionated options for the user to select from. Leave this blank if you would like to select a security group rather than generate one. The types are located in ./modules/security_group/types.tf. - If specified, must be one of: specific, internal, egress, or public. + If specified, must be one of: project, egress, or public. EOT default = "project" validation { @@ -160,13 +162,26 @@ variable "load_balancer_name" { default = "" } variable "load_balancer_access_cidrs" { - type = map(list(string)) + type = map(object({ + port = number + cidrs = list(string) + protocol = string + })) description = <<-EOT - A list of maps relating a port to a list of CIDRs that are allowed to access the load balancer external to the VPC. - If this is not provided, no IP addresses will be allowed to access the load balancer externally. - exmaple: [{"443" = ["1.1.1.1/32"]}] would allow IP address 1.1.1.1 to access the load balancer on port 443. + A map of access information objects. + The port is the port to expose on the load balancer. + The cidrs is a list of external cidr blocks to allow access to the load balancer. + The protocol is the network protocol to expose on, this can be 'udp' or 'tcp'. + Example: + { + test = { + port = 443 + cidrs = ["1.1.1.1/32"] + protocol = "tcp" + } + } EOT - default = {} + default = null } # domain @@ -179,6 +194,7 @@ variable "domain_use_strategy" { or 'create' to generate new domain resources. The default is 'create', which requires a domain name to be provided. When selecting a domain, the domain must be provided and a domain with the matching name must exist. + When adding a domain, it will be attached to all load balancer ports with a certificate for secure access. EOT default = "create" validation {