Terraform Cheatsheet
Resources
Use this Terraform reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Resource Block Syntax
resource "<TYPE>" "<LOCAL_NAME>" { argument = value }
<TYPE> is the provider prefix plus the object kind (aws_instance), and it determines which provider handles the block. <LOCAL_NAME> is yours to choose and only ever used inside Terraform. Together they form the address aws_instance.web.
Terraform builds its dependency graph from attribute references, so an explicit depends_on is rarely needed:
resource "aws_instance" "web" { subnet_id = aws_subnet.public.id # implicit dependency } # <type>.<name>.<attribute>, and (known after apply) values are fine output "public_ip" { value = aws_instance.web.public_ip }
Every resource also accepts count, for_each, depends_on, provider and lifecycle. Those are covered in the Meta-Arguments and Lifecycle topic, along with dynamic blocks.
Common AWS Resources
# EC2 instance resource "aws_instance" "web" { ami = data.aws_ami.al2023.id instance_type = "t3.micro" subnet_id = aws_subnet.public.id vpc_security_group_ids = [aws_security_group.web.id] key_name = aws_key_pair.deployer.key_name tags = { Name = "web-server" } } # S3 bucket: modern providers split every setting into its own resource resource "aws_s3_bucket" "assets" { bucket = "myapp-assets-${random_id.suffix.hex}" } resource "aws_s3_bucket_versioning" "assets" { bucket = aws_s3_bucket.assets.id versioning_configuration { status = "Enabled" } } resource "aws_s3_bucket_public_access_block" "assets" { bucket = aws_s3_bucket.assets.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true } # Security group resource "aws_security_group" "web" { name = "web-sg" vpc_id = aws_vpc.main.id ingress { from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } }
Inline
ingress/egressblocks and separateaws_vpc_security_group_ingress_ruleresources fight each other. Pick one style per security group.
Sensitive Values
resource "aws_db_instance" "main" { identifier = "myapp-db" engine = "postgres" engine_version = "17" instance_class = "db.t3.medium" allocated_storage = 20 db_name = "myapp" username = var.db_user password = var.db_password # marked sensitive at the variable skip_final_snapshot = false }
What sensitive does and does not do:
- Providers mark some attributes sensitive themselves. Terraform then redacts them from plan and apply output and refuses to print them.
- Sensitivity is contagious. Any expression touching a sensitive value becomes sensitive, which is why an innocent-looking
tagsmap can suddenly print as(sensitive value). - It is a display control, not encryption. Sensitive values are written to state and to plan files in plaintext.
nonsensitive(x)strips the marker when you are certain the result is safe. Use it sparingly.
The only way to keep a secret out of state is to make it ephemeral.
Ephemeral Resources and Write-Only Arguments
An ephemeral block (1.10+) produces a value Terraform re-reads every phase and never persists to state or a plan file. A write-only argument (1.11+) accepts such a value on a normal resource and discards it after the API call.
ephemeral "random_password" "db" { length = 20 override_special = "!#%&*()-_=+[]{}<>:?" } resource "aws_db_instance" "example" { engine = "postgres" instance_class = "db.t3.micro" allocated_storage = 5 username = "example" skip_final_snapshot = true password_wo = ephemeral.random_password.db.result password_wo_version = 1 # bump this to push a new value }
- Write-only arguments are named by the provider, conventionally
<attr>_wo, paired with<attr>_wo_version. Terraform sends the value only when the version changes. - Ephemeral values may be used in provider config, provisioners, other ephemeral resources, write-only arguments, and
ephemeral = truevariables and outputs. They may not be used in ordinary resource arguments, because that would persist them. ephemeral "aws_secretsmanager_secret_version"is the usual way to read a secret at apply time without dragging it into state.
action Blocks (1.14+)
Actions are provider-defined operations that fall outside create, read, update, delete. Invoking one changes no state.
action "aws_lambda_invoke" "warm" { config { function_name = aws_lambda_function.api.function_name payload = jsonencode({ warm = true }) } } resource "aws_lambda_function" "api" { # ... lifecycle { action_trigger { events = [after_create, after_update] actions = [action.aws_lambda_invoke.warm] on_failure = halt # halt (default) | continue | taint, 1.16+ } } }
terraform plan -invoke=action.aws_lambda_invoke.warm # plan just the action terraform apply -invoke=action.aws_lambda_invoke.warm # run just the action
action blocks accept config, count, for_each and provider. Inside config, the caller symbol holds the triggering resource object, so one action block can serve many resource instances. Available actions are entirely up to the provider, so check its registry docs.
List Resources and terraform query (1.14+)
List Resources search live infrastructure for objects Terraform does not manage. They live in *.tfquery.hcl files, kept separate from your configuration so a query never becomes part of a plan.
# unmanaged.tfquery.hcl list "aws_instance" "prod" { provider = aws limit = 50 # default 100 config { region = "us-east-2" filter { name = "tag:Name" values = ["prod-*", "staging-*"] } } }
terraform query # print matches terraform query -json # machine-readable terraform query -generate-config-out=found.tf # import + resource blocks terraform query -var 'env=prod' # query files take variables
Results are addressed list.<type>.<label>. Add include_resource = true to fetch full resource state per match, at a performance cost. See the Import and Refactoring topic for using the generated blocks.
Timeouts, Provisioners and terraform_data
resource "aws_db_instance" "main" { # ... timeouts { create = "60m" update = "30m" delete = "20m" } }
timeouts is provider-defined, so which keys exist depends on the resource. It is not a meta-argument.
# terraform_data: a built-in resource, no extra provider needed (1.4+) resource "terraform_data" "deploy" { triggers_replace = [aws_instance.web.id] # replace when this changes input = var.release_tag # tracked, readable as .output provisioner "local-exec" { command = "./deploy.sh ${self.output}" } }
Prefer terraform_data over null_resource, which needs hashicorp/null. Since 1.16 it also supports a store block that can hold ephemeral and sensitive values across plan and apply.
resource "aws_instance" "web" { ami = var.ami_id instance_type = "t3.micro" provisioner "remote-exec" { inline = ["sudo apt-get install -y nginx"] connection { type = "ssh" user = "ubuntu" private_key = file("~/.ssh/id_rsa") host = self.public_ip } } }
Provisioners are a last resort, and HashiCorp says so in its own docs. They run only on create (or on destroy with
when = destroy), never on update, they are invisible toplan, and a failure taints the resource. Useuser_data, cloud-init, a baked image, or a configuration management tool instead.