Skip to content

Instantly share code, notes, and snippets.

@kaihendry
Created February 4, 2026 07:44
Show Gist options
  • Select an option

  • Save kaihendry/ce6574744e161d4ba8ba283b8dada85c to your computer and use it in GitHub Desktop.

Select an option

Save kaihendry/ce6574744e161d4ba8ba283b8dada85c to your computer and use it in GitHub Desktop.

Scalable Go Web Service with PostgreSQL on AWS

Summary

Architecture for a Go binary web service with PostgreSQL database, scaling from 5 to hundreds of users over a year. Optimized for operational simplicity and fast iteration.

Why CDK (TypeScript) for Infrastructure

Factor CDK CloudFormation AWS CLI
AI implementation ease ⭐⭐⭐ Best ⭐⭐ Medium ⭐ Poor
Error detection Compile-time Deploy-time Runtime
Refactoring Easy Copy-paste Manual
Single deploy command cdk deploy Multi-step Many calls

CDK advantages for this project:

  • Type safety catches errors before deployment
  • Mature L2 constructs for RDS (DatabaseInstance)
  • App Runner alpha construct (@aws-cdk/aws-apprunner-alpha) works well
  • Source.fromAsset() builds container directly from local Dockerfile
  • Single command deployment: cdk deploy

Lead Time for Changes

Change Type Time Command
Go code change ~30-60 sec docker push → App Runner auto-deploys
CDK infra change ~1-5 min cdk deploy
Add new AWS resource ~2-10 min Edit TypeScript → cdk deploy

Architecture

┌─────────────────────────────────────────────────────────┐
│                     CDK Stack                           │
├─────────────────────────────────────────────────────────┤
│                                                         │
│   ┌─────────────┐     ┌─────────────────────────────┐  │
│   │    ECR      │────▶│      App Runner Service     │  │
│   │  (Go image) │     │  - Auto-scaling (1-10)      │  │
│   └─────────────┘     │  - HTTPS built-in           │  │
│                       │  - Auto-deploy on push      │  │
│                       └──────────────┬──────────────┘  │
│                                      │                  │
│                       ┌──────────────▼──────────────┐  │
│                       │      VPC Connector          │  │
│                       └──────────────┬──────────────┘  │
│                                      │                  │
│   ┌─────────────┐     ┌──────────────▼──────────────┐  │
│   │  Secrets    │────▶│     RDS PostgreSQL          │  │
│   │  Manager    │     │  - db.t4g.micro/small       │  │
│   └─────────────┘     │  - Private subnet           │  │
│                       └─────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘

Project Structure

my-go-app/
├── app/                      # Go application
│   ├── main.go
│   ├── go.mod
│   ├── go.sum
│   └── Dockerfile
├── infra/                    # CDK infrastructure
│   ├── package.json
│   ├── tsconfig.json
│   ├── cdk.json
│   └── lib/
│       └── app-stack.ts      # Main stack definition
└── README.md

CDK Implementation

infra/lib/app-stack.ts

import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as rds from 'aws-cdk-lib/aws-rds';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import * as apprunner from '@aws-cdk/aws-apprunner-alpha';
import { Construct } from 'constructs';
import * as path from 'path';

export class AppStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // VPC with private subnets for RDS
    const vpc = new ec2.Vpc(this, 'AppVpc', {
      maxAzs: 2,
      natGateways: 1,  // Reduce to 0 for cost savings if App Runner doesn't need outbound
    });

    // Database credentials in Secrets Manager
    const dbSecret = new secretsmanager.Secret(this, 'DbSecret', {
      generateSecretString: {
        secretStringTemplate: JSON.stringify({ username: 'postgres' }),
        generateStringKey: 'password',
        excludePunctuation: true,
      },
    });

    // RDS PostgreSQL instance
    const database = new rds.DatabaseInstance(this, 'Database', {
      engine: rds.DatabaseInstanceEngine.postgres({
        version: rds.PostgresEngineVersion.VER_16,
      }),
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T4G,
        ec2.InstanceSize.MICRO,  // Upgrade to SMALL as needed
      ),
      vpc,
      vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
      credentials: rds.Credentials.fromSecret(dbSecret),
      allocatedStorage: 20,
      maxAllocatedStorage: 100,
      databaseName: 'appdb',
      removalPolicy: cdk.RemovalPolicy.SNAPSHOT,  // Change to DESTROY for dev
    });

    // VPC Connector for App Runner to reach RDS
    const vpcConnector = new apprunner.VpcConnector(this, 'VpcConnector', {
      vpc,
      vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
      securityGroups: [database.connections.securityGroups[0]],
    });

    // App Runner service from local Dockerfile
    const service = new apprunner.Service(this, 'AppRunner', {
      source: apprunner.Source.fromAsset({
        imageConfiguration: {
          port: 8080,
          environmentVariables: {
            DB_HOST: database.dbInstanceEndpointAddress,
            DB_PORT: database.dbInstanceEndpointPort,
            DB_NAME: 'appdb',
          },
          environmentSecrets: {
            DB_USER: apprunner.Secret.fromSecretsManager(dbSecret, 'username'),
            DB_PASSWORD: apprunner.Secret.fromSecretsManager(dbSecret, 'password'),
          },
        },
        asset: new cdk.aws_ecr_assets.DockerImageAsset(this, 'GoAppImage', {
          directory: path.join(__dirname, '../../app'),
        }),
      }),
      vpcConnector,
      cpu: apprunner.Cpu.ONE_VCPU,
      memory: apprunner.Memory.TWO_GB,
      autoDeploymentsEnabled: true,
    });

    // Allow App Runner to connect to RDS
    database.connections.allowFrom(vpcConnector, ec2.Port.tcp(5432));

    // Outputs
    new cdk.CfnOutput(this, 'ServiceUrl', {
      value: `https://${service.serviceUrl}`,
    });
    new cdk.CfnOutput(this, 'DbEndpoint', {
      value: database.dbInstanceEndpointAddress,
    });
  }
}

app/Dockerfile

FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main .

FROM alpine:3.19
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]

Deployment Commands

# Initial setup (one time)
cd infra
npm install
cdk bootstrap

# Deploy everything
cdk deploy

# After Go code changes - rebuild and push
cd ../app
docker build -t my-app .
# If auto-deploy enabled, App Runner picks up new image automatically

# Destroy (cleanup)
cdk destroy

Scaling Path

Users Compute Database Est. Monthly Cost
5-50 App Runner (1 instance) db.t4g.micro ~$30-50
50-200 App Runner (2-4 auto) db.t4g.small ~$80-150
200-500 App Runner (4-8 auto) db.t4g.medium + Multi-AZ ~$200-400

Verification Steps

  1. cdk deploy completes without errors
  2. Access the ServiceUrl output - verify HTTPS works
  3. Test database connectivity from the Go app
  4. Check CloudWatch logs for App Runner service
  5. Optional: Load test with k6 or hey to verify auto-scaling

Files to Create

  1. infra/package.json - CDK dependencies
  2. infra/tsconfig.json - TypeScript config
  3. infra/cdk.json - CDK app config
  4. infra/lib/app-stack.ts - Main infrastructure
  5. infra/bin/app.ts - CDK app entry point
  6. app/Dockerfile - Go container build
  7. app/main.go - Your Go application
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment