AWS SOLUTION GUIDE · TEMP ENVIRONMENT SETUP

Deploy a Secure Temp AWS Stack
Spring Boot + React + Redis

A complete, step-by-step guide to spinning up a temporary cloud environment using Terraform, AWS VPC, EC2, ElastiCache, and S3/CloudFront — with secure API-to-database networking and interview prep.

Terraform AWS VPC EC2 ElastiCache S3 + CloudFront Spring Boot ReactJS Redis
🗺
Architecture Overview

What We're Building

We're setting up a temporary, cost-effective AWS environment that mirrors a real production stack. This guide is beginner-friendly — every command is explained. Here's how the pieces connect:

👤 User / Browser
↕ HTTPS (443)
☁️ CloudFront CDN
SSL Termination
🪣 S3 Bucket
React Static Files
API Calls
PUBLIC SUBNET
⚖️ Application Load Balancer
Port 8080 → EC2
PRIVATE SUBNET
🖥 EC2 Instance
Spring Boot :8080
↕ Port 6379 (internal)
⚡ ElastiCache Redis
Port 6379 only
VPC: 10.0.0.0/16 · Public: 10.0.1.0/24 · Private: 10.0.2.0/24
🌐
VPC Network
Isolated private network. Public + Private subnets. Internet Gateway for outbound traffic.
🖥
Spring Boot API
EC2 t3.small in private subnet. Talks to Redis internally. Exposed only via ALB.
Redis Cache
ElastiCache in private subnet. Only EC2 security group can reach port 6379.
React Frontend
Static build on S3. Served via CloudFront with HTTPS. No servers to manage.
💡 New to AWS? Think of a VPC like your own private data center inside AWS. The Public Subnet is like the lobby — internet-facing. The Private Subnet is the back office — no direct internet access, much more secure.
01
Step 1 — Prerequisites

Tools You Need Installed

Install all of these before starting. Click the links to go to official installation pages.

Tool Version Purpose Install Link
AWS CLI v2.x Talk to AWS from terminal docs.aws.amazon.com
Terraform v1.7+ Infrastructure as Code hashicorp.com/terraform
Docker v24+ Containerize Spring Boot docs.docker.com
Java JDK 17 LTS Build Spring Boot app adoptium.net
Node.js v20 LTS Build React app nodejs.org
Git latest Version control git-scm.com
bash — Verify all tools
# Run each line and confirm you see version numbers
aws --version          # aws-cli/2.x.x
terraform --version    # Terraform v1.x.x
docker --version       # Docker version 24.x
java -version          # openjdk 17
node --version         # v20.x.x
git --version          # git version 2.x
⚠️ Windows Users: Use WSL2 (Windows Subsystem for Linux) for the best experience. All commands in this guide assume a Unix-like shell (bash/zsh).
02
Step 2 — IAM & Credentials

Setting Up AWS Access

🚨 Never use your root AWS account! Always create a dedicated IAM user. Never commit credentials to Git. Never share access keys.

Create an IAM User (AWS Console)

  1. Log in to AWS IAM Console
  2. Go to Users → Create user
  3. Name it: terraform-deploy
  4. Select Attach policies directly
  5. Attach: AdministratorAccess (for temp env — restrict in prod)
  6. Go to the user → Security credentials → Create access key
  7. Choose CLI → Download the CSV file

Configure AWS CLI

bash
aws configure

# You will be prompted:
AWS Access Key ID [None]: AKIAIOSFODNN7EXAMPLE
AWS Secret Access Key [None]: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Default region name [None]: us-east-1
Default output format [None]: json

# Test it works:
aws sts get-caller-identity
💡 What is STS? get-caller-identity calls the AWS Security Token Service (STS) to verify your credentials. If you see your Account ID and ARN, you're authenticated!
bash — Expected output
{
    "UserId": "AIDAIOSFODNN7EXAMPLE",
    "Account": "123456789012",
    "Arn": "arn:aws:iam::123456789012:user/terraform-deploy"
}
03
Step 3 — Terraform Project Setup

Infrastructure as Code Foundation

Terraform lets you define your entire AWS infrastructure in code files. This means you can create everything with one command and destroy it just as easily — perfect for temp environments.

Project Folder Structure

directory layout
aws-temp-env/
├── terraform/
│   ├── main.tf          # Root config & providers
│   ├── variables.tf     # Input variables
│   ├── outputs.tf       # Output values (IPs, URLs)
│   ├── vpc.tf           # VPC, subnets, gateways
│   ├── security.tf      # Security groups
│   ├── ec2.tf           # EC2 for Spring Boot
│   ├── redis.tf         # ElastiCache Redis
│   ├── s3.tf            # S3 + CloudFront for React
│   └── terraform.tfvars # Your actual values (don't commit!)
├── spring-api/
│   ├── src/
│   └── Dockerfile
└── react-app/
    ├── src/
    └── build/
terraform/main.tf — Provider configuration
# main.tf — Tells Terraform we're using AWS

terraform {
  required_version = ">= 1.7.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

# This tells Terraform which AWS region to use
provider "aws" {
  region = var.aws_region

  # Tag every resource so we know it's temp
  default_tags {
    tags = {
      Project     = "temp-env"
      Environment = "temporary"
      ManagedBy   = "terraform"
      Owner       = var.owner_name
    }
  }
}
terraform/variables.tf
variable "aws_region" {
  description = "AWS region to deploy to"
  type        = string
  default     = "us-east-1"
}

variable "owner_name" {
  description = "Your name — for resource tagging"
  type        = string
}

variable "instance_type" {
  description = "EC2 size (t3.small is cheap for testing)"
  type        = string
  default     = "t3.small"
}

variable "redis_node_type" {
  description = "ElastiCache Redis node size"
  type        = string
  default     = "cache.t3.micro"
}
terraform/terraform.tfvars — Your values
# terraform.tfvars — Fill in your actual values
# IMPORTANT: Add this file to .gitignore!

aws_region    = "us-east-1"
owner_name    = "your-name-here"
instance_type = "t3.small"

Initialize Terraform

bash
# Navigate into the terraform directory
cd aws-temp-env/terraform

# Download the AWS provider plugin
terraform init

# Expected output:
# Initializing provider plugins...
# - Installing hashicorp/aws v5.x.x...
# Terraform has been successfully initialized!
💡 terraform init downloads the AWS provider plugin — think of it like npm install for infrastructure. It creates a .terraform/ folder. You only need to run this once per project.
04
Step 4 — VPC & Networking

Building Your Private Network

A VPC (Virtual Private Cloud) is your isolated section of AWS. We create two subnets: a public one for internet-facing resources, and a private one for the API and Redis that should never be directly accessible.

🌍 Public Subnet (10.0.1.0/24)
  • Application Load Balancer
  • NAT Gateway (for private subnet's outbound)
  • Has direct internet access
🔒 Private Subnet (10.0.2.0/24)
  • EC2 (Spring Boot API)
  • ElastiCache (Redis)
  • No direct inbound internet
terraform/vpc.tf
# ── VPC ──────────────────────────────────────────
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true  # Required for RDS/ElastiCache
  enable_dns_support   = true

  tags = { Name = "temp-vpc" }
}

# ── PUBLIC SUBNET (Load Balancer lives here) ─────
resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "us-east-1a"
  map_public_ip_on_launch = true  # EC2 here gets public IP

  tags = { Name = "temp-public-subnet" }
}

# ── PRIVATE SUBNET (API + Redis live here) ───────
resource "aws_subnet" "private" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.2.0/24"
  availability_zone = "us-east-1a"

  tags = { Name = "temp-private-subnet" }
}

# Need a second AZ subnet for ElastiCache subnet group
resource "aws_subnet" "private_b" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.3.0/24"
  availability_zone = "us-east-1b"

  tags = { Name = "temp-private-subnet-b" }
}

# ── INTERNET GATEWAY (door to the internet) ──────
resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.main.id
  tags   = { Name = "temp-igw" }
}

# ── NAT GATEWAY (private subnet → internet, 1-way)
resource "aws_eip" "nat" {
  domain = "vpc"
}

resource "aws_nat_gateway" "nat" {
  allocation_id = aws_eip.nat.id
  subnet_id     = aws_subnet.public.id  # NAT lives in PUBLIC
  tags          = { Name = "temp-nat" }
}

# ── ROUTE TABLES ─────────────────────────────────
# Public: 0.0.0.0/0 → Internet Gateway
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }
  tags = { Name = "temp-public-rt" }
}

resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

# Private: 0.0.0.0/0 → NAT Gateway (outbound only)
resource "aws_route_table" "private" {
  vpc_id = aws_vpc.main.id
  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.nat.id
  }
  tags = { Name = "temp-private-rt" }
}

resource "aws_route_table_association" "private" {
  subnet_id      = aws_subnet.private.id
  route_table_id = aws_route_table.private.id
}
💡 NAT Gateway Explained: Private subnet instances can't receive inbound internet traffic BUT they can reach out (for yum/apt updates, etc.) through the NAT Gateway. Think of it as a one-way mirror.
05
Step 5 — Security Groups

Firewall Rules Between Services

Security Groups are virtual firewalls. The key principle: only allow the minimum required traffic. Redis should only be reachable from Spring Boot. Spring Boot should only be reachable from the Load Balancer.

Security Group Inbound From Port Why
sg-alb Internet (0.0.0.0/0) 443, 80 Public web traffic to load balancer
sg-api sg-alb only 8080 ALB forwards traffic to Spring Boot
sg-redis sg-api only 6379 Only Spring Boot API can reach Redis
terraform/security.tf
# ── ALB SECURITY GROUP (Internet → Load Balancer) ─
resource "aws_security_group" "alb" {
  name        = "temp-sg-alb"
  description = "Allow HTTP/HTTPS from internet to ALB"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "HTTP from anywhere"
  }
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "HTTPS from anywhere"
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "temp-sg-alb" }
}

# ── API SECURITY GROUP (ALB → Spring Boot EC2) ───
resource "aws_security_group" "api" {
  name        = "temp-sg-api"
  description = "Allow port 8080 only from ALB"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port       = 8080
    to_port         = 8080
    protocol        = "tcp"
    # Only from ALB security group — NOT from internet!
    security_groups = [aws_security_group.alb.id]
    description     = "Spring Boot from ALB only"
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "temp-sg-api" }
}

# ── REDIS SECURITY GROUP (API → Redis only) ──────
resource "aws_security_group" "redis" {
  name        = "temp-sg-redis"
  description = "Allow Redis port only from API security group"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port       = 6379
    to_port         = 6379
    protocol        = "tcp"
    # This is the KEY security rule — only EC2 API can talk to Redis
    security_groups = [aws_security_group.api.id]
    description     = "Redis from API EC2 only"
  }
  # No egress needed for Redis
  tags = { Name = "temp-sg-redis" }
}
Security Best Practice: By referencing security_groups instead of cidr_blocks, you're saying "traffic from instances with THIS security group" — not a fixed IP. This is more dynamic and secure.
06
Step 6 — EC2 Instance (Spring Boot)

Deploying the Java API Server

terraform/ec2.tf
# Get the latest Amazon Linux 2023 AMI
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]
  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }
}

# IAM Role — allows EC2 to pull from ECR
resource "aws_iam_role" "ec2_role" {
  name = "temp-ec2-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action    = "sts:AssumeRole"
      Effect    = "Allow"
      Principal = { Service = "ec2.amazonaws.com" }
    }]
  })
}

resource "aws_iam_role_policy_attachment" "ecr" {
  role       = aws_iam_role.ec2_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
}

resource "aws_iam_instance_profile" "ec2" {
  name = "temp-ec2-profile"
  role = aws_iam_role.ec2_role.name
}

# EC2 Instance in PRIVATE subnet
resource "aws_instance" "api" {
  ami                    = data.aws_ami.amazon_linux.id
  instance_type          = var.instance_type
  subnet_id              = aws_subnet.private.id
  vpc_security_group_ids = [aws_security_group.api.id]
  iam_instance_profile   = aws_iam_instance_profile.ec2.name

  # Bootstrap script — runs on first boot
  user_data = templatefile("${path.module}/userdata.sh", {
    redis_endpoint = aws_elasticache_cluster.redis.cache_nodes[0].address
    ecr_image      = var.ecr_image_uri
    aws_region     = var.aws_region
  })

  tags = { Name = "temp-spring-api" }
}
spring-api/Dockerfile — Multi-stage build
# Stage 1: Build the JAR using Maven
FROM eclipse-temurin:17-jdk-alpine AS builder
WORKDIR /app

# Copy Maven wrapper and pom first (layer caching)
COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
RUN ./mvnw dependency:go-offline -B

# Now copy source and build
COPY src src
RUN ./mvnw package -DskipTests

# ─────────────────────────────────────────────────
# Stage 2: Lean runtime image (no JDK, just JRE)
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app

# Create non-root user for security
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

COPY --from=builder /app/target/*.jar app.jar

EXPOSE 8080
ENTRYPOINT ["java", "-jar", "-Xmx512m", "app.jar"]
spring-api/src/main/resources/application.yml
spring:
  application:
    name: temp-api

  # Redis connection — reads from environment variable
  data:
    redis:
      host: ${REDIS_HOST}       # injected at runtime
      port: 6379
      timeout: 2000
      connect-timeout: 2000

server:
  port: 8080

# Health check endpoint for ALB
management:
  endpoints:
    web:
      exposure:
        include: health,info
  endpoint:
    health:
      show-details: always
terraform/userdata.sh — EC2 bootstrap
#!/bin/bash
# This script runs automatically when EC2 boots for the first time

# Update and install Docker
yum update -y
yum install -y docker
systemctl start docker
systemctl enable docker

# Log in to ECR (AWS container registry)
aws ecr get-login-password --region ${aws_region} | \
  docker login --username AWS --password-stdin \
  ${ecr_image}

# Pull and run the Spring Boot container
docker pull ${ecr_image}

docker run -d \
  --name spring-api \
  --restart always \
  -p 8080:8080 \
  -e REDIS_HOST=${redis_endpoint} \
  -e SPRING_PROFILES_ACTIVE=prod \
  ${ecr_image}

# Confirm it's running
docker ps
07
Step 7 — Redis (ElastiCache)

Setting Up the Cache Layer

AWS ElastiCache is a managed Redis service. No server to patch, no Redis config to write. We place it in the private subnet — only accessible from the EC2 API via the security group rule we created earlier.

terraform/redis.tf
# ElastiCache needs a subnet group (min 2 AZs)
resource "aws_elasticache_subnet_group" "redis" {
  name       = "temp-redis-subnet-group"
  subnet_ids = [
    aws_subnet.private.id,
    aws_subnet.private_b.id
  ]
}

# Redis cluster — single node for temp environment
resource "aws_elasticache_cluster" "redis" {
  cluster_id           = "temp-redis"
  engine               = "redis"
  node_type            = var.redis_node_type  # cache.t3.micro
  num_cache_nodes      = 1
  parameter_group_name = "default.redis7"
  engine_version       = "7.1"
  port                 = 6379

  subnet_group_name  = aws_elasticache_subnet_group.redis.name
  security_group_ids = [aws_security_group.redis.id]

  # Disable backups for temp env (saves cost)
  snapshot_retention_limit = 0

  tags = { Name = "temp-redis" }
}
terraform/outputs.tf — Get the Redis hostname
output "redis_endpoint" {
  description = "ElastiCache Redis hostname"
  value       = aws_elasticache_cluster.redis.cache_nodes[0].address
  sensitive   = false
}

output "alb_dns_name" {
  description = "Load balancer URL for Spring Boot API"
  value       = aws_lb.api.dns_name
}

output "cloudfront_url" {
  description = "React app CloudFront URL"
  value       = aws_cloudfront_distribution.react.domain_name
}
⚠️ Cost Tip: NAT Gateway costs ~$0.045/hr. ElastiCache cache.t3.micro costs ~$0.017/hr. Remember to run terraform destroy when done to stop all charges!
08
Step 8 — React Frontend

S3 + CloudFront Static Hosting

React apps are just static files after you run npm run build. We upload them to S3 and serve them globally via CloudFront CDN — no web server needed, and it scales automatically.

bash — Build your React app first
cd react-app

# Install dependencies
npm install

# Set the API URL as an env variable
# Create a .env file:
echo "REACT_APP_API_URL=https://your-alb-dns.amazonaws.com" > .env

# Build for production
npm run build

# The build/ folder is now ready to upload to S3
ls build/
terraform/s3.tf
# S3 bucket for React static files
resource "aws_s3_bucket" "react" {
  bucket = "temp-react-app-${random_id.suffix.hex}"
  tags   = { Name = "temp-react-frontend" }
}

resource "aws_random_id" "suffix" {
  byte_length = 4
}

# Block all public access — CloudFront uses OAC instead
resource "aws_s3_bucket_public_access_block" "react" {
  bucket                  = aws_s3_bucket.react.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# CloudFront Origin Access Control
resource "aws_cloudfront_origin_access_control" "react" {
  name                              = "temp-react-oac"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

# CloudFront Distribution
resource "aws_cloudfront_distribution" "react" {
  enabled             = true
  default_root_object = "index.html"
  price_class         = "PriceClass_100"  # US/EU only — cheapest

  origin {
    domain_name              = aws_s3_bucket.react.bucket_regional_domain_name
    origin_id                = "S3-React"
    origin_access_control_id = aws_cloudfront_origin_access_control.react.id
  }

  default_cache_behavior {
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    target_origin_id       = "S3-React"
    viewer_protocol_policy = "redirect-to-https"

    forwarded_values {
      query_string = false
      cookies { forward = "none" }
    }
  }

  # SPA routing — serve index.html for 404s
  custom_error_response {
    error_code         = 404
    response_code      = 200
    response_page_path = "/index.html"
  }

  restrictions {
    geo_restriction { restriction_type = "none" }
  }
  viewer_certificate {
    cloudfront_default_certificate = true
  }
}
bash — Deploy React build to S3
# After terraform apply, get the bucket name:
BUCKET=$(terraform output -raw react_bucket_name)

# Sync the React build folder to S3
aws s3 sync react-app/build/ s3://$BUCKET \
  --delete \
  --cache-control "max-age=31536000,immutable"

# Always upload index.html with no-cache
aws s3 cp react-app/build/index.html s3://$BUCKET/index.html \
  --cache-control "no-cache, no-store, must-revalidate"

# Invalidate CloudFront cache to serve fresh files
CF_ID=$(terraform output -raw cloudfront_id)
aws cloudfront create-invalidation \
  --distribution-id $CF_ID \
  --paths "/*"
09
Step 9 — AWS CloudFormation Alternative

Native AWS Infrastructure as Code

CloudFormation is AWS's built-in IaC tool (no extra install needed). While we use Terraform in this guide, here's an equivalent snippet so you understand both approaches.

✅ Use Terraform when:
  • Multi-cloud environments
  • Team already knows HCL
  • Better state management
  • Richer ecosystem of modules
✅ Use CloudFormation when:
  • AWS-only environment
  • Native AWS integration
  • No extra tools to install
  • StackSets for multi-account
cloudformation/vpc-template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Temp environment VPC - Spring Boot + Redis'

Parameters:
  OwnerName:
    Type: String
    Description: Your name for tagging

Resources:
  # VPC
  MainVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: temp-vpc
        - Key: Owner
          Value: !Ref OwnerName

  # Public Subnet
  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MainVPC
      CidrBlock: 10.0.1.0/24
      AvailabilityZone: !Select [0, !GetAZs '']
      MapPublicIpOnLaunch: true

  # Redis Security Group
  RedisSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Redis access from API only
      VpcId: !Ref MainVPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 6379
          ToPort: 6379
          SourceSecurityGroupId: !Ref ApiSecurityGroup

Outputs:
  VpcId:
    Value: !Ref MainVPC
    Export:
      Name: TempEnv-VpcId
bash — Deploy CloudFormation stack
aws cloudformation deploy \
  --template-file cloudformation/vpc-template.yaml \
  --stack-name temp-env-vpc \
  --parameter-overrides OwnerName=YourName \
  --capabilities CAPABILITY_IAM

# Check stack status:
aws cloudformation describe-stacks \
  --stack-name temp-env-vpc \
  --query 'Stacks[0].StackStatus'

# Delete when done:
aws cloudformation delete-stack --stack-name temp-env-vpc
10
Step 10 — Docker & ECR

Containerize & Push to AWS Registry

bash — Full Docker + ECR workflow
# ── 1. Set variables ───────────────────────────────
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
AWS_REGION=us-east-1
ECR_REPO=temp-spring-api

# ── 2. Create ECR repository ───────────────────────
aws ecr create-repository \
  --repository-name $ECR_REPO \
  --region $AWS_REGION

# ── 3. Build the Docker image ──────────────────────
cd spring-api
docker build -t $ECR_REPO:latest .

# ── 4. Tag for ECR ────────────────────────────────
ECR_URI=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_REPO
docker tag $ECR_REPO:latest $ECR_URI:latest

# ── 5. Log in to ECR ──────────────────────────────
aws ecr get-login-password --region $AWS_REGION | \
  docker login --username AWS --password-stdin \
  $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com

# ── 6. Push to ECR ────────────────────────────────
docker push $ECR_URI:latest

# ── 7. Confirm it uploaded ────────────────────────
aws ecr list-images --repository-name $ECR_REPO
💡 What is ECR? Amazon Elastic Container Registry (ECR) is like Docker Hub but inside AWS. It's faster, cheaper, and integrates with IAM permissions — so your EC2 can pull images without any passwords.
11
Step 11 — Run Everything

Apply Terraform & Go Live

bash — Full Terraform apply sequence
cd terraform/

# Preview what will be created (always do this first!)
terraform plan -out=tfplan

# Review the plan output, then apply:
terraform apply tfplan

# ── This will create: ─────────────────────────────
# + aws_vpc.main
# + aws_subnet.public
# + aws_subnet.private (x2)
# + aws_internet_gateway.igw
# + aws_nat_gateway.nat
# + aws_security_group.alb / api / redis
# + aws_instance.api (EC2)
# + aws_elasticache_cluster.redis
# + aws_s3_bucket.react
# + aws_cloudfront_distribution.react
# ─ ~15-20 minutes total ──────────────────────────

# After apply, get your URLs:
terraform output alb_dns_name
terraform output cloudfront_url
terraform output redis_endpoint

🧪 Verify Everything Works

bash — End-to-end health checks
# 1. Check the Spring Boot health endpoint
ALB=$(terraform output -raw alb_dns_name)
curl http://$ALB/actuator/health
# Expected: {"status":"UP","components":{"redis":{"status":"UP"}}}

# 2. Confirm Redis is reachable from API (in health response)
curl http://$ALB/actuator/health | python3 -m json.tool

# 3. Check React is loading
CF=$(terraform output -raw cloudfront_url)
curl -I https://$CF
# Expected: HTTP/2 200

# 4. Test CORS: React → API
curl -H "Origin: https://$CF" \
     -H "Access-Control-Request-Method: GET" \
     -X OPTIONS http://$ALB/api/test -v
🧹
IMPORTANT — Teardown

Destroying the Temp Environment

🚨 Always destroy temp environments when done! Running AWS resources cost money 24/7. Even a small temp env with NAT Gateway can cost $30-50/month if left running.
bash — Safe teardown sequence
# 1. Empty the S3 bucket first (Terraform can't delete non-empty buckets)
BUCKET=$(terraform output -raw react_bucket_name)
aws s3 rm s3://$BUCKET --recursive

# 2. Destroy ALL infrastructure with one command
cd terraform/
terraform destroy

# Type 'yes' when prompted
# This removes everything: VPC, EC2, Redis, S3, CloudFront...

# 3. Delete the ECR repository and images
aws ecr delete-repository \
  --repository-name temp-spring-api \
  --force

# 4. Confirm nothing is running (should be empty)
aws ec2 describe-instances \
  --filters "Name=tag:Environment,Values=temporary" \
  --query 'Reservations[].Instances[].InstanceId'
💰 Estimated hourly costs while running:
NAT Gateway ~$0.045/hr Highest cost item
EC2 t3.small ~$0.023/hr Linux on-demand
ElastiCache t3.micro ~$0.017/hr Redis node
CloudFront + S3 ~$0.001/hr Pay-per-request
Total: ~$0.09/hr = ~$2.10/day if left running
🎤
Interview Prep

Q&A — Prove You Understand the Setup

Click each question to reveal a strong, detailed answer. These cover common interview topics around AWS networking, security, and infrastructure decisions.

1. Why put Redis in a private subnet instead of a public one?

Redis is a cache/data store — it should never be directly accessible from the internet. Placing it in a private subnet means it has no public IP and no direct internet route. The only way to reach it is from within the VPC.

We then use a Security Group rule that only allows port 6379 from the EC2 API's security group. This means even other resources inside the VPC can't reach Redis unless they have that specific security group attached.

This follows the principle of least privilege and defense in depth — two core security concepts. If the EC2 instance were somehow compromised, Redis would still be an additional layer behind the security group.

Security Principle Least Privilege Defense in Depth
2. What is the difference between a Security Group and a Network ACL?
FeatureSecurity GroupNetwork ACL
LevelInstance levelSubnet level
Stateful?✅ Yes — return traffic auto-allowed❌ No — must allow both directions
Rule typeAllow onlyAllow + Deny
Use caseFine-grained per-resource controlBlock IP ranges at subnet level

In this setup we use Security Groups because they're stateful (easier to manage) and allow us to reference other security groups as sources — a powerful pattern not available in NACLs. NACLs are useful as an additional layer to block known-bad IP ranges.

3. Why use Terraform instead of just clicking in the AWS Console?

Several key reasons:

  • Reproducibility: Run terraform apply in any AWS account/region and get an identical environment. No "it works on my setup" problems.
  • Version control: Infrastructure changes are tracked in Git with commit history, code reviews, and rollback capability.
  • Speed: Creating 15+ resources manually in the console takes 45+ minutes. Terraform does it in one command.
  • Drift detection: terraform plan shows you if someone manually changed something in the console — it detects configuration drift.
  • Clean teardown: terraform destroy removes everything it created — no orphaned resources, no forgotten NAT Gateways racking up charges.
4. Explain the VPC architecture: what is a NAT Gateway and why do we need it?

Our Spring Boot EC2 instance lives in a private subnet with no public IP. This is secure — attackers can't directly reach it. But the server still needs to reach out to the internet: to pull Docker images from ECR, download OS updates, call external APIs, etc.

A NAT (Network Address Translation) Gateway solves this: it sits in the public subnet and acts as a one-way door. Private resources can send traffic out through it, but no inbound connections can originate from the internet.

Analogy: It's like a hotel concierge. Guests in private rooms (private subnet) can ask the concierge to go get things from outside (outbound traffic), but strangers can't walk directly into the private rooms (no inbound).

Private Subnet Outbound Only Cost: $0.045/hr
5. Why serve the React app from S3 + CloudFront instead of EC2?

React is a static site after npm run build — just HTML, CSS, and JavaScript files. There's no reason to waste an EC2 instance serving static files.

  • Cost: S3 + CloudFront costs pennies vs. ~$20/month for EC2
  • Scale: CloudFront has 400+ edge locations globally. Zero configuration needed to handle millions of users.
  • Security: No servers to patch or harden — the S3 bucket is private, only CloudFront can access it via Origin Access Control (OAC).
  • HTTPS: CloudFront provides SSL/TLS certificates automatically via ACM.
  • SPA routing: We configure CloudFront to return index.html on 404s, which is how React Router works.
6. How does Spring Boot connect to Redis securely in this setup?

The connection is secured at multiple layers:

  1. Network isolation: Both EC2 and Redis are in the same private subnet — no traffic leaves AWS.
  2. Security Group enforcement: Redis port 6379 only accepts connections from the EC2's security group ID — not even other EC2 instances in the same subnet can connect unless they have that security group.
  3. Private DNS: Spring Boot connects using the ElastiCache internal DNS name (e.g., temp-redis.xyz.cache.amazonaws.com), which only resolves inside the VPC.
  4. No credentials in code: The Redis hostname is injected via the REDIS_HOST environment variable, set in the EC2 userdata script. Never hardcoded.

For production, you'd add TLS encryption on the Redis connection and store the endpoint in AWS Secrets Manager instead of environment variables.

7. What is an Application Load Balancer and why do we use one here?

An Application Load Balancer (ALB) operates at Layer 7 (HTTP/HTTPS) and serves multiple purposes here:

  • Bridge between subnets: The ALB lives in the public subnet and forwards traffic to EC2 in the private subnet — our EC2 never needs a public IP.
  • Health checks: ALB pings /actuator/health every 30 seconds. If EC2 becomes unhealthy, ALB stops routing traffic to it.
  • SSL termination: You attach an ACM certificate to the ALB. HTTPS decryption happens at the ALB; EC2 only handles plain HTTP internally.
  • Routing rules: You can route /api/* to one target group and /admin/* to another — all from one endpoint.
  • Horizontal scaling: When you add more EC2 instances, the ALB automatically distributes traffic across all of them.
8. What would you change to make this production-ready?

Several improvements for production:

🔒 Security
  • Enable Redis AUTH + TLS
  • Store secrets in AWS Secrets Manager
  • Enable VPC Flow Logs
  • Enable GuardDuty threat detection
  • Remove AdministratorAccess — use least-privilege IAM
🚀 Reliability
  • Multi-AZ for EC2 (Auto Scaling Group)
  • Redis Replication Group (multi-AZ)
  • Terraform remote state in S3 + DynamoDB lock
  • CI/CD pipeline (GitHub Actions / CodePipeline)
  • CloudWatch alarms + SNS alerts
9. What is Terraform state and why does it matter?

Terraform keeps a state file (terraform.tfstate) that maps your configuration to real AWS resources. Without it, Terraform wouldn't know that the aws_instance.api in your code corresponds to instance ID i-0abc123 in AWS.

Problems with local state (default):

  • If you lose the file, Terraform loses track of your infrastructure
  • Team members can't share it safely
  • Concurrent applies can corrupt the file

Solution — Remote State in S3:

terraform {
  backend "s3" {
    bucket         = "my-terraform-state-bucket"
    key            = "temp-env/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-state-lock" # prevents concurrent apply
    encrypt        = true
  }
}
10. Walk me through what happens when a user opens the React app and makes an API call.
  1. User types the CloudFront URL in their browser
  2. DNS resolves to the nearest CloudFront edge node (global CDN)
  3. CloudFront serves cached React files from S3 via OAC (no public S3 access)
  4. Browser loads React app, which renders the UI
  5. User triggers an action → React calls REACT_APP_API_URL/api/data
  6. Request hits the ALB in the public subnet (port 443)
  7. ALB checks the security group — allows traffic from internet
  8. ALB health-checks and routes to EC2 (port 8080) in the private subnet
  9. Spring Boot checks Redis for cached data — connects via internal DNS to ElastiCache
  10. If cache hit: returns data immediately. If miss: queries DB, stores in Redis, returns data
  11. Response flows back: EC2 → ALB → User

The entire path from browser to Redis never exposes Redis to the internet. All internal communication stays within the VPC on the private subnet.

📚
References & Further Learning

Official Documentation Links

GUIDE COMPLETE
You're Ready to Deploy ✓
Run terraform apply to spin up your stack. Run terraform destroy when done. Never leave temp environments running unattended.
Built with IBM Design Language · AWS Best Practices · Terraform 1.7+