@thunderso/cdk-webservice
v0.3.1
Published
Deploy any container web service on ECS Fargate with AWS CDK
Readme
CDK Web Service
Deploy containerized web services on AWS Fargate with an Application Load Balancer, using AWS CDK.
Supports custom domains, environment variables, secrets, and CI/CD with GitHub Actions.
Features
- Deploy any web service as a Docker container to AWS Fargate
- Automatic VPC and ECS Cluster creation
- Application Load Balancer (ALB) with public DNS
- Environment variables and AWS Secrets Manager integration
- Custom domain and SSL via Route53 and Certificate Manager
- CI/CD ready with Github Actions
Prerequisites
You need an AWS account to create and deploy the required resources for the site on AWS.
Before you begin, make sure you have the following:
Node.js and npm: Ensure you have Node.js (v20 or later) and npm installed.
AWS CLI: Install and configure the AWS Command Line Interface.
AWS CDK: Install the AWS CDK globally
npm install -g aws-cdk- Before deploying, bootstrap your AWS environment:
cdk bootstrap aws://your-aws-account-id/us-east-1This package uses the npm package manager and is an ES6+ Module.
Nixpacks Integration
You can use Nixpacks to build your container images automatically. To enable Nixpacks, set buildProps.buildSystem to 'Nixpacks' in your WebServiceProps configuration:
const svcProps: WebServiceProps = {
// ... other props
buildProps: {
buildSystem: 'Nixpacks',
installcmd: 'pnpm install', // optional
buildcmd: 'pnpm run build', // optional
startcmd: 'pnpm start', // optional
environment: [
{ NODE_ENV: 'production' }
],
secrets: [
{ key: 'DB_URL', resource: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:/my-app/DB_URL-abc123' }
]
},
};When enabled, the pipeline will:
- Install Nixpacks in the CodeBuild environment
- Generate a Dockerfile using Nixpacks
- Build and push the image to ECR
- Deploy the image to ECS Fargate
You can customize install, build, and start commands using the respective properties. If not set, Nixpacks will auto-detect the best commands for your project.
For more advanced usage, see the Nixpacks documentation.
Installation
Navigate to your project directory and install the package and its required dependencies:
npm i tsx @thunderso/cdk-webservice --save-devSetup
Login into the AWS console and note the
Account ID. You will need it in the configuration step.Run the following commands to create the required CDK stack entrypoint at
stack/index.ts.
mkdir stack
cd stack
touch index.ts You should adapt the file to your project's needs.
[!NOTE] Use different filenames such as
production.tsanddev.tsfor environments.
Configuration
// stack/index.ts
import { Cdk, WebServiceStack, type WebServiceProps } from "@thunderso/cdk-webservice";
const svcProps: WebServiceProps = {
env: {
account: 'your-account-id',
region: 'us-west-2'
},
application: 'your-application-id',
service: 'your-service-id',
environment: 'production',
rootDir: '', // supports monorepos. e.g. app/
// ... other props
};
new WebServiceStack(
new Cdk.App(),
`${svcProps.application}-${svcProps.service}-${svcProps.environment}-stack`,
svcProps
);Deploy
Run npm run build before you deploy.
By running the following script, the CDK stack will be deployed to AWS.
npx cdk deploy --all --app="npx tsx stack/index.ts" After deployment, the stack outputs the public DNS name of the Application Load Balancer:
Outputs:
LoadBalancerDNS: <your-load-balancer-url>Deploy using GitHub Actions
In your GitHub repository, add a new workflow file under .github/workflows/deploy.yml with the following content:
name: Deploy WebService to AWS
on:
push:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '20'
- run: npm ci
- run: npm run build # if you have a build step
- run: npx cdk deploy --require-approval never --all --app="npx tsx stack/index.ts"
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}Add AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY as repository secrets in GitHub.
Destroy the Stack
If you want to destroy the stack and all its resources (including storage, e.g., access logs), run the following script:
npx cdk destroy --all --app="npx tsx stack/index.ts" Manage Domain with Route53
- Create a hosted zone in Route53 for the desired domain, if you don't have one yet.
This is required to create DNS records for the domain to make the app publicly available on that domain. On the hosted zone details you should see the Hosted zone ID of the hosted zone.
- Request a public regional certificate in the AWS Certificate Manager (ACM) for the desired domain in the same region as the service and validate it, if you don't have one yet.
This is required to provide the app via HTTPS on the public internet. Take note of the displayed ARN for the certificate.
[!IMPORTANT] The certificate must be issued in the same region as the service.
// stack/index.ts
const svcProps: WebServiceProps = {
// ... other props
domain: 'api.example.com',
hostedZoneId: 'XXXXXXXXXXXXXXX',
regionalCertificateArn: 'arn:aws:acm:us-east-1:123456789012:certificate/abcd1234-abcd-1234-abcd-1234abcd1234',
};Configure the Service
Each configuration property provides a means to fine-tune the performance and operational characteristics.
// stack/index.ts
import { CpuArchitecture } from 'aws-cdk-lib/aws-ecs';
const svcProps: WebServiceProps = {
// ... other props
serviceProps: {
// ... other props
dockerFile: 'Dockerfile',
architecture: CpuArchitecture.ARM64,
desiredCount: 1,
cpu: 256,
memorySize: 512,
port: 3000,
},
};dockerFile
Path to your Dockerfile relative to your project directory.
- Type:
text - Examples:
Dockerfile.bun,dir/Dockerfile, - Default: Looks for a
Dockerfilein the root directory.
architecture
Defines the instruction set architecture for the task.
- Type:
CpuArchitecture - Examples:
CpuArchitecture.ARM64,CpuArchitecture.X86_64 - Default: The architecture defaults to
CpuArchitecture.ARM64.
desiredCount
Specifies the number of container instances of your service to run.
- Type:
number - Examples:
1,2,3 - Default:
1
cpu
The amount of CPU units to allocate to the container (per task).
- Type:
number - Examples:
256,512,1024 - Default:
256
memorySize
The amount of memory (in MiB) to allocate to the container (per task).
- Type:
number - Examples:
512,1024,2048 - Default:
512
port
The port number on which your container listens for incoming traffic.
- Type:
number - Examples:
4321,3000,8080 - Default:
3000
Environment variables
Pass environment variables to your service by:
variables: Array of key-value pairs for plain environment variables.secrets: Array of objects withkeyandresource(Secrets Manager ARN). The library automatically adds permissions for Lambda to read these secrets.
To create a plaintext secret in AWS Secrets Manager using the AWS CLI:
aws secretsmanager create-secret --name "your-secret-name" --secret-string "your-secret-value"// stack/index.ts
const svcProps: WebServiceProps = {
serviceProps: {
// ... other props
variables: [
{ PUBLIC_FRONTEND_URL: 'https://example.com' },
{ PUBLIC_ANALYTICS_ID: 'UA-XXXXXX' }
],
secrets: [
{
key: 'DB_URL',
resource: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:/my-app/DB_URL-abc123'
},
{
key: 'DB_KEY',
resource: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:/my-app/DB_KEY-def456'
},
],
},
};Docker example
Here is an example Dockerfile which can serve any Node application:
# ---- Build Stage ----
FROM public.ecr.aws/docker/library/node:22-alpine AS builder
WORKDIR /app
# Copy all files
COPY . .
# Install pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate
# Install dependencies
RUN pnpm install
# Build Next.js app
RUN pnpm run build
# ---- Production Stage ----
FROM public.ecr.aws/docker/library/node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Install pnpm in production image
RUN corepack enable && corepack prepare pnpm@latest --activate
# Copy only necessary files from builder
COPY --from=builder /app/ ./
EXPOSE 3000
CMD ["pnpm", "start"]You might want to add a .dockerignore to minimize the context size:
.DS_Store
.git
.gitignore
node_modules
stack
cdk.outCost
Below are estimated AWS costs for a typical deployment of this stack.
Example: Next.js container in us-east-1 with default configuration.
Baseline Cost (No Users, Always-On Service)
| Component | Monthly Cost (No Free Tier) | |-------------------|----------------------------| | Fargate (1 task) | ~$9 | | ALB | ~$22 | | ECR | $0 (first 500MB free) | | VPC/Subnets | $0 | | CloudWatch Logs | <$1 | | Route53 | $0.50 | | ACM (SSL) | $0 | | Data Transfer | $0 (first 100GB free) | | Total | ~$33/month |
- With Free Tier: $0 (if eligible, for 12 months, and under limits)
- Without Free Tier: ~$33/month
1000 Daily Active Users (DAU) Estimate
Assumptions:
- Each user visits once/day, 5 pageviews, 500KB/page (2.5MB/user/day)
- Total data: 2.5GB/day, ~75GB/month
- ALB: 1 LCU covers up to 25 new connections/sec, 3,000 active connections, 5Gbps, or 1M requests/hour (well within 1 LCU for 1000 DAU)
- 2 Fargate tasks for redundancy
| Component | Monthly Cost (No Free Tier) | |-------------------|----------------------------| | Fargate (2 tasks) | ~$18 | | ALB | ~$22 | | ECR | $0 | | VPC/Subnets | $0 | | CloudWatch Logs | <$1 | | Route53 | $0.50 | | ACM (SSL) | $0 | | Data Transfer | ~$6.75 (75GB @ $0.09/GB) | | Total | ~$47/month |
- With Free Tier: ~$41/month (if still eligible, and under 100GB data out)
- Without Free Tier: ~$47/month
Notes
- These are estimates; actual costs may vary based on usage, region, and AWS pricing changes.
- If you add more tasks, NAT gateways, or use more storage/logs, costs will increase.
- For production, consider scaling, backups, and monitoring costs.
Support
For help or feature requests, open an issue on GitHub.
