100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Terraform & Infrastructure as Code
55 minintermediate

Provisioning Practice — Two-Cloud Deploy

In this exercise you will build the complete CricketPulse multi-cloud provisioning pipeline: Terraform provisions compute on AWS (ap-south-1) and a managed data store on GCP (asia-south1 BigQuery), then Ansible configures the provisioned AWS EC2 instances with the CricketPulse API application. You will use workspace-based environment separation, generate an Ansible dynamic inventory from Terraform outputs, and validate the full pipeline from blank credentials to a running CricketPulse API serving live score requests from AWS compute with GCP analytics ingestion. The exercise reinforces Lessons 17, 18, and 19 in a realistic end-to-end scenario.

Analogy🏏Cricket
🏏 Think of it like cricket: This exercise is the full pre-season camp for CricketPulse: setting up the Mumbai home ground (AWS India region) for match-day operations, establishing the ICC analytics centre in Colombo (GCP BigQuery), and training the full ground crew (Ansible configuration) to run the stadium from first principles. By the end, the stadium is operational: the API is serving live match data from AWS, and every scoring event is streaming to the GCP analytics platform for post-match analysis. This is the equivalent of Rohit Sharma's team completing a full preparation camp — infrastructure ready, software deployed, analytics flowing, all from a standing start.

Setup

You will need: an AWS account (free tier sufficient), a GCP project with BigQuery API enabled, the AWS CLI and gcloud CLI configured, Terraform ~> 1.6 installed, Ansible 2.15+ installed, and an SSH key pair. The exercise uses the `dev` workspace throughout to avoid production infrastructure costs. All resources use free-tier or minimal-cost instance types.

Analogy🏏Cricket
🏏 Think of it like cricket: gathering your AWS account, GCP project, CLIs, Terraform, Ansible, and an SSH key before the exercise is like a team checking every piece of kit and every clearance before a tour — passports, visas, bats, pads, and net bookings all confirmed on the eve of departure. Just as a touring side won't leave for an overseas series until every player's paperwork and gear is verified, this two-cloud deploy won't run until both cloud accounts, both CLIs, and the SSH key are in place. Just as the squad uses practice nets rather than the Test arena to keep costs and risk down, the exercise stays in the `dev` workspace on free-tier instances to avoid production infrastructure costs. Confirming prerequisites up front prevents the mid-tour disaster of arriving without a visa — or hitting `apply` only to discover a missing credential. The payoff: like a fully-cleared squad landing ready to play, you begin the deploy with every dependency confirmed and nothing blocking the first delivery.
bash
# Prerequisites check and project structure setup

# Verify tools
terraform --version   # Should be >= 1.6.0
ansible --version     # Should be >= 2.15.0
aws sts get-caller-identity  # Confirm AWS credentials
gcloud auth list             # Confirm GCP credentials

# Create project structure
mkdir -p cricketpulse-multicloud/{
  terraform/{modules/{api-server,gcp-analytics},environments},
  ansible/{playbooks,roles,inventory,templates}
}

cd cricketpulse-multicloud

# .gitignore
cat > .gitignore << 'EOF'
.terraform/
*.tfstate
*.tfstate.backup
*.tfplan
.terraform.lock.hcl
ansible/inventory/aws_ec2.yml  # Generated file
*.pem
!*.tfvars.example
EOF

# Generate SSH key for this exercise
ssh-keygen -t rsa -b 4096 \
  -f ~/.ssh/cricketpulse-deploy \
  -C "cricketpulse-deploy" \
  -N ''

Step 1 — Terraform Multi-Cloud Configuration

Write the Terraform configuration that provisions an AWS EC2 instance (the CricketPulse API server) and a GCP BigQuery dataset (the analytics warehouse). Use separate provider blocks, the S3 backend, and the dev workspace. Reference common locals for tagging across both providers.

Analogy🏏Cricket
🏏 Think of it like cricket: Step 1 is groundbreaking — pouring the foundations of both the Mumbai stadium (AWS EC2) and the Colombo analytics centre (GCP BigQuery) simultaneously from the same construction blueprint (Terraform configuration). The architect (Terraform core) coordinates both the Mumbai construction crew (AWS provider) and the Colombo construction crew (GCP provider) in parallel, tracking both projects in a single project register (state file).
hcl
# terraform/providers.tf
terraform {
  required_version = ">= 1.6.0"
  required_providers {
    aws    = { source = "hashicorp/aws", version = "~> 5.0" }
    google = { source = "hashicorp/google", version = "~> 5.0" }
  }
  # Use S3 backend (or local backend if no S3 bucket available)
  backend "local" {
    path = "terraform.tfstate"
  }
}

provider "aws" {
  region = var.aws_region
}

provider "google" {
  project = var.gcp_project_id
  region  = var.gcp_region
}

# terraform/locals.tf
locals {
  env     = terraform.workspace
  project = "cricketpulse"
  common_tags = {
    project    = local.project
    environment = local.env
    managed_by  = "terraform"
  }
}

# terraform/variables.tf
variable "aws_region"     { default = "ap-south-1" }
variable "gcp_project_id" { type = string }
variable "gcp_region"     { default = "asia-south1" }
variable "instance_type"  { default = "t3.micro" }
variable "ssh_public_key" { type = string }

# terraform/main.tf — AWS + GCP resources
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]  # Canonical
  filter { name = "name",                values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"] }
  filter { name = "virtualization-type", values = ["hvm"] }
}

resource "aws_key_pair" "deploy" {
  key_name   = "cricketpulse-deploy-${local.env}"
  public_key = var.ssh_public_key
}

resource "aws_security_group" "api" {
  name   = "cricketpulse-api-${local.env}"
  tags   = local.common_tags
  ingress { from_port = 22,   to_port = 22,   protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
  ingress { from_port = 3000, to_port = 3000, 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"] }
}

resource "aws_instance" "cricketpulse_api" {
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = var.instance_type
  key_name               = aws_key_pair.deploy.key_name
  vpc_security_group_ids = [aws_security_group.api.id]
  tags                   = merge(local.common_tags, { Name = "cricketpulse-api-${local.env}", Role = "cricketpulse_api" })
}

# GCP: BigQuery analytics dataset
resource "google_bigquery_dataset" "analytics" {
  dataset_id  = "cricketpulse_analytics_${local.env}"
  description = "CricketPulse match analytics — ${local.env}"
  location    = "ASIA"
  labels      = local.common_tags
}

resource "google_bigquery_table" "match_events" {
  dataset_id = google_bigquery_dataset.analytics.dataset_id
  table_id   = "match_events"
  schema     = jsonencode([{
    name = "match_id",   type = "STRING", mode = "REQUIRED"
  },{ name = "event_type", type = "STRING", mode = "REQUIRED"
  },{ name = "timestamp",  type = "TIMESTAMP", mode = "REQUIRED"
  },{ name = "player",     type = "STRING", mode = "NULLABLE"
  },{ name = "runs",       type = "INTEGER", mode = "NULLABLE" }])
}

# terraform/outputs.tf
output "api_public_ip"        { value = aws_instance.cricketpulse_api.public_ip }
output "api_instance_id"      { value = aws_instance.cricketpulse_api.id }
output "bq_dataset_id"        { value = google_bigquery_dataset.analytics.dataset_id }
output "bq_project"           { value = var.gcp_project_id }

Step 2 — Init, Workspace, and Apply

Initialise Terraform, create and select the dev workspace, validate the plan, and apply to provision both the AWS and GCP resources in a single run.

Analogy🏏Cricket
🏏 Think of it like cricket: running init, creating and selecting the dev workspace, validating, then applying to provision AWS and GCP in one go is like the pre-match routine — inspect the ground, pick which end and which practice pitch you'll use, confirm the lineup, then send both openers out together. Just as `terraform init` downloads the providers, a team first assembles all its gear and staff on arrival. Just as selecting the dev workspace isolates your play to a specific net rather than the main square, choosing the workspace keeps this run's state separate from production. Just as a captain validates the field is legal before the first ball, `terraform validate` and the plan confirm the moves are sound before anything is committed. Then the single apply provisions both clouds at once, like both innings' preparations completing in one coordinated start. The payoff: from one clean, workspace-scoped command you bring an entire two-cloud stack onto the field, checked and isolated, ready to play.
bash
# Step 2: Terraform workflow
cd terraform

# Create terraform.tfvars (never committed — in .gitignore)
cat > terraform.tfvars << EOF
gcp_project_id = "your-gcp-project-id-here"
ssh_public_key = "$(cat ~/.ssh/cricketpulse-deploy.pub)"
EOF

# Initialise providers
terraform init
# Downloads: hashicorp/aws ~5.0 + hashicorp/google ~5.0

# Create and select dev workspace
terraform workspace new dev
# Created and switched to workspace "dev"

# Plan — observe both AWS and GCP resources
terraform plan -out=dev.tfplan
# Plan: 5 to add (aws_key_pair, aws_security_group, aws_instance,
#                  google_bigquery_dataset, google_bigquery_table)

# Apply
terraform apply dev.tfplan

# Capture outputs
API_IP=$(terraform output -raw api_public_ip)
BQ_DATASET=$(terraform output -raw bq_dataset_id)
echo "API IP: $API_IP"
echo "BigQuery dataset: $BQ_DATASET"

# Verify both resources exist
aws ec2 describe-instances \
  --filters "Name=tag:project,Values=cricketpulse" \
  --query 'Reservations[*].Instances[*].[InstanceId,State.Name,PublicIpAddress]' \
  --output table

bq ls --project_id=$(terraform output -raw bq_project) dataset
# cricketpulse_analytics_dev

Step 3 — Generate Ansible Inventory and Configure

Generate an Ansible inventory from Terraform outputs and run the CricketPulse configuration playbook against the provisioned AWS instance. Verify the API is running and accessible.

Analogy🏏Cricket
🏏 Think of it like cricket: generating an Ansible inventory from Terraform outputs and then running the configuration playbook is like Terraform naming the eleven players onto the team sheet, and Ansible then handing each one their specific role briefing. Just as Terraform provisions the AWS instance and outputs its address, the selectors name who's in the side and where they'll field; Ansible then reads that sheet and drills each player — 'you keep wicket, you open the bowling' — configuring the CricketPulse API onto the freshly-provisioned host. Just as a coach verifies each player has understood and can execute their role in the nets before the match, you verify the API is actually running and accessible after the playbook runs. The handoff from Terraform (provisioning) to Ansible (configuration) mirrors selection handing off to coaching — one decides what exists, the other makes it perform. The payoff: like a named squad turned into a match-ready team, your provisioned server becomes a running, verified application.
bash
# Step 3: Ansible configuration
cd ../ansible

# Generate inventory from Terraform output
API_IP=$(cd ../terraform && terraform output -raw api_public_ip)

cat > inventory/hosts.ini << EOF
[cricketpulse_api]
${API_IP} ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/cricketpulse-deploy
EOF

# Test SSH connectivity
ansible -i inventory/hosts.ini cricketpulse_api -m ping
# api-ip | SUCCESS => { "ping": "pong" }

# Playbook: install Node.js and a minimal CricketPulse API
cat > playbooks/cricketpulse.yml << 'EOF'
---
- name: Configure CricketPulse API
  hosts: cricketpulse_api
  become: true
  vars:
    node_version: '20'
    bq_dataset: "{{ lookup('pipe', 'cd ../../terraform && terraform output -raw bq_dataset_id') }}"

  tasks:
    - name: Install Node.js repo
      shell: curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
      args: { creates: /usr/bin/node }

    - name: Install Node.js and nginx
      apt:
        name: [nodejs, nginx]
        state: present
        update_cache: yes

    - name: Write minimal CricketPulse API
      copy:
        dest: /opt/cricketpulse.js
        content: |
          const http = require('http');
          const server = http.createServer((req, res) => {
            res.writeHead(200, {'Content-Type': 'application/json'});
            res.end(JSON.stringify({
              status: 'ok',
              service: 'CricketPulse API',
              bq_dataset: '{{ bq_dataset }}',
              match: { teams: 'MI vs CSK', score: '185/4' }
            }));
          });
          server.listen(3000, () => console.log('CricketPulse listening on :3000'));

    - name: Start CricketPulse API
      shell: |
        pkill -f cricketpulse.js || true
        nohup node /opt/cricketpulse.js &> /var/log/cricketpulse.log &
      changed_when: true
EOF

# Run the playbook
ansible-playbook -i inventory/hosts.ini playbooks/cricketpulse.yml

# Verify the API is running
curl http://${API_IP}:3000
# {"status":"ok","service":"CricketPulse API",
#  "bq_dataset":"cricketpulse_analytics_dev",
#  "match":{"teams":"MI vs CSK","score":"185/4"}}

Step 4 — Workspace Switch and Teardown

Verify workspace isolation by switching to a new staging workspace and confirming Terraform sees no existing resources. Then tear down all resources to avoid ongoing costs.

Analogy🏏Cricket
🏏 Think of it like cricket: switching to a fresh staging workspace and finding zero existing resources is like walking onto a brand-new, untouched practice pitch — a clean square with no footmarks, no prior innings, no state carried over from the last game. Just as a new net is pristine regardless of what happened on the adjacent one, a new Terraform workspace sees none of the dev environment's resources, proving each workspace's state is genuinely isolated. Then tearing everything down with destroy is like the groundstaff clearing and covering the ground after stumps so no costs — or damage — accrue overnight. Just as a responsible touring side leaves the facility as they found it rather than leaving gear strewn about incurring fees, you destroy all resources to avoid ongoing charges. The payoff: you prove workspaces don't bleed into each other, like separate nets on one ground, and you leave a clean slate behind with nothing left running to bill you.
bash
# Step 4: Workspace isolation check and cleanup
cd terraform

# Verify dev workspace state
terraform workspace show
# dev
terraform state list
# aws_instance.cricketpulse_api
# aws_key_pair.deploy
# aws_security_group.api
# google_bigquery_dataset.analytics
# google_bigquery_table.match_events

# Switch to staging — see empty state (no staging resources created)
terraform workspace new staging
terraform state list
# (empty — staging has its own independent state file)

# Switch back to dev and verify resources still present
terraform workspace select dev
terraform state list  # All 5 resources still here

# Teardown: destroy dev resources (workspace-scoped)
terraform workspace select dev
terraform destroy -var-file=terraform.tfvars -auto-approve

# Verify teardown
terraform state list  # Empty
terraform workspace show  # dev

# Clean up staging workspace (empty, no resources)
terraform workspace select default
terraform workspace delete staging
terraform workspace delete dev

Validation Checklist

  • terraform plan shows exactly 5 resources to add: aws_key_pair, aws_security_group, aws_instance, google_bigquery_dataset, google_bigquery_table — no more, no less.
  • terraform apply completes successfully, with both AWS (EC2) and GCP (BigQuery) resources appearing in terraform state list.
  • Both providers use common_tags/labels from the shared locals block — confirmed by checking the AWS instance's tags and GCP dataset's labels in the cloud consoles.
  • Ansible ping succeeds (pong response) against the provisioned EC2 instance before running the playbook.
  • The Ansible playbook runs to completion with all tasks reporting 'ok' or 'changed' on first run and all tasks reporting 'ok' on the second run (idempotency validated).
  • curl http://<api_ip>:3000 returns a JSON response containing the bq_dataset name from Terraform output — confirming cross-tool data flow works.
  • terraform workspace select staging followed by terraform state list returns empty — confirming workspace state isolation.
  • terraform destroy -auto-approve in the dev workspace successfully removes all 5 resources without errors.
Lesson 20 of 24
0% complete