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
75 changes: 75 additions & 0 deletions .github/workflows/deploy-backend.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: Deploy Backend

# Required GitHub secrets:
# GCP_SA_KEY - Service account JSON key (base64 encoded)
# GCP_VM_IP - External IP of the GCP VM
# GCP_VM_SSH_PRIVATE_KEY - SSH private key for connecting to the VM

on:
push:
branches: [main]
paths: [backend/**]
workflow_dispatch:
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this empty workflow dispatch key?


concurrency:
group: deploy-backend
cancel-in-progress: false
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does this concurrency block do? it should queue if there is an existing deployment not deploy 2 pipelines at the same time


jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Run tests
run: go test ./backend/... -cover

deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Build static binary
run: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o smolterms ./backend/cmd/server/main.go
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does the CGO_ENabled=0 flag do


- name: Set up SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.GCP_VM_SSH_PRIVATE_KEY }}" > ~/.ssh/id_deploy
chmod 600 ~/.ssh/id_deploy
ssh-keyscan -H ${{ secrets.GCP_VM_IP }} >> ~/.ssh/known_hosts

- name: Copy binary to VM
run: |
scp -i ~/.ssh/id_deploy smolterms deploy@${{ secrets.GCP_VM_IP }}:/tmp/smolterms

- name: Deploy and restart service
run: |
ssh -i ~/.ssh/id_deploy deploy@${{ secrets.GCP_VM_IP }} << 'EOF'
sudo mv /tmp/smolterms /opt/smolterms/smolterms
sudo chown smolterms:smolterms /opt/smolterms/smolterms
sudo chmod 755 /opt/smolterms/smolterms
sudo systemctl restart smolterms
EOF

- name: Health check
run: |
for i in $(seq 1 10); do
if curl -sf http://${{ secrets.GCP_VM_IP }}:8080/api/v1/health; then
echo "Health check passed"
exit 0
fi
echo "Attempt $i/10 failed, retrying in 3s..."
sleep 3
done
echo "Health check failed after 30 seconds"
exit 1
179 changes: 179 additions & 0 deletions infra/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# SmolTerms Infrastructure

Terraform configuration for deploying SmolTerms on GCP's always-free tier (1x e2-micro VM in us-central1).

## Prerequisites

- [Terraform CLI](https://developer.hashicorp.com/terraform/install) (>= 1.0)
- [gcloud CLI](https://cloud.google.com/sdk/docs/install)
- GCP account with billing enabled (free tier still requires a billing account)

## GCP Project Setup

### 1. Create a GCP Project

```bash
gcloud projects create YOUR_PROJECT_ID --name="SmolTerms"
gcloud config set project YOUR_PROJECT_ID
```

Link a billing account (required even for free tier):

```bash
gcloud billing accounts list
gcloud billing projects link YOUR_PROJECT_ID --billing-account=BILLING_ACCOUNT_ID
```

### 2. Enable Required APIs

```bash
gcloud services enable compute.googleapis.com
gcloud services enable iam.googleapis.com
```

### 3. Authenticate for Terraform

For local development, use Application Default Credentials:

```bash
gcloud auth application-default login
```

## Deploy with Terraform

```bash
cd infra/terraform

# Create your variables file
cp terraform.tfvars.example terraform.tfvars
# Edit terraform.tfvars and set your project_id

terraform init
terraform plan
terraform apply
```

After `terraform apply`, the VM external IP is printed as output. Note this IP — it's needed for the GitHub Actions workflow and DNS configuration.

## Service Account for GitHub Actions

Terraform creates a `smolterms-deploy` service account with these minimal roles:

| Role | Purpose |
|------|---------|
| `roles/compute.osAdminLogin` | SSH into the VM via OS Login |
| `roles/compute.viewer` | Read instance metadata (get IP, zone) |
| `roles/iam.serviceAccountUser` | Act as the service account |

### Create a Service Account Key

After `terraform apply`, create a JSON key for GitHub Actions:

```bash
gcloud iam service-accounts keys create sa-key.json \
--iam-account=smolterms-deploy@YOUR_PROJECT_ID.iam.gserviceaccount.com
```

**Important:** This key grants deploy access to your VM. Do not commit it to the repository. Delete it from your local machine after adding it to GitHub.

### Generate an SSH Key for Deployment

Create a dedicated SSH key pair for the deploy workflow:

```bash
ssh-keygen -t ed25519 -C "smolterms-deploy" -f deploy_key -N ""
```

Add the **public** key to the VM's authorized keys for a `deploy` user:

```bash
gcloud compute ssh smolterms --zone=us-central1-a --command="
sudo useradd -m -s /bin/bash deploy
sudo mkdir -p /home/deploy/.ssh
sudo tee /home/deploy/.ssh/authorized_keys << 'PUBKEY'
$(cat deploy_key.pub)
PUBKEY
sudo chown -R deploy:deploy /home/deploy/.ssh
sudo chmod 700 /home/deploy/.ssh
sudo chmod 600 /home/deploy/.ssh/authorized_keys
echo 'deploy ALL=(ALL) NOPASSWD: /bin/mv, /bin/chown, /bin/chmod, /bin/systemctl' | sudo tee /etc/sudoers.d/deploy
"
```

### Add to GitHub Actions Repository Secrets

Go to your GitHub repo → **Settings** → **Secrets and variables** → **Actions** → **New repository secret** and add:

| Secret Name | Value |
|-------------|-------|
| `GCP_SA_KEY` | Contents of `sa-key.json` (base64 encoded) |
| `GCP_VM_IP` | VM external IP (from `terraform output instance_ip`) |
| `GCP_VM_SSH_PRIVATE_KEY` | Contents of `deploy_key` (the private key) |

Environment variables on the VM (set separately, see below):

| Variable | Purpose |
|----------|---------|
| `ANTHROPIC_API_KEY` | Anthropic API key for LLM analysis |
| `OPENAI_API_KEY` | OpenAI API key for embeddings |

### Clean Up Local Key Files

```bash
rm sa-key.json deploy_key deploy_key.pub
```

## VM Details

- **Machine type:** e2-micro (GCP always-free tier)
- **Region/Zone:** us-central1-a (free tier eligible)
- **OS:** Debian 12
- **Boot disk:** 30 GB pd-standard (free tier limit)
- **Open ports:** 22 (SSH), 8080 (API)

### Application Layout on VM

```
/opt/smolterms/
├── smolterms # Go binary (deployed by GitHub Actions)
└── .env # Environment variables (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)
```

The `smolterms` systemd service runs `/opt/smolterms/smolterms` as the `smolterms` user, reading environment variables from `/opt/smolterms/.env`.

### Managing the Service

SSH into the VM:

```bash
gcloud compute ssh smolterms --zone=us-central1-a
```

Then:

```bash
sudo systemctl status smolterms # Check status
sudo systemctl restart smolterms # Restart after deploy
sudo journalctl -u smolterms -f # Follow logs
```

### Setting Environment Variables on the VM

```bash
sudo tee /opt/smolterms/.env << 'EOF'
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
PORT=8080
EOF
sudo chown smolterms:smolterms /opt/smolterms/.env
sudo chmod 600 /opt/smolterms/.env
```

## Free Tier Limits

GCP's always-free tier includes:
- 1x e2-micro VM in us-central1, us-west1, or us-east1
- 30 GB pd-standard persistent disk
- 1 GB outbound network (to most destinations, excluding China and Australia)

If you exceed these limits, charges will apply. Monitor usage in the [GCP Console](https://console.cloud.google.com/billing).
5 changes: 5 additions & 0 deletions infra/terraform/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.terraform/
*.tfstate
*.tfstate.backup
*.tfvars
!*.tfvars.example
107 changes: 107 additions & 0 deletions infra/terraform/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
terraform {
required_version = ">= 1.0"

required_providers {
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
}
}

provider "google" {
project = var.project_id
region = var.region
zone = var.zone
}

# --- Service Account for GitHub Actions CI/CD ---

resource "google_service_account" "deploy" {
account_id = "smolterms-deploy"
display_name = "SmolTerms Deploy"
description = "Service account for GitHub Actions to deploy SmolTerms via SSH/SCP"
}

resource "google_project_iam_member" "deploy_os_login" {
project = var.project_id
role = "roles/compute.osAdminLogin"
member = "serviceAccount:${google_service_account.deploy.email}"
}

resource "google_project_iam_member" "deploy_sa_user" {
project = var.project_id
role = "roles/iam.serviceAccountUser"
member = "serviceAccount:${google_service_account.deploy.email}"
}

resource "google_project_iam_member" "deploy_instance_viewer" {
project = var.project_id
role = "roles/compute.viewer"
member = "serviceAccount:${google_service_account.deploy.email}"
}

# --- Network / Firewall ---

resource "google_compute_firewall" "allow_ssh" {
name = "smolterms-allow-ssh"
network = "default"

allow {
protocol = "tcp"
ports = ["22"]
}

source_ranges = ["0.0.0.0/0"]
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can i use a env var from github env vars over here to set this to my own public ip?

target_tags = ["smolterms"]
}

resource "google_compute_firewall" "allow_http" {
name = "smolterms-allow-http"
network = "default"

allow {
protocol = "tcp"
ports = ["8080"]
}

source_ranges = ["0.0.0.0/0"]
target_tags = ["smolterms"]
}

# --- Compute Instance ---

resource "google_compute_instance" "smolterms" {
name = "smolterms"
machine_type = "e2-micro"
zone = var.zone

tags = ["smolterms"]

boot_disk {
initialize_params {
image = "debian-cloud/debian-12"
size = 30
type = "pd-standard"
}
}

network_interface {
network = "default"

access_config {
# Ephemeral public IP
}
}

metadata = {
enable-oslogin = "TRUE"
}

metadata_startup_script = file("${path.module}/startup.sh")

service_account {
email = google_service_account.deploy.email
scopes = ["cloud-platform"]
}
}
9 changes: 9 additions & 0 deletions infra/terraform/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
output "instance_ip" {
description = "External IP address of the SmolTerms VM"
value = google_compute_instance.smolterms.network_interface[0].access_config[0].nat_ip
}

output "deploy_service_account_email" {
description = "Email of the deploy service account (for GitHub Actions)"
value = google_service_account.deploy.email
}
Loading