Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1 +1,42 @@
/target
target/
Dockerfile*
.dockerignore
.git/
.gitignore
*.md
README*
CHANGELOG*
LICENSE*
.env
.env.*
node_modules/
.sst/
.cache/
test/
docs/
*.log
.DS_Store

# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~

# OS files
.DS_Store
Thumbs.db

# Temporary files
*.tmp
*.temp

# Build artifacts that don't affect the build
*.deb
*.rpm
*.tar.gz
*.zip

# sst
.sst
153 changes: 153 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# TinyCloud GitHub Workflows

This directory contains automated deployment workflows for TinyCloud using SST and AWS.

## Workflows

### 1. PR Preview Deploy (`pr-deploy.yml`)
- **Triggers**: On PR open, synchronize, or reopen
- **Actions**:
- **Build & Push**: Builds Docker image and pushes to ECR with tag `pr-{number}`
- **Deploy**: Creates isolated environment with stage name `pr-{number}`
- **Infrastructure**: Deploys with its own database (Aurora Serverless)
- **Notification**: Posts/updates a comment with the preview URL
- **Optimization**: Uses smaller resources and pre-built containers to save costs and time

### 2. PR Preview Cleanup (`pr-cleanup.yml`)
- **Triggers**: On PR close
- **Actions**:
- Removes all AWS resources for the PR
- Updates the PR comment to show cleanup status
- Ensures no orphaned resources

### 3. Production Deploy (`deploy-production.yml`)
- **Triggers**: On push to `main` branch
- **Actions**:
- **Test**: Runs Rust tests before deployment
- **Build & Push**: Builds optimized Docker image and pushes to ECR with tags `latest` and `main-{sha}`
- **Deploy**: Deploys to production stage using pre-built container
- **Resources**: Uses production-grade resources
- **Record**: Creates GitHub deployment record
- **Cleanup**: Configures ECR lifecycle policies to manage image retention

## Required GitHub Secrets

Set these in your repository's Settings → Secrets:

### AWS Deployment
- `AWS_DEPLOY_ROLE_ARN`: ARN of the IAM role for GitHub Actions (uses OIDC)

### TinyCloud Secrets
- `TINYCLOUD_AWS_ACCESS_KEY_ID`: AWS access key for TinyCloud S3 operations
- `TINYCLOUD_AWS_SECRET_ACCESS_KEY`: AWS secret key for TinyCloud S3 operations
- `PROD_TINYCLOUD_KEYS_SECRET`: Production static key secret (base64 encoded, 32+ bytes)
- `PROD_TINYCLOUD_AWS_ACCESS_KEY_ID`: Production AWS access key
- `PROD_TINYCLOUD_AWS_SECRET_ACCESS_KEY`: Production AWS secret key

## AWS IAM Setup

### Quick Fix (if you get IAM permissions error)

If deployment fails with `iam:CreateRole` permission denied:

```bash
aws iam attach-role-policy \
--role-name GitHubActions-TinyCloud-Deploy \
--policy-arn arn:aws:iam::aws:policy/IAMFullAccess
```

### Secure Setup (Recommended)

For new setups, use the secure script with minimal permissions:

```bash
cd scripts
./setup-github-oidc-secure.sh YOUR_AWS_ACCOUNT_ID YOUR_ORG/REPO_NAME
```

This creates:
- OIDC provider for GitHub Actions
- IAM role with trust policy
- Custom policy with only required IAM permissions (not full IAM access)

### Manual Setup

1. Run the basic setup script:
```bash
./scripts/setup-github-oidc.sh YOUR_AWS_ACCOUNT_ID YOUR_ORG/REPO_NAME
```

2. The script attaches these policies:
- `PowerUserAccess` (for most AWS services)
- `IAMFullAccess` (for ECS role creation)

### Why IAM Permissions Are Needed

SST creates IAM roles for:
- ECS task execution roles
- ECS service roles
- Lambda execution roles (if using functions)
- Other service-linked roles

The deployment fails without IAM permissions because PowerUserAccess specifically excludes IAM and Organizations services.

## Container Optimization

### ECR Setup

Before first deployment, set up the ECR repository:

```bash
./scripts/setup-ecr.sh
```

This creates:
- ECR repository named `tinycloud`
- Lifecycle policies for automatic image cleanup
- Security scanning enabled

### Build Optimization Strategy

**Build Once, Deploy Everywhere:**
1. **GitHub Actions**: Builds Docker image with Rust compilation
2. **ECR Storage**: Stores tagged images (`pr-123`, `main-abc1234`, `latest`)
3. **SST Deploy**: Uses pre-built image, skips compilation entirely

**Benefits:**
- ⚡ **Faster deployments**: No Rust compilation during deploy (5-10x faster)
- 🔄 **Reliable retries**: Same image for retries, no rebuild needed
- 🎯 **Consistent environments**: Exact same container in test and production
- 💰 **Cost savings**: Less compute time in deployment phase

**Image Tagging Strategy:**
- PR environments: `pr-123`, `pr-123-abc1234`
- Production: `latest`, `main-abc1234`
- Automatic cleanup via lifecycle policies

### Caching Strategy

- **Docker layer cache**: Shared between workflow runs via GitHub Actions cache
- **Cargo dependencies**: Cached using cargo-chef in multi-stage build
- **Incremental builds**: Only changed layers rebuilt

## Environment Isolation

Each PR gets:
- Isolated Aurora Serverless database
- Separate S3 bucket for block storage
- Unique secrets and configuration
- Independent scaling settings

## Cost Optimization

PR environments are configured to minimize costs:
- Aurora auto-pauses after 10 minutes of inactivity
- Smaller container sizes (0.5 vCPU, 1GB RAM)
- Maximum 2 containers (vs 20 in production)
- Automatic cleanup on PR close

## Monitoring

- Check deployment status in GitHub Actions tab
- View SST console: `npx sst console --stage pr-123`
- CloudWatch logs available in AWS Console
176 changes: 176 additions & 0 deletions .github/workflows/deploy-production.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
name: Production Deploy

on:
push:
branches: [main]
workflow_dispatch:

permissions:
id-token: write
contents: read

env:
AWS_REGION: us-east-1
ECR_REPOSITORY: tinycloud

jobs:
build-and-test:
runs-on: ubuntu-latest
outputs:
image: ${{ steps.image.outputs.image }}

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
cache: true

- name: Run tests
run: cargo test

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}

- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2

- name: Create ECR repository if it doesn't exist
run: |
aws ecr describe-repositories --repository-names ${{ env.ECR_REPOSITORY }} || \
aws ecr create-repository --repository-name ${{ env.ECR_REPOSITORY }} --image-scanning-configuration scanOnPush=true

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:latest
${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:main-${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64

- name: Output image
id: image
run: |
echo "image=${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:main-${{ github.sha }}" >> $GITHUB_OUTPUT

deploy:
needs: build-and-test
runs-on: ubuntu-latest
environment: production

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install Bun
uses: oven-sh/setup-bun@v1

- name: Install dependencies
run: bun install

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}

- name: Deploy to Production
id: deploy
env:
STAGE: production
TINYCLOUD_IMAGE: ${{ needs.build-and-test.outputs.image }}
run: |
# Set production secrets (these should already exist)
npx sst secret set TINYCLOUD_KEYS_SECRET "${{ secrets.PROD_TINYCLOUD_KEYS_SECRET }}" --stage $STAGE
npx sst secret set AWS_ACCESS_KEY_ID "${{ secrets.PROD_TINYCLOUD_AWS_ACCESS_KEY_ID }}" --stage $STAGE
npx sst secret set AWS_SECRET_ACCESS_KEY "${{ secrets.PROD_TINYCLOUD_AWS_SECRET_ACCESS_KEY }}" --stage $STAGE

# Deploy with production stage and pre-built image
npx sst deploy --stage $STAGE

# Capture outputs
SERVICE_URL=$(npx sst output --stage $STAGE --key serviceUrl)
echo "url=$SERVICE_URL" >> $GITHUB_OUTPUT
echo "Production URL: $SERVICE_URL"

- name: Create deployment record
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const deployment = await github.rest.repos.createDeployment({
owner: context.repo.owner,
repo: context.repo.repo,
ref: context.sha,
environment: 'production',
required_contexts: [],
auto_merge: false,
});

await github.rest.repos.createDeploymentStatus({
owner: context.repo.owner,
repo: context.repo.repo,
deployment_id: deployment.data.id,
state: 'success',
environment_url: '${{ steps.deploy.outputs.url }}',
description: 'Deployed to production',
});

- name: Configure ECR lifecycle policy
run: |
# Keep only the latest 10 production images
cat <<EOF > lifecycle-policy.json
{
"rules": [
{
"rulePriority": 1,
"description": "Keep last 10 production images",
"selection": {
"tagStatus": "tagged",
"tagPrefixList": ["main-"],
"countType": "imageCountMoreThan",
"countNumber": 10
},
"action": {
"type": "expire"
}
},
{
"rulePriority": 2,
"description": "Remove untagged images after 1 day",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 1
},
"action": {
"type": "expire"
}
}
]
}
EOF

aws ecr put-lifecycle-policy \
--repository-name ${{ env.ECR_REPOSITORY }} \
--lifecycle-policy-text file://lifecycle-policy.json
Loading