Terraform makes infrastructure repeatable, but repeatable does not automatically mean secure.
If you write an insecure S3 bucket policy, a security group that opens SSH to the world, or an unencrypted EBS volume, Terraform will happily create it consistently every time. That is why Infrastructure as Code security matters so much.
Misconfigured cloud infrastructure is one of the most common causes of cloud incidents. In many breaches, the root problem is not a sophisticated zero-day. It is something simpler:
0.0.0.0/0Terraform security is therefore about two related goals:
This lesson covers both.
The biggest win in Terraform security is catching issues before terraform apply ever runs.
That is the idea behind IaC scanners such as:
These tools parse your Terraform code and look for risky patterns. They do not replace cloud-native security controls, but they dramatically improve early feedback.
tfsec is a popular Terraform-focused static analysis tool. It looks for insecure Terraform patterns and reports findings with severity information.
Common installation options include:
brew install tfsecor with Go:
go install github.com/aquasecurity/tfsec/cmd/tfsec@latestIn CI, many teams use a prebuilt container or download the binary during the job.
From the Terraform project directory:
tfsec .You can also target a specific directory:
tfsec terraform/env/prodA finding usually includes:
That makes tfsec useful not just as a blocker, but also as a teaching tool.
Tools vary slightly, but the common severity language is:
Not every project treats severities the same way. A common policy is:
Checkov is another widely used IaC security scanner. It supports Terraform and many other IaC formats, which makes it attractive for teams with mixed tooling.
A common local install method is:
pip install checkovYou can also run it in containers or dedicated CI images.
checkov -d .Or for a specific folder:
checkov -d terraform/env/prodCheckov produces rule-based findings similar to tfsec and is especially useful when you want broader platform coverage beyond Terraform alone.
Security scanners are helpful, but no scanner understands every business decision automatically. Sometimes you intentionally choose a pattern that looks risky in isolation.
Example: a tutorial bucket that is intentionally public for static website hosting.
In those cases, you may need to suppress a specific finding.
resource "aws_s3_bucket_policy" "assets" {
#checkov:skip=CKV_AWS_70:Intentional public-read bucket for tutorial static website example
bucket = aws_s3_bucket.assets.id
policy = data.aws_iam_policy_document.assets_public_read.json
}If you suppress a finding:
False positives happen. Lazy suppressions also happen. Mature teams distinguish between the two.
Some Terraform mistakes appear constantly in real projects. Learn these early because scanners will find them often.
A public bucket is not always wrong, but it is dangerous by default.
Example risk:
resource "aws_s3_bucket_policy" "bad" {
bucket = aws_s3_bucket.data.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = "*"
Action = "s3:GetObject"
Resource = "${aws_s3_bucket.data.arn}/*"
}]
})
}If the bucket contains internal data, logs, backups, or application artifacts, public read access is a serious issue.
Ask first:
One of the most common scanner findings is this:
cidr_blocks = ["0.0.0.0/0"]For HTTP or HTTPS on a public service, that may be correct. For SSH, RDP, database ports, or internal application ports, it is usually a red flag.
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}Restrict admin access to:
Storage encryption is another common check.
Example of safer explicit encryption:
resource "aws_ebs_volume" "data" {
availability_zone = "us-east-1a"
size = 20
encrypted = true
}Many AWS accounts now enable encryption by default, but being explicit in Terraform is still valuable for clarity and policy enforcement.
IAM misconfiguration is often harder to spot than public networking problems because the syntax looks harmless while the impact is large.
Examples of risky patterns:
Action = "*"Resource = "*"Least privilege is slower to design, but much safer to operate.
Static scanning becomes truly valuable when it is automatic.
Here is a simple GitLab CI example that adds IaC security checks to a Terraform pipeline.
stages:
- validate
- security
- plan
- apply
terraform-validate:
stage: validate
image: hashicorp/terraform:1.9.8
script:
- terraform init -backend=false
- terraform fmt -check -recursive
- terraform validate
terraform-tfsec:
stage: security
image: aquasec/tfsec:latest
script:
- tfsec .
terraform-checkov:
stage: security
image: bridgecrew/checkov:latest
script:
- checkov -d .
terraform-plan:
stage: plan
image: hashicorp/terraform:1.9.8
script:
- terraform init
- terraform plan -out=tfplanBecause it keeps the pipeline readable and makes the policy explicit:
You can decide whether security findings should block all plans or only fail on higher severities.
This is one of the most important Terraform security topics.
Terraform state can contain sensitive data, including:
Even when Terraform marks some values as sensitive in CLI output, the state file often still stores the real value in plaintext or recoverable form, depending on provider behavior.
That means state is sensitive data and must be protected accordingly.
At minimum:
terraform.tfstate to GitIf an attacker can read your state, they may learn far more than you intended.
Terraform lets you mark variables as sensitive.
variable "db_password" {
description = "Database password"
type = string
sensitive = true
}This mainly affects display behavior. Terraform will try not to print the value in normal CLI output.
Marking a variable as sensitive = true does not guarantee it never appears anywhere. It helps reduce accidental exposure in output, but you still must protect state, logs, and downstream systems.
Outputs can also be marked sensitive.
output "db_password" {
value = var.db_password
sensitive = true
}Again, this hides values in many CLI displays, but it does not magically remove them from state handling concerns.
The right lesson is:
sensitive to reduce accidental exposureIf you use an S3 backend, secure the bucket aggressively.
A good pattern includes:
Example backend configuration:
terraform {
backend "s3" {
bucket = "devopslesson-terraform-state"
key = "prod/network/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-state-locks"
encrypt = true
kms_key_id = "alias/terraform-state"
}
}KMS gives you tighter control over who can decrypt the state object and provides richer auditability than relying only on default storage behavior.
You should also secure the backend resources themselves with:
Terraform is not a secrets manager. If your workflow needs dynamic secrets, short-lived credentials, or centrally managed secret access, HashiCorp Vault is often the better tool.
Terraform can integrate with Vault in a few ways:
That improves security because the pipeline does not need permanent credentials sitting in repository settings forever.
Terraform Sentinel is a policy-as-code feature associated with Terraform Enterprise and some higher-end workflow setups.
You do not need Sentinel to use Terraform safely, but it is worth knowing what it is.
Sentinel lets organizations enforce rules such as:
Think of Sentinel as an organizational guardrail layer on top of normal Terraform execution.
When reviewing a Terraform repo, ask these questions:
This checklist catches a large percentage of common IaC problems.
A strong but realistic Terraform security workflow looks like this:
terraform fmt and terraform validatetfsec and checkovThis is not security perfection. It is operational discipline, which is what prevents most avoidable mistakes.
Terraform security is not only about finding bad code patterns. It is equally about protecting the Terraform workflow itself.
You need both:
If you remember only one thing from this lesson, make it this: a Terraform repo can be dangerous even before anything is applied. The code, the state, the CI variables, and the backend are all part of the security boundary.
<MCQExercise exercise={{ id: 'terraform-security-q1', title: 'Question 1: State File Risk', description: 'Why is the Terraform state file considered sensitive?', options: [ { id: 'a', text: 'Because it only stores harmless metadata like resource names and timestamps' }, { id: 'b', text: 'Because it can contain real infrastructure details and sometimes sensitive values such as passwords or connection information' }, { id: 'c', text: 'Because Terraform encrypts it automatically with your local SSH key' }, { id: 'd', text: 'Because it can only be opened by Terraform Cloud administrators' }, ], correctId: 'b', explanation: 'Terraform state may include infrastructure metadata and, depending on the resources and providers involved, sensitive values too. That is why state must be stored remotely, encrypted, and access-controlled.', wrongExplanations: { a: 'State can contain much more than harmless metadata.', c: 'Terraform does not automatically encrypt state with your SSH key.', d: 'State access depends on your backend and IAM design, not on a universal Terraform Cloud admin rule.', }, }} />
<MCQExercise exercise={{ id: 'terraform-security-q2', title: 'Question 2: Scanner Purpose', description: 'What is the main benefit of tools like tfsec and Checkov?', options: [ { id: 'a', text: 'They apply Terraform changes faster than the standard CLI' }, { id: 'b', text: 'They statically scan IaC code for insecure patterns before infrastructure is created' }, { id: 'c', text: 'They replace AWS IAM entirely when used in CI/CD' }, { id: 'd', text: 'They automatically encrypt every resource defined in Terraform' }, ], correctId: 'b', explanation: 'tfsec and Checkov inspect Terraform code before apply and flag risky patterns such as public buckets, overly broad security groups, and missing encryption settings.', wrongExplanations: { a: 'These tools are security scanners, not faster apply engines.', c: 'They do not replace IAM or platform security controls.', d: 'They detect issues; they do not silently rewrite every resource into a secure configuration.', }, }} />
<MCQExercise
exercise={{
id: 'terraform-security-q3',
title: 'Question 3: Sensitive Variables',
description: 'What does sensitive = true on a Terraform variable primarily do?',
options: [
{ id: 'a', text: 'It helps hide the value from normal Terraform CLI output, but does not remove the need to protect state and backends' },
{ id: 'b', text: 'It stores the value outside the Terraform state file automatically' },
{ id: 'c', text: 'It forces Terraform to retrieve the value from HashiCorp Vault' },
{ id: 'd', text: 'It prevents the value from ever being written to logs or plan files in any form' },
],
correctId: 'a',
explanation: 'Marking a variable as sensitive mainly affects how Terraform displays it in CLI output. It is useful, but you still must secure state, logs, CI variables, and backends properly.',
wrongExplanations: {
b: 'Sensitive variables are not automatically removed from state handling concerns.',
c: 'Terraform does not force Vault integration just because a variable is marked sensitive.',
d: 'The flag reduces exposure but is not a universal guarantee against all logging or storage risk.',
},
}}
/>
On every pull request, before the plan is approved, that is the whole point of shifting left, since a misconfiguration caught in review costs minutes and the same one caught in production costs an incident. Scanning is fast enough to run on every commit. Add a second scan of the deployed state periodically, because policies change and a configuration that passed six months ago may not pass today.
Fail on high and critical severity in changed code; report the rest without blocking. A policy of zero findings across an existing codebase gets disabled within a month, because the backlog blocks unrelated work and people route around it. Gating on newly introduced high-severity issues keeps the signal meaningful while the existing backlog is worked through separately.
Use an inline annotation, #tfsec:ignore:aws-s3-enable-versioning or #checkov:skip=CKV_AWS_21:Bucket is ephemeral build cache, placed on the specific resource, with a reason. Two rules make this safe: never suppress globally when a resource-level suppression will do, and always include the justification, because an unexplained skip is indistinguishable from someone silencing a real finding. Review suppressions periodically; they accumulate.
Publicly accessible S3 buckets, security groups allowing 0.0.0.0/0 on SSH or database ports, unencrypted EBS volumes and RDS instances, IAM policies granting wildcard actions or resources, missing access logging, and unencrypted data in transit. Most are one line to fix and are far cheaper to catch in a pull request than in a penetration test or a breach.
No. Static analysis catches known misconfiguration patterns; it cannot tell you that a change will replace your production database, delete a bucket that holds data, or apply to the wrong account. Those come from reading the plan. Particularly the -/+ replacement markers. Scanning and plan review answer different questions and both belong in the pipeline.
2d:["$","$L2e",null,{"tutorial":{"slug":"terraform-state-and-backends","title":"Terraform State and Backends","seoTitle":"Terraform State and Backends: Remote State and Locking","description":"Understand why Terraform keeps state, what is inside it, how remote backends and locking make teamwork safe, and the state commands that rescue you.","content":"$2f","difficulty":"Medium","duration":"12 min read","keywords":["terraform state","terraform backend","remote state","state locking","terraform state file","s3 backend","terraform drift","state security","terraform state and backends","terraform state overview","terraform remote backend","terraform state commands","terraform s3 backend","terraform tutorial for beginners"],"publishedDate":"2026-06-11","updatedDate":"2026-09-13","order":12,"tableOfContents":[{"id":"why-state-exists","title":"Why state exists","level":2},{"id":"what-is-actually-in-the-file","title":"What is actually in the file","level":2},{"id":"drift","title":"Drift","level":2},{"id":"local-state-does-not-survive-contact-with-a-team","title":"Local state does not survive contact with a team","level":2},{"id":"remote-backends","title":"Remote backends","level":2},{"id":"locking","title":"Locking","level":2},{"id":"the-state-commands","title":"The state commands","level":2},{"id":"what-to-do-if-state-is-lost","title":"What to do if state is lost","level":2},{"id":"explore-further-in-this-section","title":"Explore further in this section","level":2},{"id":"quick-reference","title":"Quick reference","level":2},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is Terraform state and why can't Terraform work without it?","answer":"State is Terraform's record of which real-world resources correspond to which configuration blocks. Nothing in your code or in the cloud provider records that aws_instance.web is i-0abc123. State does. Without it Terraform cannot distinguish creating a resource from updating one it made previously, so every apply would create duplicates. State also caches attributes so plans are fast, and records dependencies so destroys happen in the right order."},{"question":"Is it safe to commit the Terraform state file to Git?","answer":"No. State stores resource attributes in plain text, including generated passwords, private keys and anything passed through a variable marked sensitive, that flag only affects console output. Committing it exposes secrets to everyone with repository access and to Git history permanently. It also does not solve the problems a backend solves, since Git provides no locking, so two people can still apply simultaneously. Use a remote backend with encryption and restricted access."},{"question":"What is a Terraform backend?","answer":"The configuration that tells Terraform where to store state and how to lock it. The default is local, writing terraform.tfstate to the working directory. Remote backends such as S3, Azure Blob Storage, Google Cloud Storage or Terraform Cloud store it centrally so it can be shared, versioned and locked. Switching is a matter of adding a backend block and running terraform init, which offers to migrate the existing state."},{"question":"Why does state locking matter?","answer":"Because two simultaneous applies writing to the same state file will corrupt it, leaving Terraform with an inaccurate record of your infrastructure, which is considerably harder to fix than the original problem. Locking makes the second run wait or fail instead. It becomes essential the moment more than one person or a CI pipeline can apply, and it is the main reason local state does not scale beyond one developer."},{"question":"How do I separate state between dev, staging and production?","answer":"Give each environment its own state file, typically a different key in the same backend bucket. This means a mistake in one cannot corrupt another's state, environments can be applied independently, and credentials can be scoped per environment. Terraform workspaces offer a lighter alternative, but they share one backend and one set of credentials, which is why separate state files, and ideally separate directories, are preferred for production."},{"question":"What should I do if terraform plan shows changes I did not make?","answer":"That is drift: someone changed the infrastructure outside Terraform, and the plan is proposing to bring it back in line with your configuration. Run terraform plan -refresh-only to review what changed without mixing it with configuration changes. Then decide whether to accept the manual change by updating your code, or let Terraform revert it. Leaving drift unresolved is how teams stop trusting Terraform, and a tool people distrust gets bypassed."}]},"allTutorials":[{"slug":"introduction-to-terraform","title":"Introduction to Terraform","seoTitle":"What Is Terraform? Infrastructure as Code for Beginners","description":"Learn what Terraform is and how Infrastructure as Code works, why it beats clicking through consoles, and how it compares to Ansible and CloudFormation.","content":"$30","difficulty":"Easy","duration":"12 min read","keywords":["what is terraform","terraform introduction","infrastructure as code","terraform vs ansible","terraform vs cloudformation","hashicorp terraform","iac tutorial","terraform for beginners","why use terraform"],"publishedDate":"2026-06-11","updatedDate":"2026-07-12","order":1,"tableOfContents":[{"id":"what-is-terraform","title":"What Is Terraform?","level":2},{"id":"what-is-infrastructure-as-code","title":"What Is Infrastructure as Code?","level":2},{"id":"the-problem-iac-solves","title":"The problem IaC solves","level":3},{"id":"what-iac-gives-you","title":"What IaC gives you","level":3},{"id":"how-terraform-works-the-big-picture","title":"How Terraform Works: The Big Picture","level":2},{"id":"core-concepts-you-will-use-every-day","title":"Core Concepts You Will Use Every Day","level":2},{"id":"providers","title":"Providers","level":3},{"id":"resources","title":"Resources","level":3},{"id":"data-sources","title":"Data Sources","level":3},{"id":"variables-and-outputs","title":"Variables and Outputs","level":3},{"id":"terraform-vs-other-iac-tools","title":"Terraform vs Other IaC Tools","level":2},{"id":"terraform-vs-ansible","title":"Terraform vs Ansible","level":3},{"id":"terraform-vs-aws-cloudformation","title":"Terraform vs AWS CloudFormation","level":3},{"id":"terraform-vs-pulumi","title":"Terraform vs Pulumi","level":3},{"id":"the-terraform-workflow-in-one-minute","title":"The Terraform Workflow in One Minute","level":2},{"id":"what-this-series-covers","title":"What This Series Covers","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is Terraform used for?","answer":"Terraform provisions and manages infrastructure by describing it in configuration files rather than clicking through a console. You declare the resources you want, networks, servers, databases, DNS records, and Terraform works out what to create, change or destroy to make reality match. Because the configuration lives in version control, infrastructure changes get reviewed, diffed and rolled back the same way application code does, and an identical environment can be recreated on demand."},{"question":"What is the difference between Terraform and Ansible?","answer":"Terraform is a provisioning tool: it creates and destroys infrastructure and tracks what exists in state. Ansible is a configuration management tool: it installs packages and configures software on machines that already exist. Terraform is declarative and works out the changes needed to reach your desired state; Ansible is primarily procedural, running tasks in order. They are complementary rather than competing, and a common pattern is Terraform to build the servers and Ansible to configure what runs on them."},{"question":"Should I use Terraform or CloudFormation?","answer":"Terraform if you use more than one provider or want the skill to transfer between employers. It works across AWS, Azure, GCP, Kubernetes, Datadog, Cloudflare and hundreds of others with one language and one workflow. CloudFormation is AWS-only but needs no state file of your own, since AWS manages it, and StackSets deploy across an entire AWS Organization with no open-source Terraform equivalent. Many AWS teams run both: CloudFormation or CDK for account baselines, Terraform for application infrastructure."},{"question":"Is Terraform still open source?","answer":"Terraform moved from the Mozilla Public License to the Business Source License in 2023, which restricts using it to build a competing commercial product but leaves normal infrastructure use unaffected. In response the community forked the last MPL version as OpenTofu, now under the Linux Foundation and largely a drop-in replacement. For most teams either works and the configuration language is the same; the choice comes down to licensing preference and which ecosystem your tooling targets."},{"question":"Do I need to know a cloud provider before learning Terraform?","answer":"Yes, at least the basics of one. Terraform is a way to express infrastructure, not a substitute for understanding it. A aws_vpc resource still requires you to know what a VPC, a subnet and a route table are, and error messages come from the provider rather than from Terraform. Learn the fundamentals of one cloud first, then Terraform becomes a much faster way to work with what you already understand."},{"question":"What is the hardest part of Terraform for beginners?","answer":"State. Terraform records what it created in a state file, and almost every confusing situation. A resource it wants to recreate for no obvious reason, a change made in the console that Terraform undoes, two people applying at once and corrupting the file. Traces back to state. Set up a remote backend with locking before working in a team rather than after, and spend time understanding what state is for early; it pays back more than any other Terraform concept."}]},{"slug":"installing-terraform","title":"Installing Terraform","seoTitle":"How to Install Terraform on macOS, Linux and Windows","description":"Install Terraform on macOS, Linux and Windows with the official package managers, verify the version, switch versions with tfenv and run your first project.","content":"$31","difficulty":"Easy","duration":"10 min read","keywords":["install terraform","terraform installation macos","terraform installation linux","terraform installation windows","terraform homebrew","terraform apt","verify terraform install","terraform first project","tfenv terraform version manager"],"publishedDate":"2026-06-11","updatedDate":"2026-07-12","order":2,"tableOfContents":[{"id":"before-you-begin","title":"Before You Begin","level":2},{"id":"installing-on-macos","title":"Installing on macOS","level":2},{"id":"option-1-homebrew-recommended","title":"Option 1: Homebrew (Recommended)","level":3},{"id":"option-2-manual-download","title":"Option 2: Manual Download","level":3},{"id":"installing-on-linux","title":"Installing on Linux","level":2},{"id":"debian-and-ubuntu-apt","title":"Debian and Ubuntu (apt)","level":3},{"id":"rhel-centos-amazon-linux-yumdnf","title":"RHEL, CentOS, Amazon Linux (yum/dnf)","level":3},{"id":"manual-download-any-linux","title":"Manual Download (any Linux)","level":3},{"id":"installing-on-windows","title":"Installing on Windows","level":2},{"id":"option-1-winget-windows-package-manager","title":"Option 1: Winget (Windows Package Manager)","level":3},{"id":"option-2-chocolatey","title":"Option 2: Chocolatey","level":3},{"id":"option-3-manual-download","title":"Option 3: Manual Download","level":3},{"id":"managing-multiple-terraform-versions-with-tfenv","title":"Managing Multiple Terraform Versions with tfenv","level":2},{"id":"install-tfenv-macoslinux","title":"Install tfenv (macOS/Linux)","level":3},{"id":"install-and-switch-versions","title":"Install and switch versions","level":3},{"id":"verifying-the-installation","title":"Verifying the Installation","level":2},{"id":"enabling-shell-autocomplete","title":"Enabling Shell Autocomplete","level":3},{"id":"your-first-terraform-project","title":"Your First Terraform Project","level":2},{"id":"step-1-create-a-project-folder","title":"Step 1: Create a project folder","level":3},{"id":"step-2-write-the-configuration","title":"Step 2: Write the configuration","level":3},{"id":"step-3-initialise-the-project","title":"Step 3: Initialise the project","level":3},{"id":"step-4-preview-with-terraform-plan","title":"Step 4: Preview with terraform plan","level":3},{"id":"step-5-apply-the-configuration","title":"Step 5: Apply the configuration","level":3},{"id":"step-6-inspect-the-state-file","title":"Step 6: Inspect the state file","level":3},{"id":"step-7-clean-up","title":"Step 7: Clean up","level":3},{"id":"what-goes-in-gitignore","title":"What Goes in .gitignore","level":2},{"id":"recommended-project-structure","title":"Recommended Project Structure","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"How do I install Terraform?","answer":"Use your platform's package manager: brew install terraform on macOS after adding the HashiCorp tap, the official apt or yum repository on Linux, or winget install HashiCorp.Terraform on Windows. Terraform is a single static binary, so downloading the release archive and putting the binary on your PATH works anywhere. Confirm with terraform version, which also warns you when a newer release is available."},{"question":"How do I manage multiple Terraform versions?","answer":"Use tfenv, which installs and switches between versions per project. Create a .terraform-version file in the repository and tfenv selects the matching version automatically, so everyone on the team and your CI pipeline run the same one. This matters because Terraform writes a version marker into state, and a newer version will upgrade the state format. After which older versions can no longer read it. Pinning avoids one person accidentally upgrading state for everybody."},{"question":"What should go in .gitignore for a Terraform project?","answer":"At minimum .terraform/ (downloaded providers and modules, which are large and machine-specific), .tfstate and .tfstate.backup (state belongs in a remote backend, and contains secrets in plain text), crash.log, and any *.tfvars files holding real values. Do commit .terraform.lock.hcl, which pins provider versions and makes builds reproducible. It is frequently gitignored by mistake, which defeats its purpose entirely."},{"question":"Should I commit the .terraform.lock.hcl file?","answer":"Yes. It records the exact provider versions and their checksums, so everyone on the team and your CI pipeline resolve identical providers rather than picking up a new minor release independently. It is the direct equivalent of a package-lock.json or Cargo.lock. Update it deliberately with terraform init -upgrade when you intend to move provider versions, and review the resulting diff like any other dependency change."},{"question":"How should I structure a Terraform project?","answer":"For anything small, a single directory with main.tf, variables.tf, outputs.tf and versions.tf is perfectly adequate. Splitting by resource type too early adds navigation cost with no benefit. As it grows, separate by environment with shared modules rather than copying configuration, so environments/production and environments/staging both call the same module with different values. Keep each environment's state separate, which prevents a staging mistake from touching production."},{"question":"Do I need OpenTofu instead of Terraform?","answer":"Only if the Business Source License is a problem for you. OpenTofu is a fork of the last MPL-licensed Terraform, now under the Linux Foundation, and is close to a drop-in replacement with the same configuration language and workflow. For ordinary infrastructure work either is fine. Check which your team's tooling, modules and CI integrations target, since a few vendor integrations still assume one or the other."}]},{"slug":"terraform-core-workflow","title":"The Terraform Core Workflow","seoTitle":"Terraform Workflow: init, plan, apply, destroy Explained","description":"Understand the Terraform init, plan, apply and destroy loop: what each command does, how to read a plan and the mistakes that cause outages.","content":"$32","difficulty":"Easy","duration":"12 min read","keywords":["terraform workflow","terraform init plan apply destroy","terraform commands","how terraform works","read terraform plan","terraform fmt validate","terraform workflow ci cd","terraform commands overview","terraform workflow tutorial"],"publishedDate":"2026-06-11","updatedDate":"2026-09-13","order":3,"tableOfContents":[{"id":"the-four-commands","title":"The four commands","level":2},{"id":"what-actually-happens-at-each-step","title":"What actually happens at each step","level":2},{"id":"init","title":"init","level":3},{"id":"plan","title":"plan","level":3},{"id":"apply","title":"apply","level":3},{"id":"destroy","title":"destroy","level":3},{"id":"two-commands-that-belong-in-the-loop","title":"Two commands that belong in the loop","level":2},{"id":"how-the-workflow-changes-in-a-team","title":"How the workflow changes in a team","level":2},{"id":"the-mistakes-that-cause-real-problems","title":"The mistakes that cause real problems","level":2},{"id":"explore-each-command","title":"Explore each command","level":2},{"id":"quick-reference","title":"Quick reference","level":2},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is the correct order of Terraform commands?","answer":"terraform init first, once per project and again whenever providers, modules or the backend change. Then terraform plan to see what would happen, and terraform apply to execute it. terraform destroy tears everything down when an environment is no longer needed. In practice you also run terraform fmt and terraform validate before planning, and both belong in CI so unformatted or invalid configuration cannot merge."},{"question":"Do I need to run terraform plan before apply?","answer":"terraform apply produces a plan and asks for confirmation, so technically no. You should anyway, and in a team you must. The plan is the only place Terraform tells you that a change will replace a resource rather than update it, which on a database means data loss. In CI, run plan -out=tfplan and apply that file, which guarantees the change reviewed is the change applied."},{"question":"What does the -/+ symbol mean in a Terraform plan?","answer":"Replace: Terraform will destroy the existing resource and create a new one, because the attribute you changed cannot be modified in place. The plan annotates the responsible attribute with # forces replacement. This is the single most important thing to look for, since on stateless resources it is harmless and on anything storing data it is destructive. Protect critical resources with lifecycle { prevent_destroy = true } so a replacement cannot be applied by accident."},{"question":"What happens if terraform apply fails halfway through?","answer":"Terraform stops and keeps whatever it successfully created, recording it in state. There is no rollback, because destroying working infrastructure is usually worse than leaving it. Fix the underlying problem and run apply again; Terraform reconciles from the current state and creates only what is still missing. If the process was killed rather than erroring, a state lock may remain and need clearing with terraform force-unlock."},{"question":"How often do I need to run terraform init?","answer":"Once when you start, and again after anything it configures changes. Adding or upgrading a provider, adding or changing a module source, or modifying the backend. You also need it after cloning a repository, since .terraform/ is not committed. Terraform detects when initialisation is stale and refuses to proceed with a clear message, so you rarely have to remember."},{"question":"What is the difference between terraform fmt and terraform validate?","answer":"fmt rewrites files into canonical formatting, indentation and alignment, and changes nothing about behaviour. validate checks that the configuration is syntactically valid and internally consistent: correct argument names, sensible types, references that resolve. Neither contacts a provider or touches infrastructure, both run in under a second, and both belong in CI, with fmt -check failing the build on unformatted code."}]},{"slug":"terraform-init","title":"terraform init","seoTitle":"terraform init: What It Does and When to Use -upgrade","description":"Learn what terraform init does: download providers, configure the backend, write the lock file, use -upgrade and -reconfigure and run it in CI/CD.","content":"$33","difficulty":"Easy","duration":"10 min read","keywords":["terraform init","terraform init command","terraform providers download","terraform backend init","terraform lock file","terraform init upgrade","terraform init reconfigure","what does terraform init do"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":4,"tableOfContents":[{"id":"what-is-terraform-init","title":"What Is terraform init?","level":2},{"id":"what-terraform-init-does-internally","title":"What terraform init Does Internally","level":2},{"id":"1-downloads-provider-plugins","title":"1. Downloads Provider Plugins","level":3},{"id":"2-sets-up-the-backend","title":"2. Sets Up the Backend","level":3},{"id":"3-creates-the-lock-file","title":"3. Creates the Lock File","level":3},{"id":"the-terraform-directory","title":"The .terraform Directory","level":2},{"id":"common-init-flags","title":"Common init Flags","level":2},{"id":"-upgrade","title":"-upgrade","level":3},{"id":"-reconfigure","title":"-reconfigure","level":3},{"id":"-backendfalse","title":"-backend=false","level":3},{"id":"-backend-config","title":"-backend-config","level":3},{"id":"when-to-re-run-init","title":"When to Re-run init","level":2},{"id":"init-in-cicd-pipelines","title":"init in CI/CD Pipelines","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What does terraform init actually do?","answer":"Four things. It downloads the provider plugins your configuration requires into .terraform/, fetches any external modules, configures the backend where state will be stored, and writes or verifies .terraform.lock.hcl with the exact provider versions and checksums. Until it has run, no other Terraform command works, because Terraform has no providers loaded and does not know where state lives."},{"question":"When do I need to re-run terraform init?","answer":"Whenever the things it sets up change: after adding or changing a provider or its version constraint, after adding or updating a module source, after changing the backend configuration, and after cloning a repository for the first time since .terraform/ is not committed. Terraform tells you when it is needed with an error saying the configuration has changed since initialisation. Running it again when it is not needed is harmless."},{"question":"What is the difference between terraform init and terraform init -upgrade?","answer":"Plain init respects .terraform.lock.hcl and installs exactly the provider versions recorded there, which is what makes builds reproducible. -upgrade ignores the locked versions, resolves the newest releases allowed by your version constraints, and rewrites the lock file. Use -upgrade deliberately when you intend to move provider versions, then review the lock file diff like any other dependency bump, never as a reflex when something is failing."},{"question":"Should I commit the .terraform directory?","answer":"No. It contains downloaded provider binaries which are large, platform-specific and reproducible from the lock file, so committing them bloats the repository for no benefit. Add .terraform/ to .gitignore. Do commit .terraform.lock.hcl though, that is the small text file recording which versions to install, and it is what makes everyone's init produce the same result."},{"question":"How do I run terraform init in CI?","answer":"Run it at the start of every job, since CI workspaces start empty. Use -input=false so it never waits for interactive input, and -backend-config to supply backend settings from environment variables or secrets rather than committing them. Caching the .terraform directory between runs speeds things up noticeably on large provider sets, but key the cache on the lock file so a provider change invalidates it."},{"question":"Why does init fail with a provider registry error?","answer":"Usually network access or a version constraint that cannot be satisfied. In a restricted network the registry may be unreachable, in which case a provider mirror or a network policy change is needed. If the error names a version, your constraints conflict. Perhaps a module requires a provider version your root configuration excludes. Read the constraint chain in the error, which names each source, and reconcile them in required_providers."}]},{"slug":"terraform-plan","title":"terraform plan","seoTitle":"terraform plan: Read the Output, Save with -out, Find Drift","description":"Read terraform plan symbols correctly, save a plan file with -out, target resources, detect drift and run plan in CI/CD without surprises on apply.","content":"$34","difficulty":"Easy","duration":"12 min read","keywords":["terraform plan","terraform plan command","terraform execution plan","terraform plan output","terraform plan symbols","terraform plan -out","terraform plan -target","terraform drift detection","terraform plan in CI"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":5,"tableOfContents":[{"id":"what-is-terraform-plan","title":"What Is terraform plan?","level":2},{"id":"reading-the-execution-plan","title":"Reading the Execution Plan","level":2},{"id":"example-plan-output","title":"Example Plan Output","level":3},{"id":"the---symbol-the-most-dangerous-one","title":"The -/+ Symbol, The Most Dangerous One","level":2},{"id":"saving-a-plan-file-with--out","title":"Saving a Plan File with -out","level":2},{"id":"targeting-specific-resources-with--target","title":"Targeting Specific Resources with -target","level":2},{"id":"detecting-drift","title":"Detecting Drift","level":2},{"id":"terraform-plan-in-cicd-pipelines","title":"terraform plan in CI/CD Pipelines","level":2},{"id":"common-plan-errors","title":"Common plan Errors","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What does terraform plan do?","answer":"It compares your configuration against the recorded state and the real infrastructure, then prints exactly what it would change without changing anything. Resources are marked + to create, ~ to update in place, - to destroy, and -/+ to replace. It is the review step that separates Terraform from clicking in a console, and it should be read before every apply. Particularly for the -/+ symbol, which means a resource will be destroyed and recreated."},{"question":"What does the -/+ symbol mean and why is it dangerous?","answer":"It means replace: Terraform will destroy the existing resource and create a new one, because the property you changed cannot be modified in place. On a stateless resource that is harmless. On a database, an EBS volume or anything holding data, it is data loss delivered by an apparently routine change. Terraform states the reason with a "forces replacement" annotation next to the offending attribute. Read every plan for this before applying to anything stateful."},{"question":"Why does terraform plan show changes when I have not changed anything?","answer":"Usually drift. Something was changed outside Terraform, in a console or by another tool, and Terraform intends to bring it back in line. It can also be a provider upgrade that computes a default differently, a value that genuinely changes on every run such as a timestamp, or a resource whose API returns attributes in a different order. Read which attribute is changing: drift from a console edit is obvious, whereas a perpetual diff on the same attribute every run usually means a provider quirk that needs ignore_changes."},{"question":"What is a saved plan file and should I use one?","answer":"terraform plan -out=tfplan writes the plan to a file, and terraform apply tfplan applies exactly that plan without recalculating. This matters in CI and for anything reviewed by a human, because it guarantees what was approved is what gets applied, without it, the state or the real infrastructure could change between the plan and the apply. The caveat is that a saved plan contains sensitive values in plain text, so treat it as a secret and do not archive it carelessly."},{"question":"Should I use -target?","answer":"Sparingly, and as a recovery tool rather than a habit. -target limits the plan to specific resources, which is genuinely useful when untangling a partially failed apply or working around a dependency problem. It is not a normal workflow: Terraform warns about it because operating on a subset means the rest of your configuration is not validated against reality, and it is easy to leave infrastructure in a state that does not match the code. If you reach for it routinely, the configuration probably wants splitting up."},{"question":"How do I use terraform plan in a CI pipeline?","answer":"Run terraform plan -input=false -out=tfplan on pull requests and post the output as a comment so the change is reviewable, then apply the saved plan file on merge. Use -detailed-exitcode, which returns 0 for no changes, 2 for changes present and 1 for an error, that lets a pipeline distinguish "nothing to do" from "changes await approval" and act accordingly. Never let apply run automatically from a feature branch against shared infrastructure."}]},{"slug":"terraform-apply","title":"terraform apply","seoTitle":"terraform apply: Review Plans, Auto-Approve and Fix Errors","description":"Use terraform apply safely: review the plan, apply a saved plan file, tune parallelism, use -auto-approve in CI/CD and recover from common apply errors.","content":"$35","difficulty":"Easy","duration":"12 min read","keywords":["terraform apply","terraform apply command","terraform apply auto-approve","terraform apply saved plan","terraform apply CI CD","terraform apply output","how terraform apply works","terraform apply errors"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":6,"tableOfContents":[{"id":"what-is-terraform-apply","title":"What Is terraform apply?","level":2},{"id":"the-apply-flow","title":"The Apply Flow","level":2},{"id":"real-time-apply-output","title":"Real-Time Apply Output","level":2},{"id":"parallel-execution","title":"Parallel Execution","level":2},{"id":"applying-a-saved-plan","title":"Applying a Saved Plan","level":2},{"id":"auto-approve-for-cicd","title":"Auto-Approve for CI/CD","level":2},{"id":"accessing-outputs-after-apply","title":"Accessing Outputs After Apply","level":2},{"id":"targeting-a-specific-resource","title":"Targeting a Specific Resource","level":2},{"id":"what-happens-to-state-during-apply","title":"What Happens to State During Apply","level":2},{"id":"handling-apply-errors","title":"Handling Apply Errors","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What happens during terraform apply?","answer":"Terraform builds a dependency graph from your configuration, calculates the changes, shows them for approval, then executes them. Creating, updating and destroying resources in dependency order, with independent resources handled in parallel. As each resource completes, its attributes are written to state. Once the apply finishes, state reflects what now exists and any declared outputs are available via terraform output."},{"question":"Should I use terraform apply -auto-approve?","answer":"In CI, yes, but only when applying a saved plan file that a human reviewed: terraform apply tfplan needs no approval flag because the plan is already fixed. Using -auto-approve on a fresh plan means nobody saw what was about to happen, and it recalculates against whatever state exists at that moment. Interactively, type the confirmation. The pause before destroying something is worth the three seconds."},{"question":"What happens if terraform apply fails halfway through?","answer":"Resources created before the failure are kept and recorded in state. Terraform does not roll back, because destroying successfully created infrastructure is usually worse than leaving it. Fix the cause and run apply again; Terraform reconciles from where it is, creating only what is still missing. The state file is written continuously during apply, so an interrupted run rarely loses track of what was made. If the process was killed hard, a stale lock may need clearing with terraform force-unlock."},{"question":"How does Terraform decide what to do in parallel?","answer":"It builds a dependency graph from references between resources and walks it, running up to ten operations concurrently by default. Resources that reference each other are serialised; independent ones are not. -parallelism=n adjusts the limit, which is occasionally worth lowering when a provider's API rate limits start returning errors under load, and rarely worth raising."},{"question":"How do I get values out after an apply?","answer":"Declare output blocks and read them with terraform output, or terraform output -json for machine-readable form that scripts can parse with jq. In CI this is how you pass a generated URL, an IP address or a database endpoint to a later job. Outputs marked sensitive = true are hidden in console output but still present in -json and in state, so they are not a security control, only a protection against shoulder-surfing and log leakage."},{"question":"What should I do if an apply leaves a resource in a broken state?","answer":"Check whether Terraform knows about it. If the resource was created but the provider errored afterwards, it is usually in state and a subsequent apply will fix or replace it. If it was created and Terraform lost track, import it with terraform import rather than letting a second apply create a duplicate. If state contains something that no longer exists, terraform state rm removes the entry so Terraform stops trying to manage it. Always run a plan and read it before acting on any of these."}]},{"slug":"terraform-destroy","title":"terraform destroy","seoTitle":"terraform destroy: Safely Delete Resources and Use -target","description":"Run terraform destroy safely: read the destroy plan, preview it first, remove single resources with -target, and keep code tidy with fmt and validate.","content":"$36","difficulty":"Easy","duration":"12 min read","keywords":["terraform destroy","terraform destroy command","terraform destroy target","terraform destroy environment","terraform destroy plan","terraform fmt","terraform validate","teardown infrastructure terraform","decommission terraform"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":7,"tableOfContents":[{"id":"what-is-terraform-destroy","title":"What Is terraform destroy?","level":2},{"id":"the-destroy-plan","title":"The Destroy Plan","level":2},{"id":"when-to-use-terraform-destroy","title":"When to Use terraform destroy","level":2},{"id":"partial-destroy-with--target","title":"Partial Destroy with -target","level":2},{"id":"auto-approve-in-cicd","title":"Auto-Approve in CI/CD","level":2},{"id":"preview-the-destroy-plan-without-destroying","title":"Preview the Destroy Plan Without Destroying","level":2},{"id":"the-destroy-lifecycle-hook","title":"The destroy Lifecycle Hook","level":2},{"id":"terraform-fmt-format-your-code","title":"terraform fmt, Format Your Code","level":2},{"id":"terraform-validate-validate-your-configuration","title":"terraform validate, Validate Your Configuration","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What does terraform destroy actually delete?","answer":"Every resource recorded in the state file for that workspace and configuration. It does not touch resources created outside Terraform, and it does not touch other workspaces or other state files. The destroy plan is shown for approval first, and reading it is worth the time. The count of resources is the quickest sanity check that you are pointed at the environment you think you are."},{"question":"How do I preview a destroy without actually destroying?","answer":"terraform plan -destroy produces the destroy plan without executing it, and -out saves it for review. This is the safe way to answer "what would go away", particularly before tearing down anything shared. In CI, generating a destroy plan and posting it for approval is the equivalent of the normal plan-then-apply flow."},{"question":"How do I protect resources from accidental destruction?","answer":"Add lifecycle { preventdestroy = true } to anything holding data. Terraform then refuses any plan that would destroy it, including a full terraform destroy, and you must remove the flag deliberately to proceed. Combine that with provider-level protection, RDS deletion protection, S3 bucket versioning and MFA delete, so a mistake has to get past more than one gate. Note preventdestroy cannot stop a destroy triggered by the resource being removed from configuration entirely."},{"question":"Can I destroy just one resource?","answer":"Yes, with terraform destroy -target=aws_instance.example, but treat it as a recovery tool rather than routine practice. Targeting operates on a subset, so the rest of your configuration is not reconciled and dependent resources may be left referencing something gone. A cleaner approach for permanent removal is to delete the resource from the configuration and run a normal apply, which Terraform plans with full dependency awareness."},{"question":"What is the difference between terraform destroy and deleting resources in the console?","answer":"terraform destroy removes the resources and clears them from state, leaving everything consistent. Deleting in the console removes the resources but leaves state believing they exist, so the next plan shows Terraform intending to recreate them, which is drift, and at best confusing, at worst a surprise recreation of something expensive. If resources were removed outside Terraform, use terraform state rm to bring state back in line."},{"question":"Should terraform destroy ever run automatically in CI?","answer":"Only for genuinely ephemeral environments. A per-pull-request preview stack that is torn down on merge, or a nightly cleanup of a sandbox account. Even then, scope it tightly with a separate state file and credentials that cannot reach production, so an error in the pipeline cannot destroy the wrong thing. For anything long-lived, destruction should require a human."}]},{"slug":"terraform-variables-and-outputs","title":"Terraform Variables and Outputs","seoTitle":"Terraform Variables and Outputs: Precedence and Secrets","description":"See how data flows through Terraform: input variables, locals and outputs, where each belongs, how variable precedence works and how to handle secrets safely.","content":"$37","difficulty":"Easy","duration":"11 min read","keywords":["terraform variables","terraform outputs","terraform locals","tfvars","variable precedence","sensitive variables","terraform variable types","terraform variables and outputs","terraform input variables","terraform local values","terraform output values","terraform tutorial for beginners","reusable terraform configuration"],"publishedDate":"2026-06-11","updatedDate":"2026-09-13","order":8,"tableOfContents":[{"id":"input-variables","title":"Input variables","level":2},{"id":"types","title":"Types","level":3},{"id":"validation","title":"Validation","level":3},{"id":"passing-values-in","title":"Passing values in","level":2},{"id":"secrets","title":"Secrets","level":2},{"id":"local-values","title":"Local values","level":2},{"id":"outputs","title":"Outputs","level":2},{"id":"explore-further-in-this-section","title":"Explore further in this section","level":2},{"id":"quick-reference","title":"Quick reference","level":2},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is the difference between a variable, a local and an output in Terraform?","answer":"An input variable is supplied from outside the configuration, by a tfvars file, the command line, an environment variable, or a parent module, and is how the same code serves different environments. A local value is computed inside the configuration and cannot be overridden externally; it exists to name derived logic and avoid repetition. An output exposes a value outward, either for a human to read after apply or for a parent module to consume. Variables flow in, locals stay internal, outputs flow out."},{"question":"What is the precedence order for Terraform variable values?","answer":"Lowest to highest: the default in the declaration, then TFVAR environment variables, then terraform.tfvars, then .auto.tfvars files alphabetically, then -var-file on the command line, and finally -var, which beats everything. Precedence applies per variable, so a tfvars file that sets one value leaves the others at their defaults. When a value is not what you expect, printing it through terraform console is the quickest way to find which layer supplied it."},{"question":"How do I keep secrets out of my Terraform configuration?","answer":"Do not put them in tfvars files that are committed. Mark the variable sensitive = true so it is redacted from output, then supply the value through an environment variable or a CI secret, or better, read it at plan time from AWS Secrets Manager, SSM Parameter Store or Vault with a data source, so it never exists in a file at all. Remember sensitive only hides values from console output; they are stored in plain text in state, which is why state encryption and access control matter regardless."},{"question":"Do I need a tfvars file for every environment?","answer":"It is the common and workable pattern: dev.tfvars, staging.tfvars, production.tfvars, selected with -var-file. Keep them in version control so environment differences are reviewable, and keep secrets out of them. The alternative, separate directories per environment, each with its own backend, gives stronger isolation because credentials and state can be scoped separately, and is worth the extra structure once production is involved."},{"question":"When should I use a local instead of a variable?","answer":"When the value is internal logic rather than something a caller chooses. A name prefix built from other variables, a map of common tags, a computed CIDR. All locals. If a person deploying would reasonably want to set it differently, it is a variable. A frequent mistake is making everything a variable, which produces modules with twenty inputs where three would do and makes them unpleasant to use."},{"question":"Why can't I read a value from inside a module without an output?","answer":"Because the module interface is deliberately a contract. A parent module can only see what a child explicitly exposes through output blocks, which is what lets a module change its internals without breaking callers. If you need a value from a module, add an output for it, and when writing a module, expose anything a caller might plausibly want, since adding an output later is easy but callers have no way to work around a missing one."}]},{"slug":"terraform-input-variables","title":"Terraform Input Variables","seoTitle":"Terraform Input Variables: Types, Validation and tfvars","description":"Declare Terraform input variables with types, defaults and validation, then set them via tfvars files, CLI flags and environment variables, including secrets.","content":"$38","difficulty":"Easy","duration":"12 min read","keywords":["terraform input variables","terraform variable block","terraform tfvars","terraform variable validation","terraform sensitive variables","terraform variable types","terraform tutorial for beginners"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":9,"tableOfContents":[{"id":"what-are-terraform-input-variables","title":"What Are Terraform Input Variables?","level":2},{"id":"why-use-input-variables","title":"Why Use Input Variables?","level":2},{"id":"1-reusability","title":"1. Reusability","level":3},{"id":"2-cleaner-code","title":"2. Cleaner code","level":3},{"id":"3-safer-collaboration","title":"3. Safer collaboration","level":3},{"id":"4-better-module-design","title":"4. Better module design","level":3},{"id":"5-validation-and-documentation","title":"5. Validation and documentation","level":3},{"id":"declaring-variables-with-variable-blocks","title":"Declaring Variables with variable Blocks","level":2},{"id":"common-arguments-in-a-variable-block","title":"Common arguments in a variable block","level":3},{"id":"required-variables-vs-optional-variables","title":"Required Variables vs Optional Variables","level":2},{"id":"practical-rule","title":"Practical rule","level":3},{"id":"terraform-variable-types","title":"Terraform Variable Types","level":2},{"id":"string","title":"string","level":2},{"id":"number","title":"number","level":2},{"id":"bool","title":"bool","level":2},{"id":"list","title":"list","level":2},{"id":"map","title":"map","level":2},{"id":"object","title":"object","level":2},{"id":"tuple","title":"tuple","level":2},{"id":"a-realistic-variable-set","title":"A Realistic Variable Set","level":2},{"id":"variable-validation-blocks","title":"Variable Validation Blocks","level":2},{"id":"basic-validation-example","title":"Basic validation example","level":3},{"id":"number-range-validation","title":"Number range validation","level":3},{"id":"naming-pattern-validation","title":"Naming pattern validation","level":3},{"id":"validation-for-a-list","title":"Validation for a list","level":3},{"id":"assigning-variable-values","title":"Assigning Variable Values","level":2},{"id":"1-terraformtfvars","title":"1. terraform.tfvars","level":2},{"id":"2-autotfvars","title":"2. *.auto.tfvars","level":2},{"id":"3--var-flag","title":"3. -var flag","level":2},{"id":"4--var-file-flag","title":"4. -var-file flag","level":2},{"id":"5-environment-variables-with-tf_var_","title":"5. Environment variables with TF_VAR_","level":2},{"id":"which-method-should-you-use","title":"Which method should you use?","level":2},{"id":"sensitive-variables","title":"Sensitive Variables","level":2},{"id":"important-limitation","title":"Important limitation","level":3},{"id":"real-example-variables-for-an-s3-logging-bucket","title":"Real Example: Variables for an S3 Logging Bucket","level":2},{"id":"common-beginner-mistakes","title":"Common Beginner Mistakes","level":2},{"id":"not-declaring-a-type","title":"Not declaring a type","level":3},{"id":"using-too-many-command-line--var-flags","title":"Using too many command-line -var flags","level":3},{"id":"storing-secrets-in-committed-tfvars-files","title":"Storing secrets in committed .tfvars files","level":3},{"id":"making-everything-optional","title":"Making everything optional","level":3},{"id":"skipping-validation","title":"Skipping validation","level":3},{"id":"best-practices-for-terraform-input-variables","title":"Best Practices for Terraform Input Variables","level":2},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"How do I pass values to Terraform variables?","answer":"Several ways, and they combine. A terraform.tfvars file is picked up automatically, as is any .auto.tfvars. Pass a specific file with -var-file=production.tfvars, a single value with -var="region=eu-west-1", or set an environment variable named TFVARregion. For real projects, keep values in a per-environment tfvars file under version control, as long as it contains no secrets, since that makes the configuration reproducible and reviewable."},{"question":"What is the order of precedence for Terraform variables?","answer":"From lowest to highest: the variable's default, then environment variables prefixed TFVAR, then terraform.tfvars, then .auto.tfvars in alphabetical order, then -var-file on the command line, and finally -var, which wins over everything. Later sources override earlier ones on a per-variable basis, so a tfvars file setting one value leaves the rest at their defaults."},{"question":"How do I handle secrets in Terraform variables?","answer":"Do not put them in tfvars files that get committed. Mark the variable sensitive = true so its value is redacted from plan and apply output, then supply it through an environment variable, a CI secret, or, best, a data source that reads it from AWS Secrets Manager, SSM Parameter Store or Vault at plan time. Be aware sensitive only hides values from console output: they are stored in plain text in the state file, which is why state needs encryption and restricted access regardless."},{"question":"What is the difference between a variable and a local value?","answer":"A variable is an input from outside the configuration. Something a caller supplies, and which differs between environments. A local is a value computed inside the configuration from variables, resources or expressions, and it cannot be overridden from outside. The rule of thumb: if someone deploying needs to choose it, make it a variable; if it is derived logic you want to name once and reuse, make it a local."},{"question":"How do variable validation blocks work?","answer":"A validation block inside a variable declaration runs a condition against the supplied value and fails with your error message if it does not hold. This catches bad input at plan time with a clear explanation, rather than producing a confusing provider error later. For example rejecting an environment name that is not one of dev, staging or prod, or an instance type outside an approved list. It is one of the cheapest ways to make a module pleasant for other people to use."},{"question":"Should every value be a variable?","answer":"No. Over-parameterising is a common mistake that makes a module hard to use and hard to reason about, because a caller faces twenty decisions where three would do. Make something a variable when it genuinely differs between callers or environments; use a local for internal logic, and hardcode values that should not vary. A module with sensible defaults and a small required surface is far more usable than one exposing every attribute."}]},{"slug":"terraform-local-values","title":"Terraform Local Values","seoTitle":"Terraform Locals vs Variables: When to Use Local Values","description":"Learn how Terraform local values work, when to choose locals over input variables or data sources, and how a locals block removes repeated expressions.","content":"$39","difficulty":"Easy","duration":"8 min read","keywords":["terraform locals","terraform local values","terraform locals block","terraform variables vs locals","terraform expressions","terraform tutorial for beginners"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":10,"tableOfContents":[{"id":"what-are-local-values-in-terraform","title":"What Are Local Values in Terraform?","level":2},{"id":"why-local-values-matter","title":"Why Local Values Matter","level":2},{"id":"defining-locals-with-the-locals-block","title":"Defining Locals with the locals Block","level":2},{"id":"when-to-use-locals-vs-variables-vs-data-sources","title":"When to Use Locals vs Variables vs Data Sources","level":2},{"id":"use-a-variable-when-the-value-should-come-from-outside","title":"Use a variable when the value should come from outside","level":2},{"id":"use-a-local-when-the-value-is-internal-logic","title":"Use a local when the value is internal logic","level":2},{"id":"use-a-data-source-when-terraform-must-query-an-external-system","title":"Use a data source when Terraform must query an external system","level":2},{"id":"quick-decision-guide","title":"Quick decision guide","level":2},{"id":"using-expressions-in-locals","title":"Using Expressions in Locals","level":2},{"id":"string-expressions","title":"String expressions","level":2},{"id":"arithmetic-expressions","title":"Arithmetic expressions","level":2},{"id":"map-and-list-expressions","title":"Map and list expressions","level":2},{"id":"referencing-resources","title":"Referencing resources","level":2},{"id":"using-functions-in-locals","title":"Using Functions in Locals","level":2},{"id":"merge","title":"merge()","level":2},{"id":"lower-upper-replace","title":"lower(), upper(), replace()","level":2},{"id":"concat-and-distinct","title":"concat() and distinct()","level":2},{"id":"lookup","title":"lookup()","level":2},{"id":"jsonencode","title":"jsonencode()","level":2},{"id":"using-conditionals-in-locals","title":"Using Conditionals in Locals","level":2},{"id":"common-patterns-for-local-values","title":"Common Patterns for Local Values","level":2},{"id":"1-common-tags","title":"1. Common tags","level":2},{"id":"2-name-prefixes","title":"2. Name prefixes","level":2},{"id":"3-environment-specific-values","title":"3. Environment-specific values","level":2},{"id":"4-standardized-subnet-names","title":"4. Standardized subnet names","level":2},{"id":"5-feature-flags-derived-from-environment","title":"5. Feature flags derived from environment","level":2},{"id":"a-complete-example","title":"A Complete Example","level":2},{"id":"avoiding-overuse-of-locals","title":"Avoiding Overuse of Locals","level":2},{"id":"bad-pattern-creating-locals-for-trivial-values","title":"Bad pattern: creating locals for trivial values","level":3},{"id":"bad-pattern-turning-every-resource-attribute-into-a-local","title":"Bad pattern: turning every resource attribute into a local","level":3},{"id":"bad-pattern-hiding-module-inputs-behind-unnecessary-locals","title":"Bad pattern: hiding module inputs behind unnecessary locals","level":3},{"id":"good-rule-of-thumb","title":"Good rule of thumb","level":2},{"id":"best-practices-for-terraform-locals","title":"Best Practices for Terraform Locals","level":2},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is the difference between locals and variables in Terraform?","answer":"A variable is an input supplied from outside, by a tfvars file, the command line, or a calling module, and can differ per environment. A local is computed inside the configuration and cannot be overridden externally. Use a variable when the person deploying needs to choose the value; use a local when you want to name a derived value once and reuse it. Locals are evaluated after variables, so they can reference them."},{"question":"When should I use a local instead of repeating an expression?","answer":"Whenever the same expression appears more than twice, or whenever naming it makes the configuration easier to read. A local such as name_prefix = "${var.project}-${var.environment}" used across twenty resources means one place to change the convention, and the intent is obvious at every use site. The common-tags pattern, a locals block merging standard tags with per-resource ones, is the single most useful application."},{"question":"Can locals reference other locals?","answer":"Yes, as long as there is no circular reference. Terraform resolves them as a dependency graph, so a local can build on another declared above or below it in the file. Locals can also reference variables, data sources and resource attributes. What they cannot do is reference something that depends on them, which produces a cycle error naming the chain."},{"question":"Are locals evaluated once or per resource?","answer":"Once per plan or apply, not per resource that references them. The value is computed and reused, so a local containing an expensive expression is not recalculated for each use. Note that a local referencing a resource attribute that is unknown until apply will itself be unknown at plan time, which is why a plan sometimes shows (known after apply) in a place you expected a concrete value."},{"question":"Can I use locals to avoid repeating tags?","answer":"Yes, and it is the idiomatic pattern. Define a commontags local containing your organisation's standard tags, then merge(local.commontags, { Name = "..." }) on each resource to add specifics. Many providers also support default_tags at the provider level, which applies tags to every resource automatically. Combining the two gives you provider-wide defaults plus per-resource additions with no repetition."},{"question":"Can I overuse locals?","answer":"Yes. A configuration where every value is indirected through a local becomes harder to follow than one with a few literals, because reading any resource means jumping to the locals block and back. Reserve them for values used in several places, for expressions complex enough that a name aids comprehension, and for anything you expect to change. A single-use local wrapping a simple literal usually costs more in navigation than it saves."}]},{"slug":"terraform-output-values","title":"Terraform Output Values","seoTitle":"Terraform Output Values: Sensitive Outputs and CLI Usage","description":"Declare Terraform output values, mark them sensitive, read them with the terraform output command and JSON, expose module outputs and use them in CI/CD.","content":"$3a","difficulty":"Easy","duration":"10 min read","keywords":["terraform outputs","terraform output values","terraform output command","terraform sensitive outputs","terraform module outputs","terraform output json","terraform tutorial for beginners"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":11,"tableOfContents":[{"id":"what-are-output-values-in-terraform","title":"What Are Output Values in Terraform?","level":2},{"id":"why-output-values-matter","title":"Why Output Values Matter","level":2},{"id":"1-better-usability","title":"1. Better usability","level":3},{"id":"2-module-communication","title":"2. Module communication","level":3},{"id":"3-automation","title":"3. Automation","level":3},{"id":"4-documentation-through-code","title":"4. Documentation through code","level":3},{"id":"declaring-output-values-with-output-blocks","title":"Declaring Output Values with output Blocks","level":2},{"id":"common-arguments-in-an-output-block","title":"Common arguments in an output block","level":3},{"id":"simple-output-examples","title":"Simple Output Examples","level":2},{"id":"resource-attribute-output","title":"Resource attribute output","level":2},{"id":"url-output","title":"URL output","level":2},{"id":"list-output","title":"List output","level":2},{"id":"map-output","title":"Map output","level":2},{"id":"a-realistic-example","title":"A Realistic Example","level":2},{"id":"sensitive-outputs","title":"Sensitive Outputs","level":2},{"id":"very-important-warning","title":"Very important warning","level":3},{"id":"using-the-terraform-output-command","title":"Using the terraform output Command","level":2},{"id":"show-all-outputs","title":"Show all outputs","level":2},{"id":"show-a-specific-output","title":"Show a specific output","level":2},{"id":"use--json-for-machine-readable-output","title":"Use -json for machine-readable output","level":2},{"id":"output-command-examples-in-practice","title":"Output command examples in practice","level":2},{"id":"fetch-a-load-balancer-url-for-testing","title":"Fetch a load balancer URL for testing","level":3},{"id":"save-all-outputs-for-another-tool","title":"Save all outputs for another tool","level":3},{"id":"read-a-single-output-cleanly-in-shell-automation","title":"Read a single output cleanly in shell automation","level":3},{"id":"passing-outputs-between-modules","title":"Passing Outputs Between Modules","level":2},{"id":"child-module-example","title":"Child module example","level":3},{"id":"root-module-using-those-outputs","title":"Root module using those outputs","level":3},{"id":"why-outputs-matter-in-module-design","title":"Why outputs matter in module design","level":2},{"id":"using-outputs-in-cicd-scripts","title":"Using Outputs in CI/CD Scripts","level":2},{"id":"example-scenario-dns-or-app-endpoint","title":"Example scenario: DNS or app endpoint","level":3},{"id":"example-scenario-kubernetes-bootstrap","title":"Example scenario: Kubernetes bootstrap","level":3},{"id":"best-practices-for-output-values","title":"Best Practices for Output Values","level":2},{"id":"1-output-what-is-useful-not-everything","title":"1. Output what is useful, not everything","level":3},{"id":"2-always-add-descriptions","title":"2. Always add descriptions","level":3},{"id":"3-mark-secrets-as-sensitive","title":"3. Mark secrets as sensitive","level":3},{"id":"4-prefer-stable-meaningful-outputs","title":"4. Prefer stable, meaningful outputs","level":3},{"id":"5-think-of-outputs-as-a-module-api","title":"5. Think of outputs as a module API","level":3},{"id":"common-beginner-mistakes","title":"Common Beginner Mistakes","level":2},{"id":"outputting-values-that-should-stay-internal","title":"Outputting values that should stay internal","level":3},{"id":"forgetting-to-mark-secrets-as-sensitive","title":"Forgetting to mark secrets as sensitive","level":3},{"id":"assuming-sensitive-outputs-are-fully-secure","title":"Assuming sensitive outputs are fully secure","level":3},{"id":"creating-outputs-but-never-using-them","title":"Creating outputs but never using them","level":3},{"id":"a-full-example-outputs-for-a-web-stack","title":"A Full Example: Outputs for a Web Stack","level":2},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What are Terraform outputs used for?","answer":"They expose values from a configuration, an IP address, a DNS name, a database endpoint, a generated ARN, so that they can be read after apply, consumed by a calling module, or picked up by a CI script. Within a module, outputs are the only way to return anything to the caller: a parent module cannot reach inside a child to read a resource attribute directly. Outputs are also how humans find the URL of what they just deployed."},{"question":"How do I read an output value?","answer":"terraform output prints them all, terraform output name prints one, and terraform output -json produces machine-readable output for scripts to parse with jq. The -raw flag prints a single string value without quotes, which is what you want when interpolating into a shell command. Outputs are read from state, so they are available any time after apply without re-running anything."},{"question":"What does marking an output sensitive actually do?","answer":"It redacts the value in terraform plan and terraform apply console output, showing (sensitive value) instead. That is all. The value is still stored in plain text in the state file, still returned by terraform output -json, and still visible to anyone who can read state. It protects against a secret appearing in a CI log or a screenshot. It is not encryption, and it is not an access control."},{"question":"Why does my output show "known after apply" in the plan?","answer":"Because it depends on an attribute that does not exist until the resource is created: an auto-generated ID, an assigned IP address, an ARN. Terraform cannot know it at plan time, so it shows a placeholder. This is normal and requires no action. If an output you expected to be static shows this, it is referencing a resource attribute rather than a variable or literal."},{"question":"How do I pass outputs between modules?","answer":"A child module declares an output, and the parent references it as module.<name>.<output>. That value can then be passed as an input to another module, which is how you wire a network module's subnet IDs into a compute module. For passing values between separate root configurations with separate state, use a terraformremotestate data source, though many teams prefer writing the value to SSM Parameter Store and reading it back, which avoids coupling one configuration's state to another's."},{"question":"Can I use outputs in CI/CD pipelines?","answer":"Yes, and it is the standard way to hand values to later steps. Run terraform output -json > outputs.json and parse it, or use -raw to capture a single value into a shell variable. Typical uses are passing a load balancer DNS name to a smoke test, an ECR repository URL to a build step, or a cluster name to a deployment job. Remember sensitive outputs appear unredacted in -json, so avoid printing the whole document into a log."}]},{"slug":"terraform-state-and-backends","title":"Terraform State and Backends","seoTitle":"Terraform State and Backends: Remote State and Locking","description":"Understand why Terraform keeps state, what is inside it, how remote backends and locking make teamwork safe, and the state commands that rescue you.","content":"$3b","difficulty":"Medium","duration":"12 min read","keywords":"$2d:props:tutorial:keywords","publishedDate":"2026-06-11","updatedDate":"2026-09-13","order":12,"tableOfContents":[{"id":"why-state-exists","title":"Why state exists","level":2},{"id":"what-is-actually-in-the-file","title":"What is actually in the file","level":2},{"id":"drift","title":"Drift","level":2},{"id":"local-state-does-not-survive-contact-with-a-team","title":"Local state does not survive contact with a team","level":2},{"id":"remote-backends","title":"Remote backends","level":2},{"id":"locking","title":"Locking","level":2},{"id":"the-state-commands","title":"The state commands","level":2},{"id":"what-to-do-if-state-is-lost","title":"What to do if state is lost","level":2},{"id":"explore-further-in-this-section","title":"Explore further in this section","level":2},{"id":"quick-reference","title":"Quick reference","level":2},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is Terraform state and why can't Terraform work without it?","answer":"State is Terraform's record of which real-world resources correspond to which configuration blocks. Nothing in your code or in the cloud provider records that aws_instance.web is i-0abc123. State does. Without it Terraform cannot distinguish creating a resource from updating one it made previously, so every apply would create duplicates. State also caches attributes so plans are fast, and records dependencies so destroys happen in the right order."},{"question":"Is it safe to commit the Terraform state file to Git?","answer":"No. State stores resource attributes in plain text, including generated passwords, private keys and anything passed through a variable marked sensitive, that flag only affects console output. Committing it exposes secrets to everyone with repository access and to Git history permanently. It also does not solve the problems a backend solves, since Git provides no locking, so two people can still apply simultaneously. Use a remote backend with encryption and restricted access."},{"question":"What is a Terraform backend?","answer":"The configuration that tells Terraform where to store state and how to lock it. The default is local, writing terraform.tfstate to the working directory. Remote backends such as S3, Azure Blob Storage, Google Cloud Storage or Terraform Cloud store it centrally so it can be shared, versioned and locked. Switching is a matter of adding a backend block and running terraform init, which offers to migrate the existing state."},{"question":"Why does state locking matter?","answer":"Because two simultaneous applies writing to the same state file will corrupt it, leaving Terraform with an inaccurate record of your infrastructure, which is considerably harder to fix than the original problem. Locking makes the second run wait or fail instead. It becomes essential the moment more than one person or a CI pipeline can apply, and it is the main reason local state does not scale beyond one developer."},{"question":"How do I separate state between dev, staging and production?","answer":"Give each environment its own state file, typically a different key in the same backend bucket. This means a mistake in one cannot corrupt another's state, environments can be applied independently, and credentials can be scoped per environment. Terraform workspaces offer a lighter alternative, but they share one backend and one set of credentials, which is why separate state files, and ideally separate directories, are preferred for production."},{"question":"What should I do if terraform plan shows changes I did not make?","answer":"That is drift: someone changed the infrastructure outside Terraform, and the plan is proposing to bring it back in line with your configuration. Run terraform plan -refresh-only to review what changed without mixing it with configuration changes. Then decide whether to accept the manual change by updating your code, or let Terraform revert it. Leaving drift unresolved is how teams stop trusting Terraform, and a tool people distrust gets bypassed."}]},{"slug":"terraform-state-overview","title":"Terraform State Overview","seoTitle":"Terraform State Explained: What terraform.tfstate Does","description":"Learn what Terraform state is, why terraform.tfstate exists, how it maps to real resources, and why drift, state security and locking matter.","content":"$3c","difficulty":"Medium","duration":"12 min read","keywords":["terraform state overview","terraform tfstate file","terraform state file security","terraform local state vs remote state","terraform state locking","terraform .terraform directory","terraform tutorial for beginners"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":13,"tableOfContents":[{"id":"what-is-terraform-state","title":"What Is Terraform State?","level":2},{"id":"why-terraform-state-exists","title":"Why Terraform State Exists","level":2},{"id":"example-creating-a-vpc","title":"Example: creating a VPC","level":3},{"id":"the-terraformtfstate-file","title":"The terraform.tfstate File","level":2},{"id":"what-the-state-file-stores","title":"What the state file stores","level":3},{"id":"how-state-maps-to-real-infrastructure","title":"How State Maps to Real Infrastructure","level":2},{"id":"why-this-matters-during-changes","title":"Why this matters during changes","level":3},{"id":"state-and-drift-detection","title":"State and Drift Detection","level":2},{"id":"local-state-vs-remote-state","title":"Local State vs Remote State","level":2},{"id":"local-state-advantages","title":"Local state advantages","level":3},{"id":"local-state-disadvantages","title":"Local state disadvantages","level":3},{"id":"state-file-security-concerns","title":"State File Security Concerns","level":2},{"id":"never-commit-state-to-git","title":"Never commit state to Git","level":3},{"id":"treat-state-like-a-secret-bearing-artifact","title":"Treat state like a secret-bearing artifact","level":3},{"id":"the-terraformtfstatebackup-file","title":"The terraform.tfstate.backup File","level":2},{"id":"the-terraform-directory-vs-the-state-file","title":"The .terraform Directory vs the State File","level":2},{"id":"what-the-terraform-directory-is-for","title":"What the .terraform directory is for","level":2},{"id":"what-the-state-file-is-for","title":"What the state file is for","level":2},{"id":"simple-distinction","title":"Simple distinction","level":3},{"id":"state-locking-concept","title":"State Locking Concept","level":2},{"id":"how-locking-works-conceptually","title":"How locking works conceptually","level":3},{"id":"how-state-supports-teamwork","title":"How State Supports Teamwork","level":2},{"id":"common-beginner-misunderstandings","title":"Common Beginner Misunderstandings","level":2},{"id":"terraform-reads-the-cloud-directly-so-why-does-it-need-state","title":"âTerraform reads the cloud directly, so why does it need state?â","level":3},{"id":"sensitive-variables-mean-my-state-is-safe","title":"âSensitive variables mean my state is safe.â","level":3},{"id":"the-terraform-folder-is-my-state","title":"âThe .terraform folder is my state.â","level":3},{"id":"local-state-is-fine-forever","title":"âLocal state is fine forever.â","level":3},{"id":"best-practices-for-terraform-state","title":"Best Practices for Terraform State","level":2},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is the Terraform state file and why does it exist?","answer":"State is Terraform's record of which real resources correspond to which configuration blocks. Without it, Terraform would have no way to know that aws_instance.web in your code is instance i-0abc123 in AWS, so it could not tell an update from a create. State also caches resource attributes so plans can be produced without querying every API on every run, and it tracks dependencies so destroys happen in the right order."},{"question":"Is the Terraform state file sensitive?","answer":"Yes, treat it as a secret. State stores resource attributes in plain text, which includes database passwords, generated keys and anything passed through a variable marked sensitive, that flag only redacts console output, not state. Never commit it to version control, store it in an encrypted remote backend with restricted access, and audit who can read it. A state file is often the single most valuable artifact in an infrastructure repository."},{"question":"What happens if I lose the state file?","answer":"Terraform loses all knowledge of what it manages, and the next apply will try to create everything again. Producing duplicates or failing on name conflicts. Recovery means either restoring from a backup, which is why remote backends with versioning matter, or importing every existing resource back into a fresh state with terraform import, which is slow and error-prone for anything non-trivial. Enable versioning on the state bucket before you need it."},{"question":"What is drift and how does Terraform detect it?","answer":"Drift is when real infrastructure no longer matches state, usually because someone changed something in a console or another tool. Terraform detects it by refreshing, querying the provider for current attributes, and comparing against state during a plan, then showing the difference as changes it intends to make. That is why an untouched configuration can produce a non-empty plan: Terraform is proposing to undo the out-of-band change."},{"question":"What is the difference between the .terraform directory and the state file?","answer":".terraform/ is a local working directory holding downloaded provider binaries, fetched modules and backend configuration. It is disposable, machine-specific, recreated by terraform init, and belongs in .gitignore. The state file records what infrastructure exists and is irreplaceable. Losing .terraform costs you one init, while losing state can cost a rebuild of your entire environment."},{"question":"Do I need remote state for a solo project?","answer":"Not strictly, but it is worth it early. Local state lives on one machine, so it is lost with that laptop, has no version history, and cannot be shared the moment a second person or a CI pipeline is involved. An S3 bucket with versioning and encryption takes ten minutes to set up and removes a whole category of future pain. Migrating later is possible but always less pleasant than starting that way."}]},{"slug":"terraform-remote-backends","title":"Terraform Remote Backends","seoTitle":"Terraform S3 Backend with DynamoDB Locking: Setup Guide","description":"Configure a Terraform remote backend on S3 with DynamoDB state locking, bootstrap it, compare Terraform Cloud and GCS, and migrate state safely.","content":"$3d","difficulty":"Medium","duration":"12 min read","keywords":["terraform remote backends","terraform s3 backend dynamodb","terraform state locking","terraform backend config","terraform cloud backend","terraform gcs backend","terraform migrate state"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":14,"tableOfContents":[{"id":"why-remote-backends-matter","title":"Why Remote Backends Matter","level":2},{"id":"1-team-collaboration","title":"1. Team collaboration","level":3},{"id":"2-cicd-compatibility","title":"2. CI/CD compatibility","level":3},{"id":"3-better-security-and-durability","title":"3. Better security and durability","level":3},{"id":"what-a-backend-does","title":"What a Backend Does","level":2},{"id":"why-teams-prefer-remote-backends","title":"Why Teams Prefer Remote Backends","level":2},{"id":"s3-dynamodb-backend-the-most-common-aws-pattern","title":"S3 + DynamoDB Backend: The Most Common AWS Pattern","level":2},{"id":"full-s3-backend-configuration-example","title":"Full S3 backend configuration example","level":2},{"id":"what-each-argument-means","title":"What each argument means","level":3},{"id":"example-bootstrap-infrastructure-for-the-backend","title":"Example bootstrap infrastructure for the backend","level":2},{"id":"why-versioning-matters","title":"Why versioning matters","level":3},{"id":"state-locking-with-dynamodb","title":"State Locking with DynamoDB","level":2},{"id":"conceptual-flow","title":"Conceptual flow","level":3},{"id":"backend-configuration-and-terraform-init","title":"Backend Configuration and terraform init","level":2},{"id":"partial-backend-configuration","title":"Partial Backend Configuration","level":2},{"id":"in-code","title":"In code","level":3},{"id":"in-a-backend-config-file","title":"In a backend config file","level":3},{"id":"why-partial-backend-config-is-useful-in-cicd","title":"Why partial backend config is useful in CI/CD","level":2},{"id":"terraform-cloud-backend","title":"Terraform Cloud Backend","level":2},{"id":"why-teams-choose-terraform-cloud","title":"Why teams choose Terraform Cloud","level":3},{"id":"when-terraform-cloud-is-attractive","title":"When Terraform Cloud is attractive","level":3},{"id":"gcs-backend-briefly","title":"GCS Backend (Briefly)","level":2},{"id":"migrating-state-from-local-to-remote","title":"Migrating State from Local to Remote","level":2},{"id":"basic-migration-flow","title":"Basic migration flow","level":3},{"id":"why-this-is-powerful","title":"Why this is powerful","level":3},{"id":"workspace-concept-briefly","title":"Workspace Concept Briefly","level":2},{"id":"security-best-practices-for-remote-backends","title":"Security Best Practices for Remote Backends","level":2},{"id":"for-s3-backends","title":"For S3 backends","level":3},{"id":"for-all-remote-backends","title":"For all remote backends","level":3},{"id":"common-beginner-mistakes","title":"Common Beginner Mistakes","level":2},{"id":"treating-the-backend-like-a-provider-resource","title":"Treating the backend like a provider resource","level":3},{"id":"committing-backend-secrets-carelessly","title":"Committing backend secrets carelessly","level":3},{"id":"forgetting-to-re-run-terraform-init","title":"Forgetting to re-run terraform init","level":3},{"id":"assuming-remote-state-removes-all-security-concerns","title":"Assuming remote state removes all security concerns","level":3},{"id":"best-practices","title":"Best Practices","level":2},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"Why do I need a remote backend?","answer":"Because local state cannot be shared, versioned or locked. A remote backend puts state somewhere every team member and your CI pipeline can reach, keeps it encrypted, and, critically, locks it so two people cannot apply at once and corrupt it. It also gives you version history, which is the difference between recovering from a bad state write in minutes and rebuilding an environment."},{"question":"How do I set up an S3 backend with state locking?","answer":"Create an S3 bucket with versioning and encryption enabled, then point your backend "s3" block at it with a key naming the state path. Modern Terraform supports native S3 locking via uselockfile, which removes the need for a separate DynamoDB table; older versions require a DynamoDB table with a LockID partition key referenced by dynamodbtable. Either way, the bucket and table must exist before terraform init. They cannot be created by the same configuration that stores its state in them."},{"question":"How do I bootstrap the backend that stores my state?","answer":"The chicken-and-egg problem is real: the bucket must exist before Terraform can use it for state. Two workable approaches. Create the bucket and lock table in a small separate configuration with local state, committed alongside everything else. It changes rarely, so local state is tolerable. Or create the resources with a CLI script or CloudFormation once, then reference them. Some teams create the backend resources in Terraform with local state, then migrate that configuration's own state into the bucket it just made."},{"question":"What happens if a state lock gets stuck?","answer":"Terraform refuses to proceed and reports the lock ID, who holds it and when it was acquired. This usually follows a process killed mid-apply. Confirm nobody is genuinely running an apply, then clear it with terraform force-unlock <LOCK_ID>. Do this carefully, force-unlocking while an apply is actually running is how state gets corrupted, which is exactly what locking exists to prevent."},{"question":"Can I change the backend after I have started?","answer":"Yes. Update the backend block and run terraform init, and Terraform detects the change and offers to migrate the existing state to the new location. It copies rather than moves, so the old state remains as a fallback. Take a manual backup first regardless, and run a plan immediately afterwards expecting no changes. A non-empty plan after a backend migration means something did not transfer correctly."},{"question":"How should I separate state between environments?","answer":"Use a separate state file per environment, typically by giving each its own key in the same bucket, production/terraform.tfstate, staging/terraform.tfstate. This means a mistake in staging cannot touch production state, blast radius is contained, and the two can be applied independently. Backend configuration cannot use variables, so supply the differing values with -backend-config at init time, or use separate directories per environment."}]},{"slug":"terraform-state-commands","title":"Terraform State Commands","seoTitle":"Terraform State Commands: list, show, mv, rm and import","description":"Use the Terraform state commands with confidence: state list, show, mv and rm, plus terraform import and refresh, and when each is safe to run.","content":"$3e","difficulty":"Medium","duration":"10 min read","keywords":["terraform state commands","terraform state list","terraform state show","terraform state mv","terraform state rm","terraform import","terraform refresh"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":15,"tableOfContents":[{"id":"why-terraform-state-commands-exist","title":"Why Terraform State Commands Exist","level":2},{"id":"when-should-you-use-state-commands","title":"When Should You Use State Commands?","level":2},{"id":"terraform-state-list","title":"terraform state list","level":2},{"id":"why-terraform-state-list-is-useful","title":"Why terraform state list is useful","level":3},{"id":"practical-example","title":"Practical example","level":3},{"id":"terraform-state-show","title":"terraform state show","level":2},{"id":"why-terraform-state-show-is-useful","title":"Why terraform state show is useful","level":3},{"id":"important-note","title":"Important note","level":3},{"id":"terraform-state-mv","title":"terraform state mv","level":2},{"id":"basic-syntax","title":"Basic syntax","level":3},{"id":"common-use-case-renaming-a-resource","title":"Common use case: renaming a resource","level":3},{"id":"another-use-case-moving-into-a-module","title":"Another use case: moving into a module","level":3},{"id":"terraform-state-rm","title":"terraform state rm","level":2},{"id":"syntax","title":"Syntax","level":3},{"id":"why-would-you-use-this","title":"Why would you use this?","level":3},{"id":"important-warning","title":"Important warning","level":3},{"id":"terraform-import","title":"terraform import","level":2},{"id":"example","title":"Example","level":3},{"id":"why-import-matters","title":"Why import matters","level":3},{"id":"important-detail","title":"Important detail","level":3},{"id":"terraform-refresh","title":"terraform refresh","level":2},{"id":"what-it-does-conceptually","title":"What it does conceptually","level":3},{"id":"why-it-matters","title":"Why it matters","level":3},{"id":"modern-caution","title":"Modern caution","level":3},{"id":"beginner-takeaway","title":"Beginner takeaway","level":3},{"id":"when-to-use-each-command","title":"When to Use Each Command","level":2},{"id":"refactoring-example-rename-without-recreate","title":"Refactoring Example: Rename Without Recreate","level":2},{"id":"reorganizing-into-modules","title":"Reorganizing into Modules","level":2},{"id":"import-workflow-example","title":"Import Workflow Example","level":2},{"id":"common-risks-and-mistakes","title":"Common Risks and Mistakes","level":2},{"id":"using-state-commands-casually","title":"Using state commands casually","level":2},{"id":"forgetting-to-back-up-or-rely-on-remote-backend-safeguards","title":"Forgetting to back up or rely on remote backend safeguards","level":2},{"id":"running-state-rm-but-leaving-the-resource-block-in-code","title":"Running state rm but leaving the resource block in code","level":2},{"id":"importing-without-matching-the-configuration","title":"Importing without matching the configuration","level":2},{"id":"assuming-state-show-is-a-live-api-browser","title":"Assuming state show is a live API browser","level":2},{"id":"best-practices-for-state-commands","title":"Best Practices for State Commands","level":2},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"When should I use terraform state commands?","answer":"Only when the normal plan-and-apply cycle cannot express what you need: renaming a resource in code without recreating it, moving resources into a module, removing something from Terraform's management without destroying it, or adopting existing infrastructure. They edit state directly and bypass the safety of a plan, so reach for them deliberately and take a state backup first. Every one of these has a way to make things worse."},{"question":"How do I rename a resource without destroying it?","answer":"Rename it in the configuration, then run terraform state mv awsinstance.old awsinstance.new so state points the existing object at the new address. Without that, Terraform sees the old address gone and a new one added, and plans a destroy and create. Newer Terraform supports moved blocks in configuration, which express the same intent declaratively, are reviewable in a pull request, and are generally the better option."},{"question":"What is the difference between terraform state rm and terraform destroy?","answer":"state rm removes the resource from Terraform's records but leaves the real infrastructure running. Terraform simply stops managing it. destroy deletes the actual resource. Use state rm when handing something over to another team or another configuration, or when Terraform is tracking something that no longer exists. Be careful: after state rm, a subsequent apply may try to create a fresh copy, since the configuration still declares it."},{"question":"How do I import existing infrastructure into Terraform?","answer":"Write the resource block first, then run terraform import aws_instance.web i-0abc123 to bind it to the existing object. Import populates state but does not write configuration, so you must author the block yourself and run a plan afterwards to reconcile any differences. Expect several iterations until the plan is empty. Newer Terraform supports import blocks in configuration plus -generate-config-out, which drafts the resource block for you and makes adopting a large estate considerably less tedious."},{"question":"Is terraform refresh still needed?","answer":"Rarely as a standalone command. terraform plan and terraform apply refresh state automatically before computing changes, so the separate command is largely redundant and is deprecated in favour of terraform apply -refresh-only, which shows what refreshing would change and asks for approval. That variant is genuinely useful for reviewing drift deliberately without applying any configuration changes alongside it."},{"question":"How do I safely inspect what is in state?","answer":"terraform state list enumerates every managed resource address, and terraform state show <address> prints all recorded attributes for one. Both are read-only and safe to run at any time. They are the fastest way to answer "does Terraform know about this" and "what does Terraform think this resource looks like", which between them explain most confusing plans."}]},{"slug":"terraform-modules","title":"Terraform Modules","seoTitle":"Terraform Modules Explained: Structure, Versioning, Reuse","description":"Learn what Terraform modules are, when to write one, how to design a clean interface, version and source them, and move resources in without recreating them.","content":"$3f","difficulty":"Medium","duration":"12 min read","keywords":["terraform modules","terraform module structure","terraform registry","module versioning","root module vs child module","moved block terraform","reusable terraform","terraform modules overview","terraform using modules","terraform writing modules","terraform registry modules","reusable terraform modules","terraform tutorial for beginners"],"publishedDate":"2026-06-11","updatedDate":"2026-09-13","order":16,"tableOfContents":[{"id":"what-modules-are-for","title":"What modules are for","level":2},{"id":"when-to-create-one","title":"When to create one","level":2},{"id":"using-someone-elses","title":"Using someone else's","level":2},{"id":"writing-your-own","title":"Writing your own","level":2},{"id":"moving-resources-into-a-module","title":"Moving resources into a module","level":2},{"id":"how-deep-to-nest","title":"How deep to nest","level":2},{"id":"explore-further-in-this-section","title":"Explore further in this section","level":2},{"id":"quick-reference","title":"Quick reference","level":2},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is a Terraform module?","answer":"Any directory containing .tf files. The directory you run Terraform in is the root module; any directory it calls with a module block is a child module. Modules exist to package a group of related resources behind a defined interface of inputs and outputs, so a VPC with its subnets, route tables and gateways becomes one block with a few arguments rather than sixty resources repeated across projects."},{"question":"When should I create a module instead of writing resources directly?","answer":"After the second repetition, not the first. Extracting too early produces modules shaped around one caller's needs, with awkward interfaces that do not generalise. Good signals are the same group of resources deployed more than twice, a set of resources with a coherent purpose worth naming, or several environments needing the same shape with different values. Wrapping a single resource in a module usually adds indirection without benefit."},{"question":"Should I use community modules from the Terraform Registry?","answer":"For well-trodden infrastructure, yes. The AWS VPC and EKS modules in particular handle a large number of edge cases that are slow to rediscover. Write your own for patterns specific to your organisation, or when a community module's flexibility brings more complexity than your case justifies. A common middle path is wrapping a registry module in a thin internal one that fixes your naming and tagging conventions."},{"question":"How do I pin a module version?","answer":"Set version with a pessimistic constraint such as ~> 5.0 when sourcing from a registry, which allows patch and minor updates but blocks an unannounced major version. For Git sources, use ?ref=v1.2.0 to pin a tag rather than tracking a branch. Without pinning, an upstream change can alter your infrastructure the next time anyone runs terraform init -upgrade, which is exactly the kind of surprise infrastructure code exists to prevent."},{"question":"Why does Terraform want to recreate everything after I moved resources into a module?","answer":"Because the state address changed: awsinstance.web became module.compute.awsinstance.web, and Terraform treats the old address as removed and the new one as added. Add a moved block declaring the change, which is applied by a normal plan and is reviewable in a pull request, or use terraform state mv for a one-off. Either way, run a plan afterwards and confirm it is empty before applying."},{"question":"Should a module contain its own provider block?","answer":"No. Declare required_providers in the module's versions.tf to state which providers and versions it needs, but leave the provider configuration, region, credentials, aliases, to the root module, which the child inherits. A module with its own provider block cannot be used with multiple regions or accounts and cannot be cleanly removed from state. When a module genuinely needs a non-default provider, pass an alias explicitly through the providers argument."}]},{"slug":"terraform-using-modules","title":"Using Terraform Modules","seoTitle":"How to Use Terraform Modules from the Registry or Locally","description":"Learn how to call Terraform modules with a module block from local paths or the Registry, pass inputs, pin versions, run init and use module outputs.","content":"$40","difficulty":"Medium","duration":"12 min read","keywords":["using terraform modules","terraform module block","terraform registry modules","terraform module version","terraform module outputs","terraform root module child module","terraform init modules"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":17,"tableOfContents":[{"id":"what-are-terraform-modules","title":"What Are Terraform Modules?","level":2},{"id":"why-use-modules","title":"Why Use Modules?","level":2},{"id":"benefits-of-modules","title":"Benefits of modules","level":3},{"id":"the-root-module-vs-child-modules","title":"The Root Module vs Child Modules","level":2},{"id":"root-module","title":"Root module","level":3},{"id":"child-module","title":"Child module","level":3},{"id":"mental-model","title":"Mental model","level":3},{"id":"using-a-module-with-a-module-block","title":"Using a Module with a module Block","level":2},{"id":"important-parts-of-the-module-block","title":"Important parts of the module block","level":3},{"id":"module-block-syntax-in-detail","title":"Module Block Syntax in Detail","level":2},{"id":"common-attributes","title":"Common attributes","level":3},{"id":"the-terraform-registry","title":"The Terraform Registry","level":2},{"id":"why-the-registry-is-useful","title":"Why the Registry is useful","level":3},{"id":"important-beginner-caution","title":"Important beginner caution","level":3},{"id":"using-modules-from-the-terraform-registry","title":"Using Modules from the Terraform Registry","level":2},{"id":"popular-terraform-modules","title":"Popular Terraform Modules","level":2},{"id":"aws-vpc-module","title":"AWS VPC module","level":2},{"id":"aws-eks-module","title":"AWS EKS module","level":2},{"id":"module-versions-and-version-constraints","title":"Module Versions and Version Constraints","level":2},{"id":"version-pinning-example","title":"Version pinning example","level":3},{"id":"version-constraints-example","title":"Version constraints example","level":3},{"id":"why-version-control-matters","title":"Why version control matters","level":3},{"id":"terraform-init-downloads-modules","title":"terraform init Downloads Modules","level":2},{"id":"using-a-local-module","title":"Using a Local Module","level":2},{"id":"passing-inputs-into-modules","title":"Passing Inputs into Modules","level":2},{"id":"accessing-module-outputs","title":"Accessing Module Outputs","level":2},{"id":"complete-example-vpc-module-app-module","title":"Complete Example: VPC Module + App Module","level":2},{"id":"choosing-between-writing-your-own-vs-using-registry-modules","title":"Choosing Between Writing Your Own vs Using Registry Modules","level":2},{"id":"use-a-registry-module-when","title":"Use a Registry module when:","level":3},{"id":"write-your-own-module-when","title":"Write your own module when:","level":3},{"id":"best-practices-for-using-terraform-modules","title":"Best Practices for Using Terraform Modules","level":2},{"id":"common-beginner-mistakes","title":"Common Beginner Mistakes","level":2},{"id":"using-a-registry-module-without-reading-the-readme","title":"Using a Registry module without reading the README","level":3},{"id":"forgetting-the-version","title":"Forgetting the version","level":3},{"id":"passing-too-many-inputs-blindly","title":"Passing too many inputs blindly","level":3},{"id":"confusing-root-and-child-modules","title":"Confusing root and child modules","level":3},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is a Terraform module?","answer":"Any directory containing .tf files is a module. The directory you run Terraform in is the root module; anything it calls with a module block is a child module. Modules package a set of resources behind a defined interface of inputs and outputs, so a VPC with its subnets, route tables and gateways becomes one block with a few arguments rather than sixty resources copied between projects."},{"question":"Should I use registry modules or write my own?","answer":"Use registry modules for well-trodden infrastructure where the edge cases are many and someone else has already handled them. The AWS VPC and EKS modules are the classic examples, and reimplementing either is rarely a good use of time. Write your own for patterns specific to your organisation, or when a registry module's flexibility brings more complexity than your use case justifies. A frequent middle path is wrapping a registry module in a thin internal one that fixes your conventions."},{"question":"How do I pin a module version?","answer":"Always set version when sourcing from a registry, ideally with a pessimistic constraint such as ~> 5.0 so patch updates flow but a major version cannot arrive unannounced. For Git sources, pin to a tag or commit SHA with ?ref=v1.2.0 rather than tracking a branch. An unpinned module means an upstream change can alter your infrastructure the next time anyone runs init -upgrade, which is exactly the surprise you want to avoid."},{"question":"How do I pass values into and out of a module?","answer":"Inputs are arguments in the module block, matching the child's variable declarations. Outputs come back as module.<name>.<output> and are the only way to read anything from inside a module. You cannot reference a child's resources directly. This is deliberate: the interface is the contract, and it is what lets a module's internals change without breaking callers."},{"question":"How deeply should I nest modules?","answer":"Shallowly. One or two levels is comfortable; three or more makes debugging painful, because tracing a value means walking through several interfaces and error messages arrive with long qualified addresses. A common and workable structure is a root module per environment calling a handful of composed modules, each of which may call one layer of small building-block modules. If you find yourself counting levels, flatten it."},{"question":"Why does Terraform want to recreate resources after I move them into a module?","answer":"Because moving a resource into a module changes its state address from awsinstance.web to module.compute.awsinstance.web, and Terraform sees the old address gone and a new one appear. Fix it with moved blocks in configuration, which declare the rename and are reviewable in a pull request, or with terraform state mv for a one-off. Always run a plan afterwards and confirm it is empty before applying."}]},{"slug":"terraform-writing-modules","title":"Writing Terraform Modules","seoTitle":"How to Write a Terraform Module (With S3 Bucket Example)","description":"Learn how to write a reusable Terraform module: structure main.tf, variables.tf and outputs.tf, build an S3 bucket module and call it locally.","content":"$41","difficulty":"Medium","duration":"14 min read","keywords":["writing terraform modules","terraform module structure","terraform local module","terraform module inputs outputs","terraform registry module publishing","terraform nested modules","terraform module testing"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":18,"tableOfContents":[{"id":"why-write-your-own-terraform-modules","title":"Why Write Your Own Terraform Modules?","level":2},{"id":"when-should-you-create-a-module","title":"When Should You Create a Module?","level":2},{"id":"1-you-repeat-the-same-pattern","title":"1. You repeat the same pattern","level":3},{"id":"2-you-want-standard-defaults","title":"2. You want standard defaults","level":3},{"id":"3-you-want-a-simpler-interface","title":"3. You want a simpler interface","level":3},{"id":"4-you-want-dry-infrastructure-code","title":"4. You want DRY infrastructure code","level":3},{"id":"a-typical-terraform-module-structure","title":"A Typical Terraform Module Structure","level":2},{"id":"maintf-resources-logic","title":"main.tf, Resources & Logic","level":3},{"id":"variablestf-input-definitions","title":"variables.tf, Input Definitions","level":3},{"id":"outputstf-module-file-structure","title":"outputs.tf, Module File Structure","level":3},{"id":"readmemd","title":"README.md","level":3},{"id":"building-a-simple-module-example-s3-bucket-module","title":"Building a Simple Module: Example S3 Bucket Module","level":2},{"id":"variablestf","title":"variables.tf","level":2},{"id":"maintf","title":"main.tf","level":2},{"id":"outputstf-defining-module-outputs","title":"outputs.tf, Defining Module Outputs","level":2},{"id":"calling-a-local-module","title":"Calling a Local Module","level":2},{"id":"module-inputs-designing-a-good-interface","title":"Module Inputs: Designing a Good Interface","level":2},{"id":"good-input-design-example","title":"Good input design example","level":3},{"id":"avoid-giant-interfaces-when-possible","title":"Avoid giant interfaces when possible","level":3},{"id":"module-outputs-returning-useful-results","title":"Module Outputs: Returning Useful Results","level":2},{"id":"nested-modules","title":"Nested Modules","level":2},{"id":"when-nested-modules-help","title":"When nested modules help","level":3},{"id":"when-nested-modules-become-a-problem","title":"When nested modules become a problem","level":3},{"id":"module-sources-local-path-git-and-terraform-registry","title":"Module Sources: Local Path, Git, and Terraform Registry","level":2},{"id":"1-local-path","title":"1. Local path","level":2},{"id":"2-git-source","title":"2. Git source","level":2},{"id":"3-terraform-registry","title":"3. Terraform Registry","level":2},{"id":"publishing-to-the-terraform-registry-briefly","title":"Publishing to the Terraform Registry (Briefly)","level":2},{"id":"testing-modules-with-terraform-plan","title":"Testing Modules with terraform plan","level":2},{"id":"example-test-structure","title":"Example test structure","level":3},{"id":"writing-modules-for-readability","title":"Writing Modules for Readability","level":2},{"id":"helpful-habits","title":"Helpful habits","level":3},{"id":"example-of-using-locals-inside-a-module","title":"Example of using locals inside a module","level":3},{"id":"versioning-your-own-modules","title":"Versioning Your Own Modules","level":2},{"id":"common-beginner-mistakes-when-writing-modules","title":"Common Beginner Mistakes When Writing Modules","level":2},{"id":"creating-modules-too-early","title":"Creating modules too early","level":3},{"id":"exposing-too-many-inputs","title":"Exposing too many inputs","level":3},{"id":"forgetting-outputs","title":"Forgetting outputs","level":3},{"id":"no-documentation","title":"No documentation","level":3},{"id":"no-test-example","title":"No test example","level":3},{"id":"best-practices-for-writing-terraform-modules","title":"Best Practices for Writing Terraform Modules","level":2},{"id":"complete-example-reusable-vpc-module-pattern","title":"Complete Example: Reusable VPC Module Pattern","level":2},{"id":"possible-inputs","title":"Possible inputs","level":3},{"id":"possible-outputs","title":"Possible outputs","level":3},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"When should I create a Terraform module?","answer":"When the same group of resources is deployed more than twice, or when a set of resources has a coherent purpose worth naming, "an application service" rather than a load balancer, target group, task definition and security group. Do not modularise on the first use: premature abstraction produces modules with awkward interfaces built around a single caller's needs. Write it twice, notice the pattern, then extract."},{"question":"How should I design a module's interface?","answer":"Keep the required inputs small and give everything else a sensible default, so the simplest use is a few lines. Use variable validation blocks to reject bad input at plan time with a clear message rather than letting a provider error surface later. Expose outputs for anything a caller might plausibly need, IDs, ARNs, endpoints, since adding one later is easy but callers cannot reach inside. A good module is judged by how short its typical call site is."},{"question":"What files should a module contain?","answer":"By convention main.tf for resources, variables.tf for inputs, outputs.tf for outputs, and versions.tf for Terraform and provider constraints. A README.md documenting inputs, outputs and a usage example matters more than people expect, since it is what a future colleague reads instead of the source. terraform-docs generates the input and output tables automatically from your declarations, which keeps documentation honest."},{"question":"Should a module declare its own provider?","answer":"No. Modules should declare required_providers in versions.tf to state which providers and versions they need, but the provider configuration, region, credentials, aliases, belongs in the root module and is inherited. A module with its own provider block cannot be used with multiple regions or accounts, and cannot be removed cleanly from state. Pass provider aliases explicitly via the providers argument when a module genuinely needs a non-default one."},{"question":"How do I version and share internal modules?","answer":"Put each in its own Git repository, or use a monorepo with tags, and source them with a pinned ?ref=v1.2.0. Tag releases with semantic versioning and treat an input rename as a breaking change, because it is. For larger organisations, a private Terraform registry gives proper version constraints and discoverability. Whatever you choose, pinning is the part that matters. An unpinned internal module is as risky as an unpinned external one."},{"question":"How do I test a Terraform module?","answer":"Start with the cheap layers: terraform validate and terraform fmt -check in CI, plus tfsec or Checkov for security policy. Beyond that, the native terraform test framework runs assertions against plan or apply results using .tftest.hcl files, which is enough for most modules. Terratest, which drives real infrastructure from Go, is heavier but suited to modules where a genuine end-to-end check is worth the runtime and cost."}]},{"slug":"terraform-data-sources","title":"Terraform Data Sources","seoTitle":"Terraform Data Sources: Read Existing Infrastructure","description":"Learn how Terraform data sources read existing infrastructure, how the data block works, how to reference its attributes and when to use data over resource.","content":"$42","difficulty":"Easy","duration":"12 min read","keywords":["terraform data sources","terraform data block","terraform aws_ami data source","terraform read existing infrastructure","terraform data vs resource"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":19,"tableOfContents":[{"id":"what-are-data-sources","title":"What Are Data Sources?","level":2},{"id":"why-data-sources-matter-in-real-projects","title":"Why Data Sources Matter in Real Projects","level":2},{"id":"the-data-block-syntax","title":"The data Block Syntax","level":2},{"id":"referencing-data-source-outputs","title":"Referencing Data Source Outputs","level":2},{"id":"common-aws-data-sources-you-will-use-often","title":"Common AWS Data Sources You Will Use Often","level":2},{"id":"aws_ami","title":"aws_ami","level":2},{"id":"aws_vpc","title":"aws_vpc","level":2},{"id":"aws_subnet_ids-and-related-subnet-lookups","title":"aws_subnet_ids and related subnet lookups","level":2},{"id":"aws_caller_identity","title":"aws_caller_identity","level":2},{"id":"aws_region","title":"aws_region","level":2},{"id":"aws_availability_zones","title":"aws_availability_zones","level":2},{"id":"filtering-data-sources","title":"Filtering Data Sources","level":2},{"id":"using-most_recent","title":"Using most_recent","level":3},{"id":"why-filtering-matters","title":"Why filtering matters","level":3},{"id":"data-sources-vs-resources","title":"Data Sources vs Resources","level":2},{"id":"when-to-use-a-resource","title":"When to use a resource","level":3},{"id":"when-to-use-a-data-source","title":"When to use a data source","level":3},{"id":"a-helpful-decision-rule","title":"A helpful decision rule","level":3},{"id":"data-sources-and-terraform-state","title":"Data Sources and Terraform State","level":2},{"id":"practical-implication","title":"Practical implication","level":3},{"id":"dependency-behavior-with-data-sources","title":"Dependency Behavior with Data Sources","level":2},{"id":"a-complete-practical-example-find-the-latest-ubuntu-ami-dynamically","title":"A Complete Practical Example: Find the Latest Ubuntu AMI Dynamically","level":2},{"id":"what-this-configuration-does","title":"What this configuration does","level":3},{"id":"why-this-is-better-than-hardcoding","title":"Why this is better than hardcoding","level":3},{"id":"what-to-watch-out-for","title":"What to watch out for","level":3},{"id":"another-example-reading-existing-network-information","title":"Another Example: Reading Existing Network Information","level":2},{"id":"best-practices-for-using-data-sources-well","title":"Best Practices for Using Data Sources Well","level":2},{"id":"1-prefer-stable-filters-over-fragile-ones","title":"1. Prefer stable filters over fragile ones","level":3},{"id":"2-avoid-unnecessary-hardcoding","title":"2. Avoid unnecessary hardcoding","level":3},{"id":"3-be-intentional-with-dynamic-lookups","title":"3. Be intentional with dynamic lookups","level":3},{"id":"4-keep-the-ownership-boundary-clear","title":"4. Keep the ownership boundary clear","level":3},{"id":"5-output-important-discovered-values-during-learning","title":"5. Output important discovered values during learning","level":3},{"id":"common-beginner-mistakes","title":"Common Beginner Mistakes","level":2},{"id":"mistake-1-expecting-a-data-source-to-create-infrastructure","title":"Mistake 1: Expecting a data source to create infrastructure","level":3},{"id":"mistake-2-using-filters-that-return-multiple-ambiguous-matches","title":"Mistake 2: Using filters that return multiple ambiguous matches","level":3},{"id":"mistake-3-hardcoding-values-that-should-be-discovered","title":"Mistake 3: Hardcoding values that should be discovered","level":3},{"id":"mistake-4-forgetting-that-data-sources-refresh-on-every-plan","title":"Mistake 4: Forgetting that data sources refresh on every plan","level":3},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is a Terraform data source?","answer":"A read-only lookup. Where a resource block creates and manages something, a data block queries something that already exists and makes its attributes available. Typical uses are finding the latest AMI ID, reading an existing VPC created by another team, fetching the current AWS account ID, or pulling a secret from Secrets Manager at plan time. All without Terraform taking ownership of them."},{"question":"What is the difference between a data source and a resource?","answer":"A resource is managed: Terraform creates it, tracks it in state, updates it when configuration changes, and destroys it when removed. A data source is only read: it appears in state as a cached result but Terraform never modifies or deletes the underlying object. Use a data source for anything owned elsewhere, and a resource for anything this configuration is responsible for."},{"question":"How do I stop hardcoding AMI IDs?","answer":"Use the awsami data source with filters and mostrecent = true, or read the AMI ID from the SSM public parameters that AWS publishes with the awsssmparameter data source. Both resolve the current image at plan time, which removes a value that goes stale and becomes a security liability. Be aware this means a new AMI can trigger instance replacement, so pair it with a launch template and an Auto Scaling group rather than a bare instance if you want control over when that happens."},{"question":"When are data sources evaluated?","answer":"During the plan, provided their arguments are known. If a data source depends on an attribute of a resource that does not yet exist, Terraform defers it to apply time and shows dependent values as (known after apply). This is why a data source that filters on a resource being created in the same run behaves differently from one querying pre-existing infrastructure, and why splitting such configurations into two applies is sometimes cleaner."},{"question":"Can I use a data source to read another configuration's state?","answer":"Yes, with terraformremotestate, which reads the outputs of another state file. It works, but it couples the two configurations tightly. The consumer breaks if the producer's outputs change, and it needs read access to the other state, which contains secrets. Many teams prefer publishing values to SSM Parameter Store and reading them back with a data source, which is a looser contract and avoids granting state access."},{"question":"Why does my data source fail with "no matching results"?","answer":"The filters matched nothing, usually because of the region, the account, or a tag that does not exist. Data sources fail the plan rather than returning empty, which is deliberate. Continuing with a missing value would produce worse errors later. Check you are in the expected region and account with aws sts get-caller-identity, and test the equivalent CLI query directly to confirm the filters match something before debugging the Terraform side."}]},{"slug":"terraform-count-and-for-each","title":"Terraform count and for_each","seoTitle":"Terraform count vs for_each: Which One to Use and Why","description":"Choose between Terraform count and for_each: see why count's positional identity can destroy resources, when each is correct and how to migrate safely.","content":"$43","difficulty":"Medium","duration":"11 min read","keywords":["terraform count","terraform for_each","count vs for_each","count index","each key each value","conditional resource terraform","terraform multiple resources","terraform count and for_each","terraform meta arguments","terraform repetition","terraform count overview","terraform for_each overview"],"publishedDate":"2026-06-11","updatedDate":"2026-09-13","order":20,"tableOfContents":[{"id":"count","title":"count","level":2},{"id":"the-problem-with-count","title":"The problem with count","level":2},{"id":"for_each","title":"for_each","level":2},{"id":"with-a-map","title":"With a map","level":3},{"id":"converting-a-list","title":"Converting a list","level":3},{"id":"when-count-is-still-right","title":"When count is still right","level":2},{"id":"migrating-from-count-to-for_each","title":"Migrating from count to for_each","level":2},{"id":"explore-further-in-this-section","title":"Explore further in this section","level":2},{"id":"quick-reference","title":"Quick reference","level":2},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is the difference between count and for_each in Terraform?","answer":"count takes a number and identifies instances by position, [0], [1], [2]. foreach takes a map or set and identifies them by key, ["web"], ["api"]. That difference in identity determines what happens when the collection changes: with count, removing an item shifts every subsequent index and causes Terraform to destroy and recreate unrelated resources; with foreach, each instance is bound to its key, so removing one affects only that one."},{"question":"Why did removing one item from my list destroy several resources?","answer":"Because you used count, which identifies resources by their position in the list. Removing the second of four items means index [1] now holds what used to be at [2], [2] holds what was at [3], and [3] no longer exists, so Terraform correctly plans to change two resources and destroy one. Switching to for_each with stable keys prevents this entirely, since identity no longer depends on ordering."},{"question":"Should I always use for_each instead of count?","answer":"Nearly always, with two exceptions. Conditional creation reads more clearly as count = var.enabled ? 1 : 0, and a genuine quantity of interchangeable resources, three identical workers where nothing distinguishes them, is a legitimate use of count. Anything driven by a list that could gain or lose entries should use for_each."},{"question":"How do I use for_each with a list?","answer":"You cannot directly, because for_each requires stable keys and a list has none. Convert a list of unique strings with toset(var.names), after which each.key and each.value are both the string. Convert a list of objects with a for expression keyed on a stable field, such as { for s in var.subnets : s.name => s }. Never key on the list index, which reintroduces the positional problem."},{"question":"How do I create a resource conditionally in Terraform?","answer":"count = var.enabled ? 1 : 0. Zero instances means the resource is not created. Reference it as resource.name[0], and use one(resource.name[].id) when the reference must tolerate the resource not existing, since indexing directly errors when count is zero. The same effect can be achieved with for_each over a map that is empty when disabled, which avoids index-based referencing entirely."},{"question":"How do I convert existing resources from count to for_each?","answer":"Add a moved block for each instance, mapping the old indexed address to the new keyed one, from = awss3bucket.this[0], to = awss3bucket.this["logs"]. Without them Terraform sees every address change and plans a full destroy and recreate. It is tedious for a large set, but it turns a rebuild into a no-op. Always run a plan afterwards and confirm it reports no changes before applying."}]},{"slug":"terraform-count","title":"Terraform Count","seoTitle":"Terraform count: count.index and Conditional Resources","description":"Learn how the Terraform count meta-argument works: use count.index, build conditional resources, reference counted resources and know when count fits.","content":"$44","difficulty":"Easy","duration":"10 min read","keywords":["terraform count","terraform count index","terraform conditional resource creation","terraform count vs for_each","terraform counted resources"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":21,"tableOfContents":[{"id":"what-is-the-count-meta-argument","title":"What Is the count Meta-Argument?","level":2},{"id":"why-count-is-useful","title":"Why count Is Useful","level":2},{"id":"how-count-works-internally","title":"How count Works Internally","level":2},{"id":"using-countindex","title":"Using count.index","level":2},{"id":"common-uses-of-countindex","title":"Common uses of count.index","level":3},{"id":"using-count-with-lists","title":"Using count with Lists","level":2},{"id":"why-list-alignment-matters","title":"Why list alignment matters","level":3},{"id":"creating-multiple-resources-from-a-variable","title":"Creating Multiple Resources from a Variable","level":2},{"id":"conditional-resource-creation-with-count","title":"Conditional Resource Creation with count","level":2},{"id":"why-the-ternary-expression-works-well-here","title":"Why the ternary expression works well here","level":3},{"id":"referencing-counted-resources","title":"Referencing Counted Resources","level":2},{"id":"referencing-a-single-counted-resource","title":"Referencing a single counted resource","level":3},{"id":"referencing-all-instance-ids","title":"Referencing all instance IDs","level":3},{"id":"example-output","title":"Example output","level":3},{"id":"referencing-a-conditionally-created-resource","title":"Referencing a conditionally created resource","level":3},{"id":"count-with-modules","title":"count with Modules","level":2},{"id":"the-big-problem-with-count-list-ordering","title":"The Big Problem with count: List Ordering","level":2},{"id":"why-this-matters-operationally","title":"Why this matters operationally","level":3},{"id":"when-count-works-well","title":"When count Works Well","level":2},{"id":"when-for_each-is-better","title":"When for_each Is Better","level":2},{"id":"practical-comparison-count-vs-for_each","title":"Practical Comparison: count vs for_each","level":2},{"id":"best-practices-for-using-count","title":"Best Practices for Using count","level":2},{"id":"1-keep-list-ordering-stable","title":"1. Keep list ordering stable","level":3},{"id":"2-use-countindex-only-where-it-adds-value","title":"2. Use count.index only where it adds value","level":3},{"id":"3-be-careful-with-optional-resources","title":"3. Be careful with optional resources","level":3},{"id":"4-prefer-meaningful-outputs","title":"4. Prefer meaningful outputs","level":3},{"id":"5-do-not-fight-the-identity-model","title":"5. Do not fight the identity model","level":3},{"id":"common-beginner-mistakes","title":"Common Beginner Mistakes","level":2},{"id":"mistake-1-forgetting-indexes-when-referencing-counted-resources","title":"Mistake 1: Forgetting indexes when referencing counted resources","level":3},{"id":"mistake-2-assuming-countindex-starts-at-1","title":"Mistake 2: Assuming count.index starts at 1","level":3},{"id":"mistake-3-using-count-for-named-business-objects","title":"Mistake 3: Using count for named business objects","level":3},{"id":"mistake-4-ignoring-reordering-risk","title":"Mistake 4: Ignoring reordering risk","level":3},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What does the count meta-argument do?","answer":"It creates multiple instances of a resource from one block, indexed from zero. Each instance gets an address like aws_instance.web[0], and count.index is available inside the block to differentiate them. It is the simplest way to create N identical things, and it is also the source of the most common Terraform footgun. See the next question."},{"question":"Why does removing an item from a count list destroy other resources?","answer":"Because count identifies instances by position, not by value. Remove the second item from a list of four and everything after it shifts down an index, so Terraform sees [1] changing from one value to another, [2] changing, and [3] disappearing, and plans to destroy and recreate resources you never touched. On stateless resources this is merely wasteful; on anything with data attached it is an outage. This is the single strongest argument for for_each."},{"question":"When should I use count instead of for_each?","answer":"Use count for a genuine quantity of identical things, three identical workers where the index carries no meaning, and for conditional creation with count = var.enabled ? 1 : 0, which remains the idiomatic pattern. Use foreach whenever the instances are distinguishable, because it keys them by a stable string so adding or removing one affects only that one. If the list you are counting over could ever have an element removed from the middle, use foreach."},{"question":"How do I conditionally create a resource in Terraform?","answer":"count = var.createbucket ? 1 : 0. Zero instances means the resource is not created. Reference it as awss3bucket.this[0], and note that any reference must handle the zero case or the plan errors when the resource does not exist, which is where one() or a try() wrapper helps. foreach over a conditionally empty map achieves the same thing and avoids the index-based referencing."},{"question":"How do I reference a resource created with count?","answer":"By index, awsinstance.web[0].id, or as a whole list with the splat expression awsinstance.web[].id, which returns every instance's ID and is what you usually want for passing into a target group or a security group rule. Splat works cleanly even when count is zero, returning an empty list, which makes it safer than indexing directly in conditional cases."},{"question":"Can I use count with modules?","answer":"Yes, count works on module blocks exactly as it does on resources, producing module.name[0]. The same positional-identity caveat applies and matters more, because recreating a module instance recreates every resource inside it. For anything beyond a simple conditional enable, for_each on the module block is the safer choice."}]},{"slug":"terraform-for-each","title":"Terraform for_each","seoTitle":"Terraform for_each: Loop Over Maps and Sets with Examples","description":"Learn how Terraform for_each works with sets, maps and objects, how each.key and each.value are used, how to reference instances and when it beats count.","content":"$45","difficulty":"Easy","duration":"12 min read","keywords":["terraform for_each","terraform each key each value","terraform for_each map","terraform for_each vs count","terraform set iteration"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":22,"tableOfContents":[{"id":"what-is-the-for_each-meta-argument","title":"What Is the for_each Meta-Argument?","level":2},{"id":"why-for_each-exists","title":"Why for_each Exists","level":2},{"id":"basic-for_each-syntax","title":"Basic for_each Syntax","level":2},{"id":"for_each-with-a-set-of-strings","title":"for_each with a Set of Strings","level":2},{"id":"why-toset-is-used","title":"Why toset() is used","level":3},{"id":"resulting-references","title":"Resulting references","level":3},{"id":"for_each-with-a-map","title":"for_each with a Map","level":2},{"id":"why-maps-are-powerful","title":"Why maps are powerful","level":3},{"id":"using-eachkey-and-eachvalue","title":"Using each.key and each.value","level":2},{"id":"eachkey","title":"each.key","level":3},{"id":"eachvalue","title":"each.value","level":3},{"id":"for_each-with-a-map-of-objects","title":"for_each with a Map of Objects","level":2},{"id":"for_each-with-a-list-of-objects","title":"for_each with a List of Objects","level":2},{"id":"why-this-transformation-matters","title":"Why this transformation matters","level":3},{"id":"referencing-for_each-resources","title":"Referencing for_each Resources","level":2},{"id":"output-example","title":"Output example","level":3},{"id":"getting-values-from-all-instances","title":"Getting values from all instances","level":3},{"id":"dynamic-resource-naming-with-eachkey","title":"Dynamic Resource Naming with each.key","level":2},{"id":"for_each-with-modules","title":"for_each with Modules","level":2},{"id":"why-for_each-is-safer-than-count-for-named-items","title":"Why for_each Is Safer Than count for Named Items","level":2},{"id":"for_each-vs-count","title":"for_each vs count","level":2},{"id":"quick-decision-guide","title":"Quick decision guide","level":3},{"id":"best-practices-for-for_each","title":"Best Practices for for_each","level":2},{"id":"1-choose-keys-carefully","title":"1. Choose keys carefully","level":3},{"id":"2-prefer-maps-when-instances-have-custom-configuration","title":"2. Prefer maps when instances have custom configuration","level":3},{"id":"3-convert-lists-to-maps-intentionally","title":"3. Convert lists to maps intentionally","level":3},{"id":"4-avoid-duplicate-keys","title":"4. Avoid duplicate keys","level":3},{"id":"5-keep-keys-stable-over-time","title":"5. Keep keys stable over time","level":3},{"id":"common-beginner-mistakes","title":"Common Beginner Mistakes","level":2},{"id":"mistake-1-passing-a-plain-list-directly-without-understanding-identity","title":"Mistake 1: Passing a plain list directly without understanding identity","level":3},{"id":"mistake-2-confusing-eachkey-and-eachvalue","title":"Mistake 2: Confusing each.key and each.value","level":3},{"id":"mistake-3-using-unstable-keys","title":"Mistake 3: Using unstable keys","level":3},{"id":"mistake-4-using-count-when-you-really-want-names","title":"Mistake 4: Using count when you really want names","level":3},{"id":"practical-example-per-environment-buckets","title":"Practical Example: Per-Environment Buckets","level":2},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is the difference between for_each and count?","answer":"count creates instances identified by a numeric index; foreach creates them identified by a string key from a map or set. That difference in identity is everything. With count, removing an item from the middle of a list shifts every subsequent index and causes Terraform to destroy and recreate unrelated resources. With foreach, each instance is bound to its key, so removing one affects only that one. Prefer for_each for anything where instances are distinguishable."},{"question":"Should I always use for_each instead of count?","answer":"Nearly always, with two exceptions. Conditional creation reads better as count = var.enabled ? 1 : 0, and a genuine quantity of truly interchangeable resources, three identical workers where nothing distinguishes them, is a legitimate count. Everything else, especially anything driven by a list that could change, belongs in for_each."},{"question":"How do I use for_each with a list?","answer":"You cannot directly, foreach requires a map or a set of strings, because it needs stable keys. Convert with toset(var.names) for a list of unique strings, after which each.key and each.value are both the string. For a list of objects, project it into a map first with a for expression keyed on something stable and unique, such as { for s in var.subnets : s.name => s }. Never key on an index, which reintroduces the problem foreach exists to solve."},{"question":"What is the difference between each.key and each.value?","answer":"With a set of strings they are the same value. With a map, each.key is the map key and each.value is the corresponding value, which is what makes a map of objects the most useful shape. The key names the instance and the object carries its configuration. Referencing the instance later uses the key: aws_subnet.this["public-a"].id."},{"question":"Why do I get "Invalid for_each argument" at plan time?","answer":"Because the keys are not known until apply. for_each keys must be determinable during planning, so a map derived from an attribute of a resource that does not exist yet, an ID assigned on creation, for instance, fails with this error. The fix is to key on something static, such as a variable or a literal name, and keep the unknown value inside the map's value rather than its key."},{"question":"How do I reference resources created with for_each?","answer":"By key: awssubnet.this["public-a"].id. For all of them, use a for expression such as [for s in awssubnet.this : s.id], or values(awssubnet.this)[].id. The splat syntax that works with count does not apply directly to foreach resources, because they form a map rather than a list. This is a common stumbling block when converting a resource from one to the other."}]},{"slug":"terraform-expressions-functions","title":"Terraform Expressions and Functions","seoTitle":"Terraform Expressions and Functions: A Practical Guide","description":"Learn Terraform expressions: types, references, conditionals, for expressions, key function categories, try and can, and testing in terraform console.","content":"$46","difficulty":"Medium","duration":"11 min read","keywords":["terraform expressions","terraform functions","for expression terraform","terraform conditional","terraform console","try and can terraform","terraform templatefile","terraform expressions overview","terraform functions overview","terraform string functions","terraform collection functions","terraform tutorial expressions"],"publishedDate":"2026-06-11","updatedDate":"2026-09-13","order":23,"tableOfContents":[{"id":"types","title":"Types","level":2},{"id":"referencing-things","title":"Referencing things","level":2},{"id":"conditionals","title":"Conditionals","level":2},{"id":"for-expressions","title":"for expressions","level":2},{"id":"the-function-categories-worth-knowing","title":"The function categories worth knowing","level":2},{"id":"try-can-and-one","title":"try, can and one","level":2},{"id":"test-expressions-before-you-apply","title":"Test expressions before you apply","level":2},{"id":"explore-further-in-this-section","title":"Explore further in this section","level":2},{"id":"quick-reference","title":"Quick reference","level":2},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"How do I test a Terraform expression without applying?","answer":"Use terraform console, an interactive prompt with access to your variables, locals, data sources and current state. Type an expression and it prints the result immediately. It is read-only and safe to run against production, and it answers "why is this not producing what I expect" far faster than adding a temporary output and running a plan. It is the single most underused Terraform command."},{"question":"How do I write a conditional in Terraform?","answer":"With the ternary operator: var.env == "prod" ? "m6i.large" : "t3.micro". Both branches must return the same type. For conditional resource creation, use count = var.enabled ? 1 : 0. When you find yourself chaining more than two conditions, replace it with a map of values and lookup(map, key, default), which is considerably easier to read and to extend."},{"question":"What is a for expression and when do I use it?","answer":"It transforms one collection into another, with optional filtering: [for s in var.subnets : s.id if s.public]. Square brackets produce a list, braces produce a map using key => value syntax. The most important use is converting a list into the map that foreach requires, since foreach needs stable keys. There is no separate filter function in Terraform. The if clause of a for expression is how filtering is done."},{"question":"How do I merge maps or combine tags in Terraform?","answer":"merge(local.commontags, { Name = "web-server" }), where later arguments win on key conflicts. This is the standard tagging pattern: define organisation-wide tags once in a locals block and merge per-resource additions at each use site. Many providers also support defaulttags at the provider level, which applies tags automatically to every resource. Combining both gives you provider-wide defaults plus per-resource specifics with no repetition."},{"question":"What is the difference between try, can and lookup?","answer":"lookup(map, key, default) retrieves a value from a map with a fallback when the key is missing. try(expr, fallback) evaluates expressions in order and returns the first that succeeds, handling a wider range of failures including type errors and missing attributes on nested objects. can(expr) returns a boolean instead of erroring, which is what makes it suitable inside a variable validation block where you need a condition rather than a value."},{"question":"Can I write my own functions in Terraform?","answer":"Not in Terraform itself. The function library is fixed and there is no user-defined function support. The idiomatic substitute is a locals block, which lets you name a complex expression once and reuse it everywhere, giving most of the readability benefit. For genuinely complex logic, either reshape the input data so the expression becomes simple, or move the computation outside Terraform and pass the result in as a variable."}]},{"slug":"terraform-string-functions","title":"Terraform String Functions","seoTitle":"Terraform String Functions: format, replace and templatefile","description":"Work with strings in Terraform: interpolation, format, case and whitespace helpers, replace and regex functions, templatefile and conditionals.","content":"$47","difficulty":"Easy","duration":"10 min read","keywords":["terraform string functions","terraform format function","terraform templatefile","terraform expressions","terraform string interpolation"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":24,"tableOfContents":[{"id":"why-string-functions-matter-in-terraform","title":"Why String Functions Matter in Terraform","level":2},{"id":"terraform-string-interpolation","title":"Terraform String Interpolation","level":2},{"id":"why-interpolation-is-useful","title":"Why interpolation is useful","level":3},{"id":"a-practical-note-for-beginners","title":"A practical note for beginners","level":3},{"id":"the-format-function","title":"The format() Function","level":2},{"id":"why-use-format-instead-of-interpolation","title":"Why use format() instead of interpolation?","level":3},{"id":"case-and-whitespace-helpers-lower-upper-title-trimspace","title":"Case and Whitespace Helpers: lower(), upper(), title(), trimspace()","level":2},{"id":"lower","title":"lower()","level":2},{"id":"upper","title":"upper()","level":2},{"id":"title","title":"title()","level":2},{"id":"trimspace","title":"trimspace()","level":2},{"id":"practical-example-trimspace","title":"Practical example: trimspace()","level":3},{"id":"replacing-and-matching-text-replace-regex-regexall","title":"Replacing and Matching Text: replace(), regex(), regexall()","level":2},{"id":"replace","title":"replace()","level":2},{"id":"regex","title":"regex()","level":2},{"id":"regexall","title":"regexall()","level":2},{"id":"a-caution-on-regex","title":"A caution on regex","level":3},{"id":"splitting-and-joining-strings-split-and-join","title":"Splitting and Joining Strings: split() and join()","level":2},{"id":"split","title":"split()","level":2},{"id":"join","title":"join()","level":2},{"id":"practical-example-join","title":"Practical example: join()","level":3},{"id":"substrings-length-and-membership-substr-length-contains","title":"Substrings, Length, and Membership: substr(), length(), contains()","level":2},{"id":"substr","title":"substr()","level":2},{"id":"length","title":"length()","level":2},{"id":"contains","title":"contains()","level":2},{"id":"external-templates-with-templatefile","title":"External Templates with templatefile()","level":2},{"id":"why-templatefile-is-better-than-giant-inline-strings","title":"Why templatefile() is better than giant inline strings","level":3},{"id":"ternary-conditional-expressions","title":"Ternary Conditional Expressions","level":2},{"id":"why-conditionals-matter-in-terraform-strings","title":"Why conditionals matter in Terraform strings","level":3},{"id":"combining-functions-in-real-configurations","title":"Combining Functions in Real Configurations","level":2},{"id":"a-practical-example-user-data-and-naming","title":"A Practical Example: User Data and Naming","level":2},{"id":"best-practices-for-terraform-string-expressions","title":"Best Practices for Terraform String Expressions","level":2},{"id":"1-prefer-readability-over-cleverness","title":"1. Prefer readability over cleverness","level":3},{"id":"2-normalize-user-provided-values-early","title":"2. Normalize user-provided values early","level":3},{"id":"3-use-locals-for-repeated-expressions","title":"3. Use locals for repeated expressions","level":3},{"id":"4-use-templatefile-for-long-templates","title":"4. Use templatefile() for long templates","level":3},{"id":"5-be-cautious-with-regex","title":"5. Be cautious with regex","level":3},{"id":"common-beginner-mistakes","title":"Common Beginner Mistakes","level":2},{"id":"mistake-1-hardcoding-names-everywhere","title":"Mistake 1: Hardcoding names everywhere","level":3},{"id":"mistake-2-forgetting-to-normalize-values","title":"Mistake 2: Forgetting to normalize values","level":3},{"id":"mistake-3-overusing-complex-inline-expressions","title":"Mistake 3: Overusing complex inline expressions","level":3},{"id":"mistake-4-putting-huge-scripts-directly-in-resource-blocks","title":"Mistake 4: Putting huge scripts directly in resource blocks","level":3},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"How do I join strings in Terraform?","answer":"Use interpolation for simple cases, "${var.project}-${var.environment}", and the join() function when combining a list, as in join(",", var.subnet_ids). Avoid wrapping a value that is already a string in interpolation: name = "${var.name}" should just be name = var.name, and terraform fmt will not fix it but linters flag it."},{"question":"What is the difference between format() and string interpolation?","answer":"Interpolation is more readable for straightforward concatenation. format() earns its place when you need padding, number formatting or a reusable template, format("%s-%03d", var.prefix, count.index) produces zero-padded names that sort correctly, which plain interpolation cannot do. formatlist() applies the same template across a list, which is useful for generating a set of names in one expression."},{"question":"How do I convert a string to lowercase for a resource name?","answer":"lower(var.name), often combined with replace() to strip characters a provider rejects. S3 bucket names, for instance, must be lowercase and cannot contain underscores. A common normalisation is lower(replace(var.name, "_", "-")). Doing this in a locals block once, rather than at each use site, keeps the convention in one place."},{"question":"How do I check whether a string contains a substring?","answer":"can(regex("pattern", var.string)) returns a boolean without failing when there is no match, which is what you want in a condition. regex() on its own errors if the pattern does not match, so it is only appropriate when a non-match should fail the plan, which makes it genuinely useful inside a variable validation block. startswith() and endswith() cover the simpler cases more legibly."},{"question":"How do I split a string into a list?","answer":"split(",", var.csv_value) produces a list from a delimited string, which is the usual way to accept a comma-separated value from an environment variable or a CI parameter. Combine with trimspace() inside a for expression if the input may contain spaces around the delimiters, since split does not trim."},{"question":"Where can I test Terraform expressions?","answer":"terraform console gives an interactive prompt with access to your variables, locals, data sources and state, so you can evaluate an expression against real values rather than guessing. It is the fastest way to work out why a function is not producing what you expect, and it is read-only, so it is safe to run against production state."}]},{"slug":"terraform-collection-functions","title":"Terraform Collection Functions","seoTitle":"Terraform Collection Functions: Lists, Maps and Sets","description":"Learn Terraform collection functions such as concat, flatten, distinct and slice, plus map and set helpers and for expressions for dynamic code.","content":"$48","difficulty":"Easy","duration":"10 min read","keywords":["terraform collection functions","terraform list functions","terraform map functions","terraform set functions","terraform for expressions"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":25,"tableOfContents":[{"id":"why-collection-functions-matter","title":"Why Collection Functions Matter","level":2},{"id":"list-functions","title":"List Functions","level":2},{"id":"concat","title":"concat()","level":2},{"id":"flatten","title":"flatten()","level":2},{"id":"distinct","title":"distinct()","level":2},{"id":"slice","title":"slice()","level":2},{"id":"reverse","title":"reverse()","level":2},{"id":"sort","title":"sort()","level":2},{"id":"length","title":"length()","level":2},{"id":"map-functions","title":"Map Functions","level":2},{"id":"merge","title":"merge()","level":2},{"id":"lookup","title":"lookup()","level":2},{"id":"keys","title":"keys()","level":2},{"id":"values","title":"values()","level":2},{"id":"zipmap","title":"zipmap()","level":2},{"id":"set-functions","title":"Set Functions","level":2},{"id":"toset-set-conversion","title":"toset(), Set Conversion","level":2},{"id":"setintersection","title":"setintersection()","level":2},{"id":"setunion","title":"setunion()","level":2},{"id":"setsubtract","title":"setsubtract()","level":2},{"id":"type-conversion-functions","title":"Type Conversion Functions","level":2},{"id":"tolist","title":"tolist()","level":2},{"id":"tomap","title":"tomap()","level":2},{"id":"toset-type-conversion","title":"toset(), Type Conversion","level":2},{"id":"tostring","title":"tostring()","level":2},{"id":"tonumber","title":"tonumber()","level":2},{"id":"for-expressions","title":"for Expressions","level":2},{"id":"example-filtering-environments","title":"Example: filtering environments","level":3},{"id":"example-building-a-new-list","title":"Example: building a new list","level":3},{"id":"example-building-a-map","title":"Example: building a map","level":3},{"id":"dynamic-expressions-for-complex-transformations","title":"Dynamic Expressions for Complex Transformations","level":2},{"id":"practical-tag-merging-example","title":"Practical tag-merging example","level":3},{"id":"practical-object-transformation-example","title":"Practical object transformation example","level":3},{"id":"a-realistic-example-networking-inputs","title":"A Realistic Example: Networking Inputs","level":2},{"id":"best-practices-for-collection-transformations","title":"Best Practices for Collection Transformations","level":2},{"id":"1-prefer-clarity-over-compression","title":"1. Prefer clarity over compression","level":3},{"id":"2-use-maps-for-named-configuration","title":"2. Use maps for named configuration","level":3},{"id":"3-normalize-collections-before-iteration","title":"3. Normalize collections before iteration","level":3},{"id":"4-be-careful-with-set-ordering-assumptions","title":"4. Be careful with set ordering assumptions","level":3},{"id":"5-use-for-expressions-to-express-intent","title":"5. Use for expressions to express intent","level":3},{"id":"common-beginner-mistakes","title":"Common Beginner Mistakes","level":2},{"id":"mistake-1-keeping-everything-in-parallel-lists","title":"Mistake 1: Keeping everything in parallel lists","level":3},{"id":"mistake-2-forgetting-type-expectations","title":"Mistake 2: Forgetting type expectations","level":3},{"id":"mistake-3-assuming-sets-preserve-order","title":"Mistake 3: Assuming sets preserve order","level":3},{"id":"mistake-4-writing-transformations-inline-everywhere","title":"Mistake 4: Writing transformations inline everywhere","level":3},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is the difference between a list, a set and a map in Terraform?","answer":"A list is ordered and allows duplicates, indexed by position. A set is unordered with no duplicates, and cannot be indexed. A map is a collection of key-value pairs with unique string keys. The distinction matters for for_each, which requires a map or a set of strings because it needs stable keys. A list must be converted with toset() or projected into a map first."},{"question":"How do I merge two maps?","answer":"merge(local.common_tags, { Name = "web-server" }) combines them, with later arguments winning on key conflicts. This is the standard tagging pattern: a base map of organisation-wide tags merged with per-resource additions. merge() accepts any number of maps, so layering defaults, environment values and resource specifics in one expression works cleanly."},{"question":"How do I transform a list into a map for for_each?","answer":"Use a for expression: { for s in var.subnets : s.name => s } produces a map keyed on each object's name. The key must be unique and stable, never derive it from the list index, since that reintroduces exactly the positional-identity problem for_each exists to avoid. If no field is naturally unique, combine two, such as "${s.az}-${s.tier}"."},{"question":"What does flatten() do and when do I need it?","answer":"It collapses nested lists into a single flat list. It is most often needed when a for expression produces a list of lists. For example generating every combination of subnets and availability zones, and you want one flat collection to iterate over. Pair it with a for expression producing objects, then project into a map for for_each, which is the standard pattern for generating a matrix of resources."},{"question":"What is the difference between lookup() and try()?","answer":"lookup(map, key, default) retrieves a map value with a fallback if the key is absent. try() evaluates a series of expressions and returns the first that succeeds, which handles a broader range of failures including type errors and missing attributes on objects. Use lookup for a straightforward map access with a default; use try when the expression itself might be invalid, such as reaching into a nested structure that may not exist."},{"question":"How do I filter a list in Terraform?","answer":"With a for expression and an if clause: [for s in var.subnets : s.id if s.public]. The same syntax builds filtered maps by using the key => value form. There is no dedicated filter function, and the for expression is the idiomatic approach. It also handles transformation at the same time, so filtering and mapping happen in one pass."}]},{"slug":"terraform-dynamic-blocks","title":"Terraform Dynamic Blocks","seoTitle":"Terraform Dynamic Blocks: Syntax and Security Group Examples","description":"Learn Terraform dynamic blocks: generate repeated nested blocks from a collection, with security group and IAM policy examples and the limits to respect.","content":"$49","difficulty":"Medium","duration":"10 min read","keywords":["terraform dynamic blocks","terraform dynamic","terraform dynamic security group","terraform dynamic block example","terraform nested blocks"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":26,"tableOfContents":[{"id":"what-are-dynamic-blocks","title":"What Are Dynamic Blocks?","level":2},{"id":"why-dynamic-blocks-exist","title":"Why Dynamic Blocks Exist","level":2},{"id":"dynamic-block-syntax","title":"Dynamic Block Syntax","level":2},{"id":"a-basic-security-group-example","title":"A Basic Security Group Example","level":2},{"id":"using-the-current-item-inside-content","title":"Using the Current Item Inside content","level":2},{"id":"dynamic-blocks-in-security-groups","title":"Dynamic Blocks in Security Groups","level":2},{"id":"hardcoded-approach","title":"Hardcoded approach","level":3},{"id":"dynamic-approach","title":"Dynamic approach","level":3},{"id":"dynamic-blocks-in-iam-policies","title":"Dynamic Blocks in IAM Policies","level":2},{"id":"why-this-matters","title":"Why this matters","level":3},{"id":"nesting-dynamic-blocks","title":"Nesting Dynamic Blocks","level":2},{"id":"when-nested-dynamic-blocks-are-worth-it","title":"When nested dynamic blocks are worth it","level":3},{"id":"when-nested-dynamic-blocks-are-risky","title":"When nested dynamic blocks are risky","level":3},{"id":"dynamic-block-vs-multiple-hardcoded-blocks","title":"Dynamic Block vs Multiple Hardcoded Blocks","level":2},{"id":"hardcoded-blocks-are-often-clearer","title":"Hardcoded blocks are often clearer","level":3},{"id":"dynamic-blocks-are-often-better-for-reusable-modules","title":"Dynamic blocks are often better for reusable modules","level":3},{"id":"when-not-to-use-dynamic-blocks","title":"When NOT to Use Dynamic Blocks","level":2},{"id":"avoid-dynamic-blocks-when-the-config-is-tiny-and-fixed","title":"Avoid dynamic blocks when the config is tiny and fixed","level":3},{"id":"avoid-dynamic-blocks-when-they-hide-important-intent","title":"Avoid dynamic blocks when they hide important intent","level":3},{"id":"avoid-dynamic-blocks-when-input-complexity-becomes-the-real-problem","title":"Avoid dynamic blocks when input complexity becomes the real problem","level":3},{"id":"a-complete-practical-example","title":"A Complete Practical Example","level":2},{"id":"best-practices-for-dynamic-blocks","title":"Best Practices for Dynamic Blocks","level":2},{"id":"1-use-them-for-repeated-nested-blocks-not-for-everything","title":"1. Use them for repeated nested blocks, not for everything","level":3},{"id":"2-keep-input-data-well-typed","title":"2. Keep input data well-typed","level":3},{"id":"3-consider-an-explicit-iterator","title":"3. Consider an explicit iterator","level":3},{"id":"4-prefer-explicit-blocks-when-the-structure-is-small","title":"4. Prefer explicit blocks when the structure is small","level":3},{"id":"5-be-extra-careful-with-security-related-abstractions","title":"5. Be extra careful with security-related abstractions","level":3},{"id":"common-beginner-mistakes","title":"Common Beginner Mistakes","level":2},{"id":"mistake-1-using-dynamic-blocks-to-repeat-whole-resources","title":"Mistake 1: Using dynamic blocks to repeat whole resources","level":3},{"id":"mistake-2-making-simple-configuration-harder-to-read","title":"Mistake 2: Making simple configuration harder to read","level":3},{"id":"mistake-3-confusing-iterators","title":"Mistake 3: Confusing iterators","level":3},{"id":"mistake-4-over-nesting-everything","title":"Mistake 4: Over-nesting everything","level":3},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is a dynamic block in Terraform?","answer":"A way to generate repeated nested blocks from a collection. Where a resource takes multiple ingress or setting blocks, a dynamic block iterates over a list or map and emits one for each element, using content to define the shape and <name>.value to access the current item. It removes the need to copy a nested block five times with slightly different values."},{"question":"When should I use a dynamic block?","answer":"When the number of nested blocks genuinely varies. Security group rules driven by a variable, tags on a resource type that uses block syntax, environment settings whose count differs by caller. Do not use one for a fixed, small number of blocks: two hand-written ingress blocks are far easier to read than a dynamic block over a two-element list, and readability matters more than avoiding repetition at that scale."},{"question":"What is the difference between a dynamic block and for_each on a resource?","answer":"foreach on a resource creates multiple resources; a dynamic block creates multiple nested blocks inside one resource. Five subnets means foreach on aws_subnet. Five ingress rules inside one security group means a dynamic "ingress" block. Choosing the wrong one produces either a single resource that should have been several, or several resources that should have been one."},{"question":"Why is my dynamic block producing nothing?","answer":"The collection it iterates over is empty, which produces zero blocks and no error. That is usually correct behaviour, but it surprises people who expected a default. Check the value with terraform console and confirm it is populated. The other frequent cause is referencing the wrong iterator name. Inside dynamic "ingress" the variable is ingress.value unless you override it with iterator."},{"question":"Can I nest dynamic blocks?","answer":"Yes, and it is occasionally necessary for deeply structured resources. It is also where Terraform configuration becomes genuinely hard to read, so treat a second level of nesting as a signal to reconsider the data structure. Flattening the input into a list of fully-formed objects usually produces a simpler configuration than two nested loops. Run terraform console or a plan and read the rendered output when nesting, because the mistakes are not obvious from the source."},{"question":"Should I prefer security group rule resources over dynamic ingress blocks?","answer":"Often, yes. Separate awsvpcsecuritygroupingressrule resources with foreach give each rule its own state address, so adding or removing one shows as a single change rather than a rewrite of the whole security group, and a description per rule is preserved cleanly. Dynamic blocks inside awssecuritygroup remain valid, but the standalone rule resources produce clearer plans on anything that changes regularly."}]},{"slug":"terraform-workspaces","title":"Terraform Workspaces","seoTitle":"Terraform Workspaces: Commands and When Not to Use Them","description":"Learn how Terraform workspaces separate state, the workspace new, select and list commands, terraform.workspace in code, and when to avoid them.","content":"$4a","difficulty":"Easy","duration":"10 min read","keywords":["terraform workspaces","terraform workspace commands","terraform multiple environments","terraform workspace state","terraform.workspace variable"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":27,"tableOfContents":[{"id":"what-are-terraform-workspaces","title":"What Are Terraform Workspaces?","level":2},{"id":"the-default-workspace","title":"The Default Workspace","level":2},{"id":"why-workspaces-are-useful","title":"Why Workspaces Are Useful","level":2},{"id":"terraform-workspace-commands","title":"Terraform Workspace Commands","level":2},{"id":"terraform-workspace-list","title":"terraform workspace list","level":2},{"id":"terraform-workspace-show","title":"terraform workspace show","level":2},{"id":"terraform-workspace-new","title":"terraform workspace new","level":2},{"id":"terraform-workspace-select","title":"terraform workspace select","level":2},{"id":"terraform-workspace-delete","title":"terraform workspace delete","level":2},{"id":"a-typical-workspace-flow","title":"A Typical Workspace Flow","level":2},{"id":"using-terraformworkspace-in-configuration","title":"Using terraform.workspace in Configuration","level":2},{"id":"workspace-based-conditional-logic","title":"Workspace-Based Conditional Logic","level":2},{"id":"why-this-is-convenient","title":"Why this is convenient","level":3},{"id":"why-this-can-also-become-risky","title":"Why this can also become risky","level":3},{"id":"workspace-based-variable-files","title":"Workspace-Based Variable Files","level":2},{"id":"workspaces-and-state","title":"Workspaces and State","level":2},{"id":"why-separate-state-matters","title":"Why separate state matters","level":3},{"id":"backend-behavior","title":"Backend behavior","level":3},{"id":"limitations-of-workspaces","title":"Limitations of Workspaces","level":2},{"id":"limitation-1-same-configuration-for-all-workspaces","title":"Limitation 1: Same configuration for all workspaces","level":3},{"id":"limitation-2-same-backend-context","title":"Limitation 2: Same backend context","level":3},{"id":"limitation-3-operational-safety","title":"Limitation 3: Operational safety","level":3},{"id":"limitation-4-not-always-a-good-fit-for-large-organizations","title":"Limitation 4: Not always a good fit for large organizations","level":3},{"id":"workspaces-vs-separate-directories-or-repositories","title":"Workspaces vs Separate Directories or Repositories","level":2},{"id":"workspaces-approach","title":"Workspaces approach","level":3},{"id":"separate-directories-approach","title":"Separate directories approach","level":3},{"id":"separate-repositories-approach","title":"Separate repositories approach","level":3},{"id":"when-to-use-workspaces","title":"When to Use Workspaces","level":2},{"id":"when-not-to-use-workspaces","title":"When Not to Use Workspaces","level":2},{"id":"practical-example","title":"Practical Example","level":2},{"id":"best-practices-for-using-workspaces-safely","title":"Best Practices for Using Workspaces Safely","level":2},{"id":"1-keep-environments-genuinely-similar","title":"1. Keep environments genuinely similar","level":3},{"id":"2-make-names-workspace-aware","title":"2. Make names workspace-aware","level":3},{"id":"3-pair-workspaces-with-automation","title":"3. Pair workspaces with automation","level":3},{"id":"4-be-cautious-with-production","title":"4. Be cautious with production","level":3},{"id":"5-document-the-environment-strategy","title":"5. Document the environment strategy","level":3},{"id":"common-beginner-mistakes","title":"Common Beginner Mistakes","level":2},{"id":"mistake-1-thinking-workspaces-create-separate-codebases","title":"Mistake 1: Thinking workspaces create separate codebases","level":3},{"id":"mistake-2-using-workspaces-as-the-answer-to-every-multi-environment-problem","title":"Mistake 2: Using workspaces as the answer to every multi-environment problem","level":3},{"id":"mistake-3-forgetting-to-include-workspace-in-names","title":"Mistake 3: Forgetting to include workspace in names","level":3},{"id":"mistake-4-treating-workspaces-as-a-complete-security-boundary","title":"Mistake 4: Treating workspaces as a complete security boundary","level":3},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"knowledge-check","title":"Knowledge Check","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What are Terraform workspaces for?","answer":"A workspace gives one configuration multiple independent state files, so the same code can manage several parallel instances of the same infrastructure. Every configuration starts in a workspace called default. They are genuinely useful for short-lived parallel copies, a per-developer sandbox, a per-pull-request preview environment, where the infrastructure is identical and only needs isolating."},{"question":"Should I use workspaces for dev, staging and production?","answer":"Generally no, despite it being the obvious first idea. Workspaces share one configuration and one backend, so environments cannot differ structurally, the same credentials reach all of them, and it is uncomfortably easy to run an apply against production because you forgot to check terraform workspace show. Separate directories or separate repositories with separate backends give real isolation, allow environments to differ, and let production credentials be scoped to production alone."},{"question":"How do I know which workspace I am in?","answer":"terraform workspace show prints it, and terraform workspace list marks the current one with an asterisk. Because nothing else in the output reminds you, showing it in your shell prompt is worth setting up if you use workspaces regularly. Within configuration, terraform.workspace returns the name, commonly used to derive resource names or select a map of per-workspace values."},{"question":"How do I use the workspace name in my configuration?","answer":"terraform.workspace is available anywhere as a string, so name = "app-${terraform.workspace}" gives per-workspace resource names. A common pattern is a map of settings keyed by workspace with a lookup and a default, which lets a sandbox use small instances while a shared workspace uses larger ones. Keep this light. Heavy branching on workspace name is a sign the environments should be separate configurations."},{"question":"Where is state stored for each workspace?","answer":"In the same backend, under a separate path. With the S3 backend, non-default workspaces are stored under a env:/<workspace>/ prefix alongside your configured key, while default uses the key directly. They are isolated from each other but share the bucket and its access controls, which is precisely why workspaces do not give production the credential separation it should have."},{"question":"How do I delete a workspace?","answer":"Destroy its resources first with terraform destroy while it is selected, then switch away and run terraform workspace delete <name>. Terraform refuses to delete a workspace whose state is non-empty unless you force it, and forcing leaves orphaned infrastructure nobody is tracking. For ephemeral preview environments, wire the destroy-then-delete sequence into the pipeline that tears the environment down."}]},{"slug":"terraform-with-aws","title":"Terraform with AWS","seoTitle":"Terraform with AWS: Provider Setup and OIDC Authentication","description":"Configure the Terraform AWS provider, authenticate without long-lived keys using assume role and OIDC, manage regions and accounts, and tag everything.","content":"$4b","difficulty":"Medium","duration":"12 min read","keywords":["terraform aws provider","terraform aws authentication","terraform assume role","terraform multi region","provider alias","default_tags terraform","terraform aws oidc","terraform with aws","terraform aws project","terraform aws vpc ec2 s3","aws infrastructure as code"],"publishedDate":"2026-06-11","updatedDate":"2026-09-13","order":28,"tableOfContents":[{"id":"provider-configuration","title":"Provider configuration","level":2},{"id":"authentication","title":"Authentication","level":2},{"id":"multiple-regions-and-accounts","title":"Multiple regions and accounts","level":2},{"id":"structuring-an-aws-estate","title":"Structuring an AWS estate","level":2},{"id":"reading-values-from-elsewhere","title":"Reading values from elsewhere","level":2},{"id":"things-that-bite","title":"Things that bite","level":2},{"id":"explore-further-in-this-section","title":"Explore further in this section","level":2},{"id":"quick-reference","title":"Quick reference","level":2},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"How should Terraform authenticate to AWS?","answer":"Through the standard credential chain, never with keys hardcoded in the provider block. Locally, use a named profile via AWS_PROFILE or AWS SSO. In CI, use OIDC federation. The pipeline exchanges a short-lived signed token for temporary credentials through an IAM role whose trust policy restricts which repository and branch may assume it. That removes long-lived credentials entirely, so there is nothing to rotate and nothing useful in a leaked log."},{"question":"How do I use Terraform across multiple AWS regions?","answer":"Declare additional provider blocks with an alias, then set provider = aws.useast1 on resources that belong in the other region. The case everyone meets is ACM certificates for CloudFront, which must be issued in us-east-1 regardless of where the rest of the infrastructure lives. To use an aliased provider inside a module, pass it explicitly through the providers argument. Modules should never declare their own provider configuration."},{"question":"How do I manage multiple AWS accounts with Terraform?","answer":"Use assume_role in the provider block, pointing at a role in the target account, with aliases when one configuration spans several accounts. More commonly, keep each account's infrastructure in its own root configuration with its own backend and credentials, which gives genuine isolation. A pipeline for staging then physically cannot reach production. AWS Organizations plus IAM Identity Center makes the role structure straightforward."},{"question":"Why does Terraform want to replace my AWS resource instead of updating it?","answer":"Because you changed an attribute AWS cannot modify in place: a subnet's CIDR block, an instance's availability zone, certain database engine settings. The plan marks it -/+ and annotates the specific attribute with # forces replacement, so the cause is always stated. On stateless resources this is harmless; on anything storing data it is destructive. Protect critical resources with lifecycle { prevent_destroy = true } so a replacement cannot be applied accidentally."},{"question":"How do I tag all my AWS resources consistently?","answer":"Use default_tags in the provider block, which applies the tags to every resource that supports tagging, with no repetition. Add per-resource tags normally and they merge with the defaults. This is the cheapest way to make cost allocation, ownership and automated cleanup work, and it is considerably easier to set up at the start than to retrofit across an existing estate."},{"question":"Should the VPC and the application be in the same Terraform configuration?","answer":"Usually not. Networking changes rarely and is shared by many workloads; application infrastructure changes constantly. Combining them means every application deploy plans against the whole network, the blast radius of a mistake is larger, and applies are slower. Put the VPC in its own configuration, expose IDs as outputs, and consume them from application configurations through a data source or a value published to SSM Parameter Store."}]},{"slug":"terraform-aws-vpc","title":"Terraform AWS VPC Setup","seoTitle":"Terraform AWS VPC: Public and Private Subnets with NAT","description":"Build a custom AWS VPC with Terraform: public and private subnets, an internet gateway, route tables and a NAT gateway, explained step by step.","content":"$4c","difficulty":"Medium","duration":"14 min read","keywords":["terraform aws vpc","terraform public private subnets","terraform aws internet gateway","terraform nat gateway","terraform route table association","terraform aws subnet","terraform aws provider region"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":29,"tableOfContents":[{"id":"project-overview","title":"Project Overview","level":2},{"id":"what-we-are-building","title":"What We Are Building","level":2},{"id":"why-public-and-private-subnets","title":"Why public and private subnets?","level":3},{"id":"why-a-nat-gateway","title":"Why a NAT gateway?","level":3},{"id":"why-more-than-one-availability-zone","title":"Why more than one availability zone?","level":3},{"id":"prerequisites","title":"Prerequisites","level":2},{"id":"project-structure","title":"Project Structure","level":2},{"id":"provider-configuration","title":"Provider Configuration","level":2},{"id":"discovering-availability-zones-with-a-data-source","title":"Discovering Availability Zones with a Data Source","level":2},{"id":"creating-the-vpc","title":"Creating the VPC","level":2},{"id":"why-use-a-16-cidr-block","title":"Why use a /16 CIDR block?","level":3},{"id":"creating-public-subnets","title":"Creating Public Subnets","level":2},{"id":"count","title":"count","level":3},{"id":"availability_zone","title":"availability_zone","level":3},{"id":"map_public_ip_on_launch-true","title":"map_public_ip_on_launch = true","level":3},{"id":"creating-private-subnets","title":"Creating Private Subnets","level":2},{"id":"creating-the-internet-gateway","title":"Creating the Internet Gateway","level":2},{"id":"creating-the-public-route-table","title":"Creating the Public Route Table","level":2},{"id":"associating-public-subnets-with-the-public-route-table","title":"Associating Public Subnets with the Public Route Table","level":2},{"id":"creating-the-nat-gateway","title":"Creating the NAT Gateway","level":2},{"id":"why-is-the-nat-gateway-in-a-public-subnet","title":"Why is the NAT gateway in a public subnet?","level":3},{"id":"why-only-one-nat-gateway-in-this-lesson","title":"Why only one NAT gateway in this lesson?","level":3},{"id":"creating-the-private-route-table","title":"Creating the Private Route Table","level":2},{"id":"associating-private-subnets-with-the-private-route-table","title":"Associating Private Subnets with the Private Route Table","level":2},{"id":"running-terraform-plan","title":"Running terraform plan","level":2},{"id":"what-to-review-in-the-plan-output","title":"What to review in the plan output","level":3},{"id":"complete-working-code-for-the-full-vpc-module","title":"Complete Working Code for the Full VPC Module","level":2},{"id":"maintf","title":"main.tf","level":3},{"id":"variablestf","title":"variables.tf","level":3},{"id":"outputstf","title":"outputs.tf","level":3},{"id":"terraformtfvars","title":"terraform.tfvars","level":3},{"id":"applying-and-verifying-the-network","title":"Applying and Verifying the Network","level":2},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"practice-questions","title":"Practice Questions","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"Should I write my own VPC module or use the community one?","answer":"Use the community terraform-aws-modules/vpc/aws module for anything real. VPC networking has a large number of interdependent pieces. Subnets across availability zones, route tables and associations, internet and NAT gateways, endpoints, flow logs, and the module handles combinations that take a long time to get right by hand. Writing your own is a genuinely useful learning exercise, and rarely the best production choice."},{"question":"How do I create subnets across multiple availability zones?","answer":"Query the available zones with the awsavailabilityzones data source rather than hardcoding names, then create subnets with for_each over a map keyed on a stable name. Use cidrsubnet() to carve the ranges from the VPC CIDR so they are derived rather than hand-calculated. Avoid count with an index into the AZ list, if AWS changes zone ordering or you remove one, positional identity causes unrelated subnets to be recreated."},{"question":"How many NAT gateways should Terraform create?","answer":"One per availability zone for production, since a NAT gateway is zonal and losing its AZ takes outbound access with it. The community module exposes singlenatgateway and onenatgatewayperaz to control this, and using a single gateway in non-production is a reasonable saving. Be aware NAT gateways are frequently the largest networking cost on an AWS bill, so add free S3 and DynamoDB gateway endpoints in the same configuration."},{"question":"Why does changing a subnet CIDR want to destroy and recreate it?","answer":"Because a subnet's CIDR block is immutable. AWS provides no way to change it in place, so Terraform must replace the resource. The plan shows -/+ with a "forces replacement" annotation. Anything inside that subnet is affected, so treat CIDR changes as a migration rather than an edit. This is why sizing the VPC and its subnets generously at creation matters: a /20 per subnet costs nothing extra and avoids the problem entirely."},{"question":"How do I tag subnets for EKS or load balancers?","answer":"The AWS Load Balancer Controller and EKS discover where to place resources from subnet tags, kubernetes.io/role/elb set to 1 on public subnets and kubernetes.io/role/internal-elb on private ones, plus a cluster ownership tag. The community VPC module accepts publicsubnettags and privatesubnettags for exactly this. Missing tags produce an Ingress that sits in pending with no clear error, which is a slow thing to diagnose."},{"question":"Should the VPC live in the same Terraform state as my application?","answer":"Usually not. Networking changes rarely and is shared by many workloads, while application infrastructure changes constantly. Keeping them together means every application deploy plans against the whole VPC and a mistake has a larger blast radius. Put the VPC in its own configuration and expose IDs via outputs, then consume them from application configurations with a data source or by reading a parameter from SSM."}]},{"slug":"terraform-aws-ec2","title":"Terraform AWS EC2 Web Server","seoTitle":"Terraform AWS EC2 Instance with Security Group and User Data","description":"Launch an EC2 web server with Terraform: add a security group, find the latest Ubuntu AMI, create a key pair, write user data and attach an Elastic IP.","content":"$4d","difficulty":"Medium","duration":"12 min read","keywords":["terraform aws ec2","terraform security group ec2","terraform aws ami data source","terraform ec2 user data","terraform aws key pair","terraform elastic ip ec2","terraform nginx ec2"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":30,"tableOfContents":[{"id":"continuing-the-aws-project","title":"Continuing the AWS Project","level":2},{"id":"architecture-for-this-lesson","title":"Architecture for This Lesson","level":2},{"id":"prerequisites","title":"Prerequisites","level":2},{"id":"referencing-the-existing-vpc-resources","title":"Referencing the Existing VPC Resources","level":2},{"id":"creating-the-security-group","title":"Creating the Security Group","level":2},{"id":"why-separate-ssh-and-http-rules","title":"Why separate SSH and HTTP rules?","level":3},{"id":"finding-the-latest-ubuntu-ami-with-a-data-source","title":"Finding the Latest Ubuntu AMI with a Data Source","level":2},{"id":"what-this-does","title":"What this does","level":3},{"id":"creating-a-key-pair-for-ssh-access","title":"Creating a Key Pair for SSH Access","level":2},{"id":"a-practical-note-about-","title":"A practical note about ~","level":3},{"id":"writing-the-user-data-script","title":"Writing the User Data Script","level":2},{"id":"why-user-data-matters","title":"Why user data matters","level":3},{"id":"creating-the-ec2-instance","title":"Creating the EC2 Instance","level":2},{"id":"important-arguments-explained","title":"Important arguments explained","level":3},{"id":"adding-an-elastic-ip","title":"Adding an Elastic IP","level":2},{"id":"why-use-an-elastic-ip-in-a-tutorial","title":"Why use an Elastic IP in a tutorial?","level":3},{"id":"output-values-for-the-instance","title":"Output Values for the Instance","level":2},{"id":"running-the-plan-and-apply","title":"Running the Plan and Apply","level":2},{"id":"testing-the-deployment","title":"Testing the Deployment","level":2},{"id":"test-in-the-browser","title":"Test in the browser","level":3},{"id":"test-with-ssh","title":"Test with SSH","level":3},{"id":"common-troubleshooting-tips","title":"Common Troubleshooting Tips","level":2},{"id":"1-ssh-works-but-http-does-not","title":"1. SSH works but HTTP does not","level":3},{"id":"2-ssh-does-not-work","title":"2. SSH does not work","level":3},{"id":"3-the-instance-starts-but-nginx-is-missing","title":"3. The instance starts but Nginx is missing","level":3},{"id":"complete-working-code","title":"Complete Working Code","level":2},{"id":"maintf","title":"main.tf","level":3},{"id":"variablestf","title":"variables.tf","level":3},{"id":"outputstf","title":"outputs.tf","level":3},{"id":"terraformtfvars","title":"terraform.tfvars","level":3},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"practice-questions","title":"Practice Questions","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"How do I avoid hardcoding an AMI ID in Terraform?","answer":"Use the awsami data source with owner and name filters plus mostrecent = true, or read AWS's published SSM public parameters with the awsssmparameter data source, /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 resolves the current Amazon Linux image. Both remove a value that goes stale and becomes a security liability. Note that a new AMI then triggers instance replacement, so pair this with a launch template and an Auto Scaling group if you want control over when that happens."},{"question":"Should I create EC2 instances directly or use a launch template?","answer":"Use a launch template with an Auto Scaling group for anything that matters, even at a desired capacity of one. A bare aws_instance is a pet: if it fails, nothing replaces it, and every change risks manual repair. A launch template plus an Auto Scaling group makes replacement routine and gives you rolling updates via instance refresh. Reserve standalone instances for genuinely one-off or experimental work."},{"question":"How do I pass startup configuration to an instance?","answer":"Through userdata, rendered with the templatefile() function so values can be interpolated from Terraform. Use userdatareplaceonchange = true if you want a change to the script to replace the instance, since by default a modified userdata does not affect a running instance. It only executes on first boot. For anything substantial, bake the configuration into an AMI with Packer instead, which makes instances start ready and shortens boot time."},{"question":"Why does Terraform want to replace my instance when I only changed a tag?","answer":"It usually does not, and a replacement means something else changed. Most often the AMI ID resolved by a data source, or an attribute that is immutable such as the availability zone, subnet or certain instance metadata options. Read the plan carefully: Terraform annotates the specific attribute with "forces replacement", which names the cause precisely. If it is an AMI refresh you did not intend, pin the AMI or add ignore_changes to the lifecycle block."},{"question":"How do I attach an IAM role to an EC2 instance in Terraform?","answer":"Create an awsiamrole with a trust policy allowing ec2.amazonaws.com, attach the permission policies, then create an awsiaminstanceprofile referencing the role and set iaminstance_profile on the instance or launch template. The instance profile is the piece people miss. An EC2 instance cannot reference a role directly, and forgetting it produces credentials errors inside the instance that look unrelated to Terraform."},{"question":"How do I manage EBS volumes with Terraform?","answer":"The root volume is configured inline with a rootblockdevice block; additional volumes are separate awsebsvolume resources attached with awsvolumeattachment. Use gp3 rather than gp2, since it is cheaper and faster with performance independent of size. Set encrypted = true, or better, enable EBS encryption by default at the account level so no configuration can forget it. Note that additional volumes do not delete on termination by default, which is usually what you want but does leave orphans if instances are replaced often."}]},{"slug":"terraform-aws-s3","title":"Terraform AWS S3 Static Assets","seoTitle":"Terraform S3 Bucket: Versioning, Lifecycle Rules and Policy","description":"Create an S3 bucket with Terraform: unique names with random_id, versioning, lifecycle rules, public access settings, a bucket policy and object uploads.","content":"$4e","difficulty":"Medium","duration":"10 min read","keywords":["terraform aws s3","terraform s3 bucket versioning","terraform s3 lifecycle rules","terraform random id bucket name","terraform s3 bucket policy","terraform aws s3 object","terraform static site s3"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":31,"tableOfContents":[{"id":"continuing-the-aws-project","title":"Continuing the AWS Project","level":2},{"id":"why-s3-belongs-in-this-project","title":"Why S3 Belongs in This Project","level":2},{"id":"the-bucket-naming-problem","title":"The Bucket Naming Problem","level":2},{"id":"creating-the-s3-bucket","title":"Creating the S3 Bucket","level":2},{"id":"bucket-ownership-controls","title":"Bucket Ownership Controls","level":2},{"id":"what-does-bucketownerpreferred-mean","title":"What does BucketOwnerPreferred mean?","level":3},{"id":"public-access-block-configuration","title":"Public Access Block Configuration","level":2},{"id":"why-these-settings","title":"Why these settings?","level":3},{"id":"enabling-bucket-versioning","title":"Enabling Bucket Versioning","level":2},{"id":"why-enable-versioning","title":"Why enable versioning?","level":3},{"id":"adding-lifecycle-rules","title":"Adding Lifecycle Rules","level":2},{"id":"what-this-rule-does","title":"What this rule does","level":3},{"id":"creating-the-website-configuration","title":"Creating the Website Configuration","level":2},{"id":"important-distinction","title":"Important distinction","level":3},{"id":"writing-the-bucket-policy","title":"Writing the Bucket Policy","level":2},{"id":"what-this-policy-allows","title":"What this policy allows","level":3},{"id":"uploading-files-with-aws_s3_object","title":"Uploading Files with aws_s3_object","level":2},{"id":"why-use-content-here","title":"Why use content here?","level":3},{"id":"outputting-the-bucket-name-and-website-endpoint","title":"Outputting the Bucket Name and Website Endpoint","level":2},{"id":"running-plan-and-apply","title":"Running Plan and Apply","level":2},{"id":"complete-working-code","title":"Complete Working Code","level":2},{"id":"maintf","title":"main.tf","level":3},{"id":"variablestf","title":"variables.tf","level":3},{"id":"outputstf","title":"outputs.tf","level":3},{"id":"a-practical-production-note","title":"A Practical Production Note","level":2},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"practice-questions","title":"Practice Questions","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"Why does my S3 bucket configuration need so many separate resources?","answer":"Because the AWS provider split what was once a set of inline blocks on awss3bucket into standalone resources, awss3bucketversioning, awss3bucketserversideencryptionconfiguration, awss3bucketpublicaccessblock, awss3bucketlifecycleconfiguration and others. Older tutorials showing versioning or encryption inline are written for the pre-v4 provider and will not work. Each setting is now its own resource referencing the bucket by ID."},{"question":"How do I make a bucket private and keep it that way?","answer":"Create an awss3bucketpublicaccessblock with all four settings true, which is a hard override that no bucket policy can bypass. Combine it with awss3bucketownership_controls set to BucketOwnerEnforced, which disables ACLs entirely. Object ACLs were behind most historic S3 exposure incidents. For content that genuinely must be public, front the private bucket with CloudFront using an Origin Access Control rather than opening the bucket."},{"question":"How do I stop Terraform from deleting a bucket that contains objects?","answer":"By default AWS refuses to delete a non-empty bucket and the destroy fails, which is a useful accident. forcedestroy = true makes Terraform empty it first, which is convenient for ephemeral test buckets and dangerous anywhere else. Set it deliberately and never on anything holding real data. For production, add lifecycle { preventdestroy = true } so Terraform refuses any plan that would remove it."},{"question":"How do I configure lifecycle rules in Terraform?","answer":"With awss3bucketlifecycleconfiguration, containing rule blocks with transition and expiration actions. Add an abortincompletemultipartupload rule to every bucket. Failed multipart uploads leave parts that are billed as storage but do not appear in a normal listing, and teams discover them years later as an unexplained cost. If versioning is enabled, include noncurrentversion_expiration or old versions accumulate indefinitely."},{"question":"Should I use a bucket policy or IAM policies?","answer":"Within one account, IAM policies on the principals are sufficient and simpler. Use a bucket policy for cross-account access, for conditions that must apply to every caller regardless of their IAM, such as denying any request not over HTTPS, and for granting access to AWS services like CloudFront. A common mistake is writing both when only IAM is needed, which doubles the places a permission can be wrong."},{"question":"How do I manage the S3 bucket that holds my Terraform state?","answer":"Not in the configuration that uses it, since the bucket must exist before terraform init can store state there. Create it in a small separate configuration with local state, or once via the CLI. Enable versioning so a bad state write can be recovered, enable encryption, block all public access, and restrict who can read it. State contains secrets in plain text. Keep that bootstrap configuration in version control even though its own state is local; it changes rarely."}]},{"slug":"terraform-provisioners","title":"Terraform Provisioners","seoTitle":"Terraform Provisioners: local-exec, remote-exec, file","description":"Learn how Terraform provisioners work: local-exec, remote-exec, file and connection blocks, null_resource, and why provisioners should be a last resort.","content":"$4f","difficulty":"Medium","duration":"10 min read","keywords":["terraform provisioners","terraform local-exec","terraform remote-exec","terraform file provisioner","terraform null_resource"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":32,"tableOfContents":[{"id":"what-are-terraform-provisioners","title":"What Are Terraform Provisioners?","level":2},{"id":"why-provisioners-feel-attractive-at-first","title":"Why Provisioners Feel Attractive at First","level":2},{"id":"the-three-common-provisioners","title":"The Three Common Provisioners","level":2},{"id":"local-exec-run-commands-on-the-machine-running-terraform","title":"local-exec: Run Commands on the Machine Running Terraform","level":2},{"id":"what-happens-here","title":"What happens here?","level":3},{"id":"common-uses-of-local-exec","title":"Common uses of local-exec","level":3},{"id":"important-limitation","title":"Important limitation","level":3},{"id":"remote-exec-run-commands-on-the-provisioned-resource","title":"remote-exec: Run Commands on the Provisioned Resource","level":2},{"id":"why-it-feels-useful","title":"Why it feels useful","level":3},{"id":"why-it-is-fragile","title":"Why it is fragile","level":3},{"id":"file-copy-files-to-a-remote-resource","title":"file: Copy Files to a Remote Resource","level":2},{"id":"the-connection-block","title":"The connection Block","level":2},{"id":"key-fields","title":"Key fields","level":3},{"id":"why-self-appears-here","title":"Why self appears here","level":3},{"id":"provisioners-run-on-create-or-destroy","title":"Provisioners Run on Create or Destroy","level":2},{"id":"why-hashicorp-recommends-against-provisioners","title":"Why HashiCorp Recommends Against Provisioners","level":2},{"id":"1-they-are-fragile","title":"1. They are fragile","level":3},{"id":"2-they-are-not-truly-declarative","title":"2. They are not truly declarative","level":3},{"id":"3-they-weaken-idempotence","title":"3. They weaken idempotence","level":3},{"id":"4-they-blur-responsibilities","title":"4. They blur responsibilities","level":3},{"id":"5-they-break-the-clean-planapply-model","title":"5. They break the clean plan/apply model","level":3},{"id":"better-alternatives-to-provisioners","title":"Better Alternatives to Provisioners","level":2},{"id":"1-user_data-or-cloud-init","title":"1. user_data or cloud-init","level":3},{"id":"2-packer","title":"2. Packer","level":3},{"id":"3-ansible","title":"3. Ansible","level":3},{"id":"4-external-application-deployment-pipelines","title":"4. External application deployment pipelines","level":3},{"id":"when-provisioners-are-acceptable","title":"When Provisioners Are Acceptable","level":2},{"id":"null_resource-and-triggers","title":"null_resource and Triggers","level":2},{"id":"what-are-triggers","title":"What are triggers?","level":3},{"id":"example-rerun-when-a-file-hash-changes","title":"Example: rerun when a file hash changes","level":3},{"id":"a-realistic-decision-framework","title":"A Realistic Decision Framework","level":2},{"id":"example-a-minimal-acceptable-local-exec","title":"Example: A Minimal Acceptable local-exec","level":2},{"id":"example-a-provisioner-pattern-to-avoid","title":"Example: A Provisioner Pattern to Avoid","level":2},{"id":"final-guidance","title":"Final Guidance","level":2},{"id":"practice-questions","title":"Practice Questions","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What are Terraform provisioners and should I use them?","answer":"Provisioners run scripts or commands as part of resource creation or destruction, local-exec on the machine running Terraform, remote-exec over SSH or WinRM on the created resource. HashiCorp explicitly describes them as a last resort, and that guidance is sound: they break Terraform's declarative model, are not represented in state, and cannot be planned. Use them only when no provider, cloud-init or configuration management alternative exists."},{"question":"Why are provisioners considered a last resort?","answer":"Because Terraform cannot reason about them. A provisioner's effects are invisible to the plan, are not recorded in state, and are not re-run when the underlying script changes, so the resource can be "up to date" while the configuration it applied is stale. They also introduce a network and credential dependency at apply time, which makes applies fail for reasons unrelated to infrastructure. Failures mark the resource tainted, forcing a destroy and recreate on the next run."},{"question":"What should I use instead of a provisioner?","answer":"For bootstrapping a machine, use user_data or cloud-init, which the cloud provider executes and which is part of the resource's own configuration. For configuring software, use Ansible, Chef or Puppet after Terraform has created the infrastructure. For baking software into an image, use Packer so instances start ready. For calling an API a provider does not cover, prefer a dedicated provider or an external data source over local-exec and curl."},{"question":"What does it mean when a resource is tainted?","answer":"A provisioner failed, so Terraform marks the resource as unreliable. The infrastructure exists but its configuration did not complete. The next plan proposes destroying and recreating it, because Terraform has no way to re-run just the provisioner. You can clear the mark with terraform untaint if you fixed the problem by hand, though that leaves Terraform trusting a state it did not create."},{"question":"How do destroy-time provisioners work?","answer":"Adding when = destroy runs the provisioner before the resource is destroyed, typically to deregister a node or drain connections. They come with real constraints: they cannot reference variables or most other values, only self, and they must still be present in configuration when the destroy happens, so removing the resource block and the provisioner together means the provisioner never runs. Plan the removal in two steps if a destroy-time action matters."},{"question":"Is local-exec ever acceptable?","answer":"For genuinely local, idempotent side effects that do not represent infrastructure. Writing a kubeconfig for a subsequent step, or triggering a notification. It is pragmatic and widely used. It becomes a problem when it is doing work that should be declarative, because nothing plans it, nothing detects drift in it, and nobody reviewing the plan can see what it will do. If a local-exec is creating or modifying infrastructure, that infrastructure belongs in a resource block."}]},{"slug":"terraform-cicd-pipeline","title":"Terraform CI/CD Pipeline","seoTitle":"Terraform GitLab CI/CD Pipeline: Validate, Plan, Apply","description":"Build a Terraform pipeline in GitLab CI/CD with validate, plan, manual apply and destroy stages, GitLab-managed state and secure credential handling.","content":"$50","difficulty":"Medium","duration":"14 min read","keywords":["terraform gitlab ci cd","terraform pipeline","terraform gitlab","terraform ci cd pipeline","terraform github actions","terraform automated deployment"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":33,"tableOfContents":[{"id":"why-infrastructure-as-code-belongs-in-cicd","title":"Why Infrastructure as Code Belongs in CI/CD","level":2},{"id":"1-consistency","title":"1. Consistency","level":3},{"id":"2-audit-trail","title":"2. Audit trail","level":3},{"id":"3-no-cowboy-infrastructure","title":"3. No cowboy infrastructure","level":3},{"id":"4-safer-approvals","title":"4. Safer approvals","level":3},{"id":"5-team-scale","title":"5. Team scale","level":3},{"id":"the-standard-terraform-pipeline-pattern","title":"The Standard Terraform Pipeline Pattern","level":2},{"id":"the-full-gitlab-cicd-example","title":"The Full GitLab CI/CD Example","level":2},{"id":"understanding-the-validate-stage","title":"Understanding the Validate Stage","level":2},{"id":"terraform-fmt--check--recursive","title":"terraform fmt -check -recursive","level":3},{"id":"terraform-validate","title":"terraform validate","level":3},{"id":"understanding-the-plan-stage","title":"Understanding the Plan Stage","level":2},{"id":"why-store-the-plan-as-an-artifact","title":"Why store the plan as an artifact?","level":3},{"id":"manual-apply-as-a-safety-gate","title":"Manual Apply as a Safety Gate","level":2},{"id":"why-apply-the-saved-plan-file","title":"Why apply the saved plan file?","level":3},{"id":"manual-destroy-stage","title":"Manual Destroy Stage","level":2},{"id":"storing-state-in-gitlab-managed-terraform-state","title":"Storing State in GitLab-Managed Terraform State","level":2},{"id":"minimal-terraform-backend-block","title":"Minimal Terraform backend block","level":3},{"id":"why-this-pattern-is-useful","title":"Why this pattern is useful","level":3},{"id":"storing-aws-credentials-as-gitlab-cicd-variables","title":"Storing AWS Credentials as GitLab CI/CD Variables","level":2},{"id":"why-masked-and-protected","title":"Why masked and protected?","level":3},{"id":"branch-based-workflow","title":"Branch-Based Workflow","level":2},{"id":"feature-branches-plan-only","title":"Feature branches: plan only","level":3},{"id":"default-branch-allowed-to-apply","title":"Default branch: allowed to apply","level":3},{"id":"environment-specific-pipelines","title":"Environment-Specific Pipelines","level":2},{"id":"why-separate-behavior-by-environment","title":"Why separate behavior by environment?","level":3},{"id":"suggested-multi-environment-state-naming","title":"Suggested Multi-Environment State Naming","level":2},{"id":"example-of-a-safer-project-layout","title":"Example of a Safer Project Layout","level":2},{"id":"why-ci-pipelines-beat-manual-terraform-apply","title":"Why CI Pipelines Beat Manual terraform apply","level":2},{"id":"github-actions-alternative","title":"GitHub Actions Alternative","level":2},{"id":"gitlab-strengths-in-this-pattern","title":"GitLab strengths in this pattern","level":3},{"id":"github-actions-strengths","title":"GitHub Actions strengths","level":3},{"id":"common-pipeline-improvements","title":"Common Pipeline Improvements","level":2},{"id":"final-recommendations","title":"Final Recommendations","level":2},{"id":"practice-questions","title":"Practice Questions","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"How should a Terraform CI/CD pipeline be structured?","answer":"Run fmt -check, validate and a security scan on every commit. On pull requests, run terraform plan -out=tfplan and post the output as a comment so the change is reviewable. On merge to the default branch, apply the saved plan file rather than re-planning, so what was approved is exactly what runs. Gate production behind a manual approval, and never let apply run from a feature branch against shared infrastructure."},{"question":"Why should CI apply a saved plan file?","answer":"Because without it, the apply recalculates from whatever state and real infrastructure exist at that moment, which may differ from what the reviewer saw. terraform plan -out=tfplan followed by terraform apply tfplan guarantees the approved change is the applied change. The caveat is that a plan file contains sensitive values in plain text, so treat the artifact as a secret and expire it quickly."},{"question":"How do I authenticate Terraform to AWS from CI without access keys?","answer":"Use OIDC. Register your CI provider as an OIDC identity provider in AWS, create a role whose trust policy accepts its tokens with conditions restricting the specific repository and branch, and have the pipeline exchange a short-lived token for temporary credentials. Nothing long-lived is stored, there is nothing to rotate, and a leaked log contains no reusable secret. The equivalent mechanism exists for Azure and GCP."},{"question":"How do I stop two pipeline runs applying at once?","answer":"State locking, which a proper remote backend provides. S3 with native locking or a DynamoDB table, or Terraform Cloud. The second run then waits or fails rather than corrupting state. Add pipeline-level concurrency control as well, since queueing at the CI layer produces a clearer outcome than a lock timeout. If a run is killed mid-apply the lock can stick, and terraform force-unlock clears it once you have confirmed nothing is actually running."},{"question":"Should terraform apply be fully automatic?","answer":"For non-production, yes. Fast feedback is worth more than the risk. For production, a manual approval gate is the norm, because a plan can contain a resource replacement that destroys data and no amount of automated checking reliably catches intent. A reasonable middle ground is automatic apply when the plan contains only additions and in-place updates, with approval required whenever it contains a destroy or a replacement."},{"question":"How do I handle multiple environments in one pipeline?","answer":"Keep a separate state file per environment and run the same pipeline with different backend configuration and credentials, supplied via -backend-config and -var-file. Promote by applying identical code to each environment in turn, dev, then staging, then production, rather than maintaining divergent branches. Scope each environment's credentials to that environment alone, so a pipeline misconfiguration cannot reach production from a staging job."}]},{"slug":"terraform-security","title":"Terraform Security","seoTitle":"Terraform Security: Scan IaC with tfsec and Checkov in CI","description":"Secure Terraform code by scanning with tfsec and Checkov, protecting secrets in state, fixing common misconfigurations and adding checks to CI/CD.","content":"$51","difficulty":"Medium","duration":"12 min read","keywords":["terraform security","terraform tfsec","terraform checkov","terraform iac security","terraform secrets management","terraform sensitive state"],"publishedDate":"2026-07-12","updatedDate":"2026-07-12","order":34,"tableOfContents":[{"id":"why-terraform-security-scanning-matters","title":"Why Terraform Security Scanning Matters","level":2},{"id":"shift-left-scan-before-apply","title":"Shift Left: Scan Before Apply","level":2},{"id":"tfsec","title":"tfsec","level":2},{"id":"installing-tfsec","title":"Installing tfsec","level":3},{"id":"running-tfsec","title":"Running tfsec","level":3},{"id":"understanding-tfsec-findings","title":"Understanding tfsec findings","level":3},{"id":"severity-levels","title":"Severity levels","level":3},{"id":"checkov","title":"Checkov","level":2},{"id":"installing-checkov","title":"Installing Checkov","level":3},{"id":"running-checkov","title":"Running Checkov","level":3},{"id":"suppressing-false-positives-carefully","title":"Suppressing False Positives Carefully","level":2},{"id":"example-checkov-suppression","title":"Example Checkov suppression","level":3},{"id":"good-suppression-hygiene","title":"Good suppression hygiene","level":3},{"id":"common-iac-security-problems","title":"Common IaC Security Problems","level":2},{"id":"1-public-s3-buckets","title":"1. Public S3 Buckets","level":2},{"id":"safer-mindset","title":"Safer mindset","level":3},{"id":"2-unrestricted-security-groups","title":"2. Unrestricted Security Groups","level":2},{"id":"example-of-risky-ssh","title":"Example of risky SSH","level":3},{"id":"better-approach","title":"Better approach","level":3},{"id":"3-unencrypted-ebs-volumes","title":"3. Unencrypted EBS Volumes","level":2},{"id":"4-iam-over-permissions","title":"4. IAM Over-Permissions","level":2},{"id":"integrating-tfsec-and-checkov-into-gitlab-ci","title":"Integrating tfsec and Checkov into GitLab CI","level":2},{"id":"why-a-separate-security-stage","title":"Why a separate security stage?","level":3},{"id":"secrets-in-the-terraform-state-file","title":"Secrets in the Terraform State File","level":2},{"id":"how-to-protect-state","title":"How to Protect State","level":2},{"id":"sensitive-variables","title":"Sensitive Variables","level":2},{"id":"important-limitation","title":"Important limitation","level":3},{"id":"sensitive-outputs","title":"Sensitive Outputs","level":2},{"id":"backend-security-encrypting-s3-state-with-kms","title":"Backend Security: Encrypting S3 State with KMS","level":2},{"id":"why-kms-matters","title":"Why KMS matters","level":3},{"id":"hashicorp-vault-integration","title":"HashiCorp Vault Integration","level":2},{"id":"sentinel-policies","title":"Sentinel Policies","level":2},{"id":"security-review-checklist-for-terraform-projects","title":"Security Review Checklist for Terraform Projects","level":2},{"id":"a-practical-security-workflow","title":"A Practical Security Workflow","level":2},{"id":"final-thoughts","title":"Final Thoughts","level":2},{"id":"practice-questions","title":"Practice Questions","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is the difference between tfsec and Checkov?","answer":"Both scan Terraform for insecure configuration before it is applied. tfsec is Terraform-specific, fast, and has since been folded into Trivy, which is where new development happens. Checkov covers Terraform alongside CloudFormation, Kubernetes manifests, Helm charts and Dockerfiles, has a larger policy set, and supports custom policies in Python as well as YAML. Running both catches slightly different things; if you run one, Checkov's breadth usually wins for a mixed estate."},{"question":"Where in the pipeline should security scanning run?","answer":"On every pull request, before the plan is approved, that is the whole point of shifting left, since a misconfiguration caught in review costs minutes and the same one caught in production costs an incident. Scanning is fast enough to run on every commit. Add a second scan of the deployed state periodically, because policies change and a configuration that passed six months ago may not pass today."},{"question":"Should a security finding fail the build?","answer":"Fail on high and critical severity in changed code; report the rest without blocking. A policy of zero findings across an existing codebase gets disabled within a month, because the backlog blocks unrelated work and people route around it. Gating on newly introduced high-severity issues keeps the signal meaningful while the existing backlog is worked through separately."},{"question":"How do I suppress a false positive properly?","answer":"Use an inline annotation, #tfsec:ignore:aws-s3-enable-versioning or #checkov:skip=CKVAWS21:Bucket is ephemeral build cache, placed on the specific resource, with a reason. Two rules make this safe: never suppress globally when a resource-level suppression will do, and always include the justification, because an unexplained skip is indistinguishable from someone silencing a real finding. Review suppressions periodically; they accumulate."},{"question":"What are the most common security problems these tools find?","answer":"Publicly accessible S3 buckets, security groups allowing 0.0.0.0/0 on SSH or database ports, unencrypted EBS volumes and RDS instances, IAM policies granting wildcard actions or resources, missing access logging, and unencrypted data in transit. Most are one line to fix and are far cheaper to catch in a pull request than in a penetration test or a breach."},{"question":"Does scanning replace reviewing the plan?","answer":"No. Static analysis catches known misconfiguration patterns; it cannot tell you that a change will replace your production database, delete a bucket that holds data, or apply to the wrong account. Those come from reading the plan. Particularly the -/+ replacement markers. Scanning and plan review answer different questions and both belong in the pipeline."}]}],"category":"terraform","errorPages":[{"slug":"terraform-invalid-for-each-argument","title":"Error: Invalid for_each argument","description":"Fix "Invalid for_each argument": build the map keys from known inputs instead of values only known after apply. Covers data sources, modules and -target."},{"slug":"terraform-inconsistent-dependency-lock-file","title":"Error: Inconsistent dependency lock file","description":"Fix "Inconsistent dependency lock file": run terraform init, or terraform providers lock for every platform. In CI, use -lockfile=readonly instead of -upgrade."},{"slug":"terraform-provider-configuration-not-present","title":"Error: Provider configuration not present","description":"Fix "Provider configuration not present": restore the provider block and run terraform destroy, or use terraform state rm. Covers aliased providers in modules."},{"slug":"terraform-cycle-error","title":"Error: Cycle: resources depend on each other","description":"Fix Terraform "Error: Cycle": run terraform graph to see it, then break the loop with separate security group rules, remove depends_on, or split modules."}],"children":"$L52"}]
52:["$","div",null,{"className":"max-w-none","children":"$L53"}]
53:[["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"State is the concept that separates people who find Terraform predictable from people who find it mysterious. Almost every confusing Terraform situation. A resource it wants to recreate for no obvious reason, a change made in the console that gets undone, two people applying at once and breaking everything. Traces back to it."}],"\n",["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"why-state-exists","children":"Why state exists"}],"\n",["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["Terraform needs to know that ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"aws_instance.web"}]," in your configuration is instance ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"i-0abc123"}]," in AWS. Nothing in the configuration says so, and nothing in AWS records it. That mapping is what state holds."]}],"\n",["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Without it, Terraform could not tell the difference between "create this instance" and "update the instance I made last week", so every apply would create duplicates."}],"\n",["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"State does three jobs:"}],"\n",["$","div",null,{"className":"overflow-x-auto mb-4 -mx-4 sm:mx-0","children":["$","div",null,{"className":"min-w-full px-4 sm:px-0","children":["$","table",null,{"className":"min-w-full divide-y divide-slate-300 border border-slate-300 rounded-lg text-sm","children":[["$","thead",null,{"className":"bg-slate-100","children":["$","tr",null,{"children":[["$","th",null,{"className":"px-3 sm:px-6 py-2 sm:py-3 text-left text-xs sm:text-sm font-semibold text-slate-900 whitespace-nowrap","children":"Job"}],["$","th",null,{"className":"px-3 sm:px-6 py-2 sm:py-3 text-left text-xs sm:text-sm font-semibold text-slate-900 whitespace-nowrap","children":"Why it matters"}]]}]}],["$","tbody",null,{"children":[["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":["$","strong",null,{"children":"Maps config to real resources"}]}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":["Turns ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"aws_instance.web"}]," into ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"i-0abc123"}]]}]]}],["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":["$","strong",null,{"children":"Caches attributes"}]}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Plans can be produced without querying every API every time"}]]}],["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":["$","strong",null,{"children":"Records dependencies"}]}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Destroys happen in the correct order even after resources are removed from config"}]]}]]}]]}]}]}],"\n","$L54","\n","$L55","\n","$L56","\n","$L57","\n","$L58","\n","$L59","\n","$L5a","\n","$L5b","\n","$L5c","\n","$L5d","\n","$L5e","\n","$L5f","\n","$L60","\n","$L61","\n","$L62","\n","$L63","\n","$L64","\n","$L65","\n","$L66","\n","$L67","\n","$L68","\n","$L69","\n","$L6a","\n","$L6b","\n","$L6c","\n","$L6d","\n","$L6e","\n","$L6f","\n","$L70","\n","$L71","\n","$L72","\n","$L73","\n","$L74","\n","$L75","\n","$L76","\n","$L77","\n","$L78","\n","$L79","\n","$L7a","\n","$L7b","\n","$L7c","\n","$L7d","\n","$L7e","\n","$L7f","\n","$L80","\n","$L81","\n","$L82","\n","$L83","\n","$L84","\n","$L85","\n","$L86","\n","$L87","\n","$L88","\n","$L89","\n","$L8a","\n","$L8b","\n","$L8c","\n","$L8d"]
54:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"what-is-actually-in-the-file","children":"What is actually in the file"}]
55:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":[["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"terraform.tfstate"}]," is JSON: a serial number, the Terraform version, and an array of resource instances with every attribute the provider returned."]}]
56:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["That last part is the important one. ",["$","strong",null,{"children":"State contains resource attributes in plain text"}],", which includes generated passwords, private keys, and anything passed through a variable marked ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"sensitive"}],". The ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"sensitive"}]," flag redacts console output. It does nothing to state."]}]
57:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"So:"}]
58:["$","ul",null,{"className":"space-y-2 mb-4 ml-4 sm:ml-6 list-disc marker:text-teal-500","children":["\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":["$","strong",null,{"children":"Never commit state to version control."}]}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":[["$","strong",null,{"children":"Encrypt it at rest"}]," and restrict who can read it."]}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"Treat a state file as one of the most valuable artifacts in the repository."}],"\n"]}]
59:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"drift","children":"Drift"}]
5a:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Terraform refreshes state before planning, querying providers for current attributes, and compares that against what it recorded. When someone changes something in a console, the plan shows Terraform intending to change it back."}]
5b:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"That is why an untouched configuration can produce a non-empty plan. It is not a bug; it is Terraform reporting that reality and code disagree."}]
5c:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":[["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"terraform plan -refresh-only"}]," shows drift without proposing configuration changes alongside it, which is the clean way to review what has been altered out of band."]}]
5d:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"local-state-does-not-survive-contact-with-a-team","children":"Local state does not survive contact with a team"}]
5e:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["A local ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"terraform.tfstate"}]," works exactly until a second person or a CI pipeline appears. Then:"]}]
5f:["$","ul",null,{"className":"space-y-2 mb-4 ml-4 sm:ml-6 list-disc marker:text-teal-500","children":["\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"It lives on one laptop and cannot be shared."}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"There is no version history, so a bad write is unrecoverable."}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"Two people applying simultaneously produce a corrupted file."}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"Losing the machine loses the record of your infrastructure."}],"\n"]}]
60:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["A ",["$","strong",null,{"children":"remote backend"}]," solves all four."]}]
61:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"remote-backends","children":"Remote backends"}]
62:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"A backend defines where state lives and how it is locked:"}]
63:["$","pre",null,{"className":"bg-slate-950 text-emerald-400 rounded-lg sm:rounded-xl p-3 sm:p-4 overflow-x-auto mb-4 border border-slate-700 shadow-lg text-xs sm:text-sm leading-relaxed [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-inherit [&_code]:rounded-none","children":["$","code",null,{"className":"language-hcl text-sm","children":"terraform {\n backend "s3" {\n bucket = "acme-tfstate"\n key = "production/terraform.tfstate"\n region = "eu-west-1"\n encrypt = true\n use_lockfile = true\n }\n}\n"}]}]
64:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["The S3 backend with locking is the most common AWS pattern. Modern Terraform supports native S3 locking via ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"use_lockfile"}],"; older versions require a DynamoDB table referenced with ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"dynamodb_table"}],"."]}]
65:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Three things to get right:"}]
66:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":[["$","strong",null,{"children":"Enable bucket versioning."}]," This is the difference between recovering from a bad state write in minutes and rebuilding an environment."]}]
67:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":[["$","strong",null,{"children":"Separate state per environment."}]," Give each its own ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"key"}],", ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"production/terraform.tfstate"}],", ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"staging/terraform.tfstate"}],". A mistake in staging then cannot touch production state."]}]
68:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":[["$","strong",null,{"children":"Bootstrap the bucket separately."}]," The bucket must exist before ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"terraform init"}]," can use it, so it cannot be created by the configuration that stores state in it. Create it in a small separate configuration with local state, or once via the CLI."]}]
69:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Backend configuration cannot use variables, so supply differing values at init time:"}]
6a:["$","pre",null,{"className":"bg-slate-950 text-emerald-400 rounded-lg sm:rounded-xl p-3 sm:p-4 overflow-x-auto mb-4 border border-slate-700 shadow-lg text-xs sm:text-sm leading-relaxed [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-inherit [&_code]:rounded-none","children":["$","code",null,{"className":"language-bash text-sm","children":"terraform init -backend-config="key=production/terraform.tfstate"\n"}]}]
6b:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"locking","children":"Locking"}]
6c:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"When Terraform starts an apply it acquires a lock, and releases it when finished. A second apply waits or fails rather than writing over the first. This is the single most important reason to use a remote backend in a team."}]
6d:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"If a process is killed mid-apply, the lock can stick:"}]
6e:["$","pre",null,{"className":"bg-slate-950 text-emerald-400 rounded-lg sm:rounded-xl p-3 sm:p-4 overflow-x-auto mb-4 border border-slate-700 shadow-lg text-xs sm:text-sm leading-relaxed [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-inherit [&_code]:rounded-none","children":["$","code",null,{"className":"language-bash text-sm","children":"terraform force-unlock <LOCK_ID>\n"}]}]
6f:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Confirm nobody is actually running an apply first. Force-unlocking a live apply is exactly the corruption locking exists to prevent."}]
70:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"the-state-commands","children":"The state commands"}]
71:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"These edit state directly and bypass the safety of a plan. Take a backup before using them."}]
72:["$","div",null,{"className":"overflow-x-auto mb-4 -mx-4 sm:mx-0","children":["$","div",null,{"className":"min-w-full px-4 sm:px-0","children":["$","table",null,{"className":"min-w-full divide-y divide-slate-300 border border-slate-300 rounded-lg text-sm","children":[["$","thead",null,{"className":"bg-slate-100","children":["$","tr",null,{"children":[["$","th",null,{"className":"px-3 sm:px-6 py-2 sm:py-3 text-left text-xs sm:text-sm font-semibold text-slate-900 whitespace-nowrap","children":"Command"}],["$","th",null,{"className":"px-3 sm:px-6 py-2 sm:py-3 text-left text-xs sm:text-sm font-semibold text-slate-900 whitespace-nowrap","children":"Use"}]]}]}],["$","tbody",null,{"children":[["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"terraform state list"}]}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"What does Terraform manage? Read-only, always safe"}]]}],["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"terraform state show <addr>"}]}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"What does Terraform think this looks like? Read-only"}]]}],["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"terraform state mv"}]}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Rename or move without destroying"}]]}],["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"terraform state rm"}]}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Stop managing something without destroying it"}]]}],["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"terraform import"}]}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Adopt existing infrastructure"}]]}]]}]]}]}]}]
73:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"The two read-only ones answer most confusing plans in seconds. Reach for them before anything else."}]
74:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["For renames and moves, prefer ",["$","strong",null,{"children":[["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"moved"}]," blocks"]}]," in configuration over ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"state mv"}],". They are declarative, reviewable in a pull request, and applied by a normal plan:"]}]
75:["$","pre",null,{"className":"bg-slate-950 text-emerald-400 rounded-lg sm:rounded-xl p-3 sm:p-4 overflow-x-auto mb-4 border border-slate-700 shadow-lg text-xs sm:text-sm leading-relaxed [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-inherit [&_code]:rounded-none","children":["$","code",null,{"className":"language-hcl text-sm","children":"moved {\n from = aws_instance.web\n to = module.compute.aws_instance.web\n}\n"}]}]
76:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"what-to-do-if-state-is-lost","children":"What to do if state is lost"}]
77:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Restore from backend versioning, which is why it should be enabled before you need it. Failing that, the recovery path is importing every resource back into a fresh state. Slow and error-prone for anything non-trivial."}]
78:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["If state contains a resource that no longer exists, ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"terraform state rm"}]," brings it back in line. If real infrastructure exists that state does not know about, ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"terraform import"}]," adopts it."]}]
79:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"explore-further-in-this-section","children":"Explore further in this section"}]
7a:["$","div",null,{"className":"overflow-x-auto mb-4 -mx-4 sm:mx-0","children":["$","div",null,{"className":"min-w-full px-4 sm:px-0","children":["$","table",null,{"className":"min-w-full divide-y divide-slate-300 border border-slate-300 rounded-lg text-sm","children":[["$","thead",null,{"className":"bg-slate-100","children":["$","tr",null,{"children":[["$","th",null,{"className":"px-3 sm:px-6 py-2 sm:py-3 text-left text-xs sm:text-sm font-semibold text-slate-900 whitespace-nowrap","children":"Sub-topic"}],["$","th",null,{"className":"px-3 sm:px-6 py-2 sm:py-3 text-left text-xs sm:text-sm font-semibold text-slate-900 whitespace-nowrap","children":"What you will learn"}]]}]}],["$","tbody",null,{"children":[["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":["$","a",null,{"className":"text-teal-600 hover:text-teal-700 underline decoration-teal-300 hover:decoration-teal-500 transition-colors break-words","href":"/tutorials/terraform/terraform-state-overview","children":"State Overview"}]}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"What state contains, drift, and security concerns"}]]}],["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":["$","a",null,{"className":"text-teal-600 hover:text-teal-700 underline decoration-teal-300 hover:decoration-teal-500 transition-colors break-words","href":"/tutorials/terraform/terraform-remote-backends","children":"Remote Backends"}]}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"S3 and locking setup, migration, bootstrapping"}]]}],["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":["$","a",null,{"className":"text-teal-600 hover:text-teal-700 underline decoration-teal-300 hover:decoration-teal-500 transition-colors break-words","href":"/tutorials/terraform/terraform-state-commands","children":"State Commands"}]}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"mv, rm, import and refactoring safely"}]]}]]}]]}]}]}]
7b:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"quick-reference","children":"Quick reference"}]
7c:["$","div",null,{"className":"overflow-x-auto mb-4 -mx-4 sm:mx-0","children":["$","div",null,{"className":"min-w-full px-4 sm:px-0","children":["$","table",null,{"className":"min-w-full divide-y divide-slate-300 border border-slate-300 rounded-lg text-sm","children":[["$","thead",null,{"className":"bg-slate-100","children":["$","tr",null,{"children":[["$","th",null,{"className":"px-3 sm:px-6 py-2 sm:py-3 text-left text-xs sm:text-sm font-semibold text-slate-900 whitespace-nowrap","children":"Task"}],["$","th",null,{"className":"px-3 sm:px-6 py-2 sm:py-3 text-left text-xs sm:text-sm font-semibold text-slate-900 whitespace-nowrap","children":"Command"}]]}]}],["$","tbody",null,{"children":[["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"List managed resources"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"terraform state list"}]}]]}],["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Inspect one"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"terraform state show <address>"}]}]]}],["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Review drift only"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"terraform plan -refresh-only"}]}]]}],["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Rename without recreating"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"terraform state mv <old> <new>"}]}]]}],["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Stop managing"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"terraform state rm <address>"}]}]]}],["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Adopt existing infra"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"terraform import <address> <id>"}]}]]}],["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Clear a stuck lock"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"$L8e"}]]}],"$L8f"]}]]}]}]}]
7d:["$","hr",null,{}]
7e:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"final-thoughts","children":"Final Thoughts"}]
7f:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"State is not an implementation detail you can ignore. It is the thing Terraform reasons from, and understanding it converts most Terraform confusion into something obvious. Set up a remote backend with locking and versioning before you need it, keep environments in separate state files, and never let state into version control."}]
80:["$","hr",null,{}]
81:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"frequently-asked-questions","children":"Frequently Asked Questions"}]
82:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["$","strong",null,{"children":[["$","a",null,{"className":"text-teal-600 hover:text-teal-700 underline decoration-teal-300 hover:decoration-teal-500 transition-colors break-words","href":"/tutorials/terraform/introduction-to-terraform","children":"What is Terraform"}]," state and why can't Terraform work without it?"]}]}]
83:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["State is Terraform's record of which real-world resources correspond to which configuration blocks. Nothing in your code or in the cloud provider records that ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"aws_instance.web"}]," is ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"i-0abc123"}],". State does. Without it Terraform cannot distinguish creating a resource from updating one it made previously, so every apply would create duplicates. State also caches attributes so plans are fast, and records dependencies so destroys happen in the right order."]}]
84:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["$","strong",null,{"children":["Is it safe to commit the ",["$","a",null,{"className":"text-teal-600 hover:text-teal-700 underline decoration-teal-300 hover:decoration-teal-500 transition-colors break-words","href":"/tutorials/terraform/terraform-state-overview","children":"Terraform state"}]," file to ",["$","a",null,{"className":"text-teal-600 hover:text-teal-700 underline decoration-teal-300 hover:decoration-teal-500 transition-colors break-words","href":"/tutorials/git/introduction-to-git","children":"Git"}],"?"]}]}]
85:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["No. State stores resource attributes in plain text, including generated passwords, private keys and anything passed through a variable marked ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"sensitive"}],", that flag only affects console output. Committing it exposes secrets to everyone with repository access and to Git history permanently. It also does not solve the problems a backend solves, since Git provides no locking, so two people can still apply simultaneously. Use a remote backend with encryption and restricted access."]}]
86:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["$","strong",null,{"children":["What is a ",["$","a",null,{"className":"text-teal-600 hover:text-teal-700 underline decoration-teal-300 hover:decoration-teal-500 transition-colors break-words","href":"/tutorials/terraform/terraform-remote-backends","children":"Terraform backend"}],"?"]}]}]
87:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["The configuration that tells Terraform where to store state and how to lock it. The default is ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"local"}],", writing ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"terraform.tfstate"}]," to the working directory. Remote backends such as S3, Azure Blob Storage, Google Cloud Storage or Terraform Cloud store it centrally so it can be shared, versioned and locked. Switching is a matter of adding a ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"backend"}]," block and running ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"terraform init"}],", which offers to migrate the existing state."]}]
88:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["$","strong",null,{"children":"Why does state locking matter?"}]}]
89:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Because two simultaneous applies writing to the same state file will corrupt it, leaving Terraform with an inaccurate record of your infrastructure, which is considerably harder to fix than the original problem. Locking makes the second run wait or fail instead. It becomes essential the moment more than one person or a CI pipeline can apply, and it is the main reason local state does not scale beyond one developer."}]
8a:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["$","strong",null,{"children":"How do I separate state between dev, staging and production?"}]}]
8b:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["Give each environment its own state file, typically a different ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"key"}]," in the same backend bucket. This means a mistake in one cannot corrupt another's state, environments can be applied independently, and credentials can be scoped per environment. ",["$","a",null,{"className":"text-teal-600 hover:text-teal-700 underline decoration-teal-300 hover:decoration-teal-500 transition-colors break-words","href":"/tutorials/terraform/terraform-workspaces","children":"Terraform workspaces"}]," offer a lighter alternative, but they share one backend and one set of credentials, which is why separate state files, and ideally separate directories, are preferred for production."]}]
8c:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["$","strong",null,{"children":["What should I do if ",["$","a",null,{"className":"text-teal-600 hover:text-teal-700 underline decoration-teal-300 hover:decoration-teal-500 transition-colors break-words","href":"/tutorials/terraform/terraform-plan","children":"terraform plan"}]," shows changes I did not make?"]}]}]
8d:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["That is drift: someone changed the infrastructure outside Terraform, and the plan is proposing to bring it back in line with your configuration. Run ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"terraform plan -refresh-only"}]," to review what changed without mixing it with configuration changes. Then decide whether to accept the manual change by updating your code, or let Terraform revert it. Leaving drift unresolved is how teams stop trusting Terraform, and a tool people distrust gets bypassed."]}]
8e:["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"terraform force-unlock <LOCK_ID>"}]
8f:["$","tr",null,{"children":[["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":"Migrate backends"}],["$","td",null,{"className":"px-3 sm:px-6 py-2 sm:py-4 text-xs sm:text-sm text-slate-700 border-t border-slate-200","children":["edit ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"backend"}]," block, then ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"terraform init"}]]}]]}]
28:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
26:null
2a:[["$","title","0",{"children":"Terraform State and Backends: Remote State and Locking"}],["$","meta","1",{"name":"description","content":"Understand why Terraform keeps state, what is inside it, how remote backends and locking make teamwork safe, and the state commands that rescue you."}],["$","link","2",{"rel":"author","href":"https://devopslesson.com"}],["$","meta","3",{"name":"author","content":"DevOpsLesson Team"}],["$","meta","4",{"name":"keywords","content":"terraform state, terraform backend, remote state, state locking, terraform state file, s3 backend, terraform drift, state security, terraform state and backends, terraform state overview, terraform remote backend, terraform state commands, terraform s3 backend, terraform tutorial for beginners, terraform tutorial, learn terraform, terraform for beginners, infrastructure as code tutorial, terraform hcl, terraform state, terraform modules, terraform variables, terraform aws, iac tutorial, terraform beginner guide, hashicorp terraform"}],["$","meta","5",{"name":"creator","content":"DevOpsLesson"}],["$","meta","6",{"name":"publisher","content":"DevOpsLesson"}],["$","meta","7",{"name":"robots","content":"index, follow"}],["$","meta","8",{"name":"googlebot","content":"index, follow, max-video-preview:-1, max-image-preview:large, max-snippet:-1"}],["$","link","9",{"rel":"canonical","href":"https://devopslesson.com/tutorials/terraform/terraform-state-and-backends"}],["$","meta","10",{"name":"google-site-verification","content":"v1hGowZeYvsOScm3hhGtqYv7ZG_yiFG-YbjDK"}],["$","meta","11",{"property":"og:title","content":"Terraform State and Backends"}],["$","meta","12",{"property":"og:description","content":"Understand why Terraform keeps state, what is inside it, how remote backends and locking make teamwork safe, and the state commands that rescue you."}],["$","meta","13",{"property":"og:url","content":"https://devopslesson.com/tutorials/terraform/terraform-state-and-backends"}],["$","meta","14",{"property":"og:site_name","content":"DevOpsLesson"}],["$","meta","15",{"property":"og:image","content":"https://devopslesson.com/api/og?title=Terraform+State+and+Backends&eyebrow=Terraform+Tutorial"}],["$","meta","16",{"property":"og:image:width","content":"1200"}],["$","meta","17",{"property":"og:image:height","content":"630"}],["$","meta","18",{"property":"og:image:alt","content":"Terraform State and Backends"}],["$","meta","19",{"property":"og:type","content":"article"}],["$","meta","20",{"property":"article:published_time","content":"2026-06-11"}],["$","meta","21",{"property":"article:modified_time","content":"2026-09-13"}],["$","meta","22",{"property":"article:author","content":"Vishvesh Patel"}],["$","meta","23",{"property":"article:tag","content":"terraform state"}],["$","meta","24",{"property":"article:tag","content":"terraform backend"}],["$","meta","25",{"property":"article:tag","content":"remote state"}],["$","meta","26",{"property":"article:tag","content":"state locking"}],["$","meta","27",{"property":"article:tag","content":"terraform state file"}],["$","meta","28",{"property":"article:tag","content":"s3 backend"}],["$","meta","29",{"property":"article:tag","content":"terraform drift"}],["$","meta","30",{"property":"article:tag","content":"state security"}],["$","meta","31",{"property":"article:tag","content":"terraform state and backends"}],["$","meta","32",{"property":"article:tag","content":"terraform state overview"}],["$","meta","33",{"property":"article:tag","content":"terraform remote backend"}],["$","meta","34",{"property":"article:tag","content":"terraform state commands"}],["$","meta","35",{"property":"article:tag","content":"terraform s3 backend"}],["$","meta","36",{"property":"article:tag","content":"terraform tutorial for beginners"}],["$","meta","37",{"name":"twitter:card","content":"summary_large_image"}],["$","meta","38",{"name":"twitter:title","content":"Terraform State and Backends"}],["$","meta","39",{"name":"twitter:description","content":"Understand why Terraform keeps state, what is inside it, how remote backends and locking make teamwork safe, and the state commands that rescue you."}],"$L90","$L91","$L92","$L93","$L94","$L95"]
96:I[86869,[],"IconMark"]
90:["$","meta","40",{"name":"twitter:image","content":"https://devopslesson.com/api/og?title=Terraform+State+and+Backends&eyebrow=Terraform+Tutorial"}]
91:["$","link","41",{"rel":"shortcut icon","href":"/favicon.svg"}]
92:["$","link","42",{"rel":"icon","href":"/icon","type":"image/png","sizes":"32x32"}]
93:["$","link","43",{"rel":"icon","href":"/favicon.svg","type":"image/svg+xml"}]
94:["$","link","44",{"rel":"apple-touch-icon","href":"/icon","sizes":"180x180","type":"image/png"}]
95:["$","$L96","45",{}
Learn what Terraform is and how Infrastructure as Code works, why it beats clicking through consoles, and how it compares to Ansible and CloudFormation.
Install Terraform on macOS, Linux and Windows with the official package managers, verify the version, switch versions with tfenv and run your first project.
Understand the Terraform init, plan, apply and destroy loop: what each command does, how to read a plan and the mistakes that cause outages.