Infrastructure as Code with Terraform
Provision S3, DynamoDB, and SQS locally using Terraform and LocalStack.
Introduction
30 Second Summary
Every time a team needs a new server, database, or storage bucket, someone has to click through a cloud console or remember the exact commands. One missed setting can break an entire deployment.
In this project, you will use Terraform to define and deploy three AWS services locally using LocalStack, a cloud emulator that runs on your machine.
What You'll Build
A complete Terraform project that provisions an S3 bucket, a DynamoDB table, and an SQS queue against a local AWS emulator.
By the end of this project, you'll have:
- A working Terraform configuration that provisions three AWS services with a single command.
- Hands-on experience with the full Terraform lifecycle: init, plan, apply, modify, and destroy.
- A reusable, parameterized project structure with variables and outputs that transfers directly to real AWS.
- Secret Mission: Connect your S3 bucket to your SQS queue with event notifications so uploads automatically trigger messages.
Are there any prerequisites?
You need Docker Desktop, Python, and VS Code installed on your machine. Basic familiarity with the command line is helpful but not required.
Before We Start
In this project, you will define cloud infrastructure as code using Terraform. Instead of clicking through the AWS console to create resources one by one, you will write configuration files that describe your infrastructure and deploy everything with a single command.
To practice safely without spending money, you will use LocalStack to emulate AWS services on your own machine. First, you need three tools installed: the Terraform CLI, a helper called tflocal, and the LocalStack emulator running in Docker.
In this step, get ready to:
- Install the Terraform CLI on your machine.
- Install the tflocal wrapper for future LocalStack convenience.
- Create a LocalStack account and start the emulator in Docker.
Install Terraform CLI
Terraform is the tool that reads your configuration files and talks to cloud providers to create, update, or destroy resources. You need the CLI binary on your machine before you can run any Terraform commands.
- Open a terminal by pressing the Windows key, typing PowerShell, and pressing Enter.
- Check if Terraform is already installed by running this command:
terraform version
✔️ I see version 1.15.6 or higher
You should see output starting with Terraform v1.15.6 (or a higher version number). You are all set. Skip ahead to the next substep.
ⓧ I see an older version
You have Terraform installed, but it is an older version. You need 1.15.6 or higher for this project.
- Visit the official Terraform install page at developer.hashicorp.com/terraform/install.
- Download the Windows AMD64 zip file.
- Extract terraform.exe from the zip and replace your existing terraform.exe file with it.
- Open a new PowerShell window (the old one still has the old version cached in its session).
- Run terraform version again to confirm it now shows 1.15.6 or higher.
Still seeing the old version?
Make sure you replaced the binary in the same folder that is on your system PATH. If you are unsure which folder that is, run where terraform in PowerShell to see the path.
Help me troubleshoot my Terraform PATH issue
ⓧ Command not found
Terraform is not installed yet. Follow these steps to install it.
- Visit the official Terraform install page at developer.hashicorp.com/terraform/install.
- Download the Windows AMD64 zip file (look for the 1.15.6 version).
- Create a new folder called C:\terraform.
- Extract terraform.exe from the zip into C:\terraform.
- Now add this folder to your system PATH so Windows can find the terraform command from any terminal.
- Press the Windows key and type environment variables.
- Click Edit the system environment variables.
- Click the Environment Variables button at the bottom.
- Under System variables, find the row named Path and click Edit.
- Click New and type C:\terraform.
- Click OK on all open dialogs to save.
- Open a brand new PowerShell window (existing windows do not pick up PATH changes).
- Verify the installation by running:
terraform version
You should see output starting with Terraform v1.15.6 followed by your platform (windows_amd64).
Still getting command not found?
Double check that terraform.exe is directly inside C:\terraform (not nested in a subfolder from the zip extraction). Also confirm you opened a new terminal after editing PATH.
Help me debug my PATH issues on Windows
What is Terraform?
Terraform is an Infrastructure as Code tool by HashiCorp. You write .tf files that describe the cloud resources you want, and Terraform creates, updates, or destroys them to match your configuration.
This project uses Terraform 1.15.6. Terraform communicates with cloud providers (like AWS) through plugins called providers, which you will configure in the next step.
Install the tflocal wrapper
The tflocal tool is a thin wrapper around the terraform command that automatically points Terraform at LocalStack. You will not use it directly in this project (you will configure endpoints manually to understand how provider configuration works), but it is useful to have for future projects.
- Install terraform-local by running this command:
pip install terraform-local
What does this command do?
This installs the terraform-local Python package (v0.26.0) from PyPI. It provides the tflocal CLI command, which uses Terraform's Override mechanism to inject LocalStack endpoints automatically.
- Verify it installed correctly by running:
tflocal --version
You should see a version number in the output (0.26.0 or similar).
tflocal not recognized?
If you see a "not recognized" error, Python's Scripts folder might not be on your PATH. Try closing your terminal and opening a new one. If it still fails, run python -m pip show terraform-local to confirm it installed, then add Python's Scripts folder to your PATH.
Help me fix my tflocal PATH issue
Create a LocalStack account and start the emulator
LocalStack emulates AWS cloud services inside a Docker container on your machine. You need a free account to generate an auth token that allows the container to start.
- Open your browser and go to localstack.cloud.
- Click Get Started for Free to create a Hobby account (no credit card required).
- Fill out your information and verify your email address.
- Once your account is activated, log in at app.localstack.cloud.
- Navigate to the Auth Tokens page in your account settings.
- Copy your Developer Auth Token. Treat it like a password. Do not commit it to any repository.
Why do I need an auth token?
Since March 2026, LocalStack requires an account and auth token even on the free Hobby tier. The token tells LocalStack which services you are entitled to use. The Hobby plan includes S3, DynamoDB, and SQS at no cost.
Now you will start LocalStack as a Docker container. Make sure Docker Desktop is running before proceeding.
- Start LocalStack by running this command (replace your-auth-token-here with the token you copied):
docker run -d --name localstack -p 4566:4566 -e LOCALSTACK_AUTH_TOKEN=your-auth-token-here localstack/localstack
What does this command do?
- docker run -d starts a new container in the background (detached mode).
- --name localstack gives the container a friendly name so you can reference it later.
- -p 4566:4566 maps port 4566 on your machine to port 4566 inside the container. All LocalStack services listen on this single port.
- -e LOCALSTACK_AUTH_TOKEN=... passes your auth token as an environment variable so LocalStack can activate.
- localstack/localstack is the official Docker image.
- Verify the container is running by running:
docker ps
You should see a row with localstack/localstack in the IMAGE column and localstack in the NAMES column.
- Confirm LocalStack's services are healthy by running:
curl http://localhost:4566/_localstack/health
You should see a JSON response listing available services and their status. If you see "s3": "available" in the output, LocalStack is ready.
Container exited immediately?
If docker ps shows nothing, the container may have failed to start. Run docker logs localstack to check the error output.
The most common cause is an invalid auth token. Double check you copied the full token from your LocalStack account without extra spaces.
Help me fix my LocalStack container
Your development environment is ready. Next up, you will write your first Terraform configuration and deploy an S3 bucket to your local cloud.
Write and Deploy Your First S3 Bucket
Your tools are installed and LocalStack is running. Now it is time to write actual Terraform code and deploy your first piece of cloud infrastructure.
In this step, you will create a Terraform project that tells the AWS provider to talk to LocalStack instead of real AWS. Then you will define an S3 bucket and deploy it with a single command.
In this step, get ready to:
- Create a project directory and configure the AWS provider to point at LocalStack.
- Define an S3 bucket resource in Terraform configuration.
- Initialize, plan, and apply your configuration to provision the bucket.
Create project directory and provider configuration
Every Terraform project lives in its own directory. Terraform reads all .tf files in the current folder when you run commands, so you need a dedicated folder for this project.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir terraform-localstack
cd terraform-localstack
- Confirm the folder exists by running:
ls
You should see the terraform-localstack folder listed in your Desktop directory.
The first file you need is provider.tf. This tells Terraform which cloud provider to use and where to send API requests.
- In VS Code, open the terraform-localstack folder by clicking File in the top menu bar.
- Click Open Folder.
- Select the terraform-localstack folder on your Desktop, then click Open.
- Create a new file called provider.tf by clicking the new file icon in the Explorer sidebar.
- Copy the following code into provider.tf:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
}
}
}
provider "aws" {
access_key = "test"
secret_key = "test"
region = "us-east-1"
s3_use_path_style = true
skip_credentials_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true
endpoints {
s3 = "http://s3.localhost.localstack.cloud:4566"
dynamodb = "http://localhost:4566"
sqs = "http://localhost:4566"
}
}
What does this code do?
The terraform block declares that you need the hashicorp/aws provider. This is the same provider you would use for real AWS.
The provider "aws" block configures how Terraform connects. Instead of real credentials, you use "test" for both keys because LocalStack does not validate them. The endpoints block redirects all API calls to LocalStack on port 4566 instead of AWS.
The skip_* settings prevent Terraform from trying to validate credentials or look up account metadata against a real AWS endpoint.
Why manual endpoints instead of tflocal?
You installed tflocal in Step 1, which can handle endpoint configuration automatically. However, writing the endpoints manually teaches you exactly how Terraform connects to any backend.
When you move to real AWS later, you remove the endpoints block and swap in real credentials. Understanding this configuration gives you full control over where your infrastructure deploys.
- Save provider.tf.
VS Code not showing the file?
Make sure you opened the terraform-localstack folder itself, not its parent (Desktop). The Explorer sidebar should show the folder name at the top. If you see your entire Desktop listed, close the folder and reopen the correct one.
Help me open the correct folder in VS Code for my Terraform project.
Define an S3 bucket resource
With the provider configured, you can now define your first cloud resource. An S3 bucket is a storage container in AWS. You will declare one in a new file called main.tf.
- Create a new file called main.tf in the same terraform-localstack folder.
- Copy the following code into main.tf:
resource "aws_s3_bucket" "app_bucket" {
bucket = "devops-lab-dev-assets"
tags = {
Name = "devops-lab-assets"
Environment = "dev"
ManagedBy = "terraform"
}
}
What does this code do?
The resource keyword tells Terraform you want to create something. "aws_s3_bucket" is the resource type (an S3 bucket), and "app_bucket" is your internal name for it within Terraform.
The bucket argument sets the bucket's actual name in AWS. The tags block attaches metadata labels so you can identify and organize your resources later.
- Save main.tf.
✔️ Awesome, I've got everything!
Great. Double check you have saved both provider.tf and main.tf.
ⓧ I'd like to double check the full code
Here is the full provider.tf:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
}
}
}
provider "aws" {
access_key = "test"
secret_key = "test"
region = "us-east-1"
s3_use_path_style = true
skip_credentials_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true
endpoints {
s3 = "http://s3.localhost.localstack.cloud:4566"
dynamodb = "http://localhost:4566"
sqs = "http://localhost:4566"
}
}
Here is the full main.tf:
resource "aws_s3_bucket" "app_bucket" {
bucket = "devops-lab-dev-assets"
tags = {
Name = "devops-lab-assets"
Environment = "dev"
ManagedBy = "terraform"
}
}
Initialize and apply
Terraform uses a three-command workflow to deploy infrastructure: init downloads the provider plugin, plan previews changes, and apply executes them. You will run all three now.
- Initialize Terraform and download the AWS provider by running this command in your terminal (make sure you are inside the terraform-localstack folder):
terraform init
You should see output ending with Terraform has been successfully initialized!.
What does terraform init do?
This command reads your provider.tf file, downloads the hashicorp/aws provider plugin, and creates a hidden .terraform directory to cache it. It also creates a lock file called .terraform.lock.hcl that pins the provider version.
You only need to run init once per project, or again if you change providers.
Seeing an error during init?
If you see a network error, make sure you have an internet connection. Terraform needs to download the provider from the HashiCorp registry.
If you see a syntax error, open provider.tf and check for missing braces or typos. Every opening { needs a matching }.
Help me troubleshoot my terraform init error.
- Preview what Terraform will create by running:
terraform plan
You should see a summary ending with Plan: 1 to add, 0 to change, 0 to destroy..
How to read the plan output
Terraform displays every resource it intends to create with a + symbol next to it. The summary line tells you the total count: 1 resource to add, nothing to change, nothing to destroy.
The plan is a preview only. Nothing has been created yet. This gives you a chance to review before committing.
- Deploy the S3 bucket by running:
terraform apply
Terraform will ask for confirmation
After showing the plan again, Terraform asks Do you want to perform these actions? and waits for you to type yes and press Enter. This is a safety mechanism so you always confirm before making changes.
- Type yes and press Enter to confirm the deployment.
You should see Apply complete! Resources: 1 added, 0 changed, 0 destroyed..
- Verify the bucket was created by checking Terraform's state:
terraform state list
You should see aws_s3_bucket.app_bucket in the output. This confirms Terraform is tracking your S3 bucket.
Apply failed or timed out?
Make sure your LocalStack container is still running. Open a new terminal and run docker ps to check. You should see the localstack/localstack container listed.
If the container stopped, restart it with the same docker run command from Step 1.
Help me debug my terraform apply failure.
Your first cloud resource is live. You wrote a Terraform configuration, previewed the changes, and deployed an S3 bucket to LocalStack with a single command. Next up, you will expand this project with a DynamoDB table and an SQS queue.
Expand with DynamoDB and SQS
Your S3 bucket is live in LocalStack. That proves Terraform can create and track a single resource.
Real applications need more than storage. They need a database for structured data and a message queue for async communication. In this step, you will add two more AWS services to your configuration and watch how Terraform intelligently handles existing resources alongside new ones.
In this step, get ready to:
- Add a DynamoDB table resource to your configuration.
- Add an SQS queue resource to your configuration.
- Plan and apply the changes while observing how Terraform tracks state.
Add a DynamoDB table to your configuration
A DynamoDB table gives your application a fast, serverless NoSQL database. You will define one in main.tf using the same tag pattern as your S3 bucket.
- In VS Code, open main.tf.
- Below the closing } of your aws_s3_bucket resource, add the following DynamoDB table resource:
resource "aws_dynamodb_table" "app_table" {
name = "devops-lab-dev-items"
billing_mode = "PAY_PER_REQUEST"
hash_key = "id"
attribute {
name = "id"
type = "S"
}
tags = {
Name = "devops-lab-items"
Environment = "dev"
ManagedBy = "terraform"
}
}
What does this code do?
This defines a DynamoDB table that Terraform will create in LocalStack.
- billing_mode = "PAY_PER_REQUEST" means you only pay for reads and writes you actually make. No capacity planning needed.
- hash_key = "id" sets the primary key for the table. Every item stored needs a unique id value.
- The attribute block declares the key's data type. "S" means string.
- Save main.tf.
You won't see a change yet. The next chunk adds the SQS queue, then you will plan and apply both together.
Add an SQS queue to your configuration
An SQS queue lets services send messages to each other without waiting for a response. It is the backbone of event-driven architectures on AWS.
- In main.tf, below the closing } of your aws_dynamodb_table resource, add the following SQS queue resource:
resource "aws_sqs_queue" "app_queue" {
name = "devops-lab-dev-tasks"
message_retention_seconds = 86400
visibility_timeout_seconds = 30
tags = {
Name = "devops-lab-tasks"
Environment = "dev"
ManagedBy = "terraform"
}
}
What does this code do?
This defines an SQS message queue in your infrastructure.
- message_retention_seconds = 86400 keeps unprocessed messages for 24 hours (86400 seconds) before automatically deleting them.
- visibility_timeout_seconds = 30 hides a message from other consumers for 30 seconds after one consumer picks it up. This prevents duplicate processing.
- Save main.tf.
Seeing a syntax error?
Make sure each resource block has matching opening and closing braces. The tags block inside each resource also needs its own pair of braces.
Check that there is a blank line separating each resource block for readability.
help me fix a syntax error in my Terraform main.tf file
✔️ Awesome, I've got everything!
Great. Double check you have saved main.tf before moving on.
ⓧ I'd like to double check the full code
Here is the complete main.tf at this point:
resource "aws_s3_bucket" "app_bucket" {
bucket = "devops-lab-dev-assets"
tags = {
Name = "devops-lab-assets"
Environment = "dev"
ManagedBy = "terraform"
}
}
resource "aws_dynamodb_table" "app_table" {
name = "devops-lab-dev-items"
billing_mode = "PAY_PER_REQUEST"
hash_key = "id"
attribute {
name = "id"
type = "S"
}
tags = {
Name = "devops-lab-items"
Environment = "dev"
ManagedBy = "terraform"
}
}
resource "aws_sqs_queue" "app_queue" {
name = "devops-lab-dev-tasks"
message_retention_seconds = 86400
visibility_timeout_seconds = 30
tags = {
Name = "devops-lab-tasks"
Environment = "dev"
ManagedBy = "terraform"
}
}
Plan and apply the changes
Now comes the powerful part. Terraform already knows about your S3 bucket from the last apply. When you plan, it compares your updated config against its stored state and figures out what is new.
- Preview what Terraform will do by running this command:
terraform plan
What should I see in the plan output?
Terraform prints a summary line at the bottom:
Plan: 2 to add, 0 to change, 0 to destroy.
The + symbol next to each new resource means "will be created." Your existing S3 bucket does not appear with any symbol because Terraform sees it already matches the config. This is state tracking in action.
- Apply the changes by running this command:
terraform apply
- When Terraform asks Do you want to perform these actions?, type yes and press Enter.
You should see a message confirming the resources were created. It looks like: Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Why did Terraform leave the S3 bucket alone?
Terraform stores the state of every resource it manages in a local file called terraform.tfstate. When you run terraform plan, it compares your config against this state file and the real infrastructure.
Because the S3 bucket already exists and nothing about it changed in your config, Terraform skips it entirely. It only acts on the delta. This is what makes Infrastructure as Code safe to run repeatedly.
Apply failed or timed out?
Make sure your LocalStack container is still running. Run docker ps and confirm you see the localstack container listed.
If it stopped, restart it with the same docker run command from Step 1. You may need to re-run terraform apply since LocalStack does not persist state across restarts.
help me fix a failed terraform apply against LocalStack
You now have three AWS services running locally. Next up, you will replace all the hardcoded names with variables so your config becomes reusable across environments.
Parameterize with Variables and Outputs
In the last step, you expanded your infrastructure to three services. But look closely at main.tf and you will spot a problem: the names devops-lab, dev, and us-east-1 are scattered everywhere as hardcoded strings.
If you ever need to deploy this same stack to a staging or production environment, you would have to manually hunt down and change every occurrence. Terraform solves this with input variables and outputs, which let you define values once and reference them everywhere.
In this step, get ready to:
- Create a variables.tf file that defines reusable input variables.
- Refactor main.tf and provider.tf to use those variables instead of hardcoded strings.
- Create an outputs.tf file that surfaces resource information after deployment.
Create a variables file
Terraform variables let you define a value in one place and reference it across your entire configuration. This means changing an environment name from dev to staging requires editing a single line instead of dozens.
- Create a new file called variables.tf in your project folder, then paste the following code:
variable "aws_region" {
description = "AWS region for resources"
type = string
default = "us-east-1"
}
variable "project_name" {
description = "Name prefix for all resources"
type = string
default = "devops-lab"
}
variable "environment" {
description = "Environment name (e.g. dev, staging, prod)"
type = string
default = "dev"
}
What does this code do?
Each variable block declares one input that your configuration can reference using var.<name> syntax.
- description documents what the variable is for.
- type constrains the value to a specific data type (here, all three are strings).
- default provides the value Terraform uses when you do not override it at apply time.
- Save variables.tf.
Why three variables?
These three capture the dimensions that change between deployments. The region controls where resources live. The project name groups related resources. The environment label separates dev, staging, and production stacks.
Refactor your configuration to use variables
Now you will replace every hardcoded name in main.tf and the hardcoded region in provider.tf with variable references. Because the default values match what you already deployed, Terraform will detect zero changes.
- In provider.tf, find this line:
region = "us-east-1"
- Replace it with:
region = var.aws_region
- Save provider.tf.
- Now open main.tf and replace the entire contents with the following refactored code:
resource "aws_s3_bucket" "app_bucket" {
# Bucket name combines project, environment, and purpose
bucket = "${var.project_name}-${var.environment}-assets"
tags = {
Name = "${var.project_name}-assets"
Environment = var.environment
ManagedBy = "terraform"
}
}
resource "aws_dynamodb_table" "app_table" {
# Table name follows the same naming pattern
name = "${var.project_name}-${var.environment}-items"
billing_mode = "PAY_PER_REQUEST"
hash_key = "id"
attribute {
name = "id"
type = "S"
}
tags = {
Name = "${var.project_name}-items"
Environment = var.environment
ManagedBy = "terraform"
}
}
resource "aws_sqs_queue" "app_queue" {
# Queue name follows the same naming pattern
name = "${var.project_name}-${var.environment}-tasks"
message_retention_seconds = 86400
visibility_timeout_seconds = 30
tags = {
Name = "${var.project_name}-tasks"
Environment = var.environment
ManagedBy = "terraform"
}
}
What changed?
Every place that previously said "devops-lab" now uses var.project_name. Every place that said "dev" now uses var.environment.
The ${...} syntax is Terraform string interpolation. It embeds a variable value inside a larger string. When a value stands alone (like Environment = var.environment), you do not need the interpolation wrapper.
- Save main.tf.
- Verify the refactoring produces no changes by running:
terraform plan
You should see a message saying your infrastructure matches the configuration, with no changes planned. This confirms the variables resolve to the same values that were previously hardcoded.
Seeing unexpected changes or errors?
- If Terraform wants to destroy and recreate resources, double-check that your default values in variables.tf match the original hardcoded strings exactly: us-east-1, devops-lab, dev.
- If you see a syntax error, make sure every interpolation uses the format ${var.name} with matching braces.
Help me debug my Terraform variable refactoring
✔️ Awesome, I've got everything!
Great. Double check you have saved both provider.tf and main.tf.
ⓧ I'd like to double check the full code
Here is the full provider.tf at this point:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
}
}
}
provider "aws" {
access_key = "test"
secret_key = "test"
region = var.aws_region
s3_use_path_style = true
skip_credentials_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true
endpoints {
s3 = "http://s3.localhost.localstack.cloud:4566"
dynamodb = "http://localhost:4566"
sqs = "http://localhost:4566"
}
}
Here is the full main.tf at this point:
resource "aws_s3_bucket" "app_bucket" {
bucket = "${var.project_name}-${var.environment}-assets"
tags = {
Name = "${var.project_name}-assets"
Environment = var.environment
ManagedBy = "terraform"
}
}
resource "aws_dynamodb_table" "app_table" {
name = "${var.project_name}-${var.environment}-items"
billing_mode = "PAY_PER_REQUEST"
hash_key = "id"
attribute {
name = "id"
type = "S"
}
tags = {
Name = "${var.project_name}-items"
Environment = var.environment
ManagedBy = "terraform"
}
}
resource "aws_sqs_queue" "app_queue" {
name = "${var.project_name}-${var.environment}-tasks"
message_retention_seconds = 86400
visibility_timeout_seconds = 30
tags = {
Name = "${var.project_name}-tasks"
Environment = var.environment
ManagedBy = "terraform"
}
}
Create outputs and apply
Terraform stores hundreds of attributes about your resources in state, but most of the time you only care about a few key values. Output blocks tell Terraform which values to display after every apply.
- Create a new file called outputs.tf in your project folder, then paste the following code:
output "s3_bucket_name" {
description = "Name of the S3 bucket"
value = aws_s3_bucket.app_bucket.id
}
output "s3_bucket_arn" {
description = "ARN of the S3 bucket"
value = aws_s3_bucket.app_bucket.arn
}
output "dynamodb_table_name" {
description = "Name of the DynamoDB table"
value = aws_dynamodb_table.app_table.name
}
output "dynamodb_table_arn" {
description = "ARN of the DynamoDB table"
value = aws_dynamodb_table.app_table.arn
}
output "sqs_queue_url" {
description = "URL of the SQS queue"
value = aws_sqs_queue.app_queue.url
}
output "sqs_queue_arn" {
description = "ARN of the SQS queue"
value = aws_sqs_queue.app_queue.arn
}
What does this code do?
Each output block exposes one attribute from a resource. The value field uses the format <resource_type>.<resource_name>.<attribute> to reference a specific piece of information.
After every terraform apply, Terraform prints these outputs to your terminal. They are also stored in state, so you can query them later with terraform output.
- Save outputs.tf.
- Apply the configuration to register the outputs by running:
terraform apply
- Type yes when prompted to confirm.
After the apply completes, you should see an Outputs: section at the bottom of your terminal listing all six values (bucket name, bucket ARN, table name, table ARN, queue URL, queue ARN).
Don't see the outputs section?
- Make sure you saved outputs.tf before running terraform apply. Terraform only reads files that are saved to disk.
- Check that outputs.tf is in the same folder as your other .tf files. Terraform loads all .tf files in the current directory.
Help me troubleshoot my Terraform outputs
✔️ Awesome, I've got everything!
Great. Double check you have saved outputs.tf.
ⓧ I'd like to double check the full code
Here is the full outputs.tf:
output "s3_bucket_name" {
description = "Name of the S3 bucket"
value = aws_s3_bucket.app_bucket.id
}
output "s3_bucket_arn" {
description = "ARN of the S3 bucket"
value = aws_s3_bucket.app_bucket.arn
}
output "dynamodb_table_name" {
description = "Name of the DynamoDB table"
value = aws_dynamodb_table.app_table.name
}
output "dynamodb_table_arn" {
description = "ARN of the DynamoDB table"
value = aws_dynamodb_table.app_table.arn
}
output "sqs_queue_url" {
description = "URL of the SQS queue"
value = aws_sqs_queue.app_queue.url
}
output "sqs_queue_arn" {
description = "ARN of the SQS queue"
value = aws_sqs_queue.app_queue.arn
}
Your configuration is now fully parameterized. Changing a single default value in variables.tf ripples through every resource name and tag automatically. Next up, you will put this flexibility to the test by modifying resources and seeing how Terraform handles the lifecycle.
Modify and Destroy Infrastructure
In the previous step, you parameterized your infrastructure with variables and created outputs to surface resource details. Now you have a clean, reusable configuration that manages three AWS resources.
The real power of Terraform shows up when things change. In this step, you will modify a resource, observe how Terraform communicates the difference between what exists and what you want, then safely tear everything down with a single command.
In this step, get ready to:
- Add a tag to your S3 bucket and read the plan diff.
- Override a variable at apply time and observe destructive changes.
- Destroy all infrastructure and stop the LocalStack container.
Modify a resource and read the plan diff
When you change a Terraform configuration file, Terraform does not immediately touch your infrastructure. You always preview the change first with a plan, then decide whether to apply it.
- In main.tf, find the tags block inside the aws_s3_bucket resource.
- Find this section:
tags = {
Name = "${var.project_name}-assets"
Environment = var.environment
ManagedBy = "terraform"
}
- Add a new line Version = "1.0" after the ManagedBy line, so the block now looks like this:
tags = {
Name = "${var.project_name}-assets"
Environment = var.environment
ManagedBy = "terraform"
Version = "1.0"
}
- Save main.tf.
- Preview the change by running this command:
terraform plan
You should see output indicating that Terraform wants to update the S3 bucket in place. Look for the ~ (tilde) symbol next to the resource name.
What do the plan symbols mean?
Terraform uses symbols to communicate what it intends to do:
- `+` means a resource will be created.
- `~` means a resource will be updated in place (no destruction needed).
- `-` means a resource will be destroyed.
- `-/+` means a resource must be destroyed and recreated (replacement).
The plan summary line at the bottom (e.g., Plan: 0 to add, 1 to change, 0 to destroy.) tells you the total count of each action.
- Apply the change by running:
terraform apply
- Terraform will show you the plan again and ask for confirmation. Type yes and press Enter.
You should see a message confirming the update was applied successfully.
Don't see the ~ symbol in your plan?
- Make sure you saved main.tf before running terraform plan. Terraform reads the file from disk.
- If Terraform shows no changes, confirm the new Version line is inside the tags block (between the opening { and closing }), not outside it.
Help me debug my terraform plan output
✔️ Awesome, I've got everything!
Great. Double check you have saved main.tf with the new Version tag.
ⓧ I'd like to double check the full code
resource "aws_s3_bucket" "app_bucket" {
bucket = "${var.project_name}-${var.environment}-assets"
tags = {
Name = "${var.project_name}-assets"
Environment = var.environment
ManagedBy = "terraform"
Version = "1.0"
}
}
resource "aws_dynamodb_table" "app_table" {
name = "${var.project_name}-${var.environment}-items"
billing_mode = "PAY_PER_REQUEST"
hash_key = "id"
attribute {
name = "id"
type = "S"
}
tags = {
Name = "${var.project_name}-items"
Environment = var.environment
ManagedBy = "terraform"
}
}
resource "aws_sqs_queue" "app_queue" {
name = "${var.project_name}-${var.environment}-tasks"
message_retention_seconds = 86400
visibility_timeout_seconds = 30
tags = {
Name = "${var.project_name}-tasks"
Environment = var.environment
ManagedBy = "terraform"
}
}
Override a variable at apply time
Terraform variables have defaults, but you can override them at the command line. This is powerful for deploying the same configuration to different environments.
- Run this command to see what happens when you change the environment name:
terraform apply -var="environment=staging"
- Terraform will show a plan with resources being destroyed and recreated. This is because changing the environment variable changes the resource names (e.g., devops-lab-dev-assets becomes devops-lab-staging-assets).
- Type no and press Enter to cancel.
Why does renaming cause destruction?
Many cloud resources are identified by their name. When the name changes, the cloud provider cannot rename the existing resource. It must destroy the old one and create a new one.
This is why naming and state management matter. In production, you would not casually override the environment variable on an existing deployment. You would create a separate workspace or state file for each environment.
Destroy all infrastructure
The final lifecycle command removes everything Terraform manages. This is the Infrastructure as Code equivalent of "clean up when you are done."
- Tear down all three resources by running:
terraform destroy
- Terraform will display a plan showing all resources marked with - (destroy).
- Type yes and press Enter to confirm.
You should see a message confirming all three resources were destroyed.
What does terraform destroy do?
It creates a plan to remove every resource in your configuration, waits for your confirmation, and then executes the deletions. Terraform uses its state file to know exactly which real resources to target.
- Stop and remove the LocalStack container by running:
docker stop localstack && docker rm localstack
- Verify the container is gone by running:
docker ps
You should see no container named localstack in the output.
Seeing an error when stopping the container?
- If Docker says the container does not exist, it may have already been removed. Run docker ps -a to check all containers including stopped ones.
- Make sure Docker Desktop is still running. If you closed it, the container would have stopped automatically.
Help me fix my Docker container cleanup
Secret mission
Connect S3 Events to SQS
You've provisioned three independent AWS resources. But in the real world, cloud services talk to each other. What if uploading a file to S3 could automatically trigger a message in your SQS queue? That's event-driven architecture, and you're about to build it with Terraform.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally using Docker and LocalStack, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- LocalStack Docker container (running on port 4566).
- Terraform project files (provider.tf, variables.tf, main.tf, outputs.tf, .terraform directory, terraform.tfstate).
- Terraform CLI (terraform.exe binary added to system PATH).
- terraform-local pip package (tflocal CLI wrapper).
Keep everything running
No action needed. Choose this if you are still actively building or plan to experiment with your Terraform configuration.
- Your LocalStack container continues running in Docker and is available at http://localhost:4566.
- Your Terraform files remain on your Desktop, ready for future edits.
- Terraform CLI and tflocal remain installed and available in your terminal.
Pause - I'll come back to this later
Stop the LocalStack container to free up Docker resources. Your project files and installed tools stay in place for when you return.
- Stop the LocalStack container by running:
docker stop localstack
What happens to your Terraform state?
LocalStack resources only exist while the container is running. When you stop it, the S3 bucket, DynamoDB table, and SQS queue are gone from the emulator.
Your Terraform state file (terraform.tfstate) still exists on disk, but it references resources that no longer exist. When you come back, start LocalStack again and run terraform apply to recreate everything.
- To resume later, start the container again with:
docker start localstack
- Then re-run terraform apply from your project folder to recreate the resources.
Delete - I don't want to use this again
Remove all project resources, the Docker container, and optionally uninstall the tools you installed during this project.
- Stop and remove the LocalStack Docker container by running:
docker stop localstack && docker rm localstack
- Delete your Terraform project folder from the Desktop by running:
macOS
cd ~/Desktop
rm -rf devops-lab-terraform
Windows
cd ~\Desktop
rmdir /s /q devops-lab-terraform
- Uninstall the terraform-local pip package by running:
pip uninstall terraform-local
- Remove the Terraform CLI. Since it was installed manually, delete the terraform.exe file from the folder you extracted it to (e.g., C:\terraform), then remove that folder from your system PATH.
- Optionally, remove the LocalStack Docker image to reclaim disk space by running:
docker rmi localstack/localstack
That's a Wrap!
That's a Wrap!
Nice work! You just built a complete Infrastructure as Code project that provisions and manages AWS cloud resources using Terraform and LocalStack.
You've learned how to:
- Configured Terraform to connect to a local cloud emulator using custom provider endpoints, mock credentials, and service-specific URLs.
- Provisioned three core AWS services (S3, DynamoDB, SQS) entirely as code, and managed them through the full Terraform lifecycle of init, plan, apply, modify, and destroy.
- Created reusable, parameterized configurations with variables and outputs that transfer directly to real AWS environments.
- Secret Mission: Connected your S3 bucket to your SQS queue with event notifications and IAM policies, so file uploads automatically trigger queue messages.
Ready to quiz yourself?