diff --git a/.github/workflows/terraform-standard-iac-pipeline-gcp-global-bootstrap.yaml b/.github/workflows/terraform-standard-iac-pipeline-gcp-global-bootstrap.yaml new file mode 100644 index 00000000..30b28f36 --- /dev/null +++ b/.github/workflows/terraform-standard-iac-pipeline-gcp-global-bootstrap.yaml @@ -0,0 +1,180 @@ +name: Terraform Standard - GCP Project Bootstrap + +concurrency: + group: terraform-bootstrap-gcp-${{ github.ref }} + cancel-in-progress: false + +on: + push: + paths: + - 'iac-template/terraform-hcl-standard/gcp-cloud/bootstrap-s3/**' + - 'iac-template/terraform-hcl-standard/gcp-cloud/bootstrap-iam/**' + - 'iac-template/terraform-hcl-standard/gcp-cloud/bootstrap-dynamodb/**' + - '.github/workflows/terraform-standard-iac-pipeline-gcp-global-bootstrap.yaml' + pull_request: + workflow_dispatch: + inputs: + deploy_action: + type: choice + options: [plan, apply, destroy] + default: plan + +env: + TF_WORKDIR: iac-template/terraform-hcl-standard/gcp-cloud + DEPLOY_ACTION: ${{ github.event.inputs.deploy_action || 'plan' }} + +jobs: + bootstrap: + name: "Bootstrap Modules" + runs-on: ubuntu-latest + + strategy: + matrix: + target: [bootstrap-dynamodb, bootstrap-s3, bootstrap-iam] + + steps: + - uses: actions/checkout@v4 + + - name: Document Bootstrap Scope + run: | + cat <<'SUMMARY' >> "$GITHUB_STEP_SUMMARY" + ## Bootstrap scope (GCP) + - Cloud Storage: create remote state bucket (versioned + uniform access) + - Firestore: enable Datastore mode database for state locking and metadata + - IAM: create Terraform bootstrap service account and bind elevated roles + + Resource names and locations follow iac-template/terraform-hcl-standard/gcp-cloud/config/accounts/bootstrap.yaml. + SUMMARY + + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.9.5 + + - name: Restore Terraform state + uses: actions/download-artifact@v4 + continue-on-error: true + with: + name: tfstate-${{ matrix.target }} + path: ${{ env.TF_WORKDIR }}/${{ matrix.target }} + + - name: Load bootstrap config + run: | + python -m pip install --quiet pyyaml + python - <<'PY' + import json + import yaml + import os + from pathlib import Path + + cfg_path = Path("iac-template/terraform-hcl-standard/gcp-cloud/config/accounts/bootstrap.yaml") + cfg = yaml.safe_load(cfg_path.read_text()) + + env_path = Path(os.environ["GITHUB_ENV"]) + current_env = env_path.read_text() if env_path.exists() else "" + env_path.write_text( + current_env + + f"BOOTSTRAP_PROJECT_ID={cfg['project_id']}\n" + + f"BOOTSTRAP_BUCKET_NAME={cfg['state']['bucket_name']}\n" + + f"BOOTSTRAP_BUCKET_LOCATION={cfg['state'].get('bucket_location', 'US')}\n" + + f"BOOTSTRAP_FIRESTORE_LOCATION={cfg['state'].get('firestore_location', 'us-central')}\n" + + f"BOOTSTRAP_SA_ID={cfg['iam'].get('service_account_id', 'terraform-bootstrap')}\n" + + f"BOOTSTRAP_SA_ROLES={json.dumps(cfg['iam'].get('service_account_roles', []))}\n" + ) + PY + + - name: Authenticate to GCP + uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCP_CREDENTIALS_JSON }} + project_id: ${{ env.BOOTSTRAP_PROJECT_ID }} + create_credentials_file: true + export_environment_variables: true + + - name: Set up gcloud CLI + uses: google-github-actions/setup-gcloud@v2 + with: + project_id: ${{ env.BOOTSTRAP_PROJECT_ID }} + + - name: Render Terraform variables + working-directory: ${{ env.TF_WORKDIR }}/${{ matrix.target }} + run: | + cat > bootstrap.auto.tfvars <> bootstrap.auto.tfvars <> bootstrap.auto.tfvars <> bootstrap.auto.tfvars < ../../outputs_${{ matrix.target }}.json + + - uses: actions/upload-artifact@v4 + if: env.DEPLOY_ACTION == 'apply' + with: + name: outputs-${{ matrix.target }} + path: iac-template/terraform-hcl-standard/gcp-cloud/outputs_${{ matrix.target }}.json + retention-days: 30 + + aggregate: + name: "Aggregate Bootstrap Outputs" + runs-on: ubuntu-latest + needs: bootstrap + + if: ${{ github.event.inputs.deploy_action == 'apply' }} + + steps: + - uses: actions/download-artifact@v4 + with: + path: ./outputs + + - name: Merge Outputs + run: | + echo "{" > final_bootstrap_outputs.json + f=true + for x in outputs/**/outputs_*.json; do + k=$(basename $x .json | sed 's/outputs_//') + [ "$f" = true ] && f=false || echo "," >> final_bootstrap_outputs.json + echo "\"$k\": $(cat $x)" >> final_bootstrap_outputs.json + done + echo "}" >> final_bootstrap_outputs.json + + - run: cat final_bootstrap_outputs.json + + - uses: actions/upload-artifact@v4 + with: + name: bootstrap-final-output + path: final_bootstrap_outputs.json diff --git a/iac-template/terraform-hcl-standard/gcp-cloud/config/accounts/bootstrap.yaml b/iac-template/terraform-hcl-standard/gcp-cloud/config/accounts/bootstrap.yaml new file mode 100644 index 00000000..9893ca67 --- /dev/null +++ b/iac-template/terraform-hcl-standard/gcp-cloud/config/accounts/bootstrap.yaml @@ -0,0 +1,14 @@ +project_id: "modern-app-bootstrap" +environment: bootstrap + +state: + bucket_name: svc-plus-iac-state + bucket_location: US + firestore_location: us-central + +iam: + service_account_id: terraform-bootstrap + service_account_roles: + - roles/resourcemanager.projectIamAdmin + - roles/storage.admin + - roles/compute.admin