Skip to content

build(keycloak): Remove tenant group provisioning #157

build(keycloak): Remove tenant group provisioning

build(keycloak): Remove tenant group provisioning #157

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