Terraform Pro count, one(), try() — the access that is not a public IP
count 0 creates nothing. [0] still asks. one() tells the truth.
<!-- hal:authoritative:yaml -->
§I — Frame
Associate-003 is closed. Five Pro fires on this arc opened run tasks, remote unlock, test-run commands, Lab 14's sockets, and Lab 08's IAM chain. Lab 18 is the leftover that matches today's overlay.
Lab 18's README is the exam: conditionally create a resource with count, avoid unsafe direct indexing when the resource may not exist, use one() for a clean conditional output, use try() where an expression may fail. The starter already names the type. The lab is not "create a bucket." The lab is the read when the bucket is missing.
Coin it: the access that is not a public IP. On Ops the public door is an access_config you omit. On Dev the copy applies that omit, then FetchInstance counts AccessConfigs. On Lab 18 the optional object is an S3 bucket because the Pro lab is AWS. The expression does not care. count = 0 creates nothing. [0] still asks for the first nothing. The plan dies. The guest that was supposed to have no public address never gets a plan.
This is not 08-27. That lesson's command was who may assume versus what the session may do. Today's command is whether the object exists versus whether the output can still be evaluated.
This is not 08-24. That lesson's command was validate versus a module call that forgot the providers map. Today's command is plan versus an index.
This is not 08-21. That lesson's command was plan versus apply and an output that was still last apply. Lab 29 owned that depth. Lab 18's tests/basic.tftest.hcl is two plan runs. They catch a bad index. They do not reopen last-apply. If a stem writes terraform apply then terraform test -filter=plan, look at which lab you are in.
§II — Domain Foundations: count zero is a real value
Brikman, Conditionals with the count Parameter (Ch.5, pp.259-261), is blunt. Terraform has no if-statement around a resource block. count = var.enable_autoscaling ? 1 : 0 creates one copy or creates none. Two properties make the trick: count 1 means one object, count 0 means the resource is not created at all; the ternary <CONDITION> ? <TRUE_VAL> : <FALSE_VAL> feeds that number. He uses it to hide aws_autoscaling_schedule from staging. Lab 18 uses it to hide a bucket. Ops uses it, via dynamic "access_config", to hide a public IP. Same altitude. Different object.
Staging still plans when the schedule count is 0. The resource is absent. The staging plan is legal until an output indexes the missing schedule. That index is Lab 18's crime on a different object. Count hid the resource (Ch.5, pp.259-261). The output still has to read the hole.
The Bootcamp README names the five skills in one list: count 0 or 1, safe conditional outputs, one(), try(), avoiding brittle index access. The starter violates the last one on purpose.
variable "create_bucket" {
type = bool
default = false
}
resource "aws_s3_bucket" "optional" {
count = var.create_bucket ? 1 : 0
bucket = var.bucket_name
}
output "bucket_name" {
value = aws_s3_bucket.optional[0].bucket
}
output "bucket_arn" {
value = try(aws_s3_bucket.optional[0].arn, null)
}
output "bucket_id_safe" {
value = try(one(aws_s3_bucket.optional[*].id), null)
}
Default create_bucket is false. Count is 0. There is no [0]. bucket_name is evaluated anyway because outputs are part of the graph. Plan fails. That failure is the lab. bucket_arn wraps the same [0] in try and becomes null. bucket_id_safe never indexes. It splats, asks one() for the single element or none, then try() in case one() would throw on a surprise extra.
one() is not tolist()[0]. one() accepts a list of zero or one elements. Zero returns null. One returns the element. Two is an error. That error is honest. A splat that somehow produced two ids should not silently pick the first. [0] is the silent pick, and when the list is empty it is not silent, it is a crash. Lab 18 wants the honest function.
try() catches the error and returns the next argument. try(aws_s3_bucket.optional[0].arn, null) is legal and ugly. It still indexes. Prefer one(aws_s3_bucket.optional[*].arn) and drop the [0]. The starter left try([0]) next to try(one(splat)) so you can see both and keep the second.
The README success mode is plan. The tests are command = plan. You do not need apply to learn that [0] is illegal at count 0. 08-21 needed apply because last-apply only exists after a write. Today the bug is in the graph. Plan is enough. Do not "just apply" to make the index exist. That is creating the bucket to avoid reading its absence, which is the opposite of the coin.
A stem that tells you to apply first so the bucket exists, then write the output, is bait. Existence is run two. Absence is run one. Both must plan. Apply to mint [0] hides the lab.
§III — Lab 18 Flavor: two plan runs, one broken output
The test file, reduced to the contract:
run "default_no_bucket_created" {
command = plan
variables {
create_bucket = false
}
assert {
condition = output.bucket_arn.value == null
error_message = "Expected bucket_arn to be null when create_bucket is false."
}
}
run "bucket_created_when_enabled" {
command = plan
variables {
create_bucket = true
}
assert {
condition = output.bucket_name.value != null
error_message = "Expected bucket_name to be set when create_bucket is true."
}
}
Run one is the coin. Count 0. bucket_arn must be null. If you left bucket_name = aws_s3_bucket.optional[0].bucket in the root, this run never reaches the assert. The plan errors first. Fix the output. Then the assert can speak.
Run two is the control. Count 1. bucket_name must be set. After you change bucket_name to one(aws_s3_bucket.optional[*].bucket), both runs can pass. A fix that hardcodes bucket_name = null greens run one and fails run two. Both runs exist so a constant cannot hide.
This is not Lab 29. Lab 29 asked whether a test run was a plan or an apply, and whether an output was still last apply. Lab 18's tests never apply. They still use the terraform test language. The Pro item will not label the lab number. Read the broken index. If the starter's crime is [0] on a counted resource, you are in Lab 18. If the starter's crime is a plan test that thinks it wrote, you are in Lab 29.
The GCP overlay does not rewrite the lab. You will not find google_compute_subnetwork in main.tf. You will find a bucket. The expression you write for bucket_id_safe is the same expression Ops needs for an optional reserved address:
output "public_ip" {
value = try(one(google_compute_address.app[*].address), null)
}
count = var.enable_public_ip ? 1 : 0 on google_compute_address.app. When the coin is true, enable_public_ip is false, count is 0, public_ip is null. That null is the access that is not a public IP, read from Terraform, still not the NIC. Dev still FetchInstance. Cert still one(). Three instruments. One absence.
A dynamic "access_config" is not a counted resource. You cannot one(access_config[*]) the same way. The reserved address is the Lab 18-shaped object. The dynamic nest is the HCL-shaped omit. Ops used the nest. Cert uses the counted address as the sibling that Lab 18 actually drills. Do not force count onto google_compute_instance to look clever. Counting the whole VM to "omit the public IP" destroys the VM. Count the address. Omit the nest. Leave the instance at count 1.
§IV — If-else with count, and the trap of two objects
Brikman also shows if-else with count (pp.261-264): two resources, one with count = var.flag ? 1 : 0, the other with count = var.flag ? 0 : 1. Exactly one exists. Lab 18 is the if, not the if-else. Do not add a second bucket that is created when create_bucket is false. The lab did not ask for a placeholder. A placeholder bucket is a public IP you minted so the output would have a [0]. That is cheating the absence.
If a stem asks you to attach one of two policies, that is Brikman's neo CloudWatch example. Two counted attachments, opposite ternaries. Outputs then need one() across each, or a single try(one(a[*].id), one(b[*].id)). Lab 18's starter does not do that. Do not "complete" Lab 18 by inventing the else bucket.
precondition and check appear in the README success criteria as "where appropriate." They are not the broken line. The broken line is [0]. Fix the outputs first. Add a precondition that bucket_name is non-empty when create_bucket is true if you want the extra. Do not add a precondition that create_bucket is true so you never have to read count 0. That precondition deletes the lab.
for_each is the sibling conditional Brikman names after count (Ch.5, Conditionals with for_each and for expressions). Lab 18's starter uses count. Do not rewrite the resource as for_each = var.create_bucket ? toset([var.bucket_name]) : toset([]) unless a stem asked you to migrate. Both can encode 0-or-1. The splat you one() over changes shape: aws_s3_bucket.optional[*] for count, values(aws_s3_bucket.optional)[*] or a for for for_each. The exam loves a candidate who migrates the resource and then keeps [0]. That index is still illegal on an empty map. one() still wins.
Sensitive outputs are Lab 24, not today. Do not mark bucket_arn sensitive to "hide" a crash. A sensitive null still has to evaluate. The crash happens before sensitivity wraps anything.
Workspaces are Lab 05 and Lab 13. Do not terraform workspace new no-bucket to avoid count 0. The variable is the switch. The workspace is a different shirt.
§V — Worked Example: one Lab 18 fix, one overlay
Replace the brittle output. Leave the resource. Leave the variable default false.
output "bucket_name" {
value = try(one(aws_s3_bucket.optional[*].bucket), null)
}
output "bucket_arn" {
value = try(one(aws_s3_bucket.optional[*].arn), null)
}
output "bucket_id_safe" {
value = try(one(aws_s3_bucket.optional[*].id), null)
}
Three outputs, one pattern. terraform test both plan runs. Then, if you are wiring the overlay in a different root, the reserved address:
resource "google_compute_address" "app" {
count = var.enable_public_ip ? 1 : 0
name = "app-ext"
region = "us-central1"
}
output "public_ip" {
value = try(one(google_compute_address.app[*].address), null)
}
public_ip == null is the Terraform-side coin. Empty AccessConfigs is the GCP-side coin. They can disagree if you attach an ephemeral access_config {} without a reserved address. Then Terraform says null and GCP says there is a natIP. That disagreement is Ops join one. Lab 18 will not see it because Lab 18 has no NIC. Mention it. Do not turn Lab 18 into a Compute lab.
§VI — Connection to Today's Ops + Dev Lessons
Ops wrote the subnet flag and omitted access_config. Dev copied the folder so two tests could apply that omit in parallel, then FetchInstance. Cert reads the optional object without indexing a hole.
If Cert only restates PGA vocabulary, it has skipped Lab 18. If Dev only wraps terraform test, it has skipped the copy. If Ops only prints output.pga, it has skipped the NIC. The hinge is absence at three altitudes. HCL nest omitted. Count 0 plus one(). GCP AccessConfigs empty. The Pro exam will hand you the bucket. The overlay is how you remember why [0] is a public door you did not mean to open: you asked for the first address of a thing you also asked not to create.
08-21's plan-versus-apply remains. A Lab 18 plan run that passes on one() still has not applied. That is correct. You do not apply an optional bucket to prove you can read its absence.
§VII — Practice Questions
create_bucket = false. bucket_arn uses try([0]). bucket_name uses [0] bare. Which output is the lab?output "bucket_name" { value = null } so the default plan passes. Run bucket_created_when_enabled fails. What was skipped?count = var.enable_public_ip ? 1 : 0 on google_compute_instance.app to "omit the public IP." What did the candidate destroy?command = plan. A candidate applies first so [0] exists, then runs the tests. Has the lab been completed?§VIII — Closing
count = 0 creates nothing. [0] still asks. one() reads zero or one and tells the truth. try() is a net, not a reason to keep the index. Plan is enough. Apply is how you hide.
Name it when you see it. The access that is not a public IP. Write the counted object. Read it with one(). Leave the nest off the NIC. Ask GCP for AccessConfigs in Go. Ask the plan for null in the lab.
Examine well. The bucket name will still be pretty. The door is the index. The latch is count. The plugin will evaluate the output even when the resource is gone.
A last habit. Read the test file before you rewrite the resource. Lab 18 already authored the two plan runs. If your local terraform plan looks clean because you flipped the default to true, you have not run the lab. Run terraform test. Watch run default_no_bucket_created first. That run is the absence. The control run is confirmation. Dev copied a folder so two Go tests could coexist. Here two plan runs coexist because the test language says so. Same idea, cheaper instrument. CopyTerraformFolderToTemp is Dev's isolation. These two runs are Cert's. Neither run should have to destroy a bucket that was never the point. Absence is the skill. Plan is the room it fits in.
Related
- Prior arc: IAM trust, instance profile, the role that is not a key (2026-08-27)
- Domain hub: Cross-References/domains/01-Earth-DevOps
- Grounding tome: Lab 18 — count, one(), try() (Lab 29 rejected as 08-21 spend; Lab 08 rejected as 08-27 spend)