Skip to main content

Command Palette

Search for a command to run...

Building a Multi-Cloud Infrastructure with Terraform

Published
16 min read
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:

  1. Multiple Availability Zones: Using count = 2 creates subnets across two AZs for high availability

  2. CIDR Calculation: cidrsubnet() function automatically calculates subnet ranges

  3. Public/Private Separation: Public subnets have internet access; private subnets don't

  4. Dynamic AZ Selection: Using data.aws_availability_zones makes 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:

  1. Fully Automated: No manual SSH or configuration needed

  2. Idempotent: Can be run multiple times safely

  3. Logged: All output goes to /var/log/user-data.log

  4. Production-Ready: Uses systemd for service management

  5. Auto-Restart: Service restarts on failure or reboot

  6. 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:

  1. Boot up

  2. Run user_data script

  3. Install Node.js

  4. Install npm packages

  5. 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:

  1. Health Check Endpoint (/health)

    • Returns JSON with status and timestamp

    • Used by ALB health checks

    • Useful for monitoring

  2. 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)