Skip to content

feat: Add Terraform CI/CD automation with plan on PR and apply on merge#12

Merged
ainsleyclark merged 3 commits intomainfrom
feat/terraform-automation
Nov 18, 2025
Merged

feat: Add Terraform CI/CD automation with plan on PR and apply on merge#12
ainsleyclark merged 3 commits intomainfrom
feat/terraform-automation

Conversation

@ainsleyclark
Copy link
Contributor

Summary

Implements full Terraform CI/CD automation using reusable GitHub Actions workflows:

  • Plan on PR: Terraform plan runs automatically, posts results as comment
  • Apply on merge: Terraform apply runs after merge with approval gate
  • Reusable workflows: helper-plan and helper-apply for DRY principles
  • Safety features: State backups, prevent_destroy, destructive change detection
  • Org secrets integration: Uses existing ORG_* secrets automatically

Changes

New Workflows

.github/workflows/helper-plan.yaml - Reusable plan workflow

  • Runs terraform plan with org secrets
  • Posts formatted plan to PR comments (updates existing comment)
  • Uploads plan artifact for later apply
  • Detects destructive changes (deletions) and warns
  • Outputs: plan-exitcode, has-changes, has-destroys

.github/workflows/helper-apply.yaml - Reusable apply workflow

  • Downloads plan artifact from PR (ensures exact plan-apply match)
  • Backs up state before applying (retained 90 days)
  • Runs in GitHub environment with approval gate
  • Falls back to fresh plan if artifact unavailable
  • Posts results to merged PR and commit

.github/workflows/terraform-apply.yaml - Apply on merge to main

  • Triggers on push to main (only if terraform files changed)
  • Uses helper-apply workflow with production environment
  • Includes manual workflow_dispatch trigger for emergencies
  • Shows summary of apply results

Modified Files

.github/workflows/pr.yaml

  • Added terraform-plan job using helper-plan workflow
  • Added to validation summary checks
  • Plan runs in parallel with existing validation jobs

terraform/services/uptime-kuma/main.tf

  • Added prevent_destroy lifecycle to volume module
  • Prevents accidental deletion of volume containing data

terraform.tfvars.example

  • Added CI/CD note explaining org secrets usage
  • Clarified this file is only for local operations

README.md

  • New "CI/CD Automation" section with comprehensive docs
  • Workflow overview and usage instructions
  • GitHub environment setup guide
  • Emergency procedures for manual operations

How It Works

On Pull Request

  1. Terraform plan runs automatically
  2. Plan output posted as PR comment with:
    • Change summary (resources to add/change/destroy)
    • Warning if destructive changes detected
    • Full plan in collapsible section
  3. Plan artifact uploaded with name tfplan-pr-{number}

On Merge to Main

  1. Workflow detects terraform file changes
  2. Downloads plan artifact from merged PR
  3. Waits for approval on production environment ⏸️
  4. Backs up current state
  5. Applies saved plan
  6. Posts results to PR and commit

Setup Required

GitHub Environment Configuration

Before merging, create the production environment:

  1. Go to Settings → Environments → New environment
  2. Name: production
  3. Add protection rules:
    • Required reviewers: Add yourself
    • Deployment branches: Restrict to main only
    • Optional: Wait timer (0-30 minutes delay)

Without this environment, the apply workflow will fail.

Testing Plan

This PR will test itself! When merged:

  1. PR plan will show the terraform changes (prevent_destroy lifecycle)
  2. Merge will trigger apply workflow
  3. Apply will wait for your approval on production environment
  4. You can approve and see the changes applied
  5. Results will be posted back to this PR

Benefits

Visibility: See infrastructure changes before merge
Safety: Approval gates, state backups, prevent_destroy
Consistency: Same process every time, no manual steps
Documentation: Automatic changelog via PR comments
DRY: Reusable workflows for future projects
Secrets: Uses existing org secrets, no manual setup

Risk Mitigation

  • ✅ State backed up before every apply (90 day retention)
  • ✅ Plan artifact ensures no drift between plan and apply
  • ✅ Approval gate on production environment
  • ✅ Volume has prevent_destroy lifecycle
  • ✅ Destructive changes detected and highlighted
  • ✅ Emergency manual apply via workflow_dispatch
  • ✅ Concurrency controls prevent parallel applies

🤖 Generated with Claude Code

ainsleyclark and others added 2 commits November 18, 2025 19:35
This implements automated Terraform workflows using reusable GitHub Actions:

**New Reusable Workflows:**
- helper-plan.yaml: Runs terraform plan, posts to PR, uploads plan artifact
- helper-apply.yaml: Applies saved plan with approval gate, backs up state

**PR Workflow Updates:**
- Added terraform-plan job using helper-plan workflow
- Plan output posted as PR comment with change summary
- Detects and warns about destructive changes (deletions)
- Plan artifact saved for exact apply on merge

**New Apply Workflow:**
- terraform-apply.yaml: Runs on push to main
- Downloads plan artifact from merged PR
- Requires approval on 'production' environment
- Backs up state before applying (retained 90 days)
- Posts results to PR and commit

**Safety Features:**
- Added prevent_destroy lifecycle to uptime-kuma volume
- State backups before every apply
- Plan artifact validation (ensures plan-apply match)
- Destructive change detection with warnings
- Concurrency controls to prevent parallel applies

**Configuration:**
- Uses org secrets: ORG_HETZNER_TOKEN, ORG_BACK_BLAZE_KEY_ID, etc.
- Secrets inherit automatically via 'secrets: inherit'
- Updated terraform.tfvars.example with CI/CD notes
- Added comprehensive README documentation

**Documentation:**
- New CI/CD Automation section in README
- Workflow overview and usage instructions
- Emergency procedures for manual operations
- GitHub environment setup guide

Requires GitHub environment 'production' with approval gates to be configured.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Lifecycle blocks cannot be used in module blocks, only resource blocks.
Data protection is still provided by:
- State backups before every apply (90 day retention)
- Approval gates on production environment
- Plan artifact validation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions
Copy link

github-actions bot commented Nov 18, 2025

Terraform Plan Results

📋 Changes detected:

Plan: 1 to add, 0 to change, 1 to destroy.

📄 View Full Plan
module.uptime_kuma.module.server.tls_private_key.this: Refreshing state... [id=4fd1b70a8627413ffd7622f1a7fc3494ca6cc3f0]
module.uptime_kuma.module.volume.hcloud_volume.this: Refreshing state... [id=103988283]
module.uptime_kuma.module.server.hcloud_ssh_key.this: Refreshing state... [id=103946138]
module.uptime_kuma.module.server.hcloud_firewall.this: Refreshing state... [id=10203178]
module.uptime_kuma.module.server.hcloud_server.this: Refreshing state... [id=113429231]
module.uptime_kuma.module.server.hcloud_firewall_attachment.this: Refreshing state... [id=10203178]
module.uptime_kuma.module.volume.hcloud_volume_attachment.this: Refreshing state... [id=103988283]
module.uptime_kuma.local_file.ansible_inventory: Refreshing state... [id=db62035f770af622c86541e9206a06e4a6a438ed]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # module.uptime_kuma.local_file.ansible_inventory must be replaced
-/+ resource "local_file" "ansible_inventory" {
      ~ content              = <<-EOT # forces replacement
            # Auto-generated by Terraform - DO NOT EDIT MANUALLY
          - # Last updated: 2025-11-18T16:38:57Z
            
            [uptime_kuma]
            uptime.ainsley.dev ansible_host=46.224.59.113 ansible_user=root
            
            [uptime_kuma:vars]
            domain=uptime.ainsley.dev
          - admin_email=hello@ainsley.dev
          + admin_email=admin@ainsley.dev
        EOT
      ~ content_base64sha256 = "+LZyjjsMZNNgzpo+xx/UeNIxlCDr+Dci/bMafudXtNY=" -> (known after apply)
      ~ content_base64sha512 = "YJNqGPF5frlHBAKliavy059OJRamAh0R6WDlGUAcAhGuPpbdbGE9bM80XNnQMYx6Pv1nmeNQxvHtVv/MzuAz7g==" -> (known after apply)
      ~ content_md5          = "6b6791c5c2461e1cdd41052cf1c2e77d" -> (known after apply)
      ~ content_sha1         = "db62035f770af622c86541e9206a06e4a6a438ed" -> (known after apply)
      ~ content_sha256       = "f8b6728e3b0c64d360ce9a3ec71fd478d2319420ebf83722fdb31a7ee757b4d6" -> (known after apply)
      ~ content_sha512       = "60936a18f1797eb9470402a589abf2d39f4e2516a6021d11e960e519401c0211ae3e96dd6c613d6ccf345cd9d0318c7a3efd6799e350c6f1ed56ffcccee033ee" -> (known after apply)
      ~ id                   = "db62035f770af622c86541e9206a06e4a6a438ed" -> (known after apply)
        # (3 unchanged attributes hidden)
    }

Plan: 1 to add, 0 to change, 1 to destroy.

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"

📝 To apply these changes: Merge this PR. Terraform will apply automatically after approval on the production environment.

…PR comments

- Remove timestamp() from local_file.ansible_inventory to prevent constant replacements
- Remove duplicate "Terraform Plan Summary" heading from PR comments
- Clean up emoji from main PR heading for consistency

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@ainsleyclark ainsleyclark merged commit e17ea86 into main Nov 18, 2025
8 checks passed
@ainsleyclark ainsleyclark deleted the feat/terraform-automation branch November 18, 2025 19:50
@github-actions
Copy link

🚀 Terraform Apply Results

Infrastructure changes applied successfully

📄 View Apply Output
module.uptime_kuma.local_file.ansible_inventory: Destroying... [id=db62035f770af622c86541e9206a06e4a6a438ed]
module.uptime_kuma.local_file.ansible_inventory: Destruction complete after 0s
module.uptime_kuma.local_file.ansible_inventory: Creating...
module.uptime_kuma.local_file.ansible_inventory: Creation complete after 0s [id=0529b92e8228a327eaccbd85442e92eef666c8e2]

Apply complete! Resources: 1 added, 0 changed, 1 destroyed.

Outputs:

environment = "production"
project_name = "ainsley-dev-platform"

🔗 View Workflow Run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant