Terraform Interview Questions and Answers
10 real questions, pulled straight from HamChops's Infrastructure as Code (IaC) chapter — scenario-based, not trivia.
You are setting up infrastructure for a financial services company. A compliance audit requires that the Production Terraform state file is stored in a completely separate AWS account from the Development state file, with no IAM role in the Dev account able to access the Prod state bucket. Your colleague suggests using Terraform Workspaces to manage dev and prod environments. Is this approach compliant? What should you use instead, and why?
Terraform Workspaces are not compliant with this requirement. All workspaces share the same backend configuration, meaning dev and prod state would reside in the same S3 bucket in the same AWS account. The correct approach is a Directory Structure with separate backend configurations per environment, each pointing to an S3 bucket in a different AWS account.
A platform engineer has written the following Terraform code to create IAM users for a new team:
```hcl
variable "team_members" {
default = ["carol", "dave", "eve"]
}
resource "aws_iam_user" "member" {
count = length(var.team_members)
name = var.team_members[count.index]
}
```
Six months later, "carol" leaves the company. The engineer removes "carol" from the list, making it ["dave", "eve"], and runs terraform plan. The plan shows 3 destroy operations and 2 create operations. The engineer expected only 1 destroy. What is the root cause, and how should the code be rewritten to prevent this?
The root cause is that count addresses resources by numeric index. Removing "carol" from position 0 shifts "dave" to index 0 and "eve" to index 1. Terraform sees that all three indexed resources have changed and plans to destroy all of them and recreate two. The fix is to use for_each = toset(var.team_members), which addresses resources by name. Removing "carol" then results in exactly one destroy operation, leaving "dave" and "eve" untouched.
Your CI/CD pipeline runs terraform apply on every merge to the main branch. A deployment started 45 minutes ago but the CI runner was forcibly terminated by a timeout after 30 minutes, leaving the apply incomplete. Now every subsequent pipeline run fails immediately with:
Error: Error locking state: Error acquiring the state lock:
ConditionalCheckFailedException: The conditional request failed
LockID: s3://my-company-state/prod/terraform.tfstate
Lock Info:
ID: f3a2b1c0-...
Operation: OperationTypeApply
Who: ci-runner@pipeline-job-4821
Created: 2024-01-15 09:23:11 UTC
What happened, what is the risk of the current situation, and what is the correct remediation procedure?
The CI runner was killed mid-apply, leaving a stale lock record in DynamoDB. No new apply can proceed until the lock is released. The correct remediation is to first investigate whether the interrupted apply left infrastructure in an inconsistent state, then manually release the lock with terraform force-unlock f3a2b1c0-... after confirming it is safe to do so.
Your organization has split its infrastructure into three Terraform layers: Networking, Data, and Application, each with its own S3 backend. A new engineer on the Application team needs the private subnet IDs created by the Networking team to deploy a new ECS service. The engineer proposes hardcoding the subnet IDs directly into the Application layer's variables.tf file, arguing it's simpler. You disagree. What is the correct approach, what are the risks of the engineer's proposal, and what must the Networking layer have in place for your approach to work?
Use the terraform_remote_state data source in the Application layer to read the subnet IDs from the Networking layer's state file. The Networking layer must declare the subnet IDs as output blocks. Hardcoding subnet IDs is fragile — they differ between environments (dev/staging/prod), require manual updates when infrastructure changes, and create undocumented dependencies that break silently.
A senior engineer is refactoring a large Terraform module. She moves several aws_security_group resources from the root module into a child module called networking. After the refactor, the resource addresses change from aws_security_group.web to module.networking.aws_security_group.web. She runs terraform plan and sees that Terraform wants to destroy the existing security groups and create new ones — which would cause a production outage by dropping all traffic to the application servers. She has not yet applied anything. What should she do, and what command(s) are involved?
She should use terraform state mv to rename the resource addresses in the state file to match the new module structure, before running terraform apply. For example: terraform state mv aws_security_group.web module.networking.aws_security_group.web. After moving all affected resources, terraform plan should show no changes.
Your security team has flagged a finding: the Terraform state file for your production environment, stored in S3, contains the plaintext master password for your RDS PostgreSQL database. The password was passed into Terraform as a variable marked sensitive = true. Your manager asks why the password is still visible in the state file despite the sensitive flag. She then asks what you would do differently going forward to prevent secrets from appearing in state. What do you tell her?
The sensitive = true flag only suppresses the value in Terraform's terminal output and plan display — it has no effect on what is written to the state file. The state file always stores the final resolved values of all resource attributes in plain text JSON. Going forward, the password should be managed outside Terraform: create the RDS instance with a placeholder password, then use AWS Secrets Manager with automatic rotation to manage the actual credential, so the real password never passes through Terraform at all.
Your security team has written the following Rego rule to prevent EC2 instances from using the m5.24xlarge instance type:
rego
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_instance"
resource.change.after.instance_type == "m5.24xlarge"
msg := sprintf("Instance '%v' uses a prohibited type.", [resource.name])
}
A developer opens a pull request that adds three EC2 instances: one t3.micro, one t3.large, and one m5.24xlarge. The CI pipeline evaluates this policy and fails. The developer argues the policy is wrong because "most of the instances are fine." How do you explain why the pipeline correctly failed, and how would you modify the policy to also block m5.16xlarge and any future large instance types without maintaining a growing blocklist?
The pipeline correctly failed because Rego's [_] iterator evaluates the rule body independently for each resource. Even though two instances pass, the m5.24xlarge instance satisfies all conditions in the rule body, so its message is added to the deny set. A non-empty deny set means the policy fails — it's not a majority vote. To avoid maintaining a blocklist, replace the exact match with an allowlist: define a set of permitted instance types and deny anything not in that set.
A mid-sized fintech company runs a mixed infrastructure environment: Terraform for cloud resources, Kubernetes for application workloads, and several internal Go microservices that make authorization decisions (e.g., "can this user access this account?"). The platform team wants a single, unified policy system that enforces rules across all three systems. They are not using Terraform Cloud — they run Terraform OSS with a GitLab CI pipeline. A junior engineer suggests using Sentinel for everything. A senior engineer pushes back. Who is right, and what should the platform team use?
The senior engineer is correct. Sentinel is tightly coupled to Terraform Cloud/Enterprise and cannot enforce policies on Kubernetes admission or microservice authorization. OPA is the right choice: it integrates with Terraform via CI pipeline evaluation, with Kubernetes via OPA Gatekeeper, and with Go microservices via the OPA Go SDK or REST API — providing a single policy language (Rego) and a single policy repository across all three systems.
Below is a simplified snippet of a Terratest written in Go. It tests a Terraform module that creates an S3 bucket.
Read the code and identify the specific line number that ensures the S3 bucket is deleted at the end of the test, even if the assertion fails. Then explain why that mechanism works — not just what it does.
go
1 package test
2
3 import (
4 "testing"
5 "github.com/gruntwork-io/terratest/modules/aws"
6 "github.com/gruntwork-io/terratest/modules/terraform"
7 "github.com/stretchr/testify/assert"
8 )
9
10 func TestS3BucketCreation(t *testing.T) {
11 terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
12 TerraformDir: "../examples/s3-bucket",
13 })
14
15 // The critical line for cleanup
16 defer terraform.Destroy(t, terraformOptions)
17
18 terraform.InitAndApply(t, terraformOptions)
19
20 bucketName := terraform.Output(t, terraformOptions, "bucket_name")
21
22 assert.True(t, aws.AssertS3BucketExists(t, "us-east-1", bucketName))
23 }
Line 16. The defer keyword in Go schedules terraform.Destroy to execute immediately before TestS3BucketCreation returns — regardless of whether the function exits normally, via a failed assertion, or via a panic. This guarantees cleanup even in failure scenarios.
Your team has a CI pipeline with three stages: tflint, OPA plan analysis, and Terratest integration tests. A developer opens a pull request that adds a new RDS database instance to an existing Terraform module. The tflint stage passes. The OPA stage passes. The Terratest integration test fails with the following error after 18 minutes:
```
TestDatabaseModule 2024/01/15 14:23:41 terraform.go:334:
Error: Error modifying DB instance: InvalidParameterCombination:
Cannot specify a publicly accessible DB instance in a VPC that has no internet gateway.
FAIL test/database_module_test.go:45
--- FAIL: TestDatabaseModule (1087.3s)
```
The developer argues: "The OPA policy check passed, so this must be a Terraform bug, not a policy violation." Is the developer correct? What is the actual cause of the failure, and what does this reveal about the limits of plan analysis?
The developer is incorrect. The failure is a configuration error in the Terraform module — the RDS instance is configured with publicly_accessible = true but is being deployed into a VPC that has no internet gateway attached. OPA passed because no policy was written to catch this specific combination. This reveals that OPA can only enforce rules that have been explicitly written — it cannot catch all possible misconfigurations, only the ones your policy authors anticipated.
Want the rest of Infrastructure as Code (IaC)?
This is 10 of hundreds of concept reviews in this chapter alone — plus 17 more chapters, with fast daily review built in.
See pricing