Building a Multi-Cloud Infrastructure with Terraform

Introduction
In today's cloud-native landscape, Infrastructure as Code (IaC) has become essential for modern DevOps practices. This project demonstrates how to deploy a full-stack web application on AWS using Terraform, showcasing automated infrastructure provisioning, configuration management, and best practices that companies look for in DevOps engineers.
Tech Stack:
Infrastructure: Terraform
Cloud Provider: AWS (extensible to GCP)
Application: Node.js + Express
Database: PostgreSQL (RDS)
Tools: AWS CLI, VS Code, Bash scripting
Project Architecture
High-Level Overview
This project implements a three-tier web application architecture on AWS:
┌─────────────────────────────────────────────────────────────┐
│ Internet Gateway │
└──────────────────┬──────────────────────────────────────────┘
│
┌─────────▼─────────┐
│ Application Load │
│ Balancer │
└─────────┬──────────┘
│
┌─────────▼─────────┐
│ Public Subnet │
│ │
│ ┌──────────────┐ │
│ │ EC2 Instance │ │
│ │ (Node.js App)│ │
│ └───────┬────────┘ │
└──────────┼───────────┘
│
┌──────────▼───────────┐
│ Private Subnet │
│ │
│ ┌─────────────────┐ │
│ │ RDS PostgreSQL │ │
│ └─────────────────┘ │
└───────────────────────┘
Architecture Components
1. Network Layer (VPC)
Custom VPC with CIDR block 10.0.0.0/16
2 Public subnets across different Availability Zones
2 Private subnets for database isolation
Internet Gateway for public internet access
Route tables for traffic management
Security groups for firewall rules
2. Compute Layer
EC2 t3.micro instance running Ubuntu 22.04
Application Load Balancer for traffic distribution
Auto-configured with the user_data script
Node.js 18.x runtime environment
Express.js web server on port 3000
Systemd service for application lifecycle management
3. Data Layer
RDS PostgreSQL 15 database (db.t3.micro)
Deployed in private subnets
Automated backups enabled
Multi-AZ capability for high availability
Security group restricting access to the application tier only
4. Security
Security groups implementing least privilege access
Private subnets for database isolation
IAM roles for service permissions
Encrypted database connections
No hardcoded credentials (using Terraform variables)
Project Structure
The project follows Terraform best practices with a modular structure:
multi-cloud-infrastructure/
├── README.md
├── .gitignore
├── terraform/
│ ├── main.tf # Root configuration
│ ├── variables.tf # Input variables
│ ├── outputs.tf # Output values
│ ├── terraform.tfvars # Variable values (gitignored)
│ └── modules/
│ ├── aws-vpc/ # VPC module
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── aws-compute/ # EC2 & ALB module
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ └── user_data.sh # App deployment script
│ └── aws-database/ # RDS module
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── scripts/
│ ├── deploy.sh # Automated deployment
│ ├── destroy.sh # Clean up resources
│ └── health-check.sh # Verify deployment
└── docs/
├── architecture.md
└── SETUP.md
Step-by-Step Implementation
Phase 1: Prerequisites and Setup
1. Install Required Tools
# Terraform
brew install terraform # macOS
# or download from https://terraform.io
# AWS CLI
brew install awscli # macOS
# or: curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
# Verify installations
terraform --version
aws --version
2. Configure AWS Credentials
# Configure AWS CLI
aws configure
# Input when prompted:
# AWS Access Key ID: [Your Key]
# AWS Secret Access Key: [Your Secret]
# Default region: us-east-1
# Default output format: json
# Verify configuration
aws sts get-caller-identity
3. Project Initialization
# Create project directory
mkdir multi-cloud-infrastructure
cd multi-cloud-infrastructure
# Initialize directory structure
mkdir -p terraform/modules/{aws-vpc,aws-compute,aws-database}
mkdir -p scripts docs
# Initialize git repository
git init
4. (Optional but Recommended) Set Up a Remote Terraform Backend
Terraform can keep its state file (terraform.tfstate) locally, but this becomes risky in real environments because the file can be lost, corrupted, or accidentally overwritten. A remote backend, such as Amazon S3, stores the state securely, enables state locking, maintains version history, and allows multiple people or machines to collaborate safely. While optional, using a remote backend is a best practice for production-ready infrastructure.
To automatically create a secure, versioned S3 bucket for Terraform state, run the backend setup script:
chmod +x setup-backend.sh
./setup-backend.sh
After it completes, it will print a bucket name—add that value to your Terraform backend configuration before running any Terraform commands, Or simply just uncomment the backend s3 block in terraform/main.tf.
Phase 2: Building the VPC Module
The VPC module creates the network foundation for our application.
File: terraform/modules/aws-vpc/main.tf
data "aws_availability_zones" "available" {
state = "available"
}
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.environment}-vpc"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.environment}-igw"
}
}
# Public subnets across 2 AZs
resource "aws_subnet" "public" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.environment}-public-subnet-${count.index + 1}"
}
}
# Private subnets for database
resource "aws_subnet" "private" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + 10)
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = {
Name = "${var.environment}-private-subnet-${count.index + 1}"
}
}
# Route table for public subnets
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "${var.environment}-public-rt"
}
}
# Associate route table with public subnets
resource "aws_route_table_association" "public" {
count = 2
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
# Security group for web traffic
resource "aws_security_group" "allow_web" {
name = "${var.environment}-allow-web"
description = "Allow web traffic"
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.environment}-allow-web"
}
}
Key Design Decisions:
Multiple Availability Zones: Using
count = 2creates subnets across two AZs for high availabilityCIDR Calculation:
cidrsubnet()function automatically calculates subnet rangesPublic/Private Separation: Public subnets have internet access; private subnets don't
Dynamic AZ Selection: Using
data.aws_availability_zonesmakes the code region-agnostic
File: terraform/modules/aws-vpc/variables.tf
variable "vpc_cidr" {
description = "CIDR block for VPC"
type = string
}
variable "environment" {
description = "Environment name"
type = string
}
File: terraform/modules/aws-vpc/outputs.tf
output "vpc_id" {
value = aws_vpc.main.id
}
output "public_subnet_ids" {
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
value = aws_subnet.private[*].id
}
output "security_group_id" {
value = aws_security_group.allow_web.id
}
Phase 3: Building the Compute Module
This module creates the EC2 instance, Application Load Balancer, and deploys the application.
File: terraform/modules/aws-compute/main.tf
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}
resource "aws_security_group" "instance" {
name = "${var.environment}-instance-sg"
description = "Security group for EC2 instance"
vpc_id = var.vpc_id
ingress {
from_port = var.app_port
to_port = var.app_port
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.environment}-instance-sg"
}
}
resource "aws_instance" "app" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
subnet_id = var.public_subnet_ids[0]
vpc_security_group_ids = [aws_security_group.instance.id]
user_data = base64encode(templatefile("${path.module}/user_data.sh", {
db_host = var.db_endpoint
db_name = var.db_name
db_user = var.db_user
db_password = var.db_password
port = var.app_port
}))
tags = {
Name = "${var.environment}-app-instance"
Cloud = "AWS"
}
}
resource "aws_lb" "main" {
name = "${var.environment}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.instance.id]
subnets = var.public_subnet_ids
tags = {
Name = "${var.environment}-alb"
}
}
resource "aws_lb_target_group" "app" {
name = "${var.environment}-tg"
port = var.app_port
protocol = "HTTP"
vpc_id = var.vpc_id
health_check {
path = "/health"
healthy_threshold = 2
unhealthy_threshold = 2
timeout = 3
interval = 30
}
tags = {
Name = "${var.environment}-tg"
}
}
resource "aws_lb_listener" "app" {
load_balancer_arn = aws_lb.main.arn
port = "80"
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
}
resource "aws_lb_target_group_attachment" "app" {
target_group_arn = aws_lb_target_group.app.arn
target_id = aws_instance.app.id
port = var.app_port
}
Critical Feature: Automated Application Deployment
File: terraform/modules/aws-compute/user_data.sh
#!/bin/bash
set -e
# Log everything to a file
exec > >(tee /var/log/user-data.log)
exec 2>&1
echo "=========================================="
echo "Starting Application Deployment"
echo "=========================================="
date
# Update system
echo "Updating system packages..."
apt-get update -y
# Install Node.js and npm
echo "Installing Node.js..."
curl -fsSL https://deb.nodesource.com/setup_18.x | bash -
apt-get install -y nodejs
# Verify installation
echo "Node.js version: $(node --version)"
echo "npm version: $(npm --version)"
# Create application directory
echo "Creating application directory..."
mkdir -p /opt/app/public
cd /opt/app
# Create package.json
echo "Creating package.json..."
cat > package.json << 'PACKAGEEOF'
{
"name": "multi-cloud-demo-app",
"version": "1.0.0",
"description": "Multi-cloud demo application",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2",
"pg": "^8.11.0"
}
}
PACKAGEEOF
# Create server.js with your working application
echo "Creating server.js..."
cat > server.js << 'SERVEREOF'
const express = require("express");
const app = express();
app.get("/", (req, res) => {
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Terraform EC2 App</title>
<style>
body {
margin: 0;
font-family: Arial, Helvetica, sans-serif;
background: linear-gradient(135deg, #4e54c8, #8f94fb);
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
}
.card {
background: rgba(255,255,255,0.1);
padding: 40px;
border-radius: 20px;
width: 400px;
text-align: center;
box-shadow: 0 8px 20px rgba(0,0,0,0.2);
backdrop-filter: blur(10px);
}
h1 {
margin-bottom: 10px;
font-size: 2rem;
}
p.subtitle {
margin-bottom: 25px;
font-size: 1.1rem;
opacity: 0.8;
}
.status {
margin: 20px 0;
font-size: 1.4rem;
background: #37c978;
padding: 10px 20px;
border-radius: 10px;
display: inline-block;
color: black;
}
button {
margin-top: 20px;
padding: 12px 25px;
font-size: 1rem;
border-radius: 8px;
border: none;
cursor: pointer;
background: #fff;
color: #4e54c8;
font-weight: bold;
transition: 0.3s ease;
}
button:hover {
transform: scale(1.05);
}
</style>
</head>
<body>
<div class="card">
<h1>🚀 Terraform EC2 App</h1>
<p class="subtitle">Deployed via Terraform on AWS</p>
<div class="status">✓ Healthy</div>
<button onclick="location.href='/health'">Check Health</button>
</div>
</body>
</html>
`);
});
app.get("/health", (req, res) => {
res.json({ status: "healthy", timestamp: new Date().toISOString() });
});
app.listen(3000, "0.0.0.0", () => {
console.log("Server running on port 3000");
});
SERVEREOF
# Install npm packages
echo "Installing npm packages..."
npm install
# Create systemd service for auto-start
echo "Creating systemd service..."
cat > /etc/systemd/system/multicloud-app.service << 'SERVICEEOF'
[Unit]
Description=Multi-Cloud Demo Application
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/app
ExecStart=/usr/bin/node /opt/app/server.js
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
SERVICEEOF
# Reload systemd, enable and start service
echo "Starting application service..."
systemctl daemon-reload
systemctl enable multicloud-app
systemctl start multicloud-app
# Wait a moment for service to start
sleep 3
# Check service status
systemctl status multicloud-app --no-pager
echo "=========================================="
echo "Application Deployment Complete!"
echo "=========================================="
echo "App should be accessible on port ${port}"
echo "Check logs: journalctl -u multicloud-app -f"
date
File: terraform/modules/aws-compute/output.tf
output "instance_id" {
value = aws_instance.app.id
}
output "instance_public_ip" {
value = aws_instance.app.public_ip
}
output "alb_dns_name" {
value = aws_lb.main.dns_name
}
File: terraform/modules/aws-compute/variables.tf
variable "vpc_id" {
description = "VPC ID"
type = string
}
variable "public_subnet_id" {
description = "Public subnet ID for EC2 instance"
type = string
}
variable "public_subnet_ids" {
description = "Public subnet IDs for load balancer"
type = list(string)
}
variable "environment" {
description = "Environment name"
type = string
}
variable "app_port" {
description = "Application port"
type = number
default = 3000
}
variable "db_endpoint" {
description = "Database endpoint"
type = string
}
variable "db_name" {
description = "Database name"
type = string
default = "appdb"
}
variable "db_user" {
description = "Database user"
type = string
default = "dbadmin"
}
variable "db_password" {
description = "Database password"
type = string
sensitive = true
}
Why This Approach Works:
Fully Automated: No manual SSH or configuration needed
Idempotent: Can be run multiple times safely
Logged: All output goes to
/var/log/user-data.logProduction-Ready: Uses systemd for service management
Auto-Restart: Service restarts on failure or reboot
Infrastructure as Code: Application deployment is part of infrastructure
Phase 4: Building the Database Module
File: terraform/modules/aws-database/main.tf
resource "aws_db_subnet_group" "main" {
name = "${var.environment}-db-subnet"
subnet_ids = var.private_subnet_ids
tags = {
Name = "${var.environment}-db-subnet-group"
}
}
resource "aws_security_group" "rds" {
name = "${var.environment}-rds-sg"
description = "Security group for RDS"
vpc_id = var.vpc_id
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.environment}-rds-sg"
}
}
resource "aws_db_instance" "main" {
identifier = "${var.environment}-postgres"
engine = "postgres"
engine_version = "18.1"
instance_class = "db.t3.micro"
allocated_storage = 20
storage_type = "gp2"
db_name = "appdb"
username = "dbadmin"
password = var.db_password
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.rds.id]
skip_final_snapshot = true
publicly_accessible = false
tags = {
Name = "${var.environment}-rds"
}
}
File: terraform/modules/aws-database/variables.tf
variable "vpc_id" {
description = "VPC ID"
type = string
}
variable "private_subnet_ids" {
description = "Private subnet IDs"
type = list(string)
}
variable "db_password" {
description = "Database password"
type = string
sensitive = true
}
variable "environment" {
description = "Environment name"
type = string
}
File: terraform/modules/aws-database/output.tf
output "db_endpoint" {
value = aws_db_instance.main.endpoint
}
output "db_name" {
value = aws_db_instance.main.db_name
}
output "db_username" {
value = aws_db_instance.main.username
}
Security Highlights:
Database in private subnets (no public IP)
Security group only allows connections from the VPC
Automated backups enabled
Maintenance windows configured
Passwords managed via Terraform variables (marked as sensitive)
Phase 5: Root Terraform Configuration
File: terraform/main.tf
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
ManagedBy = "Terraform"
Project = "MultiCloud"
}
}
}
# VPC Module
module "aws_vpc" {
source = "./modules/aws-vpc"
vpc_cidr = "10.0.0.0/16"
environment = var.environment
}
# Compute Module
module "aws_compute" {
source = "./modules/aws-compute"
vpc_id = module.aws_vpc.vpc_id
public_subnet_id = module.aws_vpc.public_subnet_ids[0]
public_subnet_ids = module.aws_vpc.public_subnet_ids
environment = var.environment
app_port = var.app_port
db_endpoint = module.aws_database.db_endpoint
db_password = var.db_password
db_name = module.aws_database.db_name
db_user = module.aws_database.db_username
depends_on = [module.aws_database]
}
# Database Module
module "aws_database" {
source = "./modules/aws-database"
vpc_id = module.aws_vpc.vpc_id
private_subnet_ids = module.aws_vpc.private_subnet_ids
db_password = var.db_password
environment = var.environment
}
File: terraform/variables.tf
variable "aws_region" {
description = "AWS region"
type = string
default = "us-east-1"
}
variable "environment" {
description = "Environment name"
type = string
default = "production"
}
variable "db_password" {
description = "Database password"
type = string
sensitive = true
}
variable "app_port" {
description = "Application port"
type = number
default = 3000
}
File: terraform/terraform.tfvars
aws_region = "us-east-1"
environment = "production"
#db_password = "YourSecurePassword123!" # Change this!
app_port = 3000
If your deployment is local, you could easily export it like this,
export TF_VAR_db_password="MySuperSecretPassword"
terraform apply
It has its pros and cons; let me list them below.
Pros
Easy to use.
No password stored in Git.
Cons
Not ideal for shared team environments.
Anyone with shell access can print env variables.
When in a production environment, it is more advisable to use AWS Secret Manager (they cost slightly more). For example,
Store secret
aws secretsmanager create-secret \
--name "prod/db_password" \
--secret-string "MySecurePassword"
In Terraform
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/db_password"
}
variable "db_password" {
type = string
sensitive = true
}
They are encrypted and audited, and can be auto-rotated.
File: terraform/outputs.tf
output "aws_instance_public_ip" {
description = "EC2 instance public IP"
value = module.aws_compute.instance_public_ip
}
output "aws_alb_dns" {
description = "Application Load Balancer DNS"
value = module.aws_compute.alb_dns_name
}
output "application_url" {
description = "Access your application at"
value = "http://${module.aws_compute.instance_public_ip}:3000"
}
output "aws_rds_endpoint" {
description = "RDS endpoint"
value = module.aws_database.db_endpoint
sensitive = true
}
Phase 6: Automation Scripts
File: scripts/deploy.sh
#!/bin/bash
set -e
echo "================================================"
echo " Multi-Cloud Infrastructure Deployment"
echo "================================================"
echo ""
# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Check required tools
echo "Checking prerequisites..."
command -v terraform >/dev/null 2>&1 || { echo -e "${RED}❌ Terraform not installed${NC}"; exit 1; }
command -v aws >/dev/null 2>&1 || { echo -e "${RED}❌ AWS CLI not installed${NC}"; exit 1; }
echo -e "${GREEN}✓ All tools installed${NC}"
echo ""
# Verify AWS credentials
echo "Verifying AWS credentials..."
if aws sts get-caller-identity >/dev/null 2>&1; then
echo -e "${GREEN}✓ AWS credentials valid${NC}"
else
echo -e "${RED}❌ AWS credentials invalid${NC}"
exit 1
fi
echo ""
# Navigate to terraform directory
cd terraform
# Check if terraform.tfvars exists
if [ ! -f "terraform.tfvars" ]; then
echo -e "${YELLOW}⚠ terraform.tfvars not found${NC}"
echo "Please create terraform.tfvars from terraform.tfvars.example"
exit 1
fi
# Initialize Terraform
echo "Initializing Terraform..."
terraform init
# Validate configuration
echo "Validating Terraform configuration..."
if terraform validate; then
echo -e "${GREEN}✓ Configuration valid${NC}"
else
echo -e "${RED}❌ Configuration invalid${NC}"
exit 1
fi
echo ""
# Plan deployment
echo "Planning deployment..."
terraform plan -out=tfplan
echo ""
# Apply deployment
read -p "Apply this plan? (yes/no): " confirm
if [ "$confirm" = "yes" ]; then
echo ""
echo "Applying Terraform configuration..."
terraform apply tfplan
echo ""
echo -e "${GREEN}================================================${NC}"
echo -e "${GREEN} Deployment Completed Successfully!${NC}"
echo -e "${GREEN}================================================${NC}"
echo ""
echo "Deployment Outputs:"
terraform output
echo ""
echo "Next steps:"
echo "1. Run './scripts/health-check.sh' to verify deployment"
echo "2. Access your application at the provided URLs"
echo "3. Check CloudWatch/Cloud Monitoring for metrics"
else
echo -e "${YELLOW}Deployment cancelled${NC}"
rm -f tfplan
fi
File: scripts/health-check.sh
#!/bin/bash
echo "================================================"
echo " Infrastructure Health Check"
echo "================================================"
echo ""
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m'
cd terraform
# Get outputs
echo "Fetching infrastructure details..."
AWS_IP=$(terraform output -raw aws_instance_public_ip 2>/dev/null || echo "N/A")
echo ""
echo "AWS Instance: $AWS_IP"
echo ""
# Check AWS instance
if [ "$AWS_IP" != "N/A" ]; then
echo "Checking AWS instance health..."
aws_status=$(curl -s -o /dev/null -w "%{http_code}" http://$AWS_IP:3000/health 2>/dev/null || echo "000")
if [ "$aws_status" = "200" ]; then
echo -e "${GREEN}✓ AWS instance healthy${NC}"
else
echo -e "${RED}✗ AWS instance unhealthy (HTTP $aws_status)${NC}"
fi
else
echo -e "${RED}✗ AWS instance not deployed${NC}"
fi
echo ""
echo "Health check completed"
File: scripts/setup-backend.sh
#!/bin/bash
set -e
echo "Setting up Terraform backend..."
# Generate unique bucket name
BUCKET_NAME="terraform-state-$(date +%s)"
AWS_REGION="us-east-1"
echo "Creating S3 bucket: $BUCKET_NAME"
aws s3 mb s3://$BUCKET_NAME --region $AWS_REGION
# Enable versioning
aws s3api put-bucket-versioning \
--bucket $BUCKET_NAME \
--versioning-configuration Status=Enabled
# Enable encryption
aws s3api put-bucket-encryption \
--bucket $BUCKET_NAME \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
}
}]
}'
echo ""
echo "Bucket created successfully: $BUCKET_NAME"
echo ""
echo "Update terraform/main.tf backend configuration with:"
echo " bucket = \"$BUCKET_NAME\""
echo " region = \"$AWS_REGION\""
File: scripts/destroy.sh
#!/bin/bash
set -e
echo "================================================"
echo " Multi-Cloud Infrastructure Destruction"
echo "================================================"
echo ""
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'
echo -e "${RED}WARNING: This will destroy all infrastructure!${NC}"
read -p "Are you sure? Type 'destroy' to confirm: " confirm
if [ "$confirm" != "destroy" ]; then
echo "Destruction cancelled"
exit 0
fi
cd terraform
echo ""
echo "Destroying infrastructure..."
terraform destroy -auto-approve
echo ""
echo -e "${YELLOW}All resources have been destroyed${NC}"
Make scripts executable:
chmod +x scripts/*.sh
Deployment Process
Step 1: Initial Setup
# Configure AWS
aws configure
# Navigate to project
cd multi-cloud-infrastructure/terraform
# Create terraform.tfvars
cat > terraform.tfvars << EOF
aws_region = "us-east-1"
environment = "production"
db_password = "YourSecurePassword123!"
app_port = 3000
EOF
# Create a remote baackend if you wish to take that route, else local tfstate works.
Step 2: Deploy Infrastructure
# Option 1: Use the deploy script
./scripts/deploy.sh
# Option 2: Manual deployment
cd terraform
terraform init
terraform plan
terraform apply
Terraform will create:
1 VPC
1 Internet Gateway
4 Subnets (2 public, 2 private)
3 Security Groups
1 EC2 Instance
1 Application Load Balancer
1 RDS Database
Associated route tables and network ACLs
Expected Output:
Apply complete! Resources: 25 added, 0 changed, 0 destroyed.
Outputs:
application_url = "http://54.91.20.8:3000"
aws_alb_dns = "production-alb-1234567890.us-east-1.elb.amazonaws.com"
aws_instance_public_ip = "54.91.20.8"
Step 3: Wait for Application Initialization
The EC2 instance needs 2-3 minutes to:
Boot up
Run user_data script
Install Node.js
Install npm packages
Start the application service
Monitor progress:
# Check if instance is running
aws ec2 describe-instances \
--filters "Name=tag:Name,Values=production-app-instance" \
--query 'Reservations[*].Instances[*].[State.Name]' \
--output text
# Once running, test health
curl http://YOUR_IP:3000/health
Step 4: Verify Deployment
# Run health check script
./scripts/health-check.sh
# Or manually test
curl http://YOUR_IP:3000/health
curl http://YOUR_IP:3000
# Open in browser
open http://YOUR_IP:3000
Application Features
What the Application Does
The deployed Node.js application is a simple web server that demonstrates:
Health Check Endpoint (
/health)Returns JSON with status and timestamp
Used by ALB health checks
Useful for monitoring
Web Interface (
/)Shows deployment status
Interactive button to check health
Demonstrates frontend capabilities
Application Architecture
User Request → ALB → EC2 (Port 3000) → Express.js → Response
↓
RDS PostgreSQL
(for future features)

