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.
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.
# 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.
# 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.
# 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_devStep 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.
# 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.
# 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 devValidation 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.