Skip to content
Back to blog The Fast Feedback Loop - Local Development with Kind, LocalStack, and Act

The Fast Feedback Loop - Local Development with Kind, LocalStack, and Act

DevOps

The Fast Feedback Loop - Local Development with Kind, LocalStack, and Act

The best engineers I know have one thing in common: tight feedback loops. They see results in seconds, not minutes. They iterate dozens of times before pushing code.

The worst development experiences? Push-to-test cycles. Change code, commit, push, wait for CI, watch it fail, repeat. Each iteration costs minutes. Multiply by dozens of iterations per feature, and you’ve lost hours.

This post shows you how to build a complete local development environment using three tools:

  • Kind - Kubernetes on your laptop
  • LocalStack - AWS services locally
  • Act - GitHub Actions without pushing

Together, they give you the entire cloud stack running locally with instant feedback.

TL;DR

  • Kind runs real Kubernetes clusters in Docker
  • LocalStack emulates AWS services locally
  • Act runs GitHub Actions on your machine
  • Combined: test infrastructure, cloud services, and CI locally
  • Feedback in seconds, not minutes

The Stack

┌─────────────────────────────────────────────────────────────┐
│                     Your Laptop                              │
│                                                              │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │    Kind     │  │  LocalStack │  │        Act          │  │
│  │ (Kubernetes)│  │   (AWS)     │  │  (GitHub Actions)   │  │
│  │             │  │             │  │                     │  │
│  │ - Pods      │  │ - S3        │  │ - Build workflows   │  │
│  │ - Services  │  │ - Lambda    │  │ - Test workflows    │  │
│  │ - Ingress   │  │ - DynamoDB  │  │ - Deploy workflows  │  │
│  │ - Helm      │  │ - SQS/SNS   │  │                     │  │
│  └─────────────┘  └─────────────┘  └─────────────────────┘  │
│                                                              │
│                    ↓ All running in Docker ↓                 │
└─────────────────────────────────────────────────────────────┘

Setting Up the Environment

Prerequisites

# Install Docker (required for all three tools)
# https://docs.docker.com/get-docker/

# Install kubectl
brew install kubectl

# Install Kind
brew install kind

# Install LocalStack
pip install localstack

# Install Act
brew install act

Docker Compose for Everything

# docker-compose.yml
version: '3.8'

services:
  localstack:
    image: localstack/localstack:latest
    ports:
      - "4566:4566"
    environment:
      - DEBUG=1
      - PERSISTENCE=1
    volumes:
      - "./localstack-data:/var/lib/localstack"
      - "/var/run/docker.sock:/var/run/docker.sock"
      - "./init-localstack.sh:/etc/localstack/init/ready.d/init.sh"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
      interval: 10s
      timeout: 5s
      retries: 3

networks:
  default:
    name: local-dev

Kind Cluster Configuration

# kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: local-dev
nodes:
  - role: control-plane
    extraPortMappings:
      - containerPort: 30080
        hostPort: 8080
        protocol: TCP
      - containerPort: 30443
        hostPort: 8443
        protocol: TCP
  - role: worker
  - role: worker

Initialization Script

#!/bin/bash
# init-localstack.sh

# Create S3 buckets
awslocal s3 mb s3://app-artifacts
awslocal s3 mb s3://app-uploads

# Create DynamoDB tables
awslocal dynamodb create-table \
  --table-name AppData \
  --attribute-definitions AttributeName=id,AttributeType=S \
  --key-schema AttributeName=id,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST

# Create SQS queues
awslocal sqs create-queue --queue-name app-events
awslocal sqs create-queue --queue-name app-events-dlq

# Create secrets
awslocal secretsmanager create-secret \
  --name app/database \
  --secret-string '{"host":"postgres","port":5432,"username":"app","password":"secret"}'

echo "LocalStack initialized!"

Makefile for Everything

# Makefile
.PHONY: up down kind-up kind-down localstack-up localstack-down test-ci clean

# Start everything
up: localstack-up kind-up
	@echo "✓ Local environment ready"
	@echo "  Kubernetes: kubectl get nodes"
	@echo "  LocalStack: http://localhost:4566"

# Stop everything
down: kind-down localstack-down
	@echo "✓ Local environment stopped"

# Kind cluster
kind-up:
	@kind get clusters | grep -q local-dev || kind create cluster --config kind-config.yaml
	@kubectl wait --for=condition=ready node --all --timeout=60s
	@echo "✓ Kind cluster ready"

kind-down:
	@kind delete cluster --name local-dev 2>/dev/null || true

# LocalStack
localstack-up:
	@docker-compose up -d localstack
	@echo "Waiting for LocalStack..."
	@until curl -s http://localhost:4566/_localstack/health | grep -q '"s3": "running"'; do sleep 2; done
	@echo "✓ LocalStack ready"

localstack-down:
	@docker-compose down

# Run CI locally
test-ci:
	@act -j test

# Run specific workflow
ci-build:
	@act -j build

ci-deploy:
	@act -j deploy --secret-file .secrets

# Deploy to local Kind cluster
deploy-local:
	@kubectl apply -k k8s/overlays/local

# Clean everything
clean: down
	@rm -rf localstack-data
	@docker volume prune -f
	@echo "✓ Cleaned"

Kind: Local Kubernetes

Create Cluster

# Create cluster
kind create cluster --config kind-config.yaml

# Verify
kubectl get nodes
# NAME                      STATUS   ROLES           AGE   VERSION
# local-dev-control-plane   Ready    control-plane   1m    v1.28.0
# local-dev-worker          Ready    <none>          1m    v1.28.0
# local-dev-worker2         Ready    <none>          1m    v1.28.0

Load Local Images

# Build your app
docker build -t myapp:dev .

# Load into Kind (no registry needed)
kind load docker-image myapp:dev --name local-dev

# Deploy
kubectl run myapp --image=myapp:dev --image-pull-policy=Never

Local Ingress

# Install Nginx Ingress
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml

# Wait for it
kubectl wait --namespace ingress-nginx \
  --for=condition=ready pod \
  --selector=app.kubernetes.io/component=controller \
  --timeout=90s
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myapp
spec:
  ingressClassName: nginx
  rules:
    - host: myapp.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: myapp
                port:
                  number: 80
# Add to /etc/hosts
echo "127.0.0.1 myapp.local" | sudo tee -a /etc/hosts

# Access via browser
open http://myapp.local:8080

Connecting Kind and LocalStack

Your app in Kubernetes needs to talk to LocalStack. Since both run in Docker, use Docker networking.

Configure AWS SDK in Pods

# k8s/overlays/local/deployment-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  template:
    spec:
      containers:
        - name: myapp
          env:
            - name: AWS_ENDPOINT_URL
              value: "http://host.docker.internal:4566"
            - name: AWS_ACCESS_KEY_ID
              value: "test"
            - name: AWS_SECRET_ACCESS_KEY
              value: "test"
            - name: AWS_DEFAULT_REGION
              value: "us-east-1"

Or Use ExternalName Service

# k8s/base/localstack-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: localstack
spec:
  type: ExternalName
  externalName: host.docker.internal
  ports:
    - port: 4566

Now pods can reach LocalStack at http://localstack:4566.


Act: Local CI/CD

GitHub Actions Workflow

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm test

  build:
    runs-on: ubuntu-latest
    needs: test
    steps:
      - uses: actions/checkout@v4
      - name: Build Docker image
        run: docker build -t myapp:${{ github.sha }} .

  deploy:
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to Kubernetes
        run: |
          kubectl apply -k k8s/overlays/production

Run Locally with Act

# Run all jobs
act

# Run specific job
act -j test
act -j build

# With secrets
act --secret-file .secrets

# Skip deploy (it needs real cluster)
act -j test -j build

Act Configuration

# .actrc
-P ubuntu-latest=catthehacker/ubuntu:act-22.04
--secret-file .secrets
--env-file .env.local
--container-daemon-socket /var/run/docker.sock

Complete Development Workflow

1. Start Environment

make up
# ✓ LocalStack ready
# ✓ Kind cluster ready

2. Develop Locally

# Run app directly (fastest feedback)
npm run dev

# Or in Docker
docker-compose up app

3. Test Against Local Services

# App talks to LocalStack S3
curl http://localhost:3000/upload -F "file=@test.txt"

# Verify in LocalStack
awslocal s3 ls s3://app-uploads/

4. Test CI Locally

# Before pushing, verify CI will pass
make test-ci

# Fix any issues locally
# Iterate in seconds, not minutes

5. Test Kubernetes Deployment

# Build and load image
docker build -t myapp:dev .
kind load docker-image myapp:dev --name local-dev

# Deploy to local cluster
make deploy-local

# Test it
curl http://myapp.local:8080/health

6. Push with Confidence

# Everything works locally, now push
git add .
git commit -m "Feature complete"
git push

# CI will pass because you already tested it

Debugging Tips

Kind: Access Node

# Shell into node
docker exec -it local-dev-worker /bin/bash

# Check container runtime
crictl ps

LocalStack: Check Logs

docker-compose logs -f localstack

# Or check specific service
curl http://localhost:4566/_localstack/health | jq

Act: Verbose Mode

# See everything
act -v

# Interactive mode
act --reuse
docker exec -it act-CI-test /bin/bash

Performance Tips

1. Pre-pull Images

# Pull commonly used images once
docker pull catthehacker/ubuntu:act-22.04
docker pull localstack/localstack:latest
docker pull kindest/node:v1.28.0

2. Use Persistence

# LocalStack data persists between restarts
environment:
  - PERSISTENCE=1
volumes:
  - "./localstack-data:/var/lib/localstack"

3. Reuse Kind Cluster

# Don't delete cluster between sessions
# Just restart containers if needed
docker start local-dev-control-plane local-dev-worker local-dev-worker2

4. Parallel Testing

# Run different tests in parallel
make test-ci &
make deploy-local &
wait

When to Test Where

WhatLocalCIStaging
Unit tests✅ Primary✅ Verify-
Integration tests✅ Primary✅ Verify-
Kubernetes manifests✅ Kind✅ Verify
AWS integrations✅ LocalStack✅ Real AWS
CI workflows✅ Act✅ Primary-
Performance tests--✅ Primary
E2E tests✅ Optional✅ Primary

Quick Reference

# Start everything
make up

# Stop everything
make down

# Test CI locally
make test-ci

# Deploy to local Kubernetes
docker build -t myapp:dev .
kind load docker-image myapp:dev --name local-dev
kubectl apply -k k8s/overlays/local

# Check LocalStack
awslocal s3 ls
awslocal dynamodb list-tables

# Check Kubernetes
kubectl get pods
kubectl logs -f deployment/myapp

# Clean slate
make clean

Conclusion

Fast feedback loops are a superpower. With Kind, LocalStack, and Act, you can:

  1. Test Kubernetes changes - No waiting for cloud clusters
  2. Test AWS integrations - No cloud costs or permissions
  3. Test CI pipelines - No push-and-pray

The investment in local tooling pays off exponentially. An engineer with 10-second feedback loops will outproduce one with 10-minute loops by 10x or more.

Set up your local environment today. Your future self will thank you.


References

Found this helpful?

Comments