diff --git a/docs/terraform/nginx-alb/README.md b/docs/terraform/nginx-alb/README.md index 4489ba987..85037556a 100644 --- a/docs/terraform/nginx-alb/README.md +++ b/docs/terraform/nginx-alb/README.md @@ -1,6 +1,6 @@ --- title: "Nginx Web Server (Load Balanced)" -description: "Deploy a VPC with a public subnet and two EC2 instances running Nginx, served by an ALB using Terraform on Spinifex." +description: "Deploy a VPC with two private EC2 instances running Nginx, fronted by an internet-facing ALB using Terraform on Spinifex." category: "Terraform Workbooks" tags: - terraform @@ -33,12 +33,12 @@ resources: ## Overview -Deploy two complete Nginx web servers behind an Application Load Balancer on Spinifex using Terraform/OpenTofu. This workbook provisions a VPC, public subnet, internet gateway, route table, security group, SSH key pair, an application load balancer (ALB) and two EC2 instances with cloud-init user-data that installs and starts Nginx. +Deploy two Nginx web servers behind an internet-facing Application Load Balancer on Spinifex using Terraform/OpenTofu. This workbook provisions a VPC, two subnets, internet gateway, route table, security group, SSH key pair, an application load balancer (ALB) and two EC2 instances with cloud-init user-data that installs and starts Nginx. Only the ALB is reachable from outside the VPC — the Nginx instances have **private IPs only** and the ALB talks to them over the private VPC network. **What you'll learn:** - Configuring the AWS Terraform provider to target Spinifex -- Creating a VPC with public internet access +- Creating a VPC with an internet-facing load balancer fronting private instances - Provisioning an EC2 instance with cloud-init user-data - Provisioning an internet-facing application load balancer - Generating SSH key pairs with the TLS provider @@ -48,11 +48,11 @@ Deploy two complete Nginx web servers behind an Application Load Balancer on Spi | Resource | Name | Purpose | |---|---|---| | VPC | `nginx-alb-vpc` | Isolated network (10.20.0.0/16) | -| Subnets | `nginx-alb-public-a`, `nginx-alb-public-b` | Two public subnets for ALB and instances | -| Internet Gateway | `nginx-alb-igw` | Routes internet traffic | +| Subnets | `nginx-alb-public-a`, `nginx-alb-public-b` | Two subnets across AZs for the ALB and instances | +| Internet Gateway | `nginx-alb-igw` | Routes internet traffic for the ALB | | Security Group | `nginx-alb-sg` | Allows SSH (22) and HTTP (80) inbound | -| EC2 Instances | `nginx-alb-1`, `nginx-alb-2` | Debian 12 with Nginx via cloud-init | -| ALB | `nginx-alb` | Application Load Balancer on port 80 | +| EC2 Instances | `nginx-alb-1`, `nginx-alb-2` | Debian 12 with Nginx via cloud-init (private IPs only) | +| ALB | `nginx-alb` | Internet-facing Application Load Balancer on port 80 | | Target Group | `nginx-alb-tg` | HTTP health-checked group for both instances | | Listener | HTTP :80 | Forwards traffic to the target group | @@ -90,26 +90,33 @@ tofu apply ### Step 3. Verify -> **Note:** EC2 instances can take 30+ seconds to boot after apply. If SSH or HTTP is unreachable, wait and retry. +> **Note:** EC2 instances can take 30+ seconds to boot after apply. If the ALB returns 5xx or HTTP is unreachable, wait and retry — the target group health checks need a moment to mark both instances healthy. -After apply completes, use the outputs to test: +The ALB is internet-facing, but the DNS name Spinifex returns (`*.elb.spinifex.local`) will not resolve from your host. Fetch the ALB's public IP with the AWS CLI: ```bash -# Hit the ALB — successive requests should alternate between Server 1 and Server 2 -curl http:// - -# Direct instance access -curl http:// -curl http:// - -# SSH into either instance -ssh -i nginx-alb-demo.pem ec2-user@ - -# Check target health via AWS CLI -aws elbv2 describe-target-health --target-group-arn +ALB_IP=$(aws elbv2 describe-load-balancers --names nginx-alb \ + --query 'LoadBalancers[0].AvailabilityZones[].LoadBalancerAddresses[].IpAddress' \ + --output text) +echo "ALB public IP: $ALB_IP" ``` -Open and refresh the `http://` output in your browser to see the page alternate content served from each instance. +Then hit the ALB — successive requests should alternate between Server 1 and Server 2: + +```bash +curl http://$ALB_IP +curl http://$ALB_IP +``` + +Open `http://$ALB_IP` in your browser and refresh to see the page alternate content served from each instance. + +The Nginx instances themselves only have private IPs (see the `instance_1_private_ip` / `instance_2_private_ip` outputs) and are only reachable from inside the VPC — go through the ALB. + +Check target health via AWS CLI: + +```bash +aws elbv2 describe-target-health --target-group-arn +``` ### Cleanup @@ -136,9 +143,17 @@ sudo systemctl status spinifex.target curl -k https://localhost:9999/ ``` -### SSH Connection Timeout +### ALB Returns 5xx / Targets Unhealthy -Check that the security group allows inbound SSH (port 22) and that the instance has a public IP assigned. Verify the instance is running: +Give the instances a moment to finish cloud-init (Nginx has to install before it can answer health checks). Check target health: + +```bash +TG_ARN=$(aws elbv2 describe-target-groups --names nginx-alb-tg \ + --query 'TargetGroups[0].TargetGroupArn' --output text) +aws elbv2 describe-target-health --target-group-arn "$TG_ARN" +``` + +If targets stay unhealthy, verify the instances are running: ```bash aws ec2 describe-instances --profile spinifex @@ -146,10 +161,10 @@ aws ec2 describe-instances --profile spinifex ### Nginx Not Responding -SSH into the instance and check cloud-init logs: +The Nginx instances have no public IP, so you can't SSH in directly from your host. If you need to inspect cloud-init logs, launch a small jump host in the same VPC or run commands via the Spinifex console, then: ```bash -ssh -i nginx-alb-demo.pem ec2-user@ +ssh -i nginx-alb-demo.pem ec2-user@ sudo journalctl -u cloud-init --no-pager sudo systemctl status nginx ``` diff --git a/docs/terraform/nginx-alb/main.tf b/docs/terraform/nginx-alb/main.tf index bf92bcef7..6c8229234 100644 --- a/docs/terraform/nginx-alb/main.tf +++ b/docs/terraform/nginx-alb/main.tf @@ -1,7 +1,9 @@ # Example: Nginx Web Servers with ALB on Spinifex # -# Deploys a VPC with two public subnets, two EC2 instances running Nginx, -# and an Application Load Balancer distributing HTTP traffic between them. +# Deploys a VPC with two subnets, two EC2 instances running Nginx with +# private-only IPs, and an internet-facing Application Load Balancer +# distributing HTTP traffic between them. The ALB reaches the instances +# over the private VPC network, so the instances do not need public IPs. # Demonstrates: VPC, subnets, internet gateway, route table, security group, # key pair, cloud-init user-data, EC2 instances, ALB, target group, and listener. # @@ -10,11 +12,15 @@ # export AWS_PROFILE=spinifex # tofu init && tofu apply # -# After apply: -# curl http:// # Load-balanced Nginx -# curl http:// # Direct to instance 1 -# curl http:// # Direct to instance 2 -# ssh -i nginx-alb-demo.pem ec2-user@ +# After apply, fetch the ALB's public IP (the *.elb.spinifex.local DNS +# name does not resolve from your host): +# +# aws elbv2 describe-load-balancers --names nginx-alb \ +# --query 'LoadBalancers[0].AvailabilityZones[].LoadBalancerAddresses[].IpAddress' \ +# --output text +# +# Then: +# curl http:// # Load-balanced Nginx (alternates between instances) terraform { required_version = ">= 1.6.0" @@ -58,9 +64,9 @@ provider "aws" { region = var.region endpoints { - ec2 = var.spinifex_endpoint - iam = var.spinifex_endpoint - sts = var.spinifex_endpoint + ec2 = var.spinifex_endpoint + iam = var.spinifex_endpoint + sts = var.spinifex_endpoint elasticloadbalancingv2 = var.spinifex_endpoint } @@ -138,10 +144,9 @@ resource "aws_internet_gateway" "igw" { # --------------------------------------------------------------------------- resource "aws_subnet" "public_a" { - vpc_id = aws_vpc.main.id - cidr_block = "10.20.1.0/24" - availability_zone = data.aws_availability_zones.available.names[0] - map_public_ip_on_launch = true + vpc_id = aws_vpc.main.id + cidr_block = "10.20.1.0/24" + availability_zone = data.aws_availability_zones.available.names[0] tags = { Name = "nginx-alb-public-a" @@ -149,10 +154,9 @@ resource "aws_subnet" "public_a" { } resource "aws_subnet" "public_b" { - vpc_id = aws_vpc.main.id - cidr_block = "10.20.2.0/24" - availability_zone = data.aws_availability_zones.available.names[0] - map_public_ip_on_launch = true + vpc_id = aws_vpc.main.id + cidr_block = "10.20.2.0/24" + availability_zone = data.aws_availability_zones.available.names[0] tags = { Name = "nginx-alb-public-b" @@ -236,8 +240,6 @@ resource "aws_instance" "nginx_1" { vpc_security_group_ids = [aws_security_group.web.id] key_name = aws_key_pair.nginx.key_name - associate_public_ip_address = true - user_data_base64 = base64encode(<<-USERDATA #!/bin/bash set -euo pipefail @@ -278,8 +280,6 @@ resource "aws_instance" "nginx_2" { vpc_security_group_ids = [aws_security_group.web.id] key_name = aws_key_pair.nginx.key_name - associate_public_ip_address = true - user_data_base64 = base64encode(<<-USERDATA #!/bin/bash set -euo pipefail @@ -388,41 +388,46 @@ resource "aws_lb_listener" "http" { # --------------------------------------------------------------------------- output "note" { - value = "EC2 instances can take 30+ seconds to boot after apply. If SSH or HTTP is unreachable, wait and retry." + value = <<-EOT + EC2 instances can take 30+ seconds to boot after apply — if HTTP is + unreachable, wait and retry. + + The Nginx instances have private IPs only. The ALB DNS name ends in + .elb.spinifex.local and will not resolve from your host, so fetch the + ALB's public IP with: + + aws elbv2 describe-load-balancers --names nginx-alb \ + --query 'LoadBalancers[0].AvailabilityZones[].LoadBalancerAddresses[].IpAddress' \ + --output text + + Then: curl http:// + EOT +} + +output "alb_name" { + value = aws_lb.web.name +} + +output "alb_arn" { + value = aws_lb.web.arn } output "alb_dns_name" { value = aws_lb.web.dns_name } -output "alb_url" { - value = "http://${aws_lb.web.dns_name}" -} - -output "alb_public_ip" { - value = aws_lb.web.public_ip -} - output "instance_1_id" { value = aws_instance.nginx_1.id } -output "instance_1_public_ip" { - value = aws_instance.nginx_1.public_ip +output "instance_1_private_ip" { + value = aws_instance.nginx_1.private_ip } output "instance_2_id" { value = aws_instance.nginx_2.id } -output "instance_2_public_ip" { - value = aws_instance.nginx_2.public_ip -} - -output "ssh_command_1" { - value = "ssh -i nginx-alb-demo.pem ec2-user@${aws_instance.nginx_1.public_ip}" -} - -output "ssh_command_2" { - value = "ssh -i nginx-alb-demo.pem ec2-user@${aws_instance.nginx_2.public_ip}" +output "instance_2_private_ip" { + value = aws_instance.nginx_2.private_ip }