diff --git a/.github/workflows/deploy-api.yml b/.github/workflows/deploy-api.yml new file mode 100644 index 0000000..4ec4b2f --- /dev/null +++ b/.github/workflows/deploy-api.yml @@ -0,0 +1,89 @@ +name: Deploy API + +on: + push: + branches: [main, test] + paths: + - 'backend/api/**' + - 'backend/shared/**' + - 'backend/email/**' + - 'common/**' + - '.github/workflows/deploy-api.yml' + +env: + REGION: us-west1 + ZONE: us-west1-b + PROJECT_ID: polylove + SERVICE_NAME: api + +jobs: + deploy: + name: Deploy API + runs-on: ubuntu-latest + environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'development' }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '19' + cache: 'yarn' + + # This caches node_modules based on yarn.lock hash + - name: Cache node_modules + uses: actions/cache@v3 + with: + path: | + **/node_modules + ~/.cache/yarn + key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-modules- + + - name: Install dependencies + working-directory: backend/api + run: yarn install --frozen-lockfile + + - name: Build API + working-directory: backend/api + run: yarn build + + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@v1 + with: + project_id: ${{ env.PROJECT_ID }} + service_account_key: ${{ secrets.GCP_SA_KEY }} + export_default_credentials: true + + - name: Configure Docker for GCP + run: | + gcloud auth configure-docker ${{ env.REGION }}-docker.pkg.dev + + - name: Set deployment variables + run: | + echo "IMAGE_TAG=$(date +%s)-${GITHUB_SHA::7}" >> $GITHUB_ENV + echo "IMAGE_URL=${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/builds/${{ env.SERVICE_NAME }}:${IMAGE_TAG}" >> $GITHUB_ENV + + - name: Build and push Docker image + working-directory: backend/api + run: | + docker build . --tag ${{ env.IMAGE_URL }} --platform linux/amd64 + docker push ${{ env.IMAGE_URL }} + + - name: Install OpenTofu + run: | + wget https://github.com/opentofu/opentofu/releases/download/v1.6.0/tofu_1.6.0_linux_amd64.zip + unzip tofu_1.6.0_linux_amd64.zip + sudo mv tofu /usr/local/bin/ + tofu --version + + - name: Deploy with OpenTofu + working-directory: backend/api + env: + TF_VAR_image_url: ${{ env.IMAGE_URL }} + TF_VAR_env: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} + run: | + tofu init + tofu apply -auto-approve diff --git a/backend/api/main.tf b/backend/api/main.tf index 82f5e63..dee5cbc 100644 --- a/backend/api/main.tf +++ b/backend/api/main.tf @@ -33,6 +33,57 @@ provider "google" { zone = local.zone } +# Service account for the API instances +resource "google_service_account" "api_service_account" { + account_id = "${local.service_name}-sa" + display_name = "Service Account for ${local.service_name}" + description = "Used by the API service instances" +} + +# Grant minimal required permissions +resource "google_project_iam_member" "api_sa_roles" { + for_each = toset([ + "roles/secretmanager.secretAccessor", # To access secrets + "roles/monitoring.metricWriter", # To write metrics + "roles/logging.logWriter", # To write logs + "roles/storage.objectViewer", # To read from storage + ]) + + project = local.project + role = each.key + member = "serviceAccount:${google_service_account.api_service_account.email}" +} + +# Service account for GitHub Actions +resource "google_service_account" "github_actions" { + account_id = "github-actions" + display_name = "GitHub Actions" + description = "Used by GitHub Actions for CI/CD" +} + +# Grant required permissions to GitHub Actions SA +resource "google_project_iam_member" "github_actions_roles" { + for_each = toset([ + "roles/compute.admin", # To manage compute resources + "roles/artifactregistry.writer", # To push Docker images + ]) + + project = local.project + role = each.key + member = "serviceAccount:${google_service_account.github_actions.email}" +} + +# Create a service account key for GitHub Actions +resource "google_service_account_key" "github_actions_key" { + service_account_id = google_service_account.github_actions.name +} + +# Output the key (this will be sensitive) +output "github_actions_key" { + value = google_service_account_key.github_actions_key.private_key + sensitive = true +} + # Firebase Storage Buckets # Note you still have to deploy the rules: `firebase deploy --only storage` resource "google_storage_bucket" "public_storage" { @@ -86,7 +137,8 @@ resource "google_compute_instance_template" "api_template" { } service_account { - scopes = ["cloud-platform"] + email = google_service_account.api_service_account.email + scopes = ["cloud-platform"] # We can be more restrictive here if needed } metadata = { diff --git a/setup_api_ci_plan.md b/setup_api_ci_plan.md new file mode 100644 index 0000000..96a6062 --- /dev/null +++ b/setup_api_ci_plan.md @@ -0,0 +1,11 @@ +1. First apply the OpenTofu changes to create the service accounts: + ```bash + cd backend/api + tofu init + tofu apply + ``` +2. Get the GitHub Actions key: + ```bash + tofu output -json github_actions_key | jq -r + ``` +3. Add this key as a GitHub secret named `GCP_SA_KEY` in your repository settings