AWS + GITHUB
HOW A WEB GAME SHIPS ITSELF
By Shane Brumback · Published · Updated · Intermediate DevOps article · about 12 minute read
No more FTP and no more "works on my machine". Here is the exact AWS and GitHub setup behind Super Soldier Battle and the other Indie Forge Studio games: push to master and the site, the backends and the leaderboards update themselves.
How do AWS and GitHub work together? (Quick Answer)
GitHub holds the code and runs GitHub Actions. On every push to master a workflow signs in to AWS (with an OIDC role or stored secrets), syncs the static site to an S3 bucket, and invalidates CloudFront so players get the new build in minutes. A second workflow tests and deploys Lambda functions with AWS SAM, only when backend folders change. Real-time features run on API Gateway WebSockets, Lambda and DynamoDB, with an always-on relay server on EC2 for fast multiplayer traffic.
Introduction
The 2023 version of this article listed ten generic advantages of using AWS with GitHub. True, but not very useful. This rewrite shows a working pipeline instead: the one that deploys this site, Super Soldier Battle and games like Tommy Gun FPS and Zombie Attack every day.
The idea is simple. GitHub is the single source of truth, and nobody uploads files by hand. Every change is a commit, every commit to master is a deploy, and every deploy can be rolled back with git revert.
If you build Three.js games, start with developing online 3D browser games, then come back here when it is time to ship.
Key Concepts
- GitHub Actions — YAML workflows in .github/workflows that run on GitHub servers when you push, on a schedule or by hand.
- S3 + CloudFront — S3 stores the static files (HTML, JS, textures). CloudFront is the CDN that serves them fast and over HTTPS worldwide.
- Cache invalidation — CloudFront keeps copies at the edge. A deploy ends with create-invalidation so players do not get yesterday's JavaScript.
- AWS SAM — a template.yaml that describes Lambda functions, API routes and DynamoDB tables. sam deploy creates or updates them as one CloudFormation stack.
- Path filters — paths: in a workflow means the Lambda deploy only runs when backend folders change, so a CSS fix never touches the payment code.
- OIDC vs access keys — OIDC lets a workflow assume an IAM role with short-lived credentials. Access keys in repository secrets also work but must be rotated and kept least-privilege.
- WebSocket API + relay — API Gateway WebSockets with Lambda handle connect, disconnect and messages serverlessly. A small EC2 relay keeps one always-on socket server for low-latency game state.
The Super Soldier Battle Stack, Piece by Piece
| Piece | AWS service | How GitHub ships it |
|---|---|---|
| Game pages, Three.js code, textures | S3 + CloudFront | deploy.yml on every push to master |
| 3D models (GLB) | S3 | Uploaded only when the file size changes, so big models are not re-sent every deploy |
| Sign-in, membership, premium code, AI assistant | Lambda + API Gateway + DynamoDB | Deploy Membership workflow: tests, then sam deploy |
| Player profiles and leaderboards | GamePlayerProfile Lambda + DynamoDB (us-west-2) | Code lives in the repo (websocket-api/src); shared by SSB, Space WOW and Tic-Tac-Toe |
| Lobby and game messages | API Gateway WebSocket + Lambda | connect, disconnect and sendMessage handlers in the repo |
| Fast multiplayer state | EC2 relay (relay.supersoldierbattle.com) | An always-on WebSocket relay; the game connects to it over wss:// |
Workflow 1: Ship the Game Site
This is the workflow that runs every time something is pushed to master. The full file is further down. The important choices: --delete keeps S3 identical to the repo, the exclude list keeps server code and huge binaries out of the public bucket, and listing files get a 60 second cache so new tutorials appear quickly.
Workflow 2: Ship the Backend Only When It Changes
Lambda code needs tests and a careful deploy. A separate workflow with a paths: filter runs only when the membership folder changes. It runs the unit tests first, and if any fail, nothing deploys.
# .github/workflows/deploy-membership.yml (trimmed)
on:
push:
branches: [master]
paths: ['membership/**', 'premium-content/**'] # only when backend code changes
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '22' }
- uses: aws-actions/configure-aws-credentials@v4
with: { aws-region: us-east-1, role-to-assume: ${{ vars.DEPLOY_ROLE }} }
- uses: aws-actions/setup-sam@v2
- name: Test, then deploy the Lambda stack
working-directory: membership
run: |
(cd lambda && npm ci && node --test test/*.test.js)
sam build
sam deploy --stack-name site-membership --no-confirm-changeset \
--no-fail-on-empty-changeset --capabilities CAPABILITY_IAM --resolve-s3Secret values such as payment keys are never in the repo or the workflow. SAM reuses the stack's existing parameter values, so the workflow never needs to see them.
Multiplayer: Serverless Sockets Plus an EC2 Relay
Turn-based and lobby traffic fits API Gateway WebSockets well: you pay per message, and Lambda handlers store connections in DynamoDB. Shooter movement is different. Players send many small updates per second, and a single always-on relay process on EC2 forwards them with less latency and cost than one Lambda call per message. Super Soldier Battle uses both, plus the shared GamePlayerProfile Lambda for scores.
// Browser side: a game talks to API Gateway WebSocket and the profile Lambda.
const ws = new WebSocket('wss://YOUR_API_ID.execute-api.us-west-2.amazonaws.com/production');
ws.onopen = () => ws.send(JSON.stringify({ action: 'sendMessage', type: 'join', name: 'Player1' }));
ws.onmessage = (e) => handleGameEvent(JSON.parse(e.data));
// Leaderboard from the shared GamePlayerProfile Lambda (HTTP API + DynamoDB)
const top = await fetch('https://YOUR_PROFILE_API/profile?leaderboard=true&game=ssb')
.then(r => r.json());Security Checklist
- Give the deploy identity only what it needs: s3:PutObject/DeleteObject/ListBucket on one bucket and cloudfront:CreateInvalidation on one distribution.
- Prefer OIDC roles to stored access keys. If you use keys, rotate them and never echo them in logs.
- Keep backend folders, .py scripts and templates out of the public bucket with --exclude.
- Protect master with required checks so a broken test blocks the deploy.
- Use concurrency groups so two pushes never deploy over each other.
Frequently Asked Questions
How do I deploy a website from GitHub to AWS S3?
Create a GitHub Actions workflow that runs on push, configures AWS credentials with aws-actions/configure-aws-credentials, runs aws s3 sync ./ s3://your-bucket --delete, then invalidates CloudFront.
Should I use OIDC or access keys in GitHub Actions?
OIDC is safer because GitHub gets short-lived credentials by assuming an IAM role, and no long-lived key is stored. Access keys in repository secrets work but must be least-privilege and rotated.
Why do players still see the old version after a deploy?
CloudFront is serving cached files. Run aws cloudfront create-invalidation with the paths you changed, or /* , and give HTML and listing files a short Cache-Control max-age.
How do I deploy AWS Lambda functions with GitHub Actions?
Describe them in an AWS SAM template, then in a workflow run your tests, sam build and sam deploy --no-confirm-changeset. Add a paths filter so it only runs when backend code changes.
Can I host a Three.js game on S3?
Yes. A Three.js game is static HTML, JavaScript, textures and models, which S3 and CloudFront serve well. Add CORS headers if other sites load your models or textures.
Do I need servers for browser game multiplayer?
Not always. API Gateway WebSockets with Lambda and DynamoDB handle lobbies, chat and turn-based games serverlessly. Fast action games often add a small always-on relay, such as an EC2 instance, for movement updates.
Is GitHub Actions free for this?
Public repositories get free minutes on standard runners and private repositories get a monthly free allowance. A static site deploy usually takes one to two minutes.
How It Works
- Keep the whole game in one repository — In plain words: HTML, Three.js code, textures and backend code all live in GitHub, with backend folders excluded from the public upload.
- Give GitHub a way into AWS — In plain words: Create an IAM role for OIDC, or a least-privilege IAM user whose keys are stored as repository secrets.
- Sync the site to S3 on every push — In plain words: deploy.yml runs aws s3 sync with --delete and an exclude list for server code, video and large models.
- Upload big models only when they change — In plain words: A size check against S3 avoids re-sending multi-megabyte GLB files on every deploy.
- Invalidate CloudFront — In plain words: The last step clears the CDN cache so players load the new build within minutes.
- Deploy Lambda backends separately — In plain words: A second workflow with a paths filter tests and deploys the SAM stack only when backend code changes.
- Connect real-time play — In plain words: The game uses an API Gateway WebSocket and the GamePlayerProfile Lambda for lobbies and scores, and the EC2 relay for fast multiplayer state.
The Full Code
Replace YOUR-GAME-BUCKET, the role ARN and YOUR_DISTRIBUTION_ID, then save as .github/workflows/deploy.yml. Copy uses a free Google sign-in.
# .github/workflows/deploy.yml
# Push to master -> sync the static game site to S3 -> refresh CloudFront.
# Same shape as the workflow that ships shanebrumback.com and Super Soldier Battle.
name: Upload Website
on:
push:
branches: [master]
workflow_dispatch:
permissions:
contents: read
id-token: write # lets GitHub get short-lived AWS credentials (OIDC)
concurrency:
group: deploy-site
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Option A (recommended): OIDC role, no long-lived keys stored in GitHub.
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-deploy-site
aws-region: us-east-1
# Option B: access keys saved as repository secrets.
# aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
# aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Sync the site to S3 (big binaries and server code stay out)
run: |
aws s3 sync ./ s3://YOUR-GAME-BUCKET --delete \
--exclude ".git/*" --exclude ".github/*" \
--exclude "*.glb" --exclude "*.mp4" --exclude "*.zip" \
--exclude "membership/*" --exclude "websocket-api/*" \
--exclude "*.py" --exclude "template.yaml"
- name: Upload a changed 3D model only when its size differs
run: |
f=models/glb/tommy_gun.glb
[ -f "$f" ] || exit 0
local=$(stat -c %s "$f")
remote=$(aws s3api head-object --bucket YOUR-GAME-BUCKET --key "$f" \
--query ContentLength --output text 2>/dev/null || echo 0)
[ "$local" = "$remote" ] || aws s3 cp "$f" "s3://YOUR-GAME-BUCKET/$f" \
--content-type model/gltf-binary
- name: Short cache for listing files so new pages show fast
run: |
aws s3 cp sitemap.xml s3://YOUR-GAME-BUCKET/sitemap.xml \
--cache-control "max-age=60, must-revalidate" --content-type application/xml
- name: Invalidate CloudFront
run: aws cloudfront create-invalidation --distribution-id YOUR_DISTRIBUTION_ID --paths "/*"
Taking It Further
- Build the game first: developing online 3D browser games with Three.js
- Play the result: Super Soldier Battle, deployed by these exact workflows
- Add a video lobby screen with YouTube Video in Three.js
- See what members get from the Lambda backend: membership: free and premium