diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml index 1e9e00a8..184cf426 100644 --- a/.github/workflows/terraform.yml +++ b/.github/workflows/terraform.yml @@ -131,7 +131,7 @@ jobs: - uses: stefanzweifel/git-auto-commit-action@v4 with: - commit_message: Update dependabot terraform entries + commit_message: Update dependabot Terraform entries commit_author: "github-actions[bot] " file_pattern: .github/dependabot.yml skip_checkout: true diff --git a/README.md b/README.md index 49f43af2..6a653c43 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ The repo for Answer Digital shared Terraform modules. ## Using these modules -You can use these modules in your own terraform projects as follows: +You can use these modules in your own Terraform projects as follows: ```hcl module "ec2_setup" { diff --git a/modules/aws/vpc/README.md b/modules/aws/vpc/README.md index 982bb6af..88db2f86 100644 --- a/modules/aws/vpc/README.md +++ b/modules/aws/vpc/README.md @@ -17,47 +17,46 @@ traffic, this is good from an auditing perspective, however you will be charged | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | ~> 1.3 | -| [aws](#requirement\_aws) | >= 4.0 | -| [random](#requirement\_random) | >= 3.4.3 | +| [aws](#requirement\_aws) | >= 5.14.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 4.0 | -| [random](#provider\_random) | >= 3.4.3 | +| [aws](#provider\_aws) | >= 5.14.0 | ## Resources | Name | Type | |------|------| -| [aws_cloudwatch_log_group.log_group](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | -| [aws_flow_log.flow_log](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/flow_log) | resource | -| [aws_iam_role.iam_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | -| [aws_iam_role_policy.iam_role_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | -| [aws_internet_gateway.ig](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/internet_gateway) | resource | -| [aws_route_table.route_table](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table) | resource | -| [aws_route_table_association.public_subnet_rt_asso](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table_association) | resource | -| [aws_subnet.private_subnets](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/subnet) | resource | -| [aws_subnet.public_subnets](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/subnet) | resource | -| [aws_vpc.vpc](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc) | resource | -| [random_uuid.log_group_guid_identifier](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/uuid) | resource | +| [aws_cloudwatch_log_group.vpc_flow_logs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | +| [aws_flow_log.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/flow_log) | resource | +| [aws_iam_role.vpc_flow_logs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy.vpc_flow_logs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_internet_gateway.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/internet_gateway) | resource | +| [aws_route_table.public](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table) | resource | +| [aws_route_table_association.public](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table_association) | resource | +| [aws_subnet.private](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/subnet) | resource | +| [aws_subnet.public](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/subnet) | resource | +| [aws_vpc.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc) | resource | | [aws_availability_zones.available](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/availability_zones) | data source | +| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [azs](#input\_azs) | This is a list that specifies all the Availability Zones that will have public and private subnets in it. Defaulting this value to an empty list selects of all the Availability Zones in the region you specify when defining the provider in your terraform project. | `list(string)` | `[]` | no | +| [azs](#input\_azs) | This is a list that specifies all the Availability Zones that will have public and private subnets in it. Defaulting this value to an empty list selects of all the Availability Zones in the region you specify when defining the provider in your Terraform project. | `list(string)` | `[]` | no | | [enable\_dns\_hostnames](#input\_enable\_dns\_hostnames) | This allows AWS DNS hostname support to be switched on or off. | `bool` | `true` | no | | [enable\_dns\_support](#input\_enable\_dns\_support) | This allows AWS DNS support to be switched on or off. | `bool` | `true` | no | | [enable\_vpc\_flow\_logs](#input\_enable\_vpc\_flow\_logs) | Whether to enable VPC Flow Logs for this VPC, this has cost but is considered a security risk without | `bool` | n/a | yes | +| [environment](#input\_environment) | The environment being deployed to - can only contain lower case letters. | `string` | n/a | yes | | [ig\_cidr](#input\_ig\_cidr) | This specifies the CIDR block for the internet gateway. | `string` | `"0.0.0.0/0"` | no | | [ig\_ipv6\_cidr](#input\_ig\_ipv6\_cidr) | This specifies the IPV6 CIDR block for the internet gateway. | `string` | `"::/0"` | no | | [num\_private\_subnets](#input\_num\_private\_subnets) | This is a number specifying how many private subnets you want. Setting this to its default value of `-1` will result in `x` private subnets where `x` is the number of Availability Zones. If the number of private subnets is greater than the number of Availability Zones the private subnets will be spread out evenly over the available AZs. The CIDR values used are of the form `10.0.{i}.0/24` where `i` starts at 101 and increases by 1 for each private subnet. | `number` | `-1` | no | | [num\_public\_subnets](#input\_num\_public\_subnets) | This is a number specifying how many public subnets you want. Setting this to its default value of `-1` will result in `x` public subnets where `x` is the number of Availability Zones. If the number of public subnets is greater than the number of Availability Zones the public subnets will be spread out evenly over the available AZs. The CIDR values used are of the form `10.0.{i}.0/24` where `i` starts at 1 and increases by 1 for each public subnet. | `number` | `-1` | no | -| [owner](#input\_owner) | This is used to identify AWS resources through its tags. | `string` | n/a | yes | -| [project\_name](#input\_project\_name) | This is used to label the VPC as "`project_name`-vpc". | `string` | n/a | yes | +| [owner](#input\_owner) | The email address of the owner. | `string` | n/a | yes | +| [project\_name](#input\_project\_name) | The projects's name - can only contain alphanumeric/underscore chatracters. | `string` | n/a | yes | | [vpc\_cidr](#input\_vpc\_cidr) | This specifies the CIDR block for the VPC. | `string` | `"10.0.0.0/16"` | no | | [vpc\_flow\_logs\_traffic\_type](#input\_vpc\_flow\_logs\_traffic\_type) | The Type of traffic to log, Requires vpc\_flow\_logs to be true | `string` | `"ALL"` | no | @@ -65,15 +64,15 @@ traffic, this is good from an auditing perspective, however you will be charged | Name | Description | |------|-------------| -| [az\_zones](#output\_az\_zones) | A list of the Availability Zones that have been used. This output is of type `string`. | +| [az\_zones](#output\_az\_zones) | A list of the Availability Zones that have been used. This output is of type `list(string)`. | | [private\_subnet\_ids](#output\_private\_subnet\_ids) | A list of the private subnet IDs that have been created. This output is of type `list(string)`. | | [public\_subnet\_ids](#output\_public\_subnet\_ids) | A list of the public subnet IDs that have been created. This output is of type `list(string)`. | -| [vpc\_id](#output\_vpc\_id) | The ID of the VPC that has been created. This output is of type `list(string)`. | +| [vpc\_id](#output\_vpc\_id) | The ID of the VPC that has been created. This output is of type `string`. | # Example Usage -Below are examples of how you would call the `vpc` module in your terraform code. +Below are examples of how you would call the `vpc` module in your Terraform code. In this example we show two ways the module can be used; the first uses the module to create a public and private subnet on each Availability Zone in your defined region, @@ -83,14 +82,14 @@ and `eu-west-3` respectively. ```hcl module "vpc_subnet" { source = "github.com/answerdigital/terraform-modules//modules/aws/vpc?ref=v2" - owner = "joe_blogs" + owner = "joe.blogs@answerdigital.com" project_name = "example_project_name" enable_vpc_flow_logs = true } module "vpc_subnet" { source = "github.com/answerdigital/terraform-modules//modules/aws/vpc?ref=v2" - owner = "joe_blogs" + owner = "joe.blogs@answerdigital.com" project_name = "example_project_name" azs = ["eu-west-1", "eu-west-3"] num_public_subnets = 1 diff --git a/modules/aws/vpc/data.tf b/modules/aws/vpc/data.tf index 87d8f482..2ce02a4d 100644 --- a/modules/aws/vpc/data.tf +++ b/modules/aws/vpc/data.tf @@ -1,3 +1,5 @@ data "aws_availability_zones" "available" { state = "available" } + +data "aws_region" "current" {} diff --git a/modules/aws/vpc/examples/example.tf b/modules/aws/vpc/examples/example.tf index 587e3205..e68a20ea 100644 --- a/modules/aws/vpc/examples/example.tf +++ b/modules/aws/vpc/examples/example.tf @@ -1,6 +1,6 @@ module "vpc_subnet" { source = "../." - owner = "joe_bloggs" + owner = "joe.blogs@answerdigital.com" project_name = "test_person_name" enable_vpc_flow_logs = true } diff --git a/modules/aws/vpc/locals.tf b/modules/aws/vpc/locals.tf index d9168df6..8ef2d297 100644 --- a/modules/aws/vpc/locals.tf +++ b/modules/aws/vpc/locals.tf @@ -2,9 +2,9 @@ locals { num_az_zones = length(var.azs) == 0 ? length(data.aws_availability_zones.available.names) : length(var.azs) az_zones = length(var.azs) == 0 ? data.aws_availability_zones.available.names : var.azs -} -locals { + aws_region_short = replace(replace(replace(replace(replace(replace(replace(data.aws_region.current.name, "north", "n"), "south", "s"), "east", "e"), "west", "w"), "central", "c"), "gov", "g"), "-", "") + public_subnet_cidrs = var.num_public_subnets == -1 ? [for i in range(1, local.num_az_zones + 1) : "10.0.${i}.0/24"] : [for i in range(1, var.num_public_subnets + 1) : "10.0.${i}.0/24"] private_subnet_cidrs = var.num_private_subnets == -1 ? [for i in range(1, local.num_az_zones + 1) : "10.0.10${i}.0/24"] : [for i in range(1, var.num_private_subnets + 1) : "10.0.10${i}.0/24"] diff --git a/modules/aws/vpc/main.tf b/modules/aws/vpc/main.tf index 4341c982..dac9cbd9 100644 --- a/modules/aws/vpc/main.tf +++ b/modules/aws/vpc/main.tf @@ -2,37 +2,33 @@ terraform { required_version = "~> 1.3" required_providers { - random = { - source = "hashicorp/random" - version = ">= 3.4.3" - } aws = { source = "hashicorp/aws" - version = ">= 4.0" + version = ">= 5.14.0" } } } -resource "aws_flow_log" "flow_log" { - iam_role_arn = aws_iam_role.iam_role[0].arn - log_destination = aws_cloudwatch_log_group.log_group[0].arn - traffic_type = var.vpc_flow_logs_traffic_type - vpc_id = aws_vpc.vpc.id - count = var.enable_vpc_flow_logs ? 1 : 0 -} +resource "aws_flow_log" "this" { + count = var.enable_vpc_flow_logs ? 1 : 0 -resource "random_uuid" "log_group_guid_identifier" { + iam_role_arn = aws_iam_role.vpc_flow_logs[0].arn + log_destination = aws_cloudwatch_log_group.vpc_flow_logs[0].arn + traffic_type = var.vpc_flow_logs_traffic_type + vpc_id = aws_vpc.this.id } -resource "aws_cloudwatch_log_group" "log_group" { - name = "${var.project_name}-vpc-flow-logs-${random_uuid.log_group_guid_identifier.result}" +resource "aws_cloudwatch_log_group" "vpc_flow_logs" { count = var.enable_vpc_flow_logs ? 1 : 0 + + name = "${replace("AWS::Logs::LogGroup", "::", "-")}-${var.project_name}-${var.environment}-${local.aws_region_short}-vpc_flow_logs" } -resource "aws_iam_role" "iam_role" { - name = "${var.project_name}-vpc-logs-iam" +resource "aws_iam_role" "vpc_flow_logs" { count = var.enable_vpc_flow_logs ? 1 : 0 + name = "${replace("AWS::IAM::Role", "::", "-")}-${var.project_name}-${var.environment}-${local.aws_region_short}-vpc_flow_logs" + assume_role_policy = < 0 ? 1 : 0 - vpc_id = aws_vpc.vpc.id +resource "aws_internet_gateway" "this" { + count = length(local.public_subnet_cidrs) > 0 ? 1 : 0 + + vpc_id = aws_vpc.this.id tags = { - Name = "${var.project_name}-vpc-ig" + Name = "${replace("AWS::EC2::InternetGateway", "::", "-")}-${var.project_name}-${var.environment}-${local.aws_region_short}-${count.index + 1}" Owner = var.owner } } -resource "aws_route_table" "route_table" { - count = length(local.public_subnet_cidrs) > 0 ? 1 : 0 - vpc_id = aws_vpc.vpc.id +resource "aws_route_table" "public" { + count = length(local.public_subnet_cidrs) > 0 ? 1 : 0 + + vpc_id = aws_vpc.this.id route { cidr_block = var.ig_cidr - gateway_id = aws_internet_gateway.ig[0].id + gateway_id = aws_internet_gateway.this[0].id } route { ipv6_cidr_block = var.ig_ipv6_cidr - gateway_id = aws_internet_gateway.ig[0].id + gateway_id = aws_internet_gateway.this[0].id } tags = { - Name = "${var.project_name}-public-route-table" + Name = "${replace("AWS::EC2::RouteTable", "::", "-")}-${var.project_name}-${var.environment}-${local.aws_region_short}-public_${count.index + 1}" Owner = var.owner } } -resource "aws_route_table_association" "public_subnet_rt_asso" { - count = length(local.public_subnet_cidrs) - subnet_id = element(aws_subnet.public_subnets[*].id, count.index) - route_table_id = aws_route_table.route_table[0].id +resource "aws_route_table_association" "public" { + count = length(local.public_subnet_cidrs) + + subnet_id = element(aws_subnet.public[*].id, count.index) + route_table_id = aws_route_table.public[0].id } diff --git a/modules/aws/vpc/output.tf b/modules/aws/vpc/output.tf index a83f5980..cb1f88d2 100644 --- a/modules/aws/vpc/output.tf +++ b/modules/aws/vpc/output.tf @@ -1,19 +1,19 @@ output "vpc_id" { - value = aws_vpc.vpc.id - description = "The ID of the VPC that has been created. This output is of type `list(string)`." + value = aws_vpc.this.id + description = "The ID of the VPC that has been created. This output is of type `string`." } output "public_subnet_ids" { - value = aws_subnet.public_subnets[*].id + value = aws_subnet.public[*].id description = "A list of the public subnet IDs that have been created. This output is of type `list(string)`." } output "private_subnet_ids" { - value = aws_subnet.private_subnets[*].id + value = aws_subnet.private[*].id description = "A list of the private subnet IDs that have been created. This output is of type `list(string)`." } output "az_zones" { value = local.az_zones - description = "A list of the Availability Zones that have been used. This output is of type `string`." + description = "A list of the Availability Zones that have been used. This output is of type `list(string)`." } diff --git a/modules/aws/vpc/variables.tf b/modules/aws/vpc/variables.tf index 03413bcb..29c75d92 100644 --- a/modules/aws/vpc/variables.tf +++ b/modules/aws/vpc/variables.tf @@ -1,11 +1,31 @@ variable "project_name" { type = string - description = "This is used to label the VPC as \"`project_name`-vpc\"." + description = "The projects's name - can only contain alphanumeric/underscore chatracters." + + validation { + condition = can(regex("^[0-9A-Za-z_]+$", var.project_name)) + error_message = "For the project_name value, only a-z, A-Z, 0-9 and _ are allowed." + } +} + +variable "environment" { + type = string + description = "The environment being deployed to - can only contain lower case letters." + + validation { + condition = can(regex("^[a-z]+$", var.environment)) + error_message = "For the environment value, only a-z are allowed." + } } variable "owner" { type = string - description = "This is used to identify AWS resources through its tags." + description = "The email address of the owner." + + validation { + condition = can(match("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", var.owner)) + error_message = "Invalid email address format. Please provide a valid email address." + } } # VPC variables @@ -13,6 +33,11 @@ variable "vpc_cidr" { type = string description = "This specifies the CIDR block for the VPC." default = "10.0.0.0/16" + + validation { + condition = can(cidrhost(var.vpc_cidr, 0)) + error_message = "Must be valid IPv4 CIDR." + } } variable "enable_vpc_flow_logs" { @@ -47,7 +72,7 @@ variable "enable_dns_hostnames" { # Availability Zone variables variable "azs" { type = list(string) - description = "This is a list that specifies all the Availability Zones that will have public and private subnets in it. Defaulting this value to an empty list selects of all the Availability Zones in the region you specify when defining the provider in your terraform project." + description = "This is a list that specifies all the Availability Zones that will have public and private subnets in it. Defaulting this value to an empty list selects of all the Availability Zones in the region you specify when defining the provider in your Terraform project." default = [] } @@ -69,10 +94,20 @@ variable "ig_cidr" { type = string description = "This specifies the CIDR block for the internet gateway." default = "0.0.0.0/0" + + validation { + condition = can(cidrhost(var.ig_cidr, 0)) + error_message = "Must be valid IPv4 CIDR." + } } variable "ig_ipv6_cidr" { type = string description = "This specifies the IPV6 CIDR block for the internet gateway." default = "::/0" + + validation { + condition = can(cidrhost(var.ig_ipv6_cidr, 0)) + error_message = "Must be valid IPv6 CIDR." + } }