{"id":2122,"date":"2026-02-14T03:05:55","date_gmt":"2026-02-13T14:05:55","guid":{"rendered":"https:\/\/www.ronella.xyz\/?p=2122"},"modified":"2026-02-14T03:05:56","modified_gmt":"2026-02-13T14:05:56","slug":"setting-up-a-localstack-vpc-with-terraform-docker-compose-and-aws-cli","status":"publish","type":"post","link":"https:\/\/www.ronella.xyz\/?p=2122","title":{"rendered":"Setting up a LocalStack VPC with Terraform, Docker Compose, and AWS CLI"},"content":{"rendered":"<h2>1. Overview and rationale<\/h2>\n<ul>\n<li>LocalStack emulates EC2\/VPC APIs locally, so Terraform can create VPCs, subnets, route tables, and gateways just like on AWS.<\/li>\n<li>The AWS CLI can talk to LocalStack by using <code>--endpoint-url http:\/\/localhost:4566<\/code>, letting you validate resources with the exact same commands you\u2019d run against real AWS.<\/li>\n<li>This keeps your workflow close to production: same Terraform provider, same CLI, different endpoint.<\/li>\n<\/ul>\n<hr \/>\n<h2>2. Run LocalStack with Docker Compose<\/h2>\n<p>Create <code>docker-compose.yml<\/code>:<\/p>\n<pre><code class=\"language-yaml\">version: &quot;3.8&quot;\n\nservices:\n  localstack:\n    image: localstack\/localstack:latest\n    container_name: localstack\n    ports:\n      - &quot;4566:4566&quot;              # Edge port: all AWS APIs\n      - &quot;4510-4559:4510-4559&quot;    # Optional service ports\n    environment:\n      - SERVICES=ec2             # Add more: ec2,lambda,rds,ecs,...\n      - AWS_DEFAULT_REGION=us-east-1\n      - DEBUG=1\n      - DOCKER_HOST=unix:\/\/\/var\/run\/docker.sock\n    volumes:\n      - &quot;\/var\/run\/docker.sock:\/var\/run\/docker.sock&quot;<\/code><\/pre>\n<p>Start LocalStack:<\/p>\n<pre><code class=\"language-bash\">docker compose up -d<\/code><\/pre>\n<p>LocalStack now exposes EC2\/VPC on <code>http:\/\/localhost:4566<\/code> (the edge endpoint).<\/p>\n<hr \/>\n<h2>3. Terraform VPC configuration<\/h2>\n<p>Keep Terraform AWS\u2011idiomatic and only change the endpoint.<\/p>\n<h3>providers.tf<\/h3>\n<pre><code class=\"language-terraform\">terraform {\n  required_version = &quot;&gt;= 1.6.0&quot;\n\n  required_providers {\n    aws = {\n      source  = &quot;hashicorp\/aws&quot;\n      version = &quot;~&gt; 5.0&quot;\n    }\n  }\n}\n\nprovider &quot;aws&quot; {\n  region                      = &quot;us-east-1&quot;\n  access_key                  = &quot;test&quot;\n  secret_key                  = &quot;test&quot;\n  skip_credentials_validation = true\n  skip_metadata_api_check     = true\n  skip_requesting_account_id  = true\n  s3_use_path_style           = true\n\n  endpoints {\n    ec2 = &quot;http:\/\/localhost:4566&quot;\n  }\n}<\/code><\/pre>\n<h3>main.tf<\/h3>\n<pre><code class=\"language-terraform\">resource &quot;aws_vpc&quot; &quot;demo&quot; {\n  cidr_block           = &quot;10.0.0.0\/16&quot;\n  enable_dns_support   = true\n  enable_dns_hostnames = true\n\n  tags = {\n    Name = &quot;localstack-demo-vpc&quot;\n  }\n}\n\nresource &quot;aws_subnet&quot; &quot;public_az1&quot; {\n  vpc_id                  = aws_vpc.demo.id\n  cidr_block              = &quot;10.0.1.0\/24&quot;\n  availability_zone       = &quot;us-east-1a&quot;\n  map_public_ip_on_launch = true\n\n  tags = {\n    Name = &quot;localstack-demo-public-az1&quot;\n  }\n}\n\nresource &quot;aws_internet_gateway&quot; &quot;igw&quot; {\n  vpc_id = aws_vpc.demo.id\n\n  tags = {\n    Name = &quot;localstack-demo-igw&quot;\n  }\n}\n\nresource &quot;aws_route_table&quot; &quot;public&quot; {\n  vpc_id = aws_vpc.demo.id\n\n  route {\n    cidr_block = &quot;0.0.0.0\/0&quot;\n    gateway_id = aws_internet_gateway.igw.id\n  }\n\n  tags = {\n    Name = &quot;localstack-demo-public-rt&quot;\n  }\n}\n\nresource &quot;aws_route_table_association&quot; &quot;public_az1_assoc&quot; {\n  subnet_id      = aws_subnet.public_az1.id\n  route_table_id = aws_route_table.public.id\n}\n\noutput &quot;vpc_id&quot; {\n  value = aws_vpc.demo.id\n}\n\noutput &quot;public_subnet_id&quot; {\n  value = aws_subnet.public_az1.id\n}<\/code><\/pre>\n<p>Apply:<\/p>\n<pre><code class=\"language-bash\">terraform init\nterraform apply<\/code><\/pre>\n<p>This models the classic \u201cpublic subnet\u201d layout so the same module can later be pointed at real AWS with only a provider change.<\/p>\n<h2>4. Configure AWS CLI for LocalStack<\/h2>\n<p>You can use the stock AWS CLI v2 and override the endpoint.<\/p>\n<ol>\n<li>Configure a local profile (optional but tidy):<\/li>\n<\/ol>\n<pre><code class=\"language-bash\">aws configure --profile localstack\n# AWS Access Key ID: test\n# AWS Secret Access Key: test\n# Default region name: us-east-1\n# Default output format: json<\/code><\/pre>\n<ol start=\"2\">\n<li>For each command, add:<\/li>\n<\/ol>\n<pre><code class=\"language-bash\">--endpoint-url http:\/\/localhost:4566 --profile localstack<\/code><\/pre>\n<p>This tells the CLI to send EC2 API calls to LocalStack instead of AWS.<\/p>\n<p>If you prefer less typing, you can define an alias (e.g. in your shell):<\/p>\n<pre><code class=\"language-bash\">alias awslocal=&#039;aws --endpoint-url http:\/\/localhost:4566 --profile localstack&#039;<\/code><\/pre>\n<p>This is conceptually the same as the <code>awslocal<\/code> wrapper LocalStack provides, but you stay completely within standard AWS CLI semantics.<\/p>\n<hr \/>\n<h2>5. Validating the VPC with AWS CLI<\/h2>\n<p>After <code>terraform apply<\/code>, use the CLI to verify each piece of the VPC.<\/p>\n<h3>5.1 List VPCs<\/h3>\n<pre><code class=\"language-bash\">aws ec2 describe-vpcs --endpoint-url http:\/\/localhost:4566 --profile localstack<\/code><\/pre>\n<p>or with the alias:<\/p>\n<pre><code class=\"language-bash\">awslocal ec2 describe-vpcs<\/code><\/pre>\n<p>Check for a VPC with:<\/p>\n<ul>\n<li><code>CidrBlock<\/code> = <code>10.0.0.0\/16<\/code>  <\/li>\n<li>Tag <code>Name = localstack-demo-vpc<\/code>  <\/li>\n<\/ul>\n<p>The shape of the output is identical to real AWS <code>describe-vpcs<\/code>.<\/p>\n<h3>5.2 List subnets<\/h3>\n<pre><code class=\"language-bash\">awslocal ec2 describe-subnets<\/code><\/pre>\n<p>Confirm a subnet exists with:<\/p>\n<ul>\n<li><code>CidrBlock<\/code> = <code>10.0.1.0\/24<\/code>  <\/li>\n<li><code>AvailabilityZone<\/code> = <code>us-east-1a<\/code>  <\/li>\n<li>Tag <code>Name = localstack-demo-public-az1<\/code>  <\/li>\n<\/ul>\n<h3>5.3 List Internet Gateways<\/h3>\n<pre><code class=\"language-bash\">awslocal ec2 describe-internet-gateways<\/code><\/pre>\n<p>You should see an IGW whose <code>Attachments<\/code> includes your VPC ID and has tag <code>localstack-demo-igw<\/code>.<\/p>\n<h3>5.4 List route tables<\/h3>\n<pre><code class=\"language-bash\">awslocal ec2 describe-route-tables<\/code><\/pre>\n<p>Verify:<\/p>\n<ul>\n<li>A route table tagged <code>localstack-demo-public-rt<\/code>.  <\/li>\n<li>A route with <code>DestinationCidrBlock<\/code> <code>0.0.0.0\/0<\/code> and <code>GatewayId<\/code> set to your IGW ID.  <\/li>\n<li>An association to your public subnet ID.<\/li>\n<\/ul>\n<p>These commands mirror the official AWS CLI usage for EC2, just with the endpoint overridden.<\/p>\n<h2>6. Why this testing approach scales<\/h2>\n<ul>\n<li>Uses only Terraform + AWS CLI, tools you already depend on in real environments.<\/li>\n<li>Easy to script: you can wrap the CLI checks into bash scripts or CI jobs to assert your VPC configuration in LocalStack before promoting to AWS.<\/li>\n<li>Mental model stays aligned with production AWS: same commands, same JSON structures, just a different base URL.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>1. Overview and rationale LocalStack emulates EC2\/VPC APIs locally, so Terraform can create VPCs, subnets, route tables, and gateways just like on AWS. The AWS CLI can talk to LocalStack by using &#8211;endpoint-url http:\/\/localhost:4566, letting you validate resources with the exact same commands you\u2019d run against real AWS. This keeps your workflow close to production: [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[94],"tags":[],"_links":{"self":[{"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/posts\/2122"}],"collection":[{"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2122"}],"version-history":[{"count":1,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/posts\/2122\/revisions"}],"predecessor-version":[{"id":2123,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/posts\/2122\/revisions\/2123"}],"wp:attachment":[{"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2122"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=2122"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=2122"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}