############################################################################### # Lab 04 — The Endpoint That Answered To Nobody # # A PrivateLink interface endpoint in a consumer VPC pointing at a provider's # endpoint service. The endpoint is available, DNS resolves, the NLB has a # healthy target, and the consumer's security group allows all egress. # Connections time out anyway. # # terraform init # terraform apply # terraform output consumer_session_command # # COST: one internal NLB (~$0.0225/hour), one interface endpoint # (~$0.01/hour per AZ), and two t3.micro instances. Cents for a 30-minute # session. Run `terraform destroy` when done. ############################################################################### terraform { required_version = ">= 1.5.0" required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } } provider "aws" { region = var.region default_tags { tags = { Project = "venerable-networks-lab" Lab = "04-privatelink-endpoint-sg" ManagedBy = "terraform" } } } variable "region" { description = "Region to deploy the lab into." type = string default = "us-east-1" } variable "name" { description = "Name prefix for lab resources." type = string default = "vn-lab-04" } data "aws_availability_zones" "available" { state = "available" } data "aws_caller_identity" "current" {} locals { # Both VPCs use the same AZ. An interface endpoint can only be placed in an # AZ where the endpoint service is available, and the service's availability # follows the NLB's subnets. az = data.aws_availability_zones.available.names[0] provider_cidr = "10.10.0.0/16" provider_subnet_cidr = "10.10.1.0/24" consumer_cidr = "10.20.0.0/16" consumer_subnet_cidr = "10.20.1.0/24" service_port = 8080 } ############################################################################### # Provider VPC — hosts the service behind an NLB ############################################################################### resource "aws_vpc" "provider" { cidr_block = local.provider_cidr enable_dns_support = true enable_dns_hostnames = true tags = { Name = "${var.name}-provider" } } resource "aws_internet_gateway" "provider" { vpc_id = aws_vpc.provider.id tags = { Name = "${var.name}-provider-igw" } } # Public only so Session Manager can reach the backend host without interface # endpoints. Lab harness, not a production pattern. resource "aws_subnet" "provider" { vpc_id = aws_vpc.provider.id cidr_block = local.provider_subnet_cidr availability_zone = local.az map_public_ip_on_launch = true tags = { Name = "${var.name}-provider-a" } } resource "aws_route_table" "provider" { vpc_id = aws_vpc.provider.id route { cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.provider.id } tags = { Name = "${var.name}-provider-rtb" } } resource "aws_route_table_association" "provider" { subnet_id = aws_subnet.provider.id route_table_id = aws_route_table.provider.id } resource "aws_security_group" "backend" { name = "${var.name}-backend" description = "Service backend behind the NLB" vpc_id = aws_vpc.provider.id # Deliberately broad so the backend is never the reason a request fails. # The only intended defect in this lab is on the consumer side. ingress { description = "Service port from anywhere in RFC1918 10/8" from_port = local.service_port to_port = local.service_port protocol = "tcp" cidr_blocks = ["10.0.0.0/8"] } egress { description = "All outbound" from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } tags = { Name = "${var.name}-sg-backend" } } resource "aws_lb" "service" { name = "${var.name}-nlb" internal = true load_balancer_type = "network" subnets = [aws_subnet.provider.id] tags = { Name = "${var.name}-nlb" } } resource "aws_lb_target_group" "service" { name = "${var.name}-tg" port = local.service_port protocol = "TCP" vpc_id = aws_vpc.provider.id target_type = "instance" # With client IP preservation on, the backend would see the consumer's # address arriving from another VPC. Disabling it keeps the source inside the # provider VPC so backend security group rules cannot become a second, # unintended cause of failure. preserve_client_ip = "false" health_check { protocol = "HTTP" path = "/" port = "traffic-port" interval = 10 healthy_threshold = 2 unhealthy_threshold = 2 } tags = { Name = "${var.name}-tg" } } resource "aws_lb_listener" "service" { load_balancer_arn = aws_lb.service.arn port = local.service_port protocol = "TCP" default_action { type = "forward" target_group_arn = aws_lb_target_group.service.arn } } resource "aws_lb_target_group_attachment" "service" { target_group_arn = aws_lb_target_group.service.arn target_id = aws_instance.backend.id port = local.service_port } ############################################################################### # Endpoint service ############################################################################### resource "aws_vpc_endpoint_service" "lab" { acceptance_required = false network_load_balancer_arns = [aws_lb.service.arn] tags = { Name = "${var.name}-endpoint-service" } } # Same-account consumer still has to be an allowed principal. resource "aws_vpc_endpoint_service_allowed_principal" "consumer" { vpc_endpoint_service_id = aws_vpc_endpoint_service.lab.id principal_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" } ############################################################################### # Consumer VPC — connects to the service through an interface endpoint ############################################################################### resource "aws_vpc" "consumer" { cidr_block = local.consumer_cidr enable_dns_support = true enable_dns_hostnames = true tags = { Name = "${var.name}-consumer" } } resource "aws_internet_gateway" "consumer" { vpc_id = aws_vpc.consumer.id tags = { Name = "${var.name}-consumer-igw" } } resource "aws_subnet" "consumer" { vpc_id = aws_vpc.consumer.id cidr_block = local.consumer_subnet_cidr availability_zone = local.az map_public_ip_on_launch = true tags = { Name = "${var.name}-consumer-a" } } resource "aws_route_table" "consumer" { vpc_id = aws_vpc.consumer.id route { cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.consumer.id } tags = { Name = "${var.name}-consumer-rtb" } } resource "aws_route_table_association" "consumer" { subnet_id = aws_subnet.consumer.id route_table_id = aws_route_table.consumer.id } # The application's own security group. Permits all egress, which is what the # team checks when the connection fails. resource "aws_security_group" "app" { name = "${var.name}-app" description = "Consumer application" vpc_id = aws_vpc.consumer.id egress { description = "All outbound" from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } tags = { Name = "${var.name}-sg-app" } } resource "aws_vpc_endpoint" "service" { vpc_id = aws_vpc.consumer.id service_name = aws_vpc_endpoint_service.lab.service_name vpc_endpoint_type = "Interface" subnet_ids = [aws_subnet.consumer.id] # NOTE: no security_group_ids. This is valid Terraform and a successful # apply. AWS attaches the VPC's default security group to the endpoint's # network interfaces when none is specified. # # private_dns_enabled stays false because private DNS for a custom endpoint # service requires domain ownership verification. Consumers use the generated # endpoint DNS name, which is the normal pattern for third-party services. private_dns_enabled = false tags = { Name = "${var.name}-vpce" } } ############################################################################### # Instances ############################################################################### data "aws_iam_policy_document" "ec2_assume" { statement { actions = ["sts:AssumeRole"] principals { type = "Service" identifiers = ["ec2.amazonaws.com"] } } } resource "aws_iam_role" "ssm" { name = "${var.name}-ssm-role" assume_role_policy = data.aws_iam_policy_document.ec2_assume.json } resource "aws_iam_role_policy_attachment" "ssm" { role = aws_iam_role.ssm.name policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" } resource "aws_iam_instance_profile" "ssm" { name = "${var.name}-ssm-profile" role = aws_iam_role.ssm.name } data "aws_ssm_parameter" "al2023" { name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64" } resource "aws_instance" "backend" { ami = data.aws_ssm_parameter.al2023.value instance_type = "t3.micro" subnet_id = aws_subnet.provider.id vpc_security_group_ids = [aws_security_group.backend.id] iam_instance_profile = aws_iam_instance_profile.ssm.name user_data = <<-EOF #!/bin/bash mkdir -p /opt/lab echo "vn-lab-04 provider backend ok" > /opt/lab/index.html nohup python3 -m http.server ${local.service_port} \ --bind 0.0.0.0 --directory /opt/lab \ >/var/log/lab-listener.log 2>&1 & EOF metadata_options { http_tokens = "required" } tags = { Name = "${var.name}-backend" } } resource "aws_instance" "app" { ami = data.aws_ssm_parameter.al2023.value instance_type = "t3.micro" subnet_id = aws_subnet.consumer.id vpc_security_group_ids = [aws_security_group.app.id] iam_instance_profile = aws_iam_instance_profile.ssm.name user_data = <<-EOF #!/bin/bash dnf install -y nmap-ncat tcpdump bind-utils >/dev/null 2>&1 EOF metadata_options { http_tokens = "required" } tags = { Name = "${var.name}-app" } } ############################################################################### # Outputs ############################################################################### output "consumer_session_command" { description = "Shell on the consumer application host." value = "aws ssm start-session --region ${var.region} --target ${aws_instance.app.id}" } output "backend_session_command" { description = "Shell on the provider backend host." value = "aws ssm start-session --region ${var.region} --target ${aws_instance.backend.id}" } output "endpoint_dns_name" { description = "The name the application connects to." value = aws_vpc_endpoint.service.dns_entry[0].dns_name } output "endpoint_url" { description = "Copy/paste target for curl." value = "http://${aws_vpc_endpoint.service.dns_entry[0].dns_name}:${local.service_port}/" } output "vpc_endpoint_id" { value = aws_vpc_endpoint.service.id } output "endpoint_service_name" { value = aws_vpc_endpoint_service.lab.service_name } output "target_group_arn" { value = aws_lb_target_group.service.arn } output "consumer_default_security_group_id" { description = "The security group AWS attached to the endpoint ENIs." value = aws_vpc.consumer.default_security_group_id } output "app_security_group_id" { value = aws_security_group.app.id }