Skip to content
Merged
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
321 changes: 321 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
name: CD

on:
push:
branches:
- main

jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
deploy-static: ${{ steps.filter.outputs.static }}
deploy-ts-web: ${{ steps.filter.outputs.ts-web }}
deploy-py: ${{ steps.filter.outputs.py }}
steps:
- uses: actions/checkout@v4

- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
static:
- 'static/**'
- 'bin/kamal'
- 'bin/iac'
- 'config/deploy/static.yml'
- '.github/workflows/cd.yml'
ts-web:
- 'ts/**'
- 'bin/kamal'
- 'bin/iac'
- 'config/deploy/ts-web.yml'
- '.github/workflows/cd.yml'
py:
- 'py/**'
- 'bin/kamal'
- 'bin/iac'
- 'config/deploy/py.yml'
- '.github/workflows/cd.yml'

deploy-static:
needs: detect-changes
if: needs.detect-changes.outputs.deploy-static == 'true'
runs-on: ubuntu-latest
environment: production
env:
DOCKER_BUILDKIT: 1
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
CI: true
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.2.2

- name: Install Kamal
run: gem install kamal --version 2.4.0

- name: Install 1Password CLI
uses: 1password/install-cli-action@v1

- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_wrapper: false

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

- name: Verify 1Password Access
run: |
source bin/vault
echo "Testing 1Password access..."
op whoami

- name: Initialize Terraform
run: bin/iac production init -input=false

- name: Setup SSH Agent
run: |
# Create .kamal directory for any temporary files
mkdir -p .kamal
chmod 700 .kamal

# Get SSH key from Terraform
echo "Getting SSH key from Terraform..."
SSH_KEY=$(bin/iac production output -raw ssh_private_key 2>&1)

# Debug output
echo "Output lines: $(echo "$SSH_KEY" | wc -l)"

# Verify we got a key
if [ -z "$SSH_KEY" ] || ! echo "$SSH_KEY" | grep -q "BEGIN.*PRIVATE KEY"; then
echo "Error: Failed to retrieve valid SSH key from Terraform"
echo "Got: $SSH_KEY"
exit 1
fi

echo "Successfully retrieved SSH key"

# Start SSH agent
eval $(ssh-agent -s)

# Add key to agent
echo "Adding SSH key to agent..."
ssh-add - <<< "$SSH_KEY"

# Make agent available to subsequent steps
echo "SSH_AUTH_SOCK=$SSH_AUTH_SOCK" >> $GITHUB_ENV
echo "SSH_AGENT_PID=$SSH_AGENT_PID" >> $GITHUB_ENV

# Verify key was added
echo "Keys in agent:"
ssh-add -l

- name: Setup SSH config
run: |
mkdir -p ~/.ssh
chmod 700 ~/.ssh
cat >> ~/.ssh/config <<EOF
Host *
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
BatchMode yes
EOF
chmod 600 ~/.ssh/config

- name: Deploy Static site
run: |
bin/kamal static production deploy

deploy-ts-web:
needs: detect-changes
if: needs.detect-changes.outputs.deploy-ts-web == 'true'
runs-on: ubuntu-latest
environment: production
env:
DOCKER_BUILDKIT: 1
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
CI: true
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.2.2

- name: Install Kamal
run: gem install kamal --version 2.4.0

- name: Install 1Password CLI
uses: 1password/install-cli-action@v1

- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_wrapper: false

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

- name: Verify 1Password Access
run: |
source bin/vault
echo "Testing 1Password access..."
op whoami

- name: Initialize Terraform
run: bin/iac production init -input=false

- name: Setup SSH Agent
run: |
# Create .kamal directory for any temporary files
mkdir -p .kamal
chmod 700 .kamal

# Get SSH key from Terraform
echo "Getting SSH key from Terraform..."
SSH_KEY=$(bin/iac production output -raw ssh_private_key 2>&1)

# Debug output
echo "Output lines: $(echo "$SSH_KEY" | wc -l)"

# Verify we got a key
if [ -z "$SSH_KEY" ] || ! echo "$SSH_KEY" | grep -q "BEGIN.*PRIVATE KEY"; then
echo "Error: Failed to retrieve valid SSH key from Terraform"
echo "Got: $SSH_KEY"
exit 1
fi

echo "Successfully retrieved SSH key"

# Start SSH agent
eval $(ssh-agent -s)

# Add key to agent
echo "Adding SSH key to agent..."
ssh-add - <<< "$SSH_KEY"

# Make agent available to subsequent steps
echo "SSH_AUTH_SOCK=$SSH_AUTH_SOCK" >> $GITHUB_ENV
echo "SSH_AGENT_PID=$SSH_AGENT_PID" >> $GITHUB_ENV

# Verify key was added
echo "Keys in agent:"
ssh-add -l

- name: Setup SSH config
run: |
mkdir -p ~/.ssh
chmod 700 ~/.ssh
cat >> ~/.ssh/config <<EOF
Host *
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
BatchMode yes
EOF
chmod 600 ~/.ssh/config

- name: Deploy TS Web app
run: |
bin/kamal ts-web production deploy

deploy-py:
needs: detect-changes
if: needs.detect-changes.outputs.deploy-py == 'true'
runs-on: ubuntu-latest
environment: production
env:
DOCKER_BUILDKIT: 1
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
CI: true
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.2.2

- name: Install Kamal
run: gem install kamal --version 2.4.0

- name: Install 1Password CLI
uses: 1password/install-cli-action@v1

- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_wrapper: false

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

- name: Verify 1Password Access
run: |
source bin/vault
echo "Testing 1Password access..."
op whoami

- name: Initialize Terraform
run: bin/iac production init -input=false

- name: Setup SSH Agent
run: |
# Create .kamal directory for any temporary files
mkdir -p .kamal
chmod 700 .kamal

# Get SSH key from Terraform
echo "Getting SSH key from Terraform..."
SSH_KEY=$(bin/iac production output -raw ssh_private_key 2>&1)

# Debug output
echo "Output lines: $(echo "$SSH_KEY" | wc -l)"

# Verify we got a key
if [ -z "$SSH_KEY" ] || ! echo "$SSH_KEY" | grep -q "BEGIN.*PRIVATE KEY"; then
echo "Error: Failed to retrieve valid SSH key from Terraform"
echo "Got: $SSH_KEY"
exit 1
fi

echo "Successfully retrieved SSH key"

# Start SSH agent
eval $(ssh-agent -s)

# Add key to agent
echo "Adding SSH key to agent..."
ssh-add - <<< "$SSH_KEY"

# Make agent available to subsequent steps
echo "SSH_AUTH_SOCK=$SSH_AUTH_SOCK" >> $GITHUB_ENV
echo "SSH_AGENT_PID=$SSH_AGENT_PID" >> $GITHUB_ENV

# Verify key was added
echo "Keys in agent:"
ssh-add -l

- name: Setup SSH config
run: |
mkdir -p ~/.ssh
chmod 700 ~/.ssh
cat >> ~/.ssh/config <<EOF
Host *
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
BatchMode yes
EOF
chmod 600 ~/.ssh/config

- name: Deploy Python app
run: |
bin/kamal py production deploy
1 change: 1 addition & 0 deletions .kamal/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ssh_key
19 changes: 12 additions & 7 deletions bin/kamal
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,18 @@ export SERVER_IP=$("$PROJECT_ROOT/bin/iac" "$STAGE" output -raw server_ip 2>/dev
export SSH_PRIVATE_KEY=$("$PROJECT_ROOT/bin/iac" "$STAGE" output -raw ssh_private_key 2>/dev/null | tail -n -13 || echo "")
export DOCKER_HUB_USERNAME=$("$PROJECT_ROOT/bin/vault" read DOCKER_HUB_USERNAME)
export PROJECT_NAME=$PROJECT_NAME
# write the ssh private key to a temp file with proper permissions
SSH_PRIVATE_KEY_FILE=$(mktemp)
echo "$SSH_PRIVATE_KEY" > "$SSH_PRIVATE_KEY_FILE"
chmod 600 "$SSH_PRIVATE_KEY_FILE"
export SSH_PRIVATE_KEY_FILE
# make it so that the temp file is *ALWAYS* removed even if the script exits abnormally
trap 'print_info "Cleaning up SSH private key file" && rm "$SSH_PRIVATE_KEY_FILE"' EXIT
# Only write SSH key file if not in CI
if [[ -z "${CI:-}" ]]; then
# Local: Use key file
SSH_PRIVATE_KEY_FILE="$PROJECT_ROOT/.kamal/ssh_key"
echo "$SSH_PRIVATE_KEY" > "$SSH_PRIVATE_KEY_FILE"
chmod 600 "$SSH_PRIVATE_KEY_FILE"
export SSH_PRIVATE_KEY_FILE
trap 'print_info "Cleaning up SSH private key file" && rm -f "$SSH_PRIVATE_KEY_FILE"' EXIT
else
# CI: SSH agent is already set up, don't export SSH_PRIVATE_KEY_FILE
print_info "Running in CI - using SSH agent for authentication"
fi

# Determine the hostname for the service given the service name and stage
HOST_NAME=$(get_service_hostname "$SERVICE" "$STAGE")
Expand Down
13 changes: 0 additions & 13 deletions config/deploy/py.yml
Original file line number Diff line number Diff line change
@@ -1,37 +1,25 @@
service: <%= ENV['PROJECT_NAME'] %>-py

# NOTE (amiller68): do not change this!
# It is important that this is kept in sync with
# the name of the service as described in the file
# name.
# i.e. if the file name is py.yml, then the service name must be <%= ENV['PROJECT_NAME'] %>-py
# Container image using Docker Hub
image: <%= ENV['DOCKER_HUB_USERNAME'] %>/<%= ENV['PROJECT_NAME'] %>-py

# Deploy to these servers
servers:
web:
- "<%= ENV['SERVER_IP'].strip %>"

# SSH configuration
ssh:
user: root
keys: ["<%= ENV['SSH_PRIVATE_KEY_FILE'] %>"]
keys_only: true

# Registry configuration for Docker Hub
registry:
username: <%= ENV['DOCKER_HUB_USERNAME'] %>
password:
- DOCKER_HUB_TOKEN

# Builder configuration
builder:
dockerfile: py/Dockerfile
context: "."
arch: amd64

# Proxy configuration for SSL
proxy:
ssl: true
host: <%= ENV['HOST_NAME'] %>
Expand All @@ -51,7 +39,6 @@ env:
- GOOGLE_O_AUTH_CLIENT_ID
- GOOGLE_O_AUTH_CLIENT_SECRET

# Accessories (PostgreSQL database - internal only)
accessories:
postgres:
image: postgres:17-alpine
Expand Down
Loading