diff --git a/README.md b/README.md index ef04a71..57e17f2 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,8 @@ Terraform Modules from [this package](https://github.com/tedilabs/terraform-aws- ### VPC - [vpc-gateway-endpoint-simple](./examples/vpc-gateway-endpoint-simple) +- [vpc-interface-endpoint-simple](./examples/vpc-interface-endpoint-simple) +- [vpc-interface-endpoint-full](./examples/vpc-interface-endpoint-full) ### VPC Lattice diff --git a/examples/vpc-gateway-endpoint-simple/main.tf b/examples/vpc-gateway-endpoint-simple/main.tf index 55832fe..4770620 100644 --- a/examples/vpc-gateway-endpoint-simple/main.tf +++ b/examples/vpc-gateway-endpoint-simple/main.tf @@ -22,7 +22,7 @@ module "endpoint" { vpc_id = data.aws_vpc.default.id - name = "aws-s3" + name = "gateway-aws-s3" service = "S3" route_tables = data.aws_route_tables.this.ids diff --git a/examples/vpc-interface-endpoint-full/main.tf b/examples/vpc-interface-endpoint-full/main.tf new file mode 100644 index 0000000..8da38a1 --- /dev/null +++ b/examples/vpc-interface-endpoint-full/main.tf @@ -0,0 +1,94 @@ +provider "aws" { + region = "us-east-1" +} + +data "aws_vpc" "default" { + default = true +} + +data "aws_subnet" "default" { + for_each = toset(["use1-az1", "use1-az2"]) + + availability_zone_id = each.key + default_for_az = true +} + + +################################################### +# Interface Endpoint +################################################### + +module "endpoint" { + source = "../../modules/vpc-interface-endpoint" + # source = "tedilabs/vpc-connectivity/aws//modules/vpc-interface-endpoint" + # version = "~> 0.2.0" + + name = "interface-aws-s3" + service_name = "com.amazonaws.us-east-1.s3" + auto_accept = true + + policy = < @@ -10,21 +13,21 @@ This module creates following resources. | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.5 | -| [aws](#requirement\_aws) | >= 3.45 | +| [terraform](#requirement\_terraform) | >= 1.6 | +| [aws](#requirement\_aws) | >= 5.20 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | 5.19.0 | +| [aws](#provider\_aws) | 5.24.0 | ## Modules | Name | Source | Version | |------|--------|---------| | [resource\_group](#module\_resource\_group) | tedilabs/misc/aws//modules/resource-group | ~> 0.10.0 | -| [security\_group](#module\_security\_group) | tedilabs/network/aws//modules/security-group | ~> 0.27.0 | +| [security\_group](#module\_security\_group) | tedilabs/network/aws//modules/security-group | ~> 0.29.0 | ## Resources @@ -32,6 +35,10 @@ This module creates following resources. |------|------| | [aws_vpc_endpoint.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_endpoint) | resource | | [aws_vpc_endpoint_connection_notification.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_endpoint_connection_notification) | resource | +| [aws_vpc_endpoint_policy.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_endpoint_policy) | resource | +| [aws_vpc_endpoint_security_group_association.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_endpoint_security_group_association) | resource | +| [aws_vpc_endpoint_subnet_association.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_endpoint_subnet_association) | resource | +| [aws_availability_zones.available](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/availability_zones) | data source | ## Inputs @@ -39,37 +46,40 @@ This module creates following resources. |------|-------------|------|---------|:--------:| | [name](#input\_name) | (Required) Desired name for the VPC Interface Endpoint. | `string` | n/a | yes | | [service\_name](#input\_service\_name) | (Required) The service name. For AWS services the service name is usually in the form `com.amazonaws..`. | `string` | n/a | yes | -| [subnets](#input\_subnets) | (Required) The ID of one or more subnets in which to create a network interface for the endpoint. | `list(string)` | n/a | yes | | [vpc\_id](#input\_vpc\_id) | (Required) The ID of the VPC in which the endpoint will be used. | `string` | n/a | yes | | [auto\_accept](#input\_auto\_accept) | (Optional) Accept the VPC endpoint (the VPC endpoint and service need to be in the same AWS account). | `bool` | `true` | no | -| [default\_security\_group](#input\_default\_security\_group) | (Optional) The configuration of the default security group for the interface endpoint. `default_security_group` block as defined below.
(Optional) `name` - The name of the default security group.
(Optional) `description` - The description of the default security group.
(Optional) `ingress_cidrs` - A list of IPv4 CIDR blocks to allow inbound traffic from.
(Optional) `ingress_ipv6_cidrs` - A list of IPv6 CIDR blocks to allow inbound traffic from.
(Optional) `ingress_prefix_lists` - A list of Prefix List IDs to allow inbound traffic from.
(Optional) `ingress_security_groups` - A list of source Security Group IDs to allow inbound traffic from. | `any` | `{}` | no | +| [connection\_notifications](#input\_connection\_notifications) | (Optional) A list of configurations of Endpoint Connection Notifications for VPC Endpoint events. Each block of `connection_notifications` as defined below.
(Required) `name` - The name of the configuration for connection notification. This value is only used internally within Terraform code.
(Required) `sns_topic` - The Amazon Resource Name (ARN) of the SNS topic for the notifications.
(Required) `events` - One or more endpoint events for which to receive notifications. Valid values are `Accept`, `Reject`, `Connect` and `Delete`. |
list(object({
name = string
sns_topic = string
events = set(string)
}))
| `[]` | no | +| [default\_security\_group](#input\_default\_security\_group) | (Optional) The configuration of the default security group for the interface endpoint. `default_security_group` block as defined below.
(Optional) `enabled` - Whether to use the default security group. Defaults to `true`.
(Optional) `name` - The name of the default security group. If not provided, the endpoint name is used for the name of security group.
(Optional) `description` - The description of the default security group.
(Optional) `ingress_rules` - A list of ingress rules in a security group. You don't need to specify `protocol`, `from_port`, `to_port`. Just specify source information. Defaults to `[{ cidr_blocks = "0.0.0.0/0" }]`. |
object({
enabled = optional(bool, true)
name = optional(string)
description = optional(string, "Managed by Terraform.")
ingress_rules = optional(any, [{
cidr_blocks = ["0.0.0.0/0"]
}])
})
| `{}` | no | +| [ip\_address\_type](#input\_ip\_address\_type) | (Optional) The type of IP addresses used by the subnets for the interface endpoint. The possible values are `IPV4`, `IPV6` and `DUALSTACK`. Defaults to `IPV4` | `string` | `"IPV4"` | no | | [module\_tags\_enabled](#input\_module\_tags\_enabled) | (Optional) Whether to create AWS Resource Tags for the module informations. | `bool` | `true` | no | -| [notification\_configurations](#input\_notification\_configurations) | (Optional) A list of configurations of Endpoint Connection Notifications for VPC Endpoint events. |
list(object({
sns_arn = string
events = list(string)
}))
| `[]` | no | +| [network\_mapping](#input\_network\_mapping) | (Optional) The configuration for the interface endpoint how routes traffic to targets in which subnets, and in accordance with IP address settings. Choose one subnet for each zone. An endpoint network interface is assigned a private IP address from the IP address range of your subnet, and keeps this IP address until the interface endpoint is deleted. Each key of `network_mapping` is the availability zone id like `apne2-az1`, `use1-az1`. Each block of `network_mapping` as defined below.
(Required) `subnet` - The id of the subnet of which to attach to the endpoint. You can specify only one subnet per Availability Zone. |
map(object({
subnet = string
}))
| `{}` | no | | [policy](#input\_policy) | (Optional) A policy to attach to the endpoint that controls access to the service. This is a JSON formatted string. Defaults to full access. All Gateway and some Interface endpoints support policies. | `string` | `null` | no | | [private\_dns\_enabled](#input\_private\_dns\_enabled) | (Optional) Whether or not to associate a private hosted zone with the specified VPC. | `bool` | `false` | no | | [resource\_group\_description](#input\_resource\_group\_description) | (Optional) The description of Resource Group. | `string` | `"Managed by Terraform."` | no | | [resource\_group\_enabled](#input\_resource\_group\_enabled) | (Optional) Whether to create Resource Group to find and group AWS resources which are created by this module. | `bool` | `true` | no | | [resource\_group\_name](#input\_resource\_group\_name) | (Optional) The name of Resource Group. A Resource Group name can have a maximum of 127 characters, including letters, numbers, hyphens, dots, and underscores. The name cannot start with `AWS` or `aws`. | `string` | `""` | no | -| [security\_groups](#input\_security\_groups) | (Optional) A set of security group IDs to associate with the network interface. | `set(string)` | `[]` | no | +| [security\_groups](#input\_security\_groups) | (Optional) A list of security group IDs to associate with the endpoint. | `list(string)` | `[]` | no | | [tags](#input\_tags) | (Optional) A map of tags to add to all resources. | `map(string)` | `{}` | no | +| [timeouts](#input\_timeouts) | (Optional) How long to wait for the endpoint to be created/updated/deleted. |
object({
create = optional(string, "10m")
update = optional(string, "10m")
delete = optional(string, "10m")
})
| `{}` | no | ## Outputs | Name | Description | |------|-------------| | [arn](#output\_arn) | The Amazon Resource Name (ARN) of the VPC endpoint. | -| [default\_security\_group](#output\_default\_security\_group) | The default security group of the VPC endpoint. | -| [dns\_configurations](#output\_dns\_configurations) | The DNS entries for the VPC Endpoint. | +| [connection\_notifications](#output\_connection\_notifications) | A list of Endpoint Connection Notifications for VPC Endpoint events. | +| [default\_security\_group](#output\_default\_security\_group) | The default security group ID of the VPC endpoint. | +| [dns\_entries](#output\_dns\_entries) | The DNS entries for the VPC Endpoint. | | [id](#output\_id) | The ID of the VPC endpoint. | -| [managed](#output\_managed) | Whether or not the VPC Endpoint is being managed by its service. | +| [ip\_address\_type](#output\_ip\_address\_type) | The type of IP addresses used by the VPC endpoint. | | [name](#output\_name) | The VPC Interface Endpoint name. | -| [network\_interface\_ids](#output\_network\_interface\_ids) | One or more network interfaces for the VPC Endpoint. | -| [notification\_configurations](#output\_notification\_configurations) | A list of Endpoint Connection Notifications for VPC Endpoint events. | +| [network\_interfaces](#output\_network\_interfaces) | One or more network interfaces for the VPC Endpoint. | +| [network\_mapping](#output\_network\_mapping) | The configuration for the endpoint how routes traffic to targets in which subnets and IP address settings. | | [owner\_id](#output\_owner\_id) | The Owner ID of the VPC endpoint. | -| [policy](#output\_policy) | The policy which is attached to the endpoint that controls access to the service. | +| [requester\_managed](#output\_requester\_managed) | Whether or not the VPC Endpoint is being managed by its service. | | [security\_groups](#output\_security\_groups) | A set of security group IDs which is assigned to the VPC endpoint. | | [service\_name](#output\_service\_name) | The service name of the VPC Interface Endpoint. | | [state](#output\_state) | The state of the VPC endpoint. | -| [subnet\_ids](#output\_subnet\_ids) | A list of Subnet IDs of the VPC endpoint. | +| [type](#output\_type) | The type of the VPC endpoint. | | [vpc\_id](#output\_vpc\_id) | The VPC ID of the VPC endpoint. | diff --git a/modules/vpc-interface-endpoint/main.tf b/modules/vpc-interface-endpoint/main.tf index b7df4fd..0181de5 100644 --- a/modules/vpc-interface-endpoint/main.tf +++ b/modules/vpc-interface-endpoint/main.tf @@ -14,19 +14,54 @@ locals { } : {} } +data "aws_availability_zones" "available" { + state = "available" +} + +locals { + available_az_ids = data.aws_availability_zones.available.zone_ids + network_mapping = { + for zone_id in local.available_az_ids : + zone_id => try(var.network_mapping[zone_id], null) + } + + security_groups = concat( + (var.default_security_group.enabled + ? module.security_group[*].id + : [] + ), + var.security_groups + ) +} + + +################################################### +# Interface Endpoint +################################################### + +# TODO: +# - `dns_options` +# INFO: Not supported attributes +# - `route_table_ids` +# INFO: Use a separate resource +# - `policy` +# - `security_group_ids` +# - `subnet_ids` resource "aws_vpc_endpoint" "this" { vpc_endpoint_type = "Interface" service_name = var.service_name - vpc_id = var.vpc_id - subnet_ids = var.subnets - security_group_ids = setunion( - [module.security_group.id], - var.security_groups, - ) + auto_accept = var.auto_accept + + vpc_id = var.vpc_id + ip_address_type = lower(var.ip_address_type) private_dns_enabled = var.private_dns_enabled - auto_accept = var.auto_accept - policy = var.policy + + timeouts { + create = var.timeouts.create + update = var.timeouts.update + delete = var.timeouts.delete + } tags = merge( { @@ -39,17 +74,62 @@ resource "aws_vpc_endpoint" "this" { ################################################### -# Notification +# Policy for Interface Endpoint +################################################### + +resource "aws_vpc_endpoint_policy" "this" { + vpc_endpoint_id = aws_vpc_endpoint.this.id + policy = var.policy +} + + +################################################### +# Subnet Associations for Interface Endpoint +################################################### + +resource "aws_vpc_endpoint_subnet_association" "this" { + for_each = var.network_mapping + + vpc_endpoint_id = aws_vpc_endpoint.this.id + subnet_id = each.value.subnet + + lifecycle { + precondition { + condition = contains(local.available_az_ids, each.key) + error_message = "Availability zone ${each.key} is not available." + } + } +} + + +################################################### +# Security Groups for Interface Endpoint +################################################### + +resource "aws_vpc_endpoint_security_group_association" "this" { + count = length(local.security_groups) + + vpc_endpoint_id = aws_vpc_endpoint.this.id + security_group_id = local.security_groups[count.index] + + replace_default_association = count.index == 0 +} + + +################################################### +# Connection Notifications ################################################### +# INFO: Not supported attributes +# - `vpc_endpoint_service_id` resource "aws_vpc_endpoint_connection_notification" "this" { for_each = { - for config in try(var.notification_configurations, []) : - config.sns_arn => config + for config in var.connection_notifications : + config.name => config } vpc_endpoint_id = aws_vpc_endpoint.this.id - connection_notification_arn = each.key - connection_events = try(each.value.events, []) + connection_notification_arn = each.value.sns_topic + connection_events = each.value.events } diff --git a/modules/vpc-interface-endpoint/outputs.tf b/modules/vpc-interface-endpoint/outputs.tf index cdd8541..381679a 100644 --- a/modules/vpc-interface-endpoint/outputs.tf +++ b/modules/vpc-interface-endpoint/outputs.tf @@ -18,17 +18,22 @@ output "arn" { value = aws_vpc_endpoint.this.arn } -output "state" { - description = "The state of the VPC endpoint." - value = aws_vpc_endpoint.this.state -} - output "owner_id" { description = "The Owner ID of the VPC endpoint." value = aws_vpc_endpoint.this.owner_id } -output "managed" { +output "type" { + description = "The type of the VPC endpoint." + value = "INTERFACE" +} + +output "state" { + description = "The state of the VPC endpoint." + value = upper(aws_vpc_endpoint.this.state) +} + +output "requester_managed" { description = "Whether or not the VPC Endpoint is being managed by its service." value = aws_vpc_endpoint.this.requester_managed } @@ -38,22 +43,19 @@ output "vpc_id" { value = aws_vpc_endpoint.this.vpc_id } -output "subnet_ids" { - description = "A list of Subnet IDs of the VPC endpoint." - value = var.subnets +output "network_mapping" { + description = "The configuration for the endpoint how routes traffic to targets in which subnets and IP address settings." + value = local.network_mapping } -output "default_security_group" { - description = "The default security group of the VPC endpoint." - value = { - id = module.security_group.id - name = module.security_group.name +output "ip_address_type" { + description = "The type of IP addresses used by the VPC endpoint." + value = upper(aws_vpc_endpoint.this.ip_address_type) +} - ingress_cidrs = try(var.default_security_group.ingress_cidrs, []) - ingress_ipv6_cidrs = try(var.default_security_group.ingress_ipv6_cidrs, []) - ingress_prefix_lists = try(var.default_security_group.ingress_prefix_lists, []) - ingress_security_groups = try(var.default_security_group.ingress_security_groups, []) - } +output "default_security_group" { + description = "The default security group ID of the VPC endpoint." + value = one(module.security_group[*].id) } output "security_groups" { @@ -61,22 +63,27 @@ output "security_groups" { value = aws_vpc_endpoint.this.security_group_ids } -output "network_interface_ids" { +output "network_interfaces" { description = "One or more network interfaces for the VPC Endpoint." value = aws_vpc_endpoint.this.network_interface_ids } -output "dns_configurations" { +output "dns_entries" { description = "The DNS entries for the VPC Endpoint." value = aws_vpc_endpoint.this.dns_entry } -output "policy" { - description = "The policy which is attached to the endpoint that controls access to the service." - value = aws_vpc_endpoint.this.policy -} - -output "notification_configurations" { - description = "A list of Endpoint Connection Notifications for VPC Endpoint events." - value = var.notification_configurations +output "connection_notifications" { + description = < { + id = notification.id + state = notification.state + events = notification.connection_events + sns_topic = notification.connection_notification_arn + } + } } diff --git a/modules/vpc-interface-endpoint/security-group.tf b/modules/vpc-interface-endpoint/security-group.tf index 708d7d9..ef5e5cf 100644 --- a/modules/vpc-interface-endpoint/security-group.tf +++ b/modules/vpc-interface-endpoint/security-group.tf @@ -4,63 +4,23 @@ module "security_group" { source = "tedilabs/network/aws//modules/security-group" - version = "~> 0.27.0" - - vpc_id = var.vpc_id - - name = try(var.default_security_group.name, local.metadata.name) - description = try(var.default_security_group.description, "Managed by Terraform.") - - ingress_rules = concat( - (length(try(var.default_security_group.ingress_cidrs, [])) > 0 - ? [{ - id = "ipv4-cidrs" - description = "Allow inbound traffic from the IPv4 CIDRs." - protocol = "tcp" - from_port = 0 - to_port = 65535 - - cidr_blocks = try(var.default_security_group.ingress_cidrs, []) - }] - : [] - ), - (length(try(var.default_security_group.ingress_ipv6_cidrs, [])) > 0 - ? [{ - id = "ipv6-cidrs" - description = "Allow inbound traffic from the IPv6 CIDRs." - protocol = "tcp" - from_port = 0 - to_port = 65535 - - ipv6_cidr_blocks = try(var.default_security_group.ingress_ipv6_cidrs, []) - }] - : [] - ), - (length(try(var.default_security_group.ingress_prefix_lists, [])) > 0 - ? [{ - id = "prefix-lists" - description = "Allow inbound traffic from the Prefix Lists." - protocol = "tcp" - from_port = 0 - to_port = 65535 - - prefix_list_ids = try(var.default_security_group.ingress_prefix_lists, []) - }] - : [] - ), - [ - for security_group in try(var.default_security_group.ingress_security_groups, []) : { - id = "security-groups" - description = "Allow inbound traffic from the source Security Groups." - protocol = "tcp" - from_port = 0 - to_port = 65535 - - source_security_group_id = security_group - } - ] - ) - egress_rules = [] + version = "~> 0.29.0" + + count = var.default_security_group.enabled ? 1 : 0 + + name = coalesce(var.default_security_group.name, local.metadata.name) + description = var.default_security_group.description + vpc_id = var.vpc_id + + ingress_rules = [ + for i, rule in var.default_security_group.ingress_rules : + merge({ + id = try(rule.id, "endpoint-${i}") + protocol = "tcp" + from_port = 443 + to_port = 443 + }, rule) + ] revoke_rules_on_delete = true resource_group_enabled = false diff --git a/modules/vpc-interface-endpoint/variables.tf b/modules/vpc-interface-endpoint/variables.tf index fba3213..d1621e0 100644 --- a/modules/vpc-interface-endpoint/variables.tf +++ b/modules/vpc-interface-endpoint/variables.tf @@ -1,80 +1,143 @@ variable "name" { description = "(Required) Desired name for the VPC Interface Endpoint." type = string + nullable = false } variable "service_name" { description = "(Required) The service name. For AWS services the service name is usually in the form `com.amazonaws..`." type = string + nullable = false +} + +variable "auto_accept" { + description = "(Optional) Accept the VPC endpoint (the VPC endpoint and service need to be in the same AWS account)." + type = bool + default = true + nullable = false } variable "vpc_id" { description = "(Required) The ID of the VPC in which the endpoint will be used." type = string + nullable = false } -variable "subnets" { - description = "(Required) The ID of one or more subnets in which to create a network interface for the endpoint." - type = list(string) +variable "network_mapping" { + description = <