Overview and What You Will Learn
Before Terraform, creating an EC2 instance at a company like Razorpay meant logging into the AWS console, clicking through seven different screens, filling in a form, and hoping the engineer who did it last time left a wiki page with the exact settings. The next time you needed the same instance — for a new environment, a disaster recovery test, a colleague who just joined — you started from scratch. You got slightly different results every time. You could not audit what changed. You could not reproduce it reliably.
Terraform replaces all of that with a text file. You describe the infrastructure you want in plain, readable code. Terraform figures out how to create it, in the right order, using the cloud provider's APIs. Every setting is visible in the file. Every change is a pull request. Every environment is reproducible in minutes.
In this lab you will build real AWS infrastructure from a blank directory using the exact workflow engineering teams use at production scale. By the end you will have:
- Installed Terraform and configured AWS authentication
- Written a complete multi-file Terraform configuration with providers, resources, variables, and outputs
- Used
terraform init,plan, andapplyto create real AWS resources - Read and understood plan output — the most critical skill for working safely with Terraform
- Made a live change and watched Terraform apply only what changed
- Cleaned up all resources with
terraform destroy
Why This Matters in Production
At Razorpay, every engineer who wants to create infrastructure opens a pull request with a Terraform configuration. Another engineer reviews the terraform plan output posted as a PR comment. If it looks right, they approve. The CI/CD pipeline applies. The infrastructure exists — and the code that created it is in Git history forever.
This workflow gives the team:
- Reproducibility — any environment can be recreated from code in minutes, not days
- Auditability — every infrastructure change has a commit, an author, a reviewer, and a timestamp
- Safety — nobody can create infrastructure without another engineer seeing exactly what it does
- Consistency — dev, staging, and prod are created from the same code with different variable values
Teams that adopt this workflow reduce environment setup time from days to under an hour and eliminate entire classes of production incidents caused by configuration drift between environments.
Core Principles
The Terraform Workflow
+------------------------------------------+| Step 1 — Write (.tf files) || Describe what you want: || resource "aws_s3_bucket" "orders" { || bucket = "swiggy-orders-prod" || } |+------------------------------------------+ | v+------------------------------------------+| Step 2 — Init || terraform init || Downloads: hashicorp/aws v5.31.0 || Sets up state backend |+------------------------------------------+ | v+------------------------------------------+| Step 3 — Plan (safe preview) || terraform plan || + aws_s3_bucket.orders will be created || Plan: 1 to add, 0 to change, 0 to destroy|+------------------------------------------+ | v+------------------------------------------+| Step 4 — Apply (make it real) || terraform apply || aws_s3_bucket.orders: Creating... || aws_s3_bucket.orders: Creation complete |+------------------------------------------+ | v+------------------------------------------+| Step 5 — State (Terraform remembers) || terraform.tfstate records: || id = "swiggy-orders-prod" || arn = "arn:aws:s3:::swiggy-orders-prod" |+------------------------------------------+How Terraform Talks to AWS
+------------------------------------------+| Your .tf files || HCL describing what you want |+------------------------------------------+ | v+------------------------------------------+| Terraform Core || Reads config, builds dependency graph || Decides creation order automatically |+------------------------------------------+ | v+------------------------------------------+| AWS Provider Plugin || Knows how to call every AWS API || Handles auth, retries, error messages |+------------------------------------------+ | v+------------------------------------------+| AWS APIs (HTTPS) || EC2 API, S3 API, IAM API, RDS API... |+------------------------------------------+ | v+------------------------------------------+| Real AWS Resources || EC2 instances, S3 buckets, security || groups running in ap-south-1 |+------------------------------------------+Detailed Step-by-Step Practical Lab
Prerequisites
Before starting this lab you need:
- An AWS account with IAM user or IAM role access
- IAM permissions: EC2 full access, S3 full access (for this lab)
- A terminal — macOS Terminal, Linux shell, or WSL on Windows
- Git installed
Step 1 — Install Terraform
Use tfenv to manage Terraform versions. It reads a .terraform-version file from your project and automatically switches to the correct version — the same way nvm works for Node.js.
# ── macOS ──────────────────────────────────────────────────────────────────brew install tfenv # ── Linux ──────────────────────────────────────────────────────────────────git clone https://github.com/tfutils/tfenv.git ~/.tfenvecho 'export PATH="$HOME/.tfenv/bin:$PATH"' >> ~/.bashrcsource ~/.bashrc # ── Install and activate Terraform 1.6.3 ──────────────────────────────────tfenv install 1.6.3tfenv use 1.6.3 # ── Verify the installation ────────────────────────────────────────────────terraform version# Terraform v1.6.3# on linux_amd64 # ── Pin the version for your project ──────────────────────────────────────# tfenv reads this file and auto-switches when you cd into the directoryecho "1.6.3" > .terraform-versionPLACEMENT PRO TIP**Tip:** If you cannot use tfenv, download Terraform directly from https://developer.hashicorp.com/terraform/downloads and place the binary in your PATH. Always use the same version as your team — version mismatches cause state file compatibility issues.
Step 2 — Configure AWS Authentication
Terraform uses your AWS credentials to call the AWS APIs. Never put credentials in .tf files — they end up in Git history and get leaked.
# ── Option 1: Environment variables (recommended for local development) ───export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"export AWS_DEFAULT_REGION="ap-south-1" # Mumbai # Verify credentials work before running Terraformaws sts get-caller-identity# {# "UserId": "AIDAIOSFODNN7EXAMPLE",# "Account": "123456789012",# "Arn": "arn:aws:iam::123456789012:user/terraform-user"# } # ── Option 2: AWS named profile (multiple accounts) ────────────────────────aws configure --profile razorpay-dev# AWS Access Key ID: AKIAIOSFODNN7EXAMPLE# AWS Secret Access Key: xxxxxxxxxxxxxxxxxxxx# Default region name: ap-south-1# Default output format: json export AWS_PROFILE="razorpay-dev"# Terraform will now use this profile automaticallyStep 3 — Create Your Project Directory
A well-organised Terraform directory separates concerns into different files. Terraform loads all .tf files in a directory together, so the separation is purely for readability.
mkdir terraform-first-lab && cd terraform-first-lab # Standard Terraform file layout — each file has one jobtouch versions.tf # Terraform version and provider version requirementstouch provider.tf # Provider configuration (region, default tags)touch variables.tf # Input variable declarationstouch main.tf # Resource declarations — the actual infrastructuretouch outputs.tf # Output value declarations # Pin the Terraform version for the whole teamecho "1.6.3" > .terraform-version # Create .gitignore immediately — before any applycat > .gitignore << 'EOF'# State files — NEVER commit to Git (contain sensitive values in plaintext)terraform.tfstateterraform.tfstate.backup*.tfstate*.tfstate.backup # Working directory — regenerated by terraform init, never commit.terraform/ # Variable files that may contain secrets*.auto.tfvars # Keep terraform.tfvars in Git only if it has no secrets# Always commit terraform.tfvars.example as documentation # Crash logscrash.logcrash.*.logEOFStep 4 — Declare Terraform and Provider Requirements
# versions.tf# This file tells Terraform three things:# 1. What minimum version of Terraform CLI is required# 2. Which provider plugins to download and from where# 3. Where to store state (the backend — add this when working with a team) terraform { # Reject older Terraform versions that may not support features we use required_version = ">= 1.6.0" required_providers { aws = { source = "hashicorp/aws" # download from registry.terraform.io/hashicorp/aws version = "~> 5.0" # any 5.x version (>= 5.0.0, < 6.0.0) # ~> allows patch updates, blocks major version jumps } } # Add this backend block before your second engineer joins the project # backend "s3" { # bucket = "razorpay-terraform-state" # key = "dev/first-lab/terraform.tfstate" # region = "ap-south-1" # dynamodb_table = "terraform-state-lock" # encrypt = true # }}# provider.tf# Configures the AWS provider — tells it which region to use# and how to authenticate (reads from environment variables automatically) provider "aws" { region = var.aws_region # use a variable, not a hardcoded string # default_tags applies these tags to EVERY resource this provider creates # Saves adding tags = {} on every single resource block default_tags { tags = { ManagedBy = "terraform" Repository = "github.com/razorpay/terraform-first-lab" Environment = var.environment } }}Step 5 — Declare Input Variables
Variables make your configuration reusable across environments. Instead of hardcoding "ap-south-1" in ten places, declare it once as a variable and reference it everywhere with var.aws_region.
# variables.tf variable "aws_region" { description = "AWS region to deploy infrastructure into" type = string default = "ap-south-1" # Mumbai — change to your nearest region # Validation catches typos before Terraform makes any API calls validation { condition = can(regex("^[a-z]{2}-[a-z]+-[0-9]$", var.aws_region)) error_message = "Must be a valid AWS region code like ap-south-1 or us-east-1." }} variable "environment" { description = "Deployment environment — used in resource names and tags" type = string default = "dev" validation { condition = contains(["dev", "staging", "prod"], var.environment) error_message = "Environment must be dev, staging, or prod." }} variable "project" { description = "Project name — used as prefix in all resource names and tags" type = string default = "razorpay-api"} variable "instance_type" { description = "EC2 instance type for the web server" type = string default = "t3.micro" # smallest — appropriate for a learning lab}Create a terraform.tfvars file to set values for this environment:
# terraform.tfvars — auto-loaded by Terraform on every plan and apply# Override variable defaults here for the dev environment# Do NOT put secrets (passwords, API keys) in this file aws_region = "ap-south-1"environment = "dev"project = "razorpay-api"instance_type = "t3.micro"Step 6 — Write Your Infrastructure Resources
# main.tf# All AWS resources go here.# Resources can be written in any order — Terraform builds the dependency# graph automatically from references between resources. # ── Locals: computed values used throughout this configuration ────────────locals { # Single source of truth for the naming prefix # razorpay-api-dev — every resource name starts with this name_prefix = "${var.project}-${var.environment}" # Extra tags merged onto resources alongside provider default_tags common_tags = { Project = var.project }} # ── Data Source: latest Amazon Linux 2 AMI ────────────────────────────────# Data sources READ existing information — they never create anything# This finds the current Amazon Linux 2 AMI in your region automatically# The AMI ID changes whenever Amazon releases a security patchdata "aws_ami" "amazon_linux" { most_recent = true # get newest matching AMI owners = ["amazon"] # official Amazon AMIs only — not community filter { name = "name" values = ["amzn2-ami-hvm-*-x86_64-gp2"] # Amazon Linux 2 naming pattern } filter { name = "virtualization-type" values = ["hvm"] # hardware virtual machine — required for t3 instances }} # ── Data Source: current AWS account ID ───────────────────────────────────# S3 bucket names are globally unique across all AWS accounts# Including the account ID in the name guarantees no naming conflictsdata "aws_caller_identity" "current" {} # ── S3 Bucket: application asset storage ──────────────────────────────────resource "aws_s3_bucket" "app_assets" { # Globally unique name — account ID suffix eliminates collision risk bucket = "${local.name_prefix}-assets-${data.aws_caller_identity.current.account_id}" tags = local.common_tags} # Block all public access — application data is never public# AWS exposes this as a separate API from the bucket itself, so it is a# separate resource blockresource "aws_s3_bucket_public_access_block" "app_assets" { bucket = aws_s3_bucket.app_assets.id # reference creates implicit dependency # Terraform will create the bucket first block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true} # Enable versioning — recover accidentally deleted or overwritten objectsresource "aws_s3_bucket_versioning" "app_assets" { bucket = aws_s3_bucket.app_assets.id versioning_configuration { status = "Enabled" }} # Enable server-side encryption — all objects encrypted at rest with AES-256resource "aws_s3_bucket_server_side_encryption_configuration" "app_assets" { bucket = aws_s3_bucket.app_assets.id rule { apply_server_side_encryption_by_default { sse_algorithm = "AES256" # SSE-S3 — free, no extra KMS charges } }} # ── Security Group: control network access to the EC2 instance ────────────resource "aws_security_group" "web" { name = "${local.name_prefix}-web-sg" description = "Security group for the ${var.environment} web server" # Allow HTTPS inbound from anywhere ingress { description = "HTTPS from internet" from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } # Allow HTTP inbound — for redirect to HTTPS ingress { description = "HTTP from internet — redirect to HTTPS" from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } # Allow all outbound — instance needs internet access to download packages egress { description = "All outbound traffic" from_port = 0 to_port = 0 protocol = "-1" # -1 means all protocols cidr_blocks = ["0.0.0.0/0"] } tags = local.common_tags} # ── EC2 Instance: the web server ──────────────────────────────────────────resource "aws_instance" "web" { # AMI from the data source — always the latest patched Amazon Linux 2 ami = data.aws_ami.amazon_linux.id # Instance type from the variable — t3.micro for dev, larger for prod instance_type = var.instance_type # Terraform creates the security group first because we reference its .id vpc_security_group_ids = [aws_security_group.web.id] # user_data: shell script that runs once when the instance first boots user_data = <<-EOF #!/bin/bash yum update -y # Install nginx web server amazon-linux-extras install nginx1 -y systemctl start nginx systemctl enable nginx # Write a simple health check response page echo "<h1>${var.project} — ${var.environment}</h1>" > /usr/share/nginx/html/index.html EOF # Require IMDSv2 — blocks SSRF attacks that steal instance credentials metadata_options { http_endpoint = "enabled" http_tokens = "required" http_put_response_hop_limit = 1 } tags = merge(local.common_tags, { Name = "${local.name_prefix}-web-server" })}Step 7 — Declare Outputs
Outputs print useful values after apply and make attributes available to other Terraform configurations.
# outputs.tf output "s3_bucket_name" { description = "S3 bucket name — set as APP_ASSETS_BUCKET env var in your application" value = aws_s3_bucket.app_assets.id} output "s3_bucket_arn" { description = "S3 bucket ARN — use in IAM policy resource fields" value = aws_s3_bucket.app_assets.arn} output "instance_id" { description = "EC2 instance ID" value = aws_instance.web.id} output "instance_public_ip" { description = "Public IP address of the web server" value = aws_instance.web.public_ip} output "instance_public_dns" { description = "Public DNS name of the web server" value = aws_instance.web.public_dns} output "ami_id_used" { description = "AMI ID selected by the data source — useful for auditing" value = data.aws_ami.amazon_linux.id}Step 8 — terraform init: Download Providers
Run terraform init from the project directory. This downloads the AWS provider plugin, sets up the backend, and creates the lock file.
terraform init # Initializing the backend...## Initializing provider plugins...# - Finding hashicorp/aws versions matching "~> 5.0"...# - Installing hashicorp/aws v5.31.0...# - Installed hashicorp/aws v5.31.0 (signed by HashiCorp)## Terraform has created a lock file .terraform.lock.hcl to record the provider# selections it made above. Include this file in your version control repository# so that Terraform can guarantee to make the same selections by default when# you run "terraform init" in the future.## Terraform has been successfully initialized! # Commit the lock file — never commit the .terraform/ directorygit add .terraform.lock.hclREMEMBER THIS**Remember:** Commit `.terraform.lock.hcl` to Git. This file records the exact provider version and cryptographic hash — it ensures every engineer and CI/CD pipeline downloads the identical provider binary. Without it, a silent provider upgrade could change behaviour between teammates.
Step 9 — terraform validate: Check for Errors Before Plan
Validate checks syntax and internal consistency without making any API calls. Run it before every plan.
terraform validate# Success! The configuration is valid. # Common errors you will see as you learn:# Error: Unsupported argument# An argument named "buckett" is not expected here. Did you mean "bucket"?## Error: Reference to undeclared resource# A resource "aws_s3_bucket" "app_aseets" has not been declared.Step 10 — terraform fmt: Format Your Code
terraform fmt normalises all .tf files to the canonical Terraform style — consistent indentation, aligned equals signs, sorted block arguments.
# Format all .tf files in placeterraform fmt # See what would change without modifying filesterraform fmt -diff # Use in CI to reject unformatted code — exits non-zero if any file needs formattingterraform fmt -checkStep 11 — terraform plan: Preview Every Change Before It Happens
This is the most important step. Read every line of the plan before applying. Nothing changes when you run plan — it is entirely read-only.
terraform plan # Terraform will perform the following actions:## # aws_instance.web will be created# + resource "aws_instance" "web" {# + ami = "ami-0f5ee92e2d63afc18" <- from data source# + instance_type = "t3.micro"# + id = (known after apply) <- AWS assigns this# + public_ip = (known after apply)# + vpc_security_group_ids = (known after apply) <- after SG is created# + tags = {# + "Environment" = "dev"# + "ManagedBy" = "terraform"# + "Name" = "razorpay-api-dev-web-server"# + "Project" = "razorpay-api"# }# }## # aws_s3_bucket.app_assets will be created# + resource "aws_s3_bucket" "app_assets" {# + bucket = "razorpay-api-dev-assets-123456789012"# + id = (known after apply)# + arn = (known after apply)# }## # aws_security_group.web will be created# + resource "aws_security_group" "web" { ... }## Plan: 6 to add, 0 to change, 0 to destroy.Plan symbols — memorise these before you ever run terraform apply:
+ Resource will be CREATED (brand new)~ Resource will be UPDATED in-place (no downtime, no recreation)-/+ Resource will be DESTROYED then RECREATED (check why — data loss risk)- Resource will be DESTROYED (irreversible — read carefully)<= Data source will be READ (read-only, completely safe)Save the plan to a file for production CI/CD — this guarantees exactly what was reviewed is what gets applied:
# Save plan as a binary fileterraform plan -out=tfplan.binary # Show the saved plan in human-readable formterraform show tfplan.binary # Apply exactly this saved plan — no re-evaluationterraform apply tfplan.binaryStep 12 — terraform apply: Create the Infrastructure
terraform apply# Terraform shows the full plan again and asks for confirmation# Enter a value: yes # aws_s3_bucket.app_assets: Creating...# aws_security_group.web: Creating... <- parallel — no dependency# aws_s3_bucket.app_assets: Creation complete after 2s# aws_s3_bucket_public_access_block.app_assets: Creating... <- depends on bucket# aws_security_group.web: Creation complete after 4s# aws_instance.web: Creating... <- depends on security group# aws_s3_bucket_public_access_block.app_assets: Creation complete after 1s# aws_s3_bucket_versioning.app_assets: Creating...# aws_s3_bucket_versioning.app_assets: Creation complete after 1s# aws_s3_bucket_server_side_encryption_configuration.app_assets: Creating...# aws_s3_bucket_server_side_encryption_configuration.app_assets: Creation complete# aws_instance.web: Still creating... [10s elapsed]# aws_instance.web: Still creating... [20s elapsed]# aws_instance.web: Creation complete after 23s [id=i-0a1b2c3d4e5f]## Apply complete! Resources: 6 added, 0 changed, 0 destroyed.## Outputs:# ami_id_used = "ami-0f5ee92e2d63afc18"# instance_id = "i-0a1b2c3d4e5f"# instance_public_dns = "ec2-13-126-x-x.ap-south-1.compute.amazonaws.com"# instance_public_ip = "13.126.x.x"# s3_bucket_arn = "arn:aws:s3:::razorpay-api-dev-assets-123456789012"# s3_bucket_name = "razorpay-api-dev-assets-123456789012"Notice Terraform creates resources in parallel wherever dependencies allow — the S3 bucket and security group start at the same time. Once the security group exists, the EC2 instance starts. Terraform manages all ordering automatically from the dependency graph — you never write "create X before Y" anywhere.
Step 13 — Verify and Inspect State
# List every resource Terraform is now trackingterraform state list# data.aws_ami.amazon_linux# data.aws_caller_identity.current# aws_instance.web# aws_s3_bucket.app_assets# aws_s3_bucket_public_access_block.app_assets# aws_s3_bucket_server_side_encryption_configuration.app_assets# aws_s3_bucket_versioning.app_assets# aws_security_group.web # Inspect every stored attribute of one resourceterraform state show aws_instance.web # Print all output valuesterraform output # Print one output value as raw text — for use in shell scriptsterraform output -raw instance_public_ip # Test the web servercurl http://$(terraform output -raw instance_public_ip)/# <h1>razorpay-api — dev</h1>Step 14 — Make a Change and Apply the Minimum Diff
# In variables.tf — change the default instance typevariable "instance_type" { default = "t3.small" # was t3.micro}terraform plan# ~ aws_instance.web will be updated in-place# ~ instance_type = "t3.micro" -> "t3.small"## Plan: 0 to add, 1 to change, 0 to destroy.# This is an in-place update — the instance is NOT recreated terraform apply# aws_instance.web: Modifying... [id=i-0a1b2c3d4e5f]# aws_instance.web: Modifications complete after 9s# Apply complete! Resources: 0 added, 1 changed, 0 destroyed.Terraform calculated that only the instance type needed to change — everything else stayed untouched. This is Terraform's declarative model: you describe the desired end state, Terraform figures out the minimum set of changes to get there.
Step 15 — terraform destroy: Clean Up All Resources
# Preview what will be destroyed before committingterraform plan -destroy # Destroy all resources — type "yes" when promptedterraform destroy # aws_s3_bucket_server_side_encryption_configuration.app_assets: Destroying...# aws_s3_bucket_versioning.app_assets: Destroying...# aws_s3_bucket_public_access_block.app_assets: Destroying...# aws_instance.web: Destroying...# aws_s3_bucket.app_assets: Destroying...# aws_instance.web: Destruction complete after 32s# aws_s3_bucket.app_assets: Destruction complete after 1s# aws_security_group.web: Destroying...# aws_security_group.web: Destruction complete after 1s## Destroy complete! Resources: 6 destroyed.COMMON MISTAKE / WARNING**Security:** Always run `terraform destroy` after finishing labs to avoid unexpected AWS charges. A t3.micro instance left running costs roughly $8 per month. Set a billing alert in the AWS console as a safety net.
Production Best Practices and Common Pitfalls
Pin provider versions with
~> MAJOR.MINOR. An unpinned provider (version = ">= 5.0") can auto-upgrade to a breaking major version. The~>operator allows patch updates while blocking major jumps —~> 5.0means any version from 5.0.0 up to but not including 6.0.0.Use
default_tagsin the provider block. Declare shared tags once in the provider'sdefault_tagsblock. They apply automatically to every AWS resource the provider creates — you cannot forget them on a resource.Never hardcode resource names. Use a locals name prefix:
"${var.project}-${var.environment}". This ensures dev, staging, and prod resources all have distinct, recognisable names without duplicating logic across files.Use data sources for AMI IDs — never hardcode them. A hardcoded AMI ID eventually becomes outdated or unavailable in another region. The
data "aws_ami"data source always returns the current patched image.Add a remote backend before your team grows. Set up S3 + DynamoDB locking as soon as a second engineer joins. Adding it later requires a state migration. Start remote from day one.
Commit
.terraform.lock.hcl, never.terraform/. The lock file records exact provider versions and cryptographic hashes — commit it so every engineer and pipeline is on identical providers. The.terraform/directory is the downloaded binary — large, regenerated byterraform init, never commit it.Read every
-/+in the plan. Replace actions destroy the existing resource and create a new one — which causes downtime or data loss for stateful resources like RDS. Always understand why a replacement is happening before you apply.
Quick Reference and Troubleshooting Commands
| Command | What It Does |
|---|---|
terraform init |
Download providers, configure backend, install modules |
terraform init -upgrade |
Upgrade providers to latest allowed version |
terraform validate |
Check syntax and internal consistency — no API calls |
terraform fmt |
Format all .tf files to standard style |
terraform fmt -check |
Check formatting without changes — for CI pipelines |
terraform plan |
Preview changes — nothing is modified |
terraform plan -out=tfplan |
Save plan to binary file for later apply |
terraform apply |
Apply changes — prompts for confirmation |
terraform apply tfplan |
Apply a saved plan file — no re-evaluation |
terraform apply -auto-approve |
Apply without confirmation — CI/CD only, never prod manually |
terraform destroy |
Destroy all managed resources — prompts for confirmation |
terraform state list |
List all resources tracked in state |
terraform state show <addr> |
Show all stored attributes of one resource |
terraform output |
Print all output values |
terraform output -raw <name> |
Print one output value without quotes — for shell scripts |
terraform console |
Interactive HCL expression evaluator |
| Error | Root Cause | Fix |
|---|---|---|
No configuration files found in directory |
Running from wrong directory | cd to the directory containing .tf files |
Provider hashicorp/aws not installed |
Did not run terraform init |
Run terraform init |
No valid credential sources found for AWS Provider |
Missing AWS credentials | Export AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY |
BucketAlreadyExists |
S3 bucket names are globally unique | Add account ID suffix using data.aws_caller_identity.current.account_id |
InvalidAMIID.NotFound |
AMI filter matches nothing | Check filter pattern and verify owners = ["amazon"] |
Error: Invalid function argument |
Wrong type passed to a function | Read the error — it shows expected vs actual type |
Error: Cycle |
Two resources reference each other circularly | Remove the circular reference |
Plan shows -/+ unexpectedly |
A change forces resource replacement | Read the (forces replacement) annotation — check provider docs for that argument |