Field to Cloud — Architecture & Operations
Everything in this course is infrastructure as code. One
terraform apply(locally, via CI, or from the student's own machine) produces the entire lab. Nothing is clicked together in a console.
1. Deployment steps — from clone to lab
flowchart TD
A["📦 git clone github.com/helhindi/devops-course"] --> B["🔑 AWS + gh CLI authenticated (OIDC — no static keys)"]
B --> C["☁️ ./scripts/bootstrap-aws.sh<br/>(once, by the owner)"]
C --> C1["creates S3 state bucket + DynamoDB lock table"]
C1 --> C2["creates GitHub OIDC provider + IAM role"]
C2 --> C3["registers repo VARIABLES via gh:<br/>TF_STATE_BUCKET · TF_LOCK_TABLE · AWS_DEPLOY_ROLE_ARN · keys"]
C3 --> D["🏗️ cd terraform && terraform init<br/>-backend-config=… (shared remote state)"]
D --> E["terraform plan — shows the whole stack, auto-tagged"]
E --> F["terraform apply — provisions everything"]
F --> G["✅ Resultant infrastructure (below)"]
G --> H1["👩🎓 Student: ssh ubuntu@lab.course.elhindi.net"]
G --> H2["🌐 Course site: https://course.elhindi.net"]
G --> H3["✅ Progress tracker auto-verifies tasks"]
G --> H4["⏹ 90-min idle → auto-stop (or pill buttons)"]
2. Resultant infrastructure
flowchart LR
subgraph EDGE["Edge (CloudFront, eu-west-1 ACM cert)"]
CF["🌐 CloudFront<br/>course.elhindi.net"]
FN["⚡ CloudFront Function<br/>/progress/ → index.html"]
end
subgraph STATIC["Serverless course (always on, ~$0 idle)"]
S3["🗄️ S3 bucket<br/>devops-course-elhindi"]
LAM_PROG["⚡ Lambda · progress tracker<br/>GET/PUT checklist + site activity"]
DDB_PROG[("🧱 DynamoDB<br/>cloud-course-progress<br/>student + auto items")]
LAM_CTRL["⚡ Lambda · lab control<br/>GET / · POST /start · POST /stop"]
DDB_ACT[("🧱 DynamoDB<br/>cloud-course-activity")]
LAM_STOP["⚡ Lambda · auto-stop<br/>(EventBridge, 10 min)"]
EB["⏰ EventBridge rule<br/>rate(10 minutes)"]
end
subgraph LAB["Training lab (EC2 — costs while running)"]
VPC["VPC 10.42.0.0/16<br/>SG: key-based SSH, world-open"]
LABBOX["🖥️ lab-box · t3.small<br/>tools · podman · terraform<br/>verify agent + heartbeat cron"]
MONBOX["🖥️ monitor-box · t3.small<br/>Grafana :3000 · Prometheus :9090<br/>module 7"]
end
subgraph DNS["Dynamic DNS (Route53 · elhindi.net)"]
R53["lab.course.elhindi.net → lab-box IP<br/>monitor.course.elhindi.net → monitor IP<br/>(refreshed on every start)"]
end
subgraph AUTH["Auth & cost (OIDC only)"]
OIDC["GitHub OIDC provider<br/>role: github-actions-cloud-course"]
BUDGET["💵 $10/mo budget<br/>alerts 30/50/80/90%"]
SSM["SSM Session Manager<br/>admin from any IP"]
end
USER["👩🎓 Student / 👨🏫 Instructor"] --> CF
CF --> FN --> S3
S3 --> LAM_PROG --> DDB_PROG
USER --> LAM_CTRL --> DDB_ACT
LAM_STOP --> EB
LAM_STOP --> DDB_ACT
LAM_STOP -->|"stop if idle > 90 min"| LABBOX
LAM_STOP -->|"stop if idle > 90 min"| MONBOX
LAM_CTRL -->|"start/stop + refresh DNS"| LABBOX
LAM_CTRL -->|"start/stop + refresh DNS"| MONBOX
LAM_CTRL --> R53
LABBOX -->|"heartbeat (SSH active)"| DDB_ACT
LABBOX -->|"auto-verify ✓"| DDB_PROG
USER --> R53
USER --> LABBOX
USER --> MONBOX
AUTH -.->|"assumes role"| CF
AUTH -.->|"assumes role"| LAM_CTRL
3. CI/CD pipeline (every merge to main)
flowchart LR
PR["PR: feature/branch"] --> CONV["✅ Commit convention<br/>[feature|bugfix|hotfix]/<branch>: desc"]
CONV --> PLAN["✅ Terraform Plan (multi-OS)<br/>ubuntu · macos · windows"]
PLAN --> MERGE["🔀 Merge to main"]
MERGE --> VER["🔖 Version & release<br/>semver tag + GitHub release"]
VER --> APPLY["✅ Terraform Apply (OIDC)<br/>S3 state + DynamoDB lock"]
APPLY --> PUB["✅ Publish Course Site<br/>mkdocs → S3 → CloudFront"]
PUB --> SITE["🌐 course.elhindi.net live"]
4. Auto-stop & auto-verify (the cost guardrails)
sequenceDiagram
participant S as Student
participant LB as lab-box
participant A as activity table
participant ST as auto-stop Lambda
participant P as progress table
participant TR as tracker page
S->>LB: SSH session opens
loop every 5 min
LB->>A: heartbeat last_seen (only while SSH active)
end
S->>TR: opens /progress/ or ticks a task
TR->>A: site activity record
loop every 10 min
ST->>A: read latest activity
alt idle > 90 min
ST->>LB: StopInstances
ST->>MON: StopInstances
end
end
loop every 15 min (lab box)
LB->>LB: run verify checks (tools, ports, files, tf, podman)
LB->>P: write id=auto results
TR->>P: GET progress + auto_keys → ✓ AUTO badges
end
5. Key commands (single source of truth)
# One-time bootstrap (owner, AWS + gh authed):
./scripts/bootstrap-aws.sh
# Any engineer / student — full stack:
cd terraform
terraform init \
-backend-config="bucket=$TF_STATE_BUCKET" \
-backend-config="key=cloud-course/terraform.tfstate" \
-backend-config="region=eu-west-2" \
-backend-config="dynamodb_table=$TF_LOCK_TABLE"
terraform plan
terraform apply # or: destroy
terraform output # SSH hostnames, token, URLs
# Manual lab control:
./scripts/lab-ctl.sh start|stop|status|ssh|verify
# CI (no secrets needed — OIDC + repo variables):
gh workflow run terraform-apply.yml
gh workflow run publish-course-site.yml
6. Design decisions at a glance
| Concern | Choice | Why |
|---|---|---|
| Course content | S3 + CloudFront + Lambda | serverless, always on, ~$0 idle |
| Hands-on labs | 2 × EC2 (t3.small) | only part that costs while running |
| Cost | $10/mo budget + 90-min auto-stop | alarms at 30/50/80/90% to both engineers |
| Access | key-based SSH (world-open) + SSM | travel-proof, no IP chasing |
| DNS | lab/monitor.course.elhindi.net A records |
IPs change on stop/start; DNS follows |
| AWS auth | OIDC only, repo variables via gh | zero manually inserted secrets |
| Tags | terraform/modules/tags/ + default_tags |
every resource tagged automatically |
| Versioning | semver tag on every mainline merge | resources carry appVersion=<tag> |
| Progress | auto-verify agent on lab-box | tasks tick ✓ AUTO when genuinely done |
| State | S3 + DynamoDB lock | two engineers, no drift |