-
Notifications
You must be signed in to change notification settings - Fork 0
Step19: Add terraform and deploy workflow #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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: | ||
|
|
||
| concurrency: | ||
| group: deploy-backend | ||
| cancel-in-progress: false | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| 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). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| .terraform/ | ||
| *.tfstate | ||
| *.tfstate.backup | ||
| *.tfvars | ||
| !*.tfvars.example |
| 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"] | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"] | ||
| } | ||
| } | ||
| 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 | ||
| } |
There was a problem hiding this comment.
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?