fix: nginx-alb terraform example uses private IPs for instances (#131)

The ALB reaches backends over the private VPC network, so the Nginx
instances no longer need public IPs. Drop associate_public_ip_address
and map_public_ip_on_launch, and swap the instance public-IP outputs
for private-IP ones.

The *.elb.spinifex.local DNS name does not resolve from the operator's
host, so document an AWS CLI one-liner (describe-load-balancers with
an AvailabilityZones[].LoadBalancerAddresses[].IpAddress query) that
retrieves the ALB's public IP for curl/browser testing.
This commit is contained in:
Josh Siviour
2026-04-10 18:54:12 +10:00
committed by GitHub
parent 8d2830481d
commit 67bdc9d4af
2 changed files with 89 additions and 69 deletions
+41 -26
View File
@@ -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://<alb_public_ip>
# Direct instance access
curl http://<instance_1_public_ip>
curl http://<instance_2_public_ip>
# SSH into either instance
ssh -i nginx-alb-demo.pem ec2-user@<instance_public_ip>
# Check target health via AWS CLI
aws elbv2 describe-target-health --target-group-arn <tg_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://<alb_public_ip>` 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 <tg_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@<instance_public_ip>
ssh -i nginx-alb-demo.pem ec2-user@<instance_private_ip>
sudo journalctl -u cloud-init --no-pager
sudo systemctl status nginx
```
+48 -43
View File
@@ -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://<alb_dns_name> # Load-balanced Nginx
# curl http://<public_ip_1> # Direct to instance 1
# curl http://<public_ip_2> # Direct to instance 2
# ssh -i nginx-alb-demo.pem ec2-user@<public_ip>
# 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://<alb_public_ip> # 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://<that-ip>
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
}