diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..b1d88a2 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,107 @@ +name: Terraform Deployment + +on: + push: + branches: + - main + +jobs: + deploy: + name: Deploy Terraform changes in changed Terramate stacks + + permissions: + id-token: write + contents: read + pull-requests: read + checks: read + + runs-on: ubuntu-latest + + steps: + ### Check out the code + + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + ### Install tooling + + - name: Install Terramate + uses: terramate-io/terramate-action@v2 + + - name: Install asdf + uses: asdf-vm/actions/setup@v3 + + - name: Install Terraform with asdf + run: | + asdf plugin add terraform + asdf install terraform + + ### Check for changed stacks + + - name: List changed stacks + id: list-changed + run: terramate list -C stacks --changed + + ### Configure cloud credentials + - name: 'Configure Azure credentials' + if: steps.list-changed.outputs.stdout + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Verify Azure credentials + if: steps.list-changed.outputs.stdout + run: | + az account show + + ### Run the Terraform deployment via Terramate in each changed stack + + - name: Run Terraform init in all changed stacks + if: steps.list-changed.outputs.stdout + run: | + terramate script run \ + -C stacks \ + --parallel 1 \ + --changed \ + init + env: + GITHUB_TOKEN: ${{ github.token }} + ARM_USE_OIDC: true + ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Run Terraform apply in all changed stacks + id: deploy + if: steps.list-changed.outputs.stdout + run: | + terramate script run \ + -C stacks \ + --parallel 5 \ + --changed \ + deploy + env: + GITHUB_TOKEN: ${{ github.token }} + ARM_USE_OIDC: true + ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Run drift detection in all deployed stacks + if: steps.list-changed.outputs.stdout && ! cancelled() && steps.deploy.outcome != 'skipped' + run: | + terramate script run \ + -C stacks \ + --parallel 5 \ + --changed \ + drift detect + env: + GITHUB_TOKEN: ${{ github.token }} + ARM_USE_OIDC: true + ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} diff --git a/.github/workflows/drift-detection.yml b/.github/workflows/drift-detection.yml new file mode 100644 index 0000000..d886017 --- /dev/null +++ b/.github/workflows/drift-detection.yml @@ -0,0 +1,106 @@ +name: Scheduled Terraform Drift Detection + +on: + schedule: + # Run this workflow at the top of every hour + - cron: '0 * * * *' + workflow_dispatch: + + +jobs: + drift-detection: + name: Detect and reconcile drift in Terraform stacks + + permissions: + id-token: write + contents: read + pull-requests: read + checks: read + + runs-on: ubuntu-latest + + steps: + ### Check out the code + + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + + ### Install tooling + + - name: Install Terramate + uses: terramate-io/terramate-action@v2 + + - name: Install asdf + uses: asdf-vm/actions/setup@v3 + + - name: Install Terraform with asdf + run: | + asdf plugin add terraform + asdf install terraform + + ### Configure cloud credentials + + - name: 'Configure Azure credentials' + if: steps.list-changed.outputs.stdout + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Verify Azure credentials + if: steps.list-changed.outputs.stdout + run: | + az account show + + ### Run Dift Check + + - name: Run Terraform init in all stacks + run: | + terramate script run \ + -C stacks \ + --parallel 1 \ + init + env: + GITHUB_TOKEN: ${{ github.token }} + ARM_USE_OIDC: true + ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Run drift detection + id: drift-detect + run: | + terramate script run \ + -C stacks \ + --parallel 5 \ + --continue-on-error \ + -- \ + drift detect + env: + GITHUB_TOKEN: ${{ github.token }} + ARM_USE_OIDC: true + ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Run drift reconciliation + id: drift-reconcile + run: | + terramate script run \ + -C stacks \ + --tags reconcile \ + --status=drifted \ + --parallel 5 \ + --continue-on-error \ + -- \ + drift reconcile + env: + GITHUB_TOKEN: ${{ github.token }} + ARM_USE_OIDC: true + ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml new file mode 100644 index 0000000..6352fe1 --- /dev/null +++ b/.github/workflows/preview.yml @@ -0,0 +1,171 @@ +name: Terraform Preview + +on: + pull_request: + branches: + - main + +jobs: + preview: + name: Plan Terraform changes in changed stacks + runs-on: ubuntu-latest + + permissions: + id-token: write + contents: read + pull-requests: write + checks: read + + steps: + ### Create Pull Request comment + + - name: Prepare pull request preview comment + if: github.event.pull_request + uses: marocchino/sticky-pull-request-comment@v2 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + header: preview + message: | + ## Preview of Terraform changes in ${{ github.event.pull_request.head.sha }} + + :warning: preview is being created... please stand by! + + ### Check out the code + + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + + ### Install tooling + + - name: Install Terramate + uses: terramate-io/terramate-action@v2 + + - name: Install asdf + uses: asdf-vm/actions/setup@v3 + + - name: Install Terraform with asdf + run: | + asdf plugin add terraform + asdf install terraform + + ### Linting + + - name: Check Terramate formatting + run: terramate fmt --check + + - name: Check Terraform formatting + run: terraform fmt -recursive -check -diff + + ### Check for changed stacks + + - name: List changed stacks + id: list-changed + run: terramate list -C stacks --changed + + ### Configure cloud credentials + + - name: 'Configure Azure credentials' + if: steps.list-changed.outputs.stdout + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Verify Azure credentials + if: steps.list-changed.outputs.stdout + run: | + az account show + + ### Run the Terraform preview via Terramate in each changed stack + + - name: Run Terraform init in all changed stacks + if: steps.list-changed.outputs.stdout + run: | + terramate script run \ + -C stacks \ + --changed \ + --parallel 1 \ + init + env: + GITHUB_TOKEN: ${{ github.token }} + ARM_USE_OIDC: true + ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Plan Terraform changes in changed stacks + if: steps.list-changed.outputs.stdout + run: | + terramate script run \ + -C stacks \ + --changed \ + --parallel 5 \ + --continue-on-error \ + -- \ + preview + env: + GITHUB_TOKEN: ${{ github.token }} + ARM_USE_OIDC: true + ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + ### Update Pull Request comment + + - name: Generate preview details + if: steps.list-changed.outputs.stdout + id: comment + run: | + echo >>pr-comment.txt "## Preview of Terraform changes in ${{ github.event.pull_request.head.sha }}" + echo >>pr-comment.txt + echo >>pr-comment.txt '> [!TIP]' + echo >>pr-comment.txt '> [:mag: View all Preview Details on Terramate Cloud](https://cloud.terramate.io/o/terramate-demo/review-requests)' + echo >>pr-comment.txt + echo >>pr-comment.txt "### Changed Stacks" + echo >>pr-comment.txt + echo >>pr-comment.txt '```bash' + echo >>pr-comment.txt "${{ steps.list-changed.outputs.stdout }}" + echo >>pr-comment.txt '```' + echo >>pr-comment.txt + echo >>pr-comment.txt "#### Terraform Plans" + echo >>pr-comment.txt + terramate script run --changed -- terraform render | dd bs=1024 count=248 >>pr-comment.txt + [ "${PIPESTATUS[0]}" == "141" ] && sed -i 's/#### Terraform Plan/#### :warning: Terraform Plan truncated: please check console output :warning:/' pr-comment.txt + cat pr-comment.txt >>$GITHUB_STEP_SUMMARY + + - name: Generate preview when no stacks changed + if: success() && !steps.list-changed.outputs.stdout + run: | + echo >>pr-comment.txt "## Preview of Terraform changes in ${{ github.event.pull_request.head.sha }}" + echo >>pr-comment.txt + echo >>pr-comment.txt "### Changed Stacks" + echo >>pr-comment.txt + echo >>pr-comment.txt 'No changed stacks, no detailed preview will be generated.' + cat pr-comment.txt >>$GITHUB_STEP_SUMMARY + + - name: Generate preview when things failed + if: failure() + run: | + echo >>pr-comment.txt "## Preview of Terraform changes in ${{ github.event.pull_request.head.sha }}" + echo >>pr-comment.txt + echo >>pr-comment.txt '> [!TIP]' + echo >>pr-comment.txt '> [:mag: View all Preview Details on Terramate Cloud](https://cloud.terramate.io/o/terramate-demo/review-requests)' + echo >>pr-comment.txt + echo >>pr-comment.txt "### Changed Stacks" + echo >>pr-comment.txt + echo >>pr-comment.txt '```bash' + echo >>pr-comment.txt "${{ steps.list-changed.outputs.stdout }}" + echo >>pr-comment.txt '```' + echo >>pr-comment.txt ':boom: Generating preview failed. Please see details in Actions output.' + cat pr-comment.txt >>$GITHUB_STEP_SUMMARY + + - name: Publish generated preview as GitHub commnent + uses: marocchino/sticky-pull-request-comment@v2 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + header: preview + path: pr-comment.txt diff --git a/.tools_versions b/.tools_versions new file mode 100644 index 0000000..3df19e7 --- /dev/null +++ b/.tools_versions @@ -0,0 +1,3 @@ +terraform 1.9.8 +opentofu 1.8.5 +terramate 0.11.1 diff --git a/01_example_outputs/workflows.tm.hcl b/01_example_outputs/workflows.tm.hcl new file mode 100644 index 0000000..84cbf15 --- /dev/null +++ b/01_example_outputs/workflows.tm.hcl @@ -0,0 +1,84 @@ +script "init" { + name = "Terraform Init" + description = "Download the required provider plugins and modules and set up the backend" + + job { + commands = [ + ["terraform", "init", "-lock-timeout=5m"], + ] + } +} + +script "preview" { + name = "Terraform Deployment Preview" + description = "Create a preview of Terraform changes and synchronize it to Terramate Cloud" + + job { + commands = [ + ["terraform", "validate"], + ["terraform", "plan", "-out", "out.tfplan", "-detailed-exitcode", "-lock=false", { + sync_preview = true + tofu_plan_file = "out.tfplan" + }], + ] + } +} + +script "deploy" { + name = "Terraform Deployment" + description = "Run a full Terraform deployment cycle and synchronize the result to Terramate Cloud" + + job { + commands = [ + ["terraform", "validate"], + ["terraform", "plan", "-out", "out.tfplan", "-lock=false"], + ["terraform", "apply", "-input=false", "-auto-approve", "-lock-timeout=5m", "out.tfplan", { + sync_deployment = true + tofu_plan_file = "out.tfplan" + }], + ] + } +} + +script "drift" "detect" { + name = "Terraform Drift Check" + description = "Detect drifts in Terraform configuration and synchronize it to Terramate Cloud" + + job { + commands = [ + ["terraform", "plan", "-out", "out.tfplan", "-detailed-exitcode", "-lock=false", { + sync_drift_status = true + tofu_plan_file = "out.tfplan" + }], + ] + } +} + +script "drift" "reconcile" { + name = "Terraform Drift Reconciliation" + description = "Reconcile drifts in all changed stacks" + + job { + commands = [ + ["terraform", "apply", "-input=false", "-auto-approve", "-lock-timeout=5m", "drift.tfplan", { + sync_deployment = true + tofu_plan_file = "drift.tfplan" + }], + + ] + } +} + +script "terraform" "render" { + name = "Terraform Show Plan" + description = "Render an Terraform plan" + + job { + commands = [ + ["echo", "Stack: `${terramate.stack.path.absolute}`"], + ["echo", "```terraform"], + ["terraform", "show", "-no-color", "out.tfplan"], + ["echo", "```"], + ] + } +} diff --git a/02_example_data_sources/network/workflows.tm.hcl b/02_example_data_sources/network/workflows.tm.hcl new file mode 100644 index 0000000..d550177 --- /dev/null +++ b/02_example_data_sources/network/workflows.tm.hcl @@ -0,0 +1,84 @@ +script "init" { + name = "OpenTofu Init" + description = "Download the required provider plugins and modules and set up the backend" + + job { + commands = [ + ["tofu", "init", "-lock-timeout=5m"], + ] + } +} + +script "preview" { + name = "OpenTofu Deployment Preview" + description = "Create a preview of OpenTofu changes and synchronize it to Terramate Cloud" + + job { + commands = [ + ["tofu", "validate"], + ["tofu", "plan", "-out", "out.tfplan", "-detailed-exitcode", "-lock=false", { + sync_preview = true + tofu_plan_file = "out.tfplan" + }], + ] + } +} + +script "deploy" { + name = "OpenTofu Deployment" + description = "Run a full OpenTofu deployment cycle and synchronize the result to Terramate Cloud" + + job { + commands = [ + ["tofu", "validate"], + ["tofu", "plan", "-out", "out.tfplan", "-lock=false"], + ["tofu", "apply", "-input=false", "-auto-approve", "-lock-timeout=5m", "out.tfplan", { + sync_deployment = true + tofu_plan_file = "out.tfplan" + }], + ] + } +} + +script "drift" "detect" { + name = "OpenTofu Drift Check" + description = "Detect drifts in OpenTofu configuration and synchronize it to Terramate Cloud" + + job { + commands = [ + ["tofu", "plan", "-out", "out.tfplan", "-detailed-exitcode", "-lock=false", { + sync_drift_status = true + tofu_plan_file = "out.tfplan" + }], + ] + } +} + +script "drift" "reconcile" { + name = "OpenTofu Drift Reconciliation" + description = "Reconcile drifts in all changed stacks" + + job { + commands = [ + ["tofu", "apply", "-input=false", "-auto-approve", "-lock-timeout=5m", "drift.tfplan", { + sync_deployment = true + tofu_plan_file = "drift.tfplan" + }], + + ] + } +} + +script "tofu" "render" { + name = "OpenTofu Show Plan" + description = "Render an OpenTofu plan" + + job { + commands = [ + ["echo", "Stack: `${terramate.stack.path.absolute}`"], + ["echo", "```terraform"], + ["tofu", "show", "-no-color", "out.tfplan"], + ["echo", "```"], + ] + } +}