Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Issue 1037] Create non-default networking configuration #1044

Merged
merged 28 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
794161d
staging network WIP
coilysiren Jan 18, 2024
0f92684
Moves DMS to networking layer module
coilysiren Jan 18, 2024
5fbf896
misc fixes
coilysiren Jan 18, 2024
5a19e50
diff reduction
coilysiren Jan 18, 2024
0aa2338
move to networking
coilysiren Jan 18, 2024
f26b775
move to networking folder
coilysiren Jan 18, 2024
1d76c83
move to networking folder
coilysiren Jan 18, 2024
a9cd45e
puts cidr back
coilysiren Jan 18, 2024
78f7975
Merge branch 'move-dms' into staging-net
coilysiren Jan 18, 2024
41e46f8
update cidr
coilysiren Jan 18, 2024
3d9a04b
Merge remote-tracking branch 'origin/main' into staging-net
coilysiren Jan 18, 2024
382a7e9
checkpoint
coilysiren Jan 18, 2024
b10fefc
filter by correct vpc
coilysiren Jan 19, 2024
d2448e9
add sg perms
coilysiren Jan 19, 2024
3d65fdf
keep default network
coilysiren Jan 19, 2024
052d287
keep default network
coilysiren Jan 19, 2024
db2f19b
git restore
coilysiren Jan 19, 2024
f630422
Merge branch 'main' into staging-net
coilysiren Jan 19, 2024
b78ea68
rm subnet backfill
coilysiren Jan 19, 2024
221ec00
git restore
coilysiren Jan 19, 2024
bcd4084
swap defaults around
coilysiren Jan 19, 2024
2d9dd24
Merge remote-tracking branch 'origin/main' into staging-net
coilysiren Jan 23, 2024
55dcb3d
Merge branch 'main' into staging-net
coilysiren Jan 23, 2024
f7cb4ac
diff reduction
coilysiren Jan 23, 2024
2f63b94
revert
coilysiren Jan 23, 2024
dcc419a
Merge branch 'main' into staging-net
coilysiren Jan 23, 2024
8036177
diff reduction
coilysiren Jan 23, 2024
e5eb607
diff reduction
coilysiren Jan 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions infra/modules/network/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
data "aws_availability_zones" "available" {}

locals {
vpc_cidr = "10.0.0.0/20"
num_availability_zones = 3
availability_zones = slice(data.aws_availability_zones.available.names, 0, local.num_availability_zones)
}

module "aws_vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.2.0"

name = var.name
azs = local.availability_zones
cidr = local.vpc_cidr

public_subnets = ["10.0.10.0/24", "10.0.11.0/24", "10.0.12.0/24"]
private_subnets = ["10.0.0.0/24", "10.0.1.0/24", "10.0.2.0/24"]
database_subnets = ["10.0.5.0/24", "10.0.6.0/24", "10.0.7.0/24"]
public_subnet_tags = { subnet_type = "public" }
private_subnet_tags = { subnet_type = "private" }
database_subnet_tags = { subnet_type = "database" }
database_subnet_group_name = var.database_subnet_group_name

# If application needs external services, then create one NAT gateway per availability zone
enable_nat_gateway = var.has_external_non_aws_service
single_nat_gateway = false
one_nat_gateway_per_az = var.has_external_non_aws_service

enable_dns_hostnames = true
enable_dns_support = true
}
3 changes: 3 additions & 0 deletions infra/modules/network/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
output "vpc_id" {
value = module.aws_vpc.vpc_id
}
26 changes: 26 additions & 0 deletions infra/modules/network/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
variable "name" {
type = string
description = "Name to give the VPC. Will be added to the VPC under the 'network_name' tag."
}

variable "aws_services_security_group_name_prefix" {
type = string
description = "Prefix for the name of the security group attached to VPC endpoints"
}

variable "database_subnet_group_name" {
type = string
description = "Name of the database subnet group"
}

variable "has_database" {
type = bool
description = "Whether the application(s) in this network have a database. Determines whether to create VPC endpoints needed by the database layer."
default = true
}

variable "has_external_non_aws_service" {
type = bool
description = "Whether the application(s) in this network need to call external non-AWS services. Determines whether or not to create NAT gateways."
default = true
}
101 changes: 101 additions & 0 deletions infra/modules/network/vpc-endpoints.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
locals {
# List of AWS services used by this VPC
# This list is used to create VPC endpoints so that the AWS services can
# be accessed without network traffic ever leaving the VPC's private network
# For a list of AWS services that integrate with AWS PrivateLink
# see https://docs.aws.amazon.com/vpc/latest/privatelink/aws-services-privatelink-support.html
#
# The database module requires VPC access from private networks to SSM, KMS, and RDS
aws_service_integrations = setunion(
# AWS services used by ECS Fargate: ECR to fetch images, S3 for image layers, and CloudWatch for logs
["ecr.api", "ecr.dkr", "s3", "logs"],

# Feature flags with AWS Evidently
["evidently", "evidently-dataplane"],

# AWS services used by the database's role manager
var.has_database ? ["ssm", "kms", "secretsmanager"] : [],
)

# S3 and DynamoDB use Gateway VPC endpoints. All other services use Interface VPC endpoints
interface_vpc_endpoints = toset([for aws_service in local.aws_service_integrations : aws_service if !contains(["s3", "dynamodb"], aws_service)])
gateway_vpc_endpoints = toset([for aws_service in local.aws_service_integrations : aws_service if contains(["s3", "dynamodb"], aws_service)])
Comment on lines +20 to +22
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Untested idea:

Suggested change
# S3 and DynamoDB use Gateway VPC endpoints. All other services use Interface VPC endpoints
interface_vpc_endpoints = toset([for aws_service in local.aws_service_integrations : aws_service if !contains(["s3", "dynamodb"], aws_service)])
gateway_vpc_endpoints = toset([for aws_service in local.aws_service_integrations : aws_service if contains(["s3", "dynamodb"], aws_service)])
# S3 and DynamoDB use Gateway VPC endpoints. All other services use Interface VPC endpoints
gateway_services = ["s3", "dynamodb"]
interface_vpc_endpoints = setsubtract(local.aws_service_integrations, local.gateway_services)
gateway_vpc_endpoints = setintersection(local.aws_service_integrations, local.gateway_services)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would try that, but this file is a straight copy from the Nava template, and I kinda wanna keep it that way.

This is a good suggestion for the Nava template though! => https://github.com/navapbc/template-infra/blob/main/infra/modules/network/vpc-endpoints.tf

}

data "aws_region" "current" {}

# VPC Endpoints for accessing AWS Services
# ----------------------------------------
#
# Since the role manager Lambda function is in the VPC (which is needed to be
# able to access the database) we need to allow the Lambda function to access
# AWS Systems Manager Parameter Store (to fetch the database password) and
# KMS (to decrypt SecureString parameters from Parameter Store). We can do
# this by either allowing internet access to the Lambda, or by using a VPC
# endpoint. The latter is more secure.
# See https://repost.aws/knowledge-center/lambda-vpc-parameter-store
# See https://docs.aws.amazon.com/vpc/latest/privatelink/create-interface-endpoint.html#create-interface-endpoint

data "aws_subnet" "private" {
count = length(module.aws_vpc.private_subnets)
id = module.aws_vpc.private_subnets[count.index]
}

# AWS services may only be available in certain regions and availability zones,
# so we use this data source to get that information and only create
# VPC endpoints in the regions / availability zones where the particular service
# is available.
data "aws_vpc_endpoint_service" "aws_service" {
for_each = local.interface_vpc_endpoints
service = each.key
}

locals {
# Map from the name of an AWS service to a list of the private subnets that are in availability
# zones where the service is available. Only create this map for AWS services where we are going
# to create an Interface VPC endpoint, which require a list of subnet ids in which to create the
# elastic network interface for the endpoint.
aws_service_subnets = {
for service in local.interface_vpc_endpoints :
service => [
for subnet in data.aws_subnet.private[*] :
subnet.id
if contains(data.aws_vpc_endpoint_service.aws_service[service].availability_zones, subnet.availability_zone)
]
}
}

resource "aws_security_group" "aws_services" {
name_prefix = var.aws_services_security_group_name_prefix
description = "VPC endpoints to access AWS services from the VPCs private subnets"
vpc_id = module.aws_vpc.vpc_id
}

resource "aws_vpc_security_group_ingress_rule" "vpc_endpoints_ingress_from_vpc_cidr" {
security_group_id = aws_security_group.aws_services.id
description = "Allow inbound requests to VPC endpoints from the VPC cidr block"
from_port = 443
to_port = 443
ip_protocol = "tcp"
cidr_ipv4 = local.vpc_cidr
}

resource "aws_vpc_endpoint" "interface" {
for_each = local.interface_vpc_endpoints

vpc_id = module.aws_vpc.vpc_id
service_name = "com.amazonaws.${data.aws_region.current.name}.${each.key}"
vpc_endpoint_type = "Interface"
security_group_ids = [aws_security_group.aws_services.id]
subnet_ids = local.aws_service_subnets[each.key]
private_dns_enabled = true
}

resource "aws_vpc_endpoint" "gateway" {
for_each = local.gateway_vpc_endpoints

vpc_id = module.aws_vpc.vpc_id
service_name = "com.amazonaws.${data.aws_region.current.name}.${each.key}"
vpc_endpoint_type = "Gateway"
route_table_ids = module.aws_vpc.private_route_table_ids
}
83 changes: 6 additions & 77 deletions infra/networks/main.tf
Original file line number Diff line number Diff line change
@@ -1,33 +1,10 @@
# TODO: This file is is a temporary implementation of the network layer
# that currently just adds resources to the default VPC
# The full network implementation is part of https://github.com/navapbc/template-infra/issues/152

data "aws_region" "current" {}

locals {
tags = merge(module.project_config.default_tags, {
description = "VPC resources"
})
region = module.project_config.default_region

# List of AWS services used by this VPC
# This list is used to create VPC endpoints so that the AWS services can
# be accessed without network traffic ever leaving the VPC's private network
# For a list of AWS services that integrate with AWS PrivateLink
# see https://docs.aws.amazon.com/vpc/latest/privatelink/aws-services-privatelink-support.html
#
# The database module requires VPC access from private networks to SSM, KMS, and RDS
aws_service_integrations = setunion(
# AWS services used by ECS Fargate: ECR to fetch images, S3 for image layers, and CloudWatch for logs
["ecr.api", "ecr.dkr", "s3", "logs"],

# AWS services used by the database's role manager
var.has_database ? ["ssm", "kms", "secretsmanager"] : [],
)

# S3 and DynamoDB use Gateway VPC endpoints. All other services use Interface VPC endpoints
interface_vpc_endpoints = toset([for aws_service in local.aws_service_integrations : aws_service if !contains(["s3", "dynamodb"], aws_service)])
gateway_vpc_endpoints = toset([for aws_service in local.aws_service_integrations : aws_service if contains(["s3", "dynamodb"], aws_service)])
}

terraform {
Expand Down Expand Up @@ -62,7 +39,7 @@ module "app_config" {

module "dms_networking" {
source = "../modules/dms-networking"
vpc_id = data.aws_vpc.default.id
vpc_id = module.network.vpc_id
}

data "aws_vpc" "default" {
Expand All @@ -76,57 +53,9 @@ data "aws_subnets" "default" {
}
}

# VPC Endpoints for accessing AWS Services
# ----------------------------------------
#
# Since the role manager Lambda function is in the VPC (which is needed to be
# able to access the database) we need to allow the Lambda function to access
# AWS Systems Manager Parameter Store (to fetch the database password) and
# KMS (to decrypt SecureString parameters from Parameter Store). We can do
# this by either allowing internet access to the Lambda, or by using a VPC
# endpoint. The latter is more secure.
# See https://repost.aws/knowledge-center/lambda-vpc-parameter-store
# See https://docs.aws.amazon.com/vpc/latest/privatelink/create-interface-endpoint.html#create-interface-endpoint

# docs: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group
resource "aws_security_group" "aws_services" {
count = length(local.aws_service_integrations) > 0 ? 1 : 0

name_prefix = module.project_config.aws_services_security_group_name_prefix
description = "VPC endpoints to access AWS services from the VPCs private subnets"
vpc_id = data.aws_vpc.default.id
}

# docs: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule
resource "aws_vpc_security_group_ingress_rule" "aws_services" {
count = length(local.aws_service_integrations) > 0 ? 1 : 0

security_group_id = aws_security_group.aws_services[0].id
description = "Allow all traffic from the VPCs CIDR block to the VPC endpoint security group"
from_port = 443
to_port = 443
ip_protocol = "tcp"
cidr_ipv4 = data.aws_vpc.default.cidr_block
}

# docs: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_endpoint
resource "aws_vpc_endpoint" "aws_service" {
for_each = local.aws_service_integrations

vpc_id = data.aws_vpc.default.id
service_name = "com.amazonaws.${data.aws_region.current.name}.${each.key}"
vpc_endpoint_type = "Interface"
security_group_ids = [aws_security_group.aws_services[0].id]
subnet_ids = [for subnet in aws_subnet.backfill_private : subnet.id]
private_dns_enabled = true
}

# docs: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_endpoint
resource "aws_vpc_endpoint" "gateway" {
for_each = local.gateway_vpc_endpoints

vpc_id = data.aws_vpc.default.id
service_name = "com.amazonaws.${data.aws_region.current.name}.${each.key}"
vpc_endpoint_type = "Gateway"
route_table_ids = [for table in aws_route_table.backfill_private : table.id]
module "network" {
source = "../modules/network"
name = var.environment_name
database_subnet_group_name = var.environment_name
aws_services_security_group_name_prefix = var.environment_name
}
4 changes: 4 additions & 0 deletions infra/networks/staging.s3.tfbackend
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
bucket = "simpler-grants-gov-315341936575-us-east-1-tf"
key = "infra/networks/staging.tfstate"
dynamodb_table = "simpler-grants-gov-315341936575-us-east-1-tf-state-locks"
region = "us-east-1"
5 changes: 5 additions & 0 deletions infra/networks/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ variable "has_database" {
description = "whether the application has a database"
default = true
}

variable "environment_name" {
type = string
description = "name of the application environment"
}