Bringing Local Stacks to Life: Terraform Meets Real Code (in LocalStack)
You define it, build it, break it, rebuild it — all without touching the cloud
The moment you first run real code through Terraform inside LocalStack, something changes.
Your infrastructure isn’t just defined anymore — it’s alive.
Buckets host content. Lambdas execute logic. DynamoDB stores data.
And the best part? You’re still running it all on your laptop.
From Definition to Reality
By now, you’ve seen Terraform describe infrastructure — buckets, functions, and tables — as lines of HCL.
But those lines don’t mean much until they connect with real code.
In this next stage, we’ll bridge that gap.
You’ll create a simple local web application — an S3 static site served through LocalStack, powered by a Lambda function that records form submissions into DynamoDB.
It’s not about complexity; it’s about seeing code meet configuration.
The Blueprint
Here’s the full Terraform configuration for our LocalStack web app — same structure as before, now wired for interaction:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0" # Works with most LocalStack setups
}
}
}
provider "aws" {
access_key = "mock_access_key"
secret_key = "mock_secret_key"
region = "us-east-1"
# LocalStack endpoints
endpoints {
s3 = "http://localhost:4566"
lambda = "http://localhost:4566"
dynamodb = "http://localhost:4566"
}
skip_credentials_validation = true
skip_requesting_account_id = true
}
resource "aws_s3_bucket" "site" {
bucket = "my-localstack-website"
# Optional: static website configuration
website {
index_document = "index.html"
}
}
resource "aws_dynamodb_table" "visitors" {
name = "visitor-table"
billing_mode = "PAY_PER_REQUEST"
hash_key = "id"
attribute {
name = "id"
type = "S"
}
}
resource "aws_lambda_function" "contact" {
function_name = "contact-handler"
handler = "lambda_function.lambda_handler"
runtime = "python3.11"
role = "arn:aws:iam::000000000000:role/mock-role" # LocalStack mock IAM role
filename = "lambda.zip"
}
Note: In production, always use secure IAM roles and environment variables — never hardcoded credentials. LocalStack simplifies this setup for local testing.
The Code That Powers It
Here’s what goes inside your Lambda (lambda_function.py
):
import json
import boto3
import uuid
import os
dynamodb = boto3.resource(
"dynamodb",
endpoint_url=os.getenv("AWS_ENDPOINT_URL", "http://localhost:4566")
)
table = dynamodb.Table("visitor-table")
def lambda_handler(event, context):
record_id = str(uuid.uuid4())
table.put_item(Item={"id": record_id, "message": "New visitor!"})
return {
"statusCode": 200,
"body": json.dumps({"message": f"Visitor recorded: {record_id}"})
}
Zip it up before applying Terraform:
zip lambda.zip lambda_function.py
The Front End
Keep it simple — one HTML file (index.html
) to upload to your S3 bucket:
<!DOCTYPE html>
<html>
<head>
<title>LocalStack Visitor Tracker</title>
</head>
<body>
<h1>Hello from LocalStack!</h1>
<p>This page lives in S3, powered by Terraform.</p>
<button onclick="recordVisit()">Record a Visit</button>
<script>
async function recordVisit() {
const response = await fetch("http://localhost:4566/2015-03-31/functions/contact-handler/invocations", {
method: "POST",
body: JSON.stringify({ action: "visit" }),
});
const result = await response.json();
alert(result.message);
}
</script>
</body>
</html>
Then upload it:
awslocal s3 cp index.html s3://my-localstack-website/
The First Apply
Now it’s showtime:
terraform apply -auto-approve
Terraform processes the plan, LocalStack simulates AWS, and — when everything aligns — you see:
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
Open your S3 website URL (e.g.,http://my-localstack-website.s3.localhost.localstack.cloud:4566
(LocalStack’s S3 website URLs may vary slightly by version; check your LocalStack console for the exact format.)
or check the LocalStack S3 dashboard).
Click Record a Visit — you’ll see a success alert pop up, and your DynamoDB table quietly logs a new item:
awslocal dynamodb scan --table-name visitor-table
{
"Items": [
{
"id": {"S": "8f3e91d7-5b6e-4c32-a36b-88acdc92d1a3"},
"message": {"S": "New visitor!"}
}
]
}
That’s your stack — alive, integrated, and running in LocalStack.
When Things Don’t Go as Planned
Maybe you forgot to zip the Lambda, or Terraform can’t find the file.
You’ll see something like this:
│ Error: Error creating Lambda function:
│ AccessDeniedException: Unable to locate deployment package "lambda.zip"
│
│ with aws_lambda_function.contact,
│ on main.tf line 45, in resource "aws_lambda_function" "contact":
│ 45: resource "aws_lambda_function" "contact" {
Or maybe your Lambda code throws an exception:
{
"errorMessage": "Unable to locate table 'visitor-table'",
"errorType": "ResourceNotFoundException"
}
These aren’t failures — they’re checkpoints.
They teach you to slow down, inspect your code, and refine your infrastructure definitions.
Each fix is a small act of understanding.
The Loop of Confidence
When it finally works, you’ll see the magic of local-first IaC:
Infrastructure you can deploy, destroy, and rebuild in minutes.
Each success message isn’t just validation — it’s feedback that your mental model matches the system’s behavior.
This is what Terraform in LocalStack is all about:
You define it, build it, break it, rebuild it — all without touching the cloud.
Closing Reflection
You started with definitions in HCL.
Then you breathed life into them with real code.
Now your stack isn’t hypothetical — it’s functional, testable, and evolving.
And when you see this again:
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
it won’t just be Terraform talking — it’ll be your environment, whispering back that you’ve achieved something real.
Coming Up Next
Next time, we’ll push the boundaries.
We’ll explore resilience — simulating downtime, retries, and recovery — and learn how to test the reliability of your local infrastructure before it ever hits production.
While we’ve been terraforming our local cloud, another declarative language has been watching from the wings: CloudFormation.
In our next parallel series, we’ll explore how AWS’s native framework builds the same stacks inside LocalStack — sometimes tighter with AWS conventions, sometimes more rigid.
Same environment. Different philosophy.
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.
Comments
Post a Comment