name: Terraform CI/CD Pipeline on: push: branches: - main - develop pull_request: branches: - main workflow_dispatch: inputs: action: description: 'Action to perform' required: true type: choice options: - plan - apply - destroy env: TF_VERSION: "1.9.0" WORKING_DIR: "./terraform" # Adjust to your terraform directory jobs: # Stage 1: Linting and Syntax Checks lint-and-validate: name: Lint and Validate runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Terraform uses: https://github.com/hashicorp/setup-terraform@v3 with: terraform_version: ${{ env.TF_VERSION }} - name: Terraform Format Check id: fmt run: terraform fmt -check -recursive working-directory: ${{ env.WORKING_DIR }} continue-on-error: true - name: Terraform Init (for validation) run: terraform init -backend=false working-directory: ${{ env.WORKING_DIR }} - name: Terraform Validate run: terraform validate working-directory: ${{ env.WORKING_DIR }} - name: Check Format Result if: steps.fmt.outcome == 'failure' run: | echo "❌ Terraform formatting check failed. Run 'terraform fmt -recursive' to fix." exit 1 # Stage 2: Security Scanning security-scan: name: Security Scan runs-on: ubuntu-latest needs: lint-and-validate steps: - name: Checkout code uses: actions/checkout@v4 - name: Run Checkov (Open Source Security Scanner) uses: https://github.com/bridgecrewio/checkov-action@v12 with: directory: ${{ env.WORKING_DIR }} framework: terraform soft_fail: false # Set to true to not fail the pipeline on security issues output_format: cli - name: Run tfsec (Terraform Security Scanner) uses: https://github.com/aquasecurity/tfsec-action@v1.0.3 with: working_directory: ${{ env.WORKING_DIR }} soft_fail: false # Stage 3: Terraform Init and Plan plan: name: Terraform Plan runs-on: ubuntu-latest needs: security-scan if: github.event_name == 'push' || github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && (github.event.inputs.action == 'plan' || github.event.inputs.action == 'apply')) steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Terraform uses: https://github.com/hashicorp/setup-terraform@v3 with: terraform_version: ${{ env.TF_VERSION }} terraform_wrapper: false - name: Configure Terraform Credentials run: | # Add your cloud provider credentials here # Example for AWS: # echo "AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }}" >> $GITHUB_ENV # echo "AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}" >> $GITHUB_ENV # Example for Azure: # echo "ARM_CLIENT_ID=${{ secrets.ARM_CLIENT_ID }}" >> $GITHUB_ENV # echo "ARM_CLIENT_SECRET=${{ secrets.ARM_CLIENT_SECRET }}" >> $GITHUB_ENV # echo "ARM_SUBSCRIPTION_ID=${{ secrets.ARM_SUBSCRIPTION_ID }}" >> $GITHUB_ENV # echo "ARM_TENANT_ID=${{ secrets.ARM_TENANT_ID }}" >> $GITHUB_ENV # For GCP, you might need to create a credentials file echo "Configure your provider credentials here" - name: Terraform Init run: terraform init working-directory: ${{ env.WORKING_DIR }} env: # Add backend configuration secrets if needed TF_CLI_ARGS_init: "-backend-config=access_key=${{ secrets.BACKEND_ACCESS_KEY }}" - name: Terraform Plan run: terraform plan -out=tfplan.binary working-directory: ${{ env.WORKING_DIR }} - name: Convert Plan to JSON run: terraform show -json tfplan.binary > tfplan.json working-directory: ${{ env.WORKING_DIR }} - name: Upload Terraform Plan uses: actions/upload-artifact@v4 with: name: terraform-plan path: | ${{ env.WORKING_DIR }}/tfplan.binary ${{ env.WORKING_DIR }}/tfplan.json ${{ env.WORKING_DIR }}/.terraform/ ${{ env.WORKING_DIR }}/.terraform.lock.hcl retention-days: 30 - name: Comment Plan on PR if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | const fs = require('fs'); const plan = fs.readFileSync('${{ env.WORKING_DIR }}/tfplan.json', 'utf8'); const output = `#### Terraform Plan 📖
Show Plan \`\`\`json ${plan} \`\`\`
`; github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: output }); # Stage 4: Terraform Apply apply: name: Terraform Apply runs-on: ubuntu-latest needs: plan if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || (github.event_name == 'workflow_dispatch' && github.event.inputs.action == 'apply') environment: name: production steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Terraform uses: https://github.com/hashicorp/setup-terraform@v3 with: terraform_version: ${{ env.TF_VERSION }} terraform_wrapper: false - name: Download Terraform Plan uses: actions/download-artifact@v4 with: name: terraform-plan path: ${{ env.WORKING_DIR }} - name: Configure Terraform Credentials run: | # Same credentials configuration as in plan stage echo "Configure your provider credentials here" - name: Restore Terraform Init Files run: | # The .terraform directory is already restored from artifacts echo "Terraform initialization files restored" - name: Terraform Apply run: terraform apply -auto-approve tfplan.binary working-directory: ${{ env.WORKING_DIR }} - name: Output Terraform Outputs run: terraform output -json > terraform-outputs.json working-directory: ${{ env.WORKING_DIR }} - name: Upload Terraform Outputs uses: actions/upload-artifact@v4 with: name: terraform-outputs path: ${{ env.WORKING_DIR }}/terraform-outputs.json retention-days: 90 # Stage 5: Terraform Destroy (Manual/Authorized Only) destroy: name: Terraform Destroy runs-on: ubuntu-latest if: github.event_name == 'workflow_dispatch' && github.event.inputs.action == 'destroy' environment: name: production-destroy # Requires manual approval in repository settings steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Terraform uses: https://github.com/hashicorp/setup-terraform@v3 with: terraform_version: ${{ env.TF_VERSION }} terraform_wrapper: false - name: Configure Terraform Credentials run: | # Same credentials configuration as previous stages echo "Configure your provider credentials here" - name: Terraform Init run: terraform init working-directory: ${{ env.WORKING_DIR }} - name: Terraform Destroy run: terraform destroy -auto-approve working-directory: ${{ env.WORKING_DIR }} - name: Notify Destroy Completion run: | echo "🔥 Terraform infrastructure has been destroyed!" echo "Destroyed by: ${{ github.actor }}" echo "Timestamp: $(date -u)"