Mission 2.4 · Stage 2 — Network
Prove It Works: A Throwaway Probe Instance
Goal: A tiny instance in a private subnet, reachable through Session Manager with no SSH, that proves NAT egress and the S3 endpoint work — then switched off with one variable.
By the end of this mission
- Look up an AMI with a filtered data source instead of hardcoding it
- Build IAM trust policies with
aws_iam_policy_document - Create optional resources with
countand read them safely withone() - Know why
-targetis for emergencies only
Part 1
Understand it first
Test infrastructure with infrastructure
A clean plan doesn't prove a network works: a route can point to the wrong NAT, or a NACL can drop replies. The quickest proof is to put a machine where the app will run and try the real paths: internet through NAT, S3 through the endpoint. Doing it in Terraform means the probe is reproducible, and a single variable removes it without leaving anything behind.
aws_iam_policy_document: policies as HCL
IAM policies are JSON. You could paste JSON strings, but the aws_iam_policy_document data source lets you write statements in HCL with references, validates the structure, and renders normalised JSON, which avoids diffs caused only by whitespace or key order. You'll use it for every IAM policy from here on.
Optional single resources: count + one()
count = var.enable_probe ? 1 : 0 makes a resource optional. Its address becomes aws_instance.probe[0] when it exists, and there are no instances when it doesn't. Referencing aws_instance.probe[0].id crashes when it's disabled, so use one(aws_instance.probe[*].id), which returns the single element or null for an empty list. The [*] splat turns the instances into a list of IDs.
Part 2
Your project after this mission · 3 files change
- bootstrap/
- backend.tf
- main.tf
- infra/
- backend.tf
- locals.tf
- logs.tf
- network.tf
- outputs.tfmodified
- probe.tfnew
- providers.tf
- refactors.tf
- security.tf
- storage.tf
- terraform.tfvars
- variables.tfmodified
- versions.tf
- .gitignore
Part 3
Build it, step by step
- 1
Add the switch
infra/variables.tfadd to filehcl variable "enable_probe" { description = "Create a throwaway instance in a private subnet to test networking." type = bool default = false } - 2
Write the probe
The AMI comes from a filtered,
most_recentlookup of Amazon's own Amazon Linux 2023 ARM images, so there's no hardcoded ID that differs per region and goes stale. The role trustsec2.amazonaws.comand gets the managed SSM policy, which is all Session Manager needs. The probe uses the APP security group, so it tests exactly the paths ShopLite tasks will use.infra/probe.tfwhole filehcl data "aws_ami" "al2023_arm" { most_recent = true owners = ["amazon"] filter { name = "name" values = ["al2023-ami-2023.*-arm64"] } } data "aws_iam_policy_document" "ec2_assume" { statement { actions = ["sts:AssumeRole"] principals { type = "Service" identifiers = ["ec2.amazonaws.com"] } } } resource "aws_iam_role" "probe" { count = var.enable_probe ? 1 : 0 name = "${local.name_prefix}-probe" assume_role_policy = data.aws_iam_policy_document.ec2_assume.json } resource "aws_iam_role_policy_attachment" "probe_ssm" { count = var.enable_probe ? 1 : 0 role = aws_iam_role.probe[0].name policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" } resource "aws_iam_instance_profile" "probe" { count = var.enable_probe ? 1 : 0 name = "${local.name_prefix}-probe" role = aws_iam_role.probe[0].name } resource "aws_instance" "probe" { count = var.enable_probe ? 1 : 0 ami = data.aws_ami.al2023_arm.id instance_type = "t4g.nano" subnet_id = aws_subnet.private[local.azs[0]].id vpc_security_group_ids = [aws_security_group.app.id] iam_instance_profile = aws_iam_instance_profile.probe[0].name metadata_options { http_tokens = "required" # IMDSv2 only } tags = { Name = "${local.name_prefix}-probe" } } - 3
Output the ID safely
one()returnsnullwhen the probe is disabled instead of erroring.infra/outputs.tfadd to filehcl output "probe_instance_id" { value = one(aws_instance.probe[*].id) } - 4
Turn it on
Make sure NAT is enabled too. The SSM agent needs outbound access to reach the Systems Manager endpoints.
terminal$ terraform apply -var enable_probe=true── expected output ──data.aws_ami.al2023_arm: Read complete after 1s [id=ami-0a1b2c3d4e5f67890]...Plan: 4 to add, 0 to change, 0 to destroy....aws_instance.probe[0]: Creation complete after 13s [id=i-0123456789abcdef0]Apply complete! Resources: 4 added, 0 changed, 0 destroyed.Outputs:...probe_instance_id = "i-0123456789abcdef0" - 5
Test both outbound paths from inside
Give the agent a minute to register, then open a shell.
checkipreturns the NAT gateway's Elastic IP, which proves the private → NAT → IGW path. Listing the assets bucket goes through the S3 gateway endpoint. It will say AccessDenied, because the probe role has no S3 permissions; getting a response from S3 at all proves the network path, and permissions come in Stage 4.terminal$ aws ssm start-session --target $(terraform output -raw probe_instance_id)# inside the session:curl -s https://checkip.amazonaws.comaws s3 ls s3://shoplite-dev-assets-3f9a --region ap-south-1── expected output ──Starting session with SessionId: priya-0a1b2c3d4e5fsh-5.2$ curl -s https://checkip.amazonaws.com13.233.41.187sh-5.2$ aws s3 ls s3://shoplite-dev-assets-3f9a --region ap-south-1An error occurred (AccessDenied) when calling the ListObjectsV2 operation: ...Compare the IP with `aws ec2 describe-addresses` — it's the NAT's EIP. - 6
Turn it off
Apply with the default (
enable_probe = false). All four probe resources go, and the output becomesnull. The whole network is now proven and ready for ShopLite's containers.terminal$ terraform apply── expected output ──Plan: 0 to add, 0 to change, 4 to destroy.Changes to Outputs:- probe_instance_id = "i-0123456789abcdef0" -> null...Apply complete! Resources: 0 added, 0 changed, 4 destroyed.
Checkpoint — you should now have
- ✓From a private instance,
checkipreturned the NAT gateway's Elastic IP. - ✓You reached S3 through the gateway endpoint (AccessDenied is expected at this stage).
- ✓You never opened port 22 or created an SSH key.
- ✓The probe is off again (
probe_instance_id = null), andprobe.tfis committed for next time.
Part 4
Break it on purpose
Make each change, run the command, and read the error before revealing the diagnosis. Recognising these messages on sight is what makes you fast on a real team. Undo the change afterwards.
Break #1
Change a security group's description while it's in use
Enable the probe. Temporarily remove lifecycle { create_before_destroy = true } from the app group, change its description to "ShopLite API" (descriptions can't be edited, so this forces replacement), and apply.
Break #2
Reach for -target
Try to create only the instance: terraform apply -var enable_probe=true -target=aws_instance.probe.
Part 5
Interview questions from this mission
How do you reference a resource that may or may not exist because of count?
Why use aws_iam_policy_document instead of writing JSON policies inline?
When is terraform apply -target appropriate?
Before you stop