
Last week I built infrastructure by clicking through consoles and running commands. This week I wrote it down as code, built it again, destroyed it, and built something bigger. Five assignments, two clouds, and one rule that mattered more than any of the code: a human reviews every change before it touches the account.
Here's how the week actually went, including the apply that failed halfway.
Part 1: One file, six resources
The first assignment was the smallest: an Azure Linux VM defined entirely in Terraform. A resource group, virtual network, subnet, public IP, network interface, and the VM, all in a single main.tf.
What struck me was how little I had to describe. The NIC points at the subnet, and the VM points at the NIC. Terraform works out the build order from those references, and terraform plan showed me exactly what would be created before anything existed. That preview is the safety net the portal never gave me.
Two habits started here and stayed all week:
- The VM password lived in a variable marked
sensitive, supplied through the environment, so it never appeared in the file or in a screenshot. - Every build ended with
terraform destroy. Create, verify, destroy.
Part 2: The same ideas on AWS
The second assignment rebuilt the pattern on AWS: a custom VPC with a public and a private subnet, an internet gateway, a public route table, a security group, and an EC2 instance that installs Nginx at boot.
It also taught me about permissions. My IAM user was read-only from an earlier audit assignment, and Terraform needs to create things. A harmless dry run caught it before I wasted any time:
aws ec2 create-vpc --cidr-block 10.99.0.0/16 --dry-run UnauthorizedOperation meant missing permission. DryRunOperation ... would have succeeded meant I was good to go. I attached the EC2 policy and moved on.
Part 3: A React app that deploys itself
The third assignment used Azure custom_data (cloud-init) to turn an empty Ubuntu VM into a running React site: install Node and Nginx, clone the repository, build it, and serve the build folder.
Two details nearly tripped me up. A script saved with Windows line endings fails silently at boot, because the first line becomes #!/bin/bash\r. And a small VM can run out of memory during npm run build, so the script creates a swap file first. Neither shows up in a tutorial, and both would have cost me an evening.
Part 4: Modules, and an apply that half-worked
The fourth assignment split an AWS build into reusable modules: network, EC2, and RDS. The root file calls each module and passes outputs between them, so the database gets its subnets and security group from the network module without anything being copied by hand.
My first apply created twelve of thirteen resources and then failed on the database. RDS rejected my master password, because it forbids characters like /, @, ", and spaces. Terraform had recorded everything it had already built, so after fixing the password, the second apply created just the one missing resource. That was my first real lesson in why state matters.
Then I connected from the EC2 instance to the private database, imported the schema, and tested a cart action in the browser to see the matching row appear in RDS.
Part 5: The capstone, with Claude Code in the loop
The capstone was a production-style three-tier deployment of a Book Review app: a VPC across two Availability Zones with six subnets, a public load balancer, a web tier running Nginx and Next.js, an internal load balancer, a private Node.js API tier, and a Multi-AZ RDS MySQL database with a read replica. Sixty-three resources in the first apply.
The assignment's real subject was the workflow. I set up Claude Code with:
- a
CLAUDE.mdholding the architecture, CIDR plan, security rules, and approval rules; - a Terraform engineer subagent that proposes a design, reads the current provider docs through the Terraform MCP server, and then builds one phase at a time;
- a read-only reviewer subagent that checks each phase and reports PASS, WARN, or FAIL;
- hooks that block auto-approved changes and format code after every edit.
Claude could validate and plan. Applying and destroying were reserved for me.
The review loop earned its keep. The reviewer caught a bootstrap script that could fail before Nginx was configured, missing validation on the admin SSH range, and a subtle risk I'd have walked into: with load balancer health checks and a manual deployment, the Auto Scaling group would eventually terminate my unhealthy instances and delete my hand-deployed work. I stretched the grace period and suspended the replacement process while I deployed.
The failure no review could catch
The first capstone apply failed on the read replica:
Creating read replicas for source instance with engine mysql where ManageMasterUserPassword is enabled is not supported.
Every plan, every review, and every doc lookup had passed. Only AWS knew that rule. I gave Claude the real error and we compared options: drop the replica (the assignment required one), or generate the password in Terraform with a write-only argument and store it in Secrets Manager, so it never lands in state or outputs. The plan showed the primary updating in place, and the second apply finished the replica.
It's a clean illustration of what the Terraform MCP server is and isn't. It serves documentation. It can't query my account, so it couldn't predict this.
I also said no to a recommendation. Claude suggested an instance refresh for the change, but nothing was deployed yet, so the refresh would have waited on replacements that couldn't become healthy. I replaced the two App instances myself with one command instead.
Deploying by hand, then proving it
User data only installed software, so I deployed the apps myself: the backend through a systemd service that fetches its database password from Secrets Manager at start, and the frontend built on each web instance. Both target groups went healthy, I registered a user, wrote a review, and then read both rows back from the private database. The credentials came from the instance's IAM role, so no password ever appeared in a terminal.
Then terraform destroy. A NAT gateway, two load balancers, and three database instances bill by the hour, and the most important command of the week was the last one.
What I'm taking away
- State is the memory. A failed apply isn't a disaster, because Terraform resumes from what it recorded. Guard the state file, and never delete the project folder before you destroy.
- AI is a fast engineer and a careful reviewer, but it can't see your live account. The best results came from a loop: plan, research, build, validate, review, then a human decides.
- Keep secrets out of everything. Sensitive variables, write-only arguments, Secrets Manager, and a rule that nothing credential-shaped goes into user data or outputs.
- Test each layer in order. Every deployment problem this week was solved by checking one hop at a time, not guessing.
- Treat cost as part of the design. A single NAT gateway and a short-lived environment were deliberate tradeoffs, and I could explain every one of them.
P.S. This post is part of the DevOps Micro Internship (DMI) with Agentic AI, Cohort 3, by Pravin Mishra.
You can follow my graded progress throughout the cohort.
Start your DevOps journey: https://dmi.pravinmishra.com/?utm_source=student&utm_medium=ps-blog&utm_campaign=cohort3
