ci(grafana): remove tenant-specific imports #156
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy LGTM Stack (GKE) | |
| # Trigger workflow - updated datasource handling strategy | |
| # Workflow Description: | |
| # This workflow automates the deployment of the LGTM (Loki, Grafana, Tempo, Mimir) monitoring stack on GKE. | |
| # It optimizes execution time by consolidating environment setup, resource importing, and planning into a single job. | |
| # It also includes binary caching for tools and smoke tests for verification. | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| terraform_action: | |
| description: 'Terraform Action' | |
| required: true | |
| type: choice | |
| options: | |
| - plan | |
| - apply | |
| default: 'plan' | |
| force_destroy: | |
| description: 'Force destroy storage buckets (use with caution)' | |
| required: true | |
| type: boolean | |
| default: false | |
| push: | |
| branches: | |
| - main | |
| - feat/5-Implement-data-isolation-and-multitenancy | |
| paths: | |
| - 'lgtm-stack/terraform/**' | |
| - '.github/workflows/deploy-lgtm-gke.yaml' | |
| # pull_request: | |
| # paths: | |
| # - 'lgtm-stack/terraform/**' | |
| # - '.github/workflows/deploy-lgtm-gke.yaml' | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| id-token: write | |
| actions: write | |
| env: | |
| TERRAFORM_VERSION: '1.6.0' | |
| KUBECTL_VERSION: '1.28.0' | |
| WORKING_DIR: 'lgtm-stack/terraform' | |
| CLOUD_PROVIDER: 'gke' | |
| jobs: | |
| # Job 1: Terraform Plan & Environment Setup | |
| # This consolidated job handles the heavy lifting of provisioning the environment, | |
| # importing existing resources, and generating the execution plan. | |
| terraform-plan: | |
| name: Setup & Plan | |
| runs-on: ubuntu-latest | |
| outputs: | |
| plan_exitcode: ${{ steps.plan.outputs.exitcode }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| # Optimization: Cache kubectl binary to avoid downloading it on every run (~30-60s saved) | |
| - name: Cache kubectl | |
| id: cache-kubectl | |
| uses: actions/cache@v4 | |
| with: | |
| path: /usr/local/bin/kubectl | |
| key: ${{ runner.os }}-kubectl-${{ env.KUBECTL_VERSION }} | |
| # Step: Install kubectl only if not cached | |
| - name: Install kubectl | |
| if: steps.cache-kubectl.outputs.cache-hit != 'true' | |
| run: | | |
| curl -LO "https://dl.k8s.io/release/v${{ env.KUBECTL_VERSION }}/bin/linux/amd64/kubectl" | |
| chmod +x kubectl | |
| sudo mv kubectl /usr/local/bin/ | |
| # Step: Authenticate to GCP using service account key | |
| - name: Setup GCP credentials | |
| uses: google-github-actions/auth@v2 | |
| with: | |
| credentials_json: ${{ secrets.GCP_SA_KEY }} | |
| # Step: Configure gcloud and GKE auth plugin | |
| - name: Setup gcloud | |
| uses: google-github-actions/setup-gcloud@v2 | |
| with: | |
| install_components: 'gke-gcloud-auth-plugin' | |
| # Step: Fetch GKE cluster credentials for kubectl/Terraform access | |
| - name: Get GKE credentials | |
| run: | | |
| gcloud container clusters get-credentials ${{ secrets.CLUSTER_NAME }} \ | |
| --region=${{ secrets.CLUSTER_LOCATION }} \ | |
| --project=${{ secrets.GCP_PROJECT_ID }} | |
| # Critical Step: Extract cluster metadata needed for the Terraform Kubernetes provider | |
| - name: Get GKE Cluster Info | |
| id: cluster_info | |
| run: | | |
| ENDPOINT=$(gcloud container clusters describe ${{ secrets.CLUSTER_NAME }} --region=${{ secrets.CLUSTER_LOCATION }} --project=${{ secrets.GCP_PROJECT_ID }} --format='value(endpoint)') | |
| CA_CERT=$(gcloud container clusters describe ${{ secrets.CLUSTER_NAME }} --region=${{ secrets.CLUSTER_LOCATION }} --project=${{ secrets.GCP_PROJECT_ID }} --format='value(masterAuth.clusterCaCertificate)') | |
| echo "endpoint=$ENDPOINT" >> $GITHUB_OUTPUT | |
| echo "ca_cert=$CA_CERT" >> $GITHUB_OUTPUT | |
| # Step: Install Terraform CLI | |
| - name: Setup Terraform | |
| uses: hashicorp/setup-terraform@v3 | |
| with: | |
| terraform_version: ${{ env.TERRAFORM_VERSION }} | |
| terraform_wrapper: false | |
| # Optimization: Consolidated Backend Configuration and Initialization | |
| - name: Initialize Terraform | |
| working-directory: ${{ env.WORKING_DIR }} | |
| run: | | |
| rm -f backend.tf backend-config.tf | |
| bash ../../.github/scripts/configure-backend.sh "${{ env.CLOUD_PROVIDER }}" | |
| terraform init | |
| env: | |
| TF_STATE_BUCKET: ${{ secrets.TF_STATE_BUCKET }} | |
| # Step: Syntax and structural validation | |
| - name: Terraform Validate | |
| working-directory: ${{ env.WORKING_DIR }} | |
| run: terraform validate | |
| # Step: Generate the variable file for the current environment | |
| # IMPORTANT: This must run BEFORE the import step so that the Terraform | |
| # providers (Grafana, Keycloak, etc.) have credentials to authenticate. | |
| - name: Create terraform.tfvars | |
| working-directory: ${{ env.WORKING_DIR }} | |
| run: | | |
| cat > terraform.tfvars <<EOF | |
| cloud_provider = "${{ env.CLOUD_PROVIDER }}" | |
| project_id = "${{ secrets.GCP_PROJECT_ID }}" | |
| cluster_name = "${{ secrets.CLUSTER_NAME }}" | |
| cluster_location = "${{ secrets.CLUSTER_LOCATION }}" | |
| region = "${{ secrets.REGION }}" | |
| environment = "production" | |
| monitoring_domain = "${{ secrets.MONITORING_DOMAIN }}" | |
| letsencrypt_email = "${{ secrets.LETSENCRYPT_EMAIL }}" | |
| grafana_admin_password = "${{ secrets.GRAFANA_ADMIN_PASSWORD }}" | |
| force_destroy = ${{ inputs.force_destroy || 'false' }} | |
| gke_endpoint = "${{ steps.cluster_info.outputs.endpoint }}" | |
| gke_ca_certificate = "${{ steps.cluster_info.outputs.ca_cert }}" | |
| keycloak_url = "${{ secrets.KEYCLOAK_URL }}" | |
| keycloak_realm = "${{ secrets.KEYCLOAK_REALM }}" | |
| keycloak_admin_user = "${{ secrets.KEYCLOAK_ADMIN_USER }}" | |
| keycloak_admin_password = "${{ secrets.KEYCLOAK_PASSWORD }}" | |
| grafana_keycloak_user = "${{ secrets.GRAFANA_KEYCLOAK_USER }}" | |
| grafana_keycloak_email = "${{ secrets.GRAFANA_KEYCLOAK_EMAIL }}" | |
| grafana_keycloak_password = "${{ secrets.GRAFANA_KEYCLOAK_PASSWORD }}" | |
| grafana_url = "https://grafana.${{ secrets.MONITORING_DOMAIN }}" | |
| EOF | |
| # Note: tenants are discovered dynamically from Keycloak by the sync script. | |
| # No 'tenants' variable needed in Terraform. | |
| # Import existing resources AFTER tfvars exist, so Terraform providers | |
| # have credentials (Grafana URL + password) to authenticate during import. | |
| - name: Import existing resources | |
| working-directory: ${{ env.WORKING_DIR }} | |
| run: bash ../../.github/scripts/import-existing-resources.sh | |
| env: | |
| CLOUD_PROVIDER: ${{ env.CLOUD_PROVIDER }} | |
| GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} | |
| NAMESPACE: "observability" | |
| GRAFANA_URL: "https://grafana.${{ secrets.MONITORING_DOMAIN }}" | |
| GRAFANA_ADMIN_PASSWORD: ${{ secrets.GRAFANA_ADMIN_PASSWORD }} | |
| continue-on-error: true | |
| # Critical Step: Generate execution plan | |
| - name: Terraform Plan | |
| id: plan | |
| working-directory: ${{ env.WORKING_DIR }} | |
| run: | | |
| # A standard plan command returns 0 on success (even with changes) and 1 on error. | |
| # We omit -detailed-exitcode to prevent false-positive pipeline failures. | |
| terraform plan -out=tfplan | |
| terraform show tfplan > plan.txt | |
| # Step: Share plan file with the Apply job | |
| - name: Upload plan | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: tfplan | |
| path: | | |
| ${{ env.WORKING_DIR }}/tfplan | |
| ${{ env.WORKING_DIR }}/plan.txt | |
| retention-days: 5 | |
| # Step: Post plan summary to PR for review | |
| - name: Comment PR with plan | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const plan = fs.readFileSync('${{ env.WORKING_DIR }}/plan.txt', 'utf8'); | |
| const truncatedPlan = plan.length > 65000 ? plan.substring(0, 65000) + '\n\n... (truncated)' : plan; | |
| github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: `## Terraform Plan (GKE)\n\`\`\`\n${truncatedPlan}\n\`\`\`` | |
| }) | |
| # Job 2: Terraform Apply | |
| # Execution job that applies the previously reviewed plan. | |
| # This job only runs on merges to main, target branch, or manual approval. | |
| terraform-apply: | |
| name: Terraform Apply | |
| runs-on: ubuntu-latest | |
| needs: [terraform-plan] | |
| if: | | |
| (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/feat/5-Implement-data-isolation-and-multitenancy')) || | |
| (github.event_name == 'workflow_dispatch' && github.event.inputs.terraform_action == 'apply') | |
| environment: | |
| name: production | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| # Tool Setup: Identical to Plan job for consistency | |
| - name: Setup Tools | |
| uses: hashicorp/setup-terraform@v3 | |
| with: | |
| terraform_version: ${{ env.TERRAFORM_VERSION }} | |
| terraform_wrapper: false | |
| - name: Setup GCP Auth | |
| uses: google-github-actions/auth@v2 | |
| with: | |
| credentials_json: ${{ secrets.GCP_SA_KEY }} | |
| - name: Setup gcloud | |
| uses: google-github-actions/setup-gcloud@v2 | |
| with: | |
| install_components: 'gke-gcloud-auth-plugin' | |
| # Step: Re-fetch GKE credentials for provider connectivity | |
| - name: Get GKE credentials | |
| run: | | |
| gcloud container clusters get-credentials ${{ secrets.CLUSTER_NAME }} \ | |
| --region=${{ secrets.CLUSTER_LOCATION }} \ | |
| --project=${{ secrets.GCP_PROJECT_ID }} | |
| # Critical Step: Fetch cluster metadata to reconstruct variable file | |
| - name: Get GKE Cluster Info | |
| id: cluster_info | |
| run: | | |
| ENDPOINT=$(gcloud container clusters describe ${{ secrets.CLUSTER_NAME }} --region=${{ secrets.CLUSTER_LOCATION }} --project=${{ secrets.GCP_PROJECT_ID }} --format='value(endpoint)') | |
| CA_CERT=$(gcloud container clusters describe ${{ secrets.CLUSTER_NAME }} --region=${{ secrets.CLUSTER_LOCATION }} --project=${{ secrets.GCP_PROJECT_ID }} --format='value(masterAuth.clusterCaCertificate)') | |
| echo "endpoint=$ENDPOINT" >> $GITHUB_OUTPUT | |
| echo "ca_cert=$CA_CERT" >> $GITHUB_OUTPUT | |
| # Step: Re-initialize backend for Apply phase | |
| - name: Initialize Backend | |
| working-directory: ${{ env.WORKING_DIR }} | |
| run: | | |
| rm -f backend.tf backend-config.tf | |
| bash ../../.github/scripts/configure-backend.sh "${{ env.CLOUD_PROVIDER }}" | |
| terraform init | |
| env: | |
| TF_STATE_BUCKET: ${{ secrets.TF_STATE_BUCKET }} | |
| # Step: Reconstruct var file | |
| - name: Create terraform.tfvars | |
| working-directory: ${{ env.WORKING_DIR }} | |
| run: | | |
| cat > terraform.tfvars <<EOF | |
| cloud_provider = "${{ env.CLOUD_PROVIDER }}" | |
| project_id = "${{ secrets.GCP_PROJECT_ID }}" | |
| cluster_name = "${{ secrets.CLUSTER_NAME }}" | |
| cluster_location = "${{ secrets.CLUSTER_LOCATION }}" | |
| region = "${{ secrets.REGION }}" | |
| environment = "production" | |
| monitoring_domain = "${{ secrets.MONITORING_DOMAIN }}" | |
| letsencrypt_email = "${{ secrets.LETSENCRYPT_EMAIL }}" | |
| grafana_admin_password = "${{ secrets.GRAFANA_ADMIN_PASSWORD }}" | |
| force_destroy = ${{ inputs.force_destroy || 'false' }} | |
| gke_endpoint = "${{ steps.cluster_info.outputs.endpoint }}" | |
| gke_ca_certificate = "${{ steps.cluster_info.outputs.ca_cert }}" | |
| keycloak_url = "${{ secrets.KEYCLOAK_URL }}" | |
| keycloak_realm = "${{ secrets.KEYCLOAK_REALM }}" | |
| keycloak_admin_user = "${{ secrets.KEYCLOAK_ADMIN_USER }}" | |
| keycloak_admin_password = "${{ secrets.KEYCLOAK_PASSWORD }}" | |
| grafana_keycloak_user = "${{ secrets.GRAFANA_KEYCLOAK_USER }}" | |
| grafana_keycloak_email = "${{ secrets.GRAFANA_KEYCLOAK_EMAIL }}" | |
| grafana_keycloak_password = "${{ secrets.GRAFANA_KEYCLOAK_PASSWORD }}" | |
| grafana_url = "https://grafana.${{ secrets.MONITORING_DOMAIN }}" | |
| EOF | |
| # Note: tenants are discovered dynamically from Keycloak by the sync script. | |
| # Import existing resources into state and clean up read-only datasources. | |
| # - Teams: imported via terraform import (prevents CREATE → 409) | |
| # - Datasources: deleted via Grafana API (can't import, so delete + recreate) | |
| - name: Import existing resources | |
| working-directory: ${{ env.WORKING_DIR }} | |
| run: bash ../../.github/scripts/import-existing-resources.sh | |
| env: | |
| CLOUD_PROVIDER: ${{ env.CLOUD_PROVIDER }} | |
| GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} | |
| NAMESPACE: "observability" | |
| GRAFANA_URL: "https://grafana.${{ secrets.MONITORING_DOMAIN }}" | |
| GRAFANA_ADMIN_PASSWORD: ${{ secrets.GRAFANA_ADMIN_PASSWORD }} | |
| continue-on-error: true | |
| # Critical Step: Apply using current state (after imports) — no stale plan file. | |
| # The plan is regenerated on-the-fly from the post-import state. | |
| - name: Terraform Apply | |
| working-directory: ${{ env.WORKING_DIR }} | |
| run: terraform apply -auto-approve | |
| # Step: Capture deployment outputs for verification | |
| - name: Output deployment info | |
| working-directory: ${{ env.WORKING_DIR }} | |
| run: terraform output -json > terraform-outputs.json | |
| - name: Upload outputs | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: terraform-outputs | |
| path: ${{ env.WORKING_DIR }}/terraform-outputs.json | |
| retention-days: 30 | |
| # Step: Run health checks and smoke tests | |
| - name: Run verification | |
| run: | | |
| bash .github/scripts/verify-deployment.sh | |
| bash .github/scripts/smoke-tests.sh | |
| env: | |
| MONITORING_DOMAIN: ${{ secrets.MONITORING_DOMAIN }} | |
| GRAFANA_ADMIN_PASSWORD: ${{ secrets.GRAFANA_ADMIN_PASSWORD }} | |
| USE_PORT_FORWARD: "true" | |
| # Step: Upload test results | |
| - name: Upload verification report | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: verification-report | |
| path: | | |
| verification-report.html | |
| smoke-test-results.json | |
| retention-days: 30 |