GitHub Terraform & Change Management Setup Guide

Architecture Overview

This guide sets up a GitHub-based change management system with two approval tracks, Azure-backed state storage and secrets, and CI/CD for Terraform.

Component Solution
RepositoriesOne repo per managed software/service
Business Changes3-level approval (Technical → IT Lead → Business Owner)
IT Changes1 approval (any IT team peer)
Who Can MergeAnyone on the IT team
Remote StateAzure Storage Account (blob backend with state locking)
SecretsAzure Key Vault (OIDC — zero secrets stored in GitHub)
CI Checksterraform plan, validate, tfsec on every PR

Prerequisites

Naming Convention: This guide uses terraform-{service} for repo names (e.g., terraform-azure-networking, terraform-m365). Replace {service} with your actual service names throughout.

Phase 1: Foundation

1.1 Create Repositories

Run this for each managed software/service. Replace the services array with your actual list.

# Define your services - one repo per managed system
SERVICES=(
  "azure-networking"
  "azure-identity"
  "m365"
  "servicenow"
)

ORG="your-org-name"

for SERVICE in "${SERVICES[@]}"; do
  gh repo create "$ORG/terraform-$SERVICE" \
    --private \
    --description "Terraform IaC for $SERVICE" \
    --clone

  cd "terraform-$SERVICE"

  # Create directory structure
  mkdir -p .github/workflows
  mkdir -p .github/PULL_REQUEST_TEMPLATE
  mkdir -p environments/{dev,staging,prod}
  mkdir -p modules

  # Initialize with a README
  cat > README.md <<EOF
# terraform-$SERVICE

Terraform configuration for $SERVICE.

## Change Request Process

- **Business Changes**: Use the "Business Change Request" PR template. Requires 3 approvals.
- **IT Changes**: Use the "IT Change Request" PR template. Requires 1 approval.

## Structure

\`\`\`
environments/    # Per-environment configs (dev, staging, prod)
modules/         # Reusable Terraform modules
.github/         # CI/CD workflows, PR templates, CODEOWNERS
\`\`\`
EOF

  git add -A
  git commit -m "Initial repo scaffold for terraform-$SERVICE"
  git push -u origin main

  cd ..
done

1.2 Create GitHub Teams

You need three teams to support both approval tracks.

ORG="your-org-name"

# IT team - all IT staff (can merge, can approve IT changes)
gh api orgs/$ORG/teams -f name="it-team" \
  -f description="All IT team members" \
  -f privacy="closed"

# IT leads - senior IT (L2 approver for business changes)
gh api orgs/$ORG/teams -f name="it-leads" \
  -f description="IT leadership for change approval" \
  -f privacy="closed"

# Business owners - non-IT stakeholders (L3 approver)
gh api orgs/$ORG/teams -f name="business-owners" \
  -f description="Business stakeholders for change approval" \
  -f privacy="closed"

Add members to each team:

# Add members to teams
# IT Team - all your IT staff
gh api orgs/$ORG/teams/it-team/memberships/username1 -f role="member"
gh api orgs/$ORG/teams/it-team/memberships/username2 -f role="member"

# IT Leads - your senior IT people
gh api orgs/$ORG/teams/it-leads/memberships/senior-username -f role="member"

# Business Owners - your business stakeholders
gh api orgs/$ORG/teams/business-owners/memberships/biz-username -f role="member"

1.3 Set Repository Permissions

Grant the IT team write access to all repos (allowing merge). Business owners only get read + PR review access.

ORG="your-org-name"

for SERVICE in "${SERVICES[@]}"; do
  REPO="terraform-$SERVICE"

  # IT team gets Write (can push branches + merge PRs)
  gh api orgs/$ORG/teams/it-team/repos/$ORG/$REPO \
    -X PUT -f permission="push"

  # IT leads also get Write
  gh api orgs/$ORG/teams/it-leads/repos/$ORG/$REPO \
    -X PUT -f permission="push"

  # Business owners get Triage (can review/approve but not push code)
  gh api orgs/$ORG/teams/business-owners/repos/$ORG/$REPO \
    -X PUT -f permission="triage"
done
Note: "Triage" permission lets business owners comment on and approve PRs but not push code. If your GitHub plan doesn't support triage, use "read" and manually add them as reviewers.

Phase 2: Azure Backend Setup

2.1 Create Resource Group

# Login to Azure if not already
az login

# Set your subscription
az account set --subscription "Your-Subscription-Name"

# Create resource group for all Terraform infrastructure
az group create \
  --name rg-terraform-state \
  --location eastus \
  --tags purpose=terraform-state managed-by=it-team

2.2 Create Storage Account for State

This stores your Terraform state files with locking via blob lease. Cost is negligible (pennies/month).

# Storage account name must be globally unique, lowercase, no dashes
STORAGE_ACCOUNT="stterraformstate$(openssl rand -hex 4)"
echo "Storage account name: $STORAGE_ACCOUNT"
echo "SAVE THIS NAME - you'll need it in backend.tf"

az storage account create \
  --name "$STORAGE_ACCOUNT" \
  --resource-group rg-terraform-state \
  --location eastus \
  --sku Standard_LRS \
  --kind StorageV2 \
  --min-tls-version TLS1_2 \
  --allow-blob-public-access false \
  --tags purpose=terraform-state

# Create a container for state files
az storage container create \
  --name tfstate \
  --account-name "$STORAGE_ACCOUNT" \
  --auth-mode login

# Enable versioning (state file history / recovery)
az storage account blob-service-properties update \
  --account-name "$STORAGE_ACCOUNT" \
  --resource-group rg-terraform-state \
  --enable-versioning true

# Enable soft delete (14 day recovery window)
az storage blob service-properties delete-policy update \
  --account-name "$STORAGE_ACCOUNT" \
  --resource-group rg-terraform-state \
  --enable true \
  --days-retained 14

2.3 Create Key Vault

# Create Key Vault for Terraform secrets
az keyvault create \
  --name "kv-terraform-secrets" \
  --resource-group rg-terraform-state \
  --location eastus \
  --sku standard \
  --enable-rbac-authorization true \
  --tags purpose=terraform-secrets

# Grant yourself admin access to manage secrets
CURRENT_USER=$(az ad signed-in-user show --query id -o tsv)

az role assignment create \
  --role "Key Vault Secrets Officer" \
  --assignee "$CURRENT_USER" \
  --scope "/subscriptions/$(az account show --query id -o tsv)/resourceGroups/rg-terraform-state/providers/Microsoft.KeyVault/vaults/kv-terraform-secrets"

# Example: add a secret
az keyvault secret set \
  --vault-name "kv-terraform-secrets" \
  --name "example-api-key" \
  --value "your-secret-value-here"

2.4 Set Up Workload Identity Federation (OIDC)

This allows GitHub Actions to authenticate to Azure with zero stored secrets. GitHub presents an OIDC token, Azure validates it.

ORG="your-org-name"
SUBSCRIPTION_ID=$(az account show --query id -o tsv)

# Create a managed identity for GitHub Actions
az identity create \
  --name "id-github-terraform" \
  --resource-group rg-terraform-state \
  --location eastus

IDENTITY_CLIENT_ID=$(az identity show \
  --name id-github-terraform \
  --resource-group rg-terraform-state \
  --query clientId -o tsv)

IDENTITY_PRINCIPAL_ID=$(az identity show \
  --name id-github-terraform \
  --resource-group rg-terraform-state \
  --query principalId -o tsv)

IDENTITY_ID=$(az identity show \
  --name id-github-terraform \
  --resource-group rg-terraform-state \
  --query id -o tsv)

echo "Client ID: $IDENTITY_CLIENT_ID"
echo "Principal ID: $IDENTITY_PRINCIPAL_ID"
echo "SAVE THESE VALUES"

# Grant the identity Contributor on the subscription
# (narrow this to specific RGs in production)
az role assignment create \
  --role "Contributor" \
  --assignee "$IDENTITY_PRINCIPAL_ID" \
  --scope "/subscriptions/$SUBSCRIPTION_ID"

# Grant the identity read access to Key Vault secrets
az role assignment create \
  --role "Key Vault Secrets User" \
  --assignee "$IDENTITY_PRINCIPAL_ID" \
  --scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/rg-terraform-state/providers/Microsoft.KeyVault/vaults/kv-terraform-secrets"

# Grant the identity access to the storage account (for state)
az role assignment create \
  --role "Storage Blob Data Contributor" \
  --assignee "$IDENTITY_PRINCIPAL_ID" \
  --scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/rg-terraform-state/providers/Microsoft.Storage/storageAccounts/$STORAGE_ACCOUNT"

# Create federated credentials for EACH repo
for SERVICE in "${SERVICES[@]}"; do
  REPO="terraform-$SERVICE"

  # For main branch (apply)
  az identity federated-credential create \
    --name "github-$SERVICE-main" \
    --identity-name "id-github-terraform" \
    --resource-group rg-terraform-state \
    --issuer "https://token.actions.githubusercontent.com" \
    --subject "repo:$ORG/$REPO:ref:refs/heads/main" \
    --audiences "api://AzureADTokenExchange"

  # For pull requests (plan)
  az identity federated-credential create \
    --name "github-$SERVICE-pr" \
    --identity-name "id-github-terraform" \
    --resource-group rg-terraform-state \
    --issuer "https://token.actions.githubusercontent.com" \
    --subject "repo:$ORG/$REPO:pull_request" \
    --audiences "api://AzureADTokenExchange"

  echo "Created federated credentials for $REPO"
done

Now store the Azure identifiers as GitHub variables (not secrets — these are non-sensitive IDs):

TENANT_ID=$(az account show --query tenantId -o tsv)

for SERVICE in "${SERVICES[@]}"; do
  REPO="$ORG/terraform-$SERVICE"

  gh variable set AZURE_CLIENT_ID --repo "$REPO" --body "$IDENTITY_CLIENT_ID"
  gh variable set AZURE_TENANT_ID --repo "$REPO" --body "$TENANT_ID"
  gh variable set AZURE_SUBSCRIPTION_ID --repo "$REPO" --body "$SUBSCRIPTION_ID"
  gh variable set STORAGE_ACCOUNT_NAME --repo "$REPO" --body "$STORAGE_ACCOUNT"

  echo "Set variables for $REPO"
done

Phase 3: Repository Configuration

3.1 PR Templates

Create these files in every repo. The script below does it for all repos at once.

for SERVICE in "${SERVICES[@]}"; do
  cd "terraform-$SERVICE"

  # Business Change Template
  cat > .github/PULL_REQUEST_TEMPLATE/business_change.md <<'TMPL'
---
name: Business Change Request
about: Changes requiring business stakeholder approval
labels: cr:business, approval:pending
---

## Change Request - Business

### Classification
- [ ] This change affects business logic, policy, or end-user experience
- [ ] Business justification documented below

### Business Justification
<!-- Why is this change needed from a business perspective? -->

### Impact Assessment
- **Users affected:**
- **Services affected:**
- **Rollback plan:**

### Approval Chain
> All three approvals required before merge.

- [ ] **L1 - Technical Review** (IT team member)
- [ ] **L2 - IT Leadership** (IT manager/lead)
- [ ] **L3 - Business Owner** (business stakeholder)

### Change Window
- **Requested date:**
- **Environment:** dev / staging / prod
TMPL

  # IT Change Template
  cat > .github/PULL_REQUEST_TEMPLATE/it_change.md <<'TMPL'
---
name: IT Change Request
about: Technical changes within IT scope only
labels: cr:it, approval:pending
---

## Change Request - IT

### Classification
- [ ] This change is purely technical (no business logic impact)
- [ ] Peer-reviewed and tested in lower environment

### Description
<!-- What is changing and why -->

### Impact Assessment
- **Services affected:**
- **Rollback plan:**

### Approval
- [ ] **IT Peer Review** (any IT team member)

### Change Window
- **Environment:** dev / staging / prod
TMPL

  git add -A
  git commit -m "Add PR templates for business and IT change requests"
  git push

  cd ..
done

3.2 CODEOWNERS

for SERVICE in "${SERVICES[@]}"; do
  cd "terraform-$SERVICE"

  cat > .github/CODEOWNERS <<EOF
# Default - IT team reviews everything
*                             @$ORG/it-team

# Production changes require IT leads
/environments/prod/           @$ORG/it-leads

# CI/CD changes require IT leads
/.github/                     @$ORG/it-leads
EOF

  git add .github/CODEOWNERS
  git commit -m "Add CODEOWNERS for review routing"
  git push

  cd ..
done

3.3 Terraform Backend Configuration

for SERVICE in "${SERVICES[@]}"; do
  cd "terraform-$SERVICE"

  # Create backend config for each environment
  for ENV in dev staging prod; do
    cat > "environments/$ENV/backend.tf" <<EOF
terraform {
  backend "azurerm" {
    resource_group_name  = "rg-terraform-state"
    storage_account_name = "$STORAGE_ACCOUNT"
    container_name       = "tfstate"
    key                  = "$SERVICE/$ENV.tfstate"
    use_oidc             = true
  }
}
EOF

    cat > "environments/$ENV/providers.tf" <<EOF
terraform {
  required_version = ">= 1.5.0"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
  }
}

provider "azurerm" {
  features {}
  use_oidc = true
}
EOF

    cat > "environments/$ENV/main.tf" <<EOF
# $SERVICE - $ENV environment
# Add your resources here
EOF
  done

  git add -A
  git commit -m "Add Terraform backend and provider config"
  git push

  cd ..
done

3.4 .gitignore

for SERVICE in "${SERVICES[@]}"; do
  cd "terraform-$SERVICE"

  cat > .gitignore <<'EOF'
# Terraform
.terraform/
*.tfstate
*.tfstate.*
*.tfplan
*.tfvars
!*.tfvars.example
crash.log
override.tf
override.tf.json
*_override.tf
*_override.tf.json

# OS
.DS_Store
Thumbs.db

# IDE
.idea/
.vscode/
*.swp
*.swo
EOF

  git add .gitignore
  git commit -m "Add .gitignore"
  git push

  cd ..
done

Phase 4: CI/CD Pipelines

4.1 Terraform Plan Workflow (runs on every PR)

for SERVICE in "${SERVICES[@]}"; do
  cd "terraform-$SERVICE"

  cat > .github/workflows/terraform-plan.yml <<'EOF'
name: Terraform Plan

on:
  pull_request:
    branches: [main]

permissions:
  id-token: write
  contents: read
  pull-requests: write

env:
  ARM_USE_OIDC: true
  ARM_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }}
  ARM_TENANT_ID: ${{ vars.AZURE_TENANT_ID }}
  ARM_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }}

jobs:
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      environments: ${{ steps.changes.outputs.environments }}
    steps:
      - uses: actions/checkout@v4
      - id: changes
        uses: dorny/paths-filter@v3
        with:
          filters: |
            dev:
              - 'environments/dev/**'
              - 'modules/**'
            staging:
              - 'environments/staging/**'
              - 'modules/**'
            prod:
              - 'environments/prod/**'
              - 'modules/**'

  plan:
    needs: detect-changes
    runs-on: ubuntu-latest
    strategy:
      matrix:
        environment: [dev, staging, prod]
    steps:
      - uses: actions/checkout@v4

      - uses: azure/login@v2
        with:
          client-id: ${{ vars.AZURE_CLIENT_ID }}
          tenant-id: ${{ vars.AZURE_TENANT_ID }}
          subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}

      - uses: hashicorp/setup-terraform@v3

      - name: Terraform Init
        working-directory: environments/${{ matrix.environment }}
        run: terraform init

      - name: Terraform Validate
        working-directory: environments/${{ matrix.environment }}
        run: terraform validate

      - name: Terraform Format Check
        working-directory: environments/${{ matrix.environment }}
        run: terraform fmt -check -recursive

      - name: Terraform Plan
        id: plan
        working-directory: environments/${{ matrix.environment }}
        run: |
          terraform plan -no-color -out=tfplan 2>&1 | tee plan-output.txt
          echo "plan_text<> $GITHUB_OUTPUT
          cat plan-output.txt >> $GITHUB_OUTPUT
          echo "PLAN_EOF" >> $GITHUB_OUTPUT

      - name: Comment Plan on PR
        uses: actions/github-script@v7
        with:
          script: |
            const plan = `${{ steps.plan.outputs.plan_text }}`;
            const truncated = plan.length > 60000
              ? plan.substring(0, 60000) + '\n\n... (truncated)'
              : plan;
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: `### Terraform Plan - \`${{ matrix.environment }}\`\n\`\`\`\n${truncated}\n\`\`\``
            });

  security-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: tfsec
        uses: aquasecurity/tfsec-action@v1.0.3
        with:
          soft_fail: false
EOF

  git add .github/workflows/terraform-plan.yml
  git commit -m "Add Terraform plan workflow"
  git push

  cd ..
done

4.2 Approval Gate Workflow

This is the key workflow that enforces the two-tier approval system. It counts unique approvals and checks against the CR label.

for SERVICE in "${SERVICES[@]}"; do
  cd "terraform-$SERVICE"

  cat > .github/workflows/approval-gate.yml <<'WORKFLOW'
name: Approval Gate

on:
  pull_request_review:
    types: [submitted]
  pull_request:
    types: [labeled, unlabeled, opened, synchronize]

jobs:
  check-approvals:
    runs-on: ubuntu-latest
    steps:
      - name: Validate approval requirements
        uses: actions/github-script@v7
        with:
          script: |
            const pr = context.payload.pull_request;
            const labels = pr.labels.map(l => l.name);

            // Determine CR type
            const isBusiness = labels.includes('cr:business');
            const isIT = labels.includes('cr:it');

            if (!isBusiness && !isIT) {
              core.setFailed(
                '❌ PR must have either "cr:business" or "cr:it" label. ' +
                'Add the appropriate label to indicate the change type.'
              );
              return;
            }

            if (isBusiness && isIT) {
              core.setFailed(
                '❌ PR cannot have both "cr:business" and "cr:it" labels. Remove one.'
              );
              return;
            }

            // Get all reviews
            const reviews = await github.rest.pulls.listReviews({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: pr.number
            });

            // Get latest review per user (handles re-reviews)
            const latestReviews = {};
            for (const review of reviews.data) {
              if (review.state === 'APPROVED' || review.state === 'CHANGES_REQUESTED') {
                latestReviews[review.user.login] = review;
              }
            }

            const approvals = Object.values(latestReviews)
              .filter(r => r.state === 'APPROVED')
              .map(r => r.user.login);

            const changesRequested = Object.values(latestReviews)
              .filter(r => r.state === 'CHANGES_REQUESTED')
              .map(r => r.user.login);

            // Block if anyone requested changes
            if (changesRequested.length > 0) {
              core.setFailed(
                `❌ Changes requested by: ${changesRequested.join(', ')}. ` +
                `Resolve feedback before merging.`
              );
              return;
            }

            const requiredCount = isBusiness ? 3 : 1;
            const crType = isBusiness ? 'Business' : 'IT';

            if (approvals.length < requiredCount) {
              core.setFailed(
                `⏳ ${crType} Change requires ${requiredCount} approval(s). ` +
                `Currently has ${approvals.length}: ${approvals.join(', ') || 'none yet'}`
              );
            } else {
              core.info(
                `✅ ${crType} Change: ${approvals.length}/${requiredCount} approvals met. ` +
                `Approved by: ${approvals.join(', ')}`
              );
            }
WORKFLOW

  git add .github/workflows/approval-gate.yml
  git commit -m "Add approval gate workflow (business=3, IT=1)"
  git push

  cd ..
done

4.3 Terraform Apply Workflow (runs after merge to main)

for SERVICE in "${SERVICES[@]}"; do
  cd "terraform-$SERVICE"

  cat > .github/workflows/terraform-apply.yml <<'EOF'
name: Terraform Apply

on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  apply:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        environment: [dev, staging, prod]
    environment: ${{ matrix.environment }}
    steps:
      - uses: actions/checkout@v4

      - uses: azure/login@v2
        with:
          client-id: ${{ vars.AZURE_CLIENT_ID }}
          tenant-id: ${{ vars.AZURE_TENANT_ID }}
          subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}

      - uses: hashicorp/setup-terraform@v3

      - name: Terraform Init
        working-directory: environments/${{ matrix.environment }}
        run: terraform init

      - name: Terraform Apply
        working-directory: environments/${{ matrix.environment }}
        run: terraform apply -auto-approve
EOF

  git add .github/workflows/terraform-apply.yml
  git commit -m "Add Terraform apply workflow"
  git push

  cd ..
done
Production safety: The apply workflow above auto-applies to all environments on merge. For additional safety, configure the prod GitHub Environment (Phase 5.2) with required reviewers so prod deploys wait for manual approval even after merge.

Phase 5: Branch Protection

5.1 Configure Branch Rulesets

Apply branch protection via the GitHub API. This makes check-approvals a required status check, which enforces the label-based approval logic from Phase 4.2.

ORG="your-org-name"

for SERVICE in "${SERVICES[@]}"; do
  REPO="terraform-$SERVICE"

  # Create branch protection rule for main
  gh api repos/$ORG/$REPO/branches/main/protection \
    -X PUT \
    -H "Accept: application/vnd.github+json" \
    --input - <<'JSON'
{
  "required_status_checks": {
    "strict": true,
    "contexts": [
      "check-approvals",
      "plan (dev)",
      "plan (staging)",
      "plan (prod)",
      "security-scan"
    ]
  },
  "enforce_admins": false,
  "required_pull_request_reviews": {
    "required_approving_review_count": 1,
    "dismiss_stale_reviews": true,
    "require_code_owner_reviews": true
  },
  "restrictions": null,
  "allow_force_pushes": false,
  "allow_deletions": false
}
JSON

  echo "Branch protection set for $REPO"
done
How the two-tier system works together: Branch protection requires 1 minimum approval (GitHub's floor) + the check-approvals status check. The status check enforces 1 for IT or 3 for business based on the label. The GitHub-level approval satisfies the branch rule; the workflow enforces the real requirement.

5.2 Configure GitHub Environments

Add a manual approval gate for production deployments:

ORG="your-org-name"

for SERVICE in "${SERVICES[@]}"; do
  REPO="terraform-$SERVICE"

  # Create environments
  for ENV in dev staging prod; do
    gh api repos/$ORG/$REPO/environments/$ENV -X PUT
  done

  # Add required reviewers to prod environment
  # Get the it-leads team ID
  TEAM_ID=$(gh api orgs/$ORG/teams/it-leads --jq '.id')

  gh api repos/$ORG/$REPO/environments/prod \
    -X PUT \
    --input - <<JSON
{
  "reviewers": [
    {
      "type": "Team",
      "id": $TEAM_ID
    }
  ],
  "deployment_branch_policy": {
    "protected_branches": true,
    "custom_branch_policies": false
  }
}
JSON

  echo "Environments configured for $REPO"
done

Phase 6: Validation

6.1 Test Both Approval Flows

Test A: IT Change Flow

  1. Create a branch: git checkout -b test/it-change
  2. Make a trivial change (add a comment to a .tf file)
  3. Push and open a PR using the IT Change template
  4. Add the cr:it label
  5. Verify: check-approvals status shows "requires 1 approval"
  6. Have one IT team member approve
  7. Verify: check-approvals passes, merge is enabled
  8. Merge and verify terraform apply runs

Test B: Business Change Flow

  1. Create a branch: git checkout -b test/business-change
  2. Make a trivial change
  3. Push and open a PR using the Business Change template
  4. Add the cr:business label
  5. Verify: check-approvals shows "requires 3 approvals"
  6. Have one person approve — verify still blocked (1/3)
  7. Second approval — verify still blocked (2/3)
  8. Third approval — verify gate passes, merge is enabled
  9. Merge and verify deploy

Test C: Missing Label

  1. Open a PR with no cr: label
  2. Verify: check-approvals fails with "must have either cr:business or cr:it label"

6.2 Go-Live Checklist

Reference: Quick Command Cheat Sheet

TaskCommand
Add new service repogh repo create $ORG/terraform-{name} --private
Add federated credentialaz identity federated-credential create ...
Add Key Vault secretaz keyvault secret set --vault-name kv-terraform-secrets --name {name} --value {val}
Read Key Vault secret in TFdata "azurerm_key_vault_secret" "x" { name = "..." }
Check stateterraform state list
Import existing resourceterraform import {resource.name} {azure-id}
Unlock stuck stateterraform force-unlock {lock-id}
Add team membergh api orgs/$ORG/teams/{team}/memberships/{user} -f role="member"

Reference: DNS Records

After deployment, add these records in Hover for easydatatransfer.accessnarrative.com. The exact values will be provided after Vercel deployment.

Export Guide