Skip to content

terraform destroy

terraform destroy #70

# MOSIP Infrastructure Destruction Workflow
#
# This workflow supports both remote (S3/Azure Storage/GCS) and local backend configurations for destruction
#
# GPG Encryption for Local Backend:
# - When BACKEND_TYPE is 'local', Terraform state files are automatically encrypted using GPG
# - Required GitHub Secret: GPG_PASSPHRASE (used for encrypting/decrypting state files)
# - Encrypted files: {cloud}-{component}-{branch}-terraform.tfstate.gpg, {cloud}-{component}-{branch}-terraform.tfstate.backup.gpg
# - On destruction, encrypted files are automatically decrypted before Terraform operations
# - State files are cleaned up after successful destruction
#
# Security Features:
# - State files never stored unencrypted in repository when using local backend
# - AES256 encryption with compression
# - Automatic cleanup of unencrypted files after encryption
#
name: terraform destroy
on:
workflow_dispatch:
inputs:
CLOUD_PROVIDER:
description: 'Choose the cloud provider (aws, azure, gcp)'
required: true
type: choice
options:
- aws
- azure
- gcp
TERRAFORM_COMPONENT:
description: 'Choose component to destroy (WARNING: base-infra destruction removes foundational resources!)'
required: true
type: choice
options:
- infra
- base-infra
- observ-infra
default: infra
BACKEND_TYPE:
description: 'Choose Terraform backend type (must match the one used during creation)'
required: true
type: choice
options:
- local
- remote
default: local
REMOTE_BACKEND_CONFIG:
description: 'Remote backend config (format: aws:bucket_base_name:region OR azure:rg_name:storage_account:container OR gcp:bucket_name). For AWS, bucket will be created as bucket_base_name-BRANCH_NAME'
required: false
type: string
ENABLE_STATE_LOCKING:
description: 'Enable state locking cleanup (must match the setting used during creation)'
required: false
type: boolean
default: true
SSH_PRIVATE_KEY:
description: 'The GitHub secret containing the private key of the SSH key named in the preceding input variable `SSH_PRIVATE_KEY` is used for SSH login purposes on nginx node.'
required: true
TERRAFORM_DESTROY:
description: 'Terraform destroy - CONFIRM DESTRUCTION'
required: false
type: boolean
default: false
env:
# TF_LOG_PATH: ./temp/terraform.log
## TRACE, DEBUG, INFO, WARN or ERROR
# TF_LOG: TRACE
# Repository-level secrets for cloud credentials
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
TF_VAR_ssh_private_key: ${{ secrets[inputs.SSH_PRIVATE_KEY] }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
terraform-destroy:
runs-on: ubuntu-latest
timeout-minutes: 120 # 2 hours maximum runtime protection
# Use dynamic environment based on current branch name
environment: ${{ github.ref_name }}
defaults:
run:
shell: bash
# We work in the cloud-specific implementation directory
working-directory: 'terraform/implementations/${{ inputs.CLOUD_PROVIDER }}/${{ inputs.TERRAFORM_COMPONENT }}'
steps:
- uses: actions/checkout@v4
- name: Check for required implementation directory
run: |
if [ ! -d "." ]; then
echo "Directory 'terraform/implementations/${{ inputs.CLOUD_PROVIDER }}/${{ inputs.TERRAFORM_COMPONENT }}' does not exist."
echo "Available providers: aws, azure, gcp"
echo "Available components: base-infra, infra"
exit 1
fi
# Check if tfvars file exists
if [ ! -f "${{ inputs.CLOUD_PROVIDER }}.tfvars" ]; then
echo "Configuration file '${{ inputs.CLOUD_PROVIDER }}.tfvars' not found in current directory."
exit 1
fi
- name: Check Cloud Storage for Remote State (All Providers)
run: |
if [ "${{ inputs.BACKEND_TYPE }}" = "remote" ]; then
echo "Checking cloud storage for remote Terraform state..."
echo "Provider: ${{ inputs.CLOUD_PROVIDER }}"
echo "Component: ${{ inputs.TERRAFORM_COMPONENT }}"
echo "Branch: ${{ github.ref_name }}"
# Parse remote configuration to check if storage exists
BACKEND_CONFIG="${{ inputs.REMOTE_BACKEND_CONFIG }}"
if [ -n "$BACKEND_CONFIG" ]; then
IFS=':' read -ra CONFIG_PARTS <<< "$BACKEND_CONFIG"
PROVIDER_TYPE="${CONFIG_PARTS[0]}"
case "$PROVIDER_TYPE" in
aws)
BUCKET_BASE_NAME="${CONFIG_PARTS[1]}"
REGION="${CONFIG_PARTS[2]:-us-east-1}"
DYNAMIC_BUCKET_NAME="${BUCKET_BASE_NAME}-${{ github.ref_name }}"
echo "Checking AWS S3 bucket: $DYNAMIC_BUCKET_NAME"
if aws s3api head-bucket --bucket "$DYNAMIC_BUCKET_NAME" 2>/dev/null; then
echo "AWS S3 bucket found - ready for destroy"
else
echo "WARNING: AWS S3 bucket not found - may already be destroyed"
fi
;;
azure)
RESOURCE_GROUP="${CONFIG_PARTS[1]}"
STORAGE_ACCOUNT="${CONFIG_PARTS[2]}"
CONTAINER="${CONFIG_PARTS[3]}"
echo "Checking Azure Storage: RG=$RESOURCE_GROUP, Account=$STORAGE_ACCOUNT, Container=$CONTAINER"
echo "INFO: Azure storage existence check requires Azure CLI - assuming exists for destroy"
;;
gcp)
BUCKET_NAME="${CONFIG_PARTS[1]}"
echo "Checking GCP Cloud Storage bucket: $BUCKET_NAME"
echo "INFO: GCP storage existence check requires gcloud CLI - assuming exists for destroy"
;;
*)
echo "WARNING: Unknown provider type: $PROVIDER_TYPE"
;;
esac
else
echo "WARNING: No remote backend config provided"
fi
echo "Cloud storage check completed"
else
echo "Skipping cloud storage check (using local backend)"
fi
- name: Configure Terraform Backend for State Management
run: |
# Use backend configuration script for cleaner code
../../../../.github/scripts/configure-backend.sh \
--type "${{ inputs.BACKEND_TYPE }}" \
--provider "${{ inputs.CLOUD_PROVIDER }}" \
--component "${{ inputs.TERRAFORM_COMPONENT }}" \
--branch "${{ github.ref_name }}" \
--remote-config "${{ inputs.REMOTE_BACKEND_CONFIG }}" \
${{ inputs.ENABLE_STATE_LOCKING == true && '--enable-locking' || '' }}
- name: Skip SSH Host key verification
run: |
mkdir -p ~/.ssh
echo -e "Host *\n StrictHostKeyChecking no" > ~/.ssh/config
- name: Setup ufw firewall
run: |
sudo ufw enable
sudo ufw allow ssh
sudo ufw allow 443/tcp
sudo ufw allow 51820/udp
sudo ufw status
if: "${{ inputs.TERRAFORM_COMPONENT != 'base-infra' }}"
- name: Install WireGuard
run: sudo apt-get install -y wireguard
if: "${{ inputs.TERRAFORM_COMPONENT != 'base-infra' }}"
- name: Configure WireGuard
run: |
# WG_CONFIG comes from environment-level secrets (dynamic based on branch name)
if [ -z "${{ secrets.TF_WG_CONFIG }}" ]; then
echo "❌ ERROR: TF_WG_CONFIG secret is not configured for environment '${{ github.ref_name }}'"
echo "Please configure the WireGuard configuration secret for this branch/environment"
exit 1
fi
# Debug: Check if secret is available (without exposing content)
echo "🔍 Checking TF_WG_CONFIG secret..."
if [ ${#TF_WG_CONFIG} -eq 0 ]; then
echo "❌ ERROR: TF_WG_CONFIG environment variable is empty"
echo "Secret may contain only whitespace or special characters"
exit 1
fi
echo "✅ Secret length: ${#TF_WG_CONFIG} characters"
# Create config file with proper handling of multiline content
echo "Creating /etc/wireguard directory with proper permissions..."
sudo mkdir -p /etc/wireguard
sudo chmod 755 /etc/wireguard
sudo chown root:root /etc/wireguard
echo "Directory created. Checking permissions..."
sudo ls -la /etc/ | grep wireguard || echo "WireGuard directory not visible"
# Debug: Check secret format without exposing sensitive data
echo "Secret contains '[Interface]': $(printf '%s' "$TF_WG_CONFIG" | grep -c '\[Interface\]')"
echo "Secret contains '[Peer]': $(printf '%s' "$TF_WG_CONFIG" | grep -c '\[Peer\]')"
# Write to temp file with explicit newline handling
printf '%s' "$TF_WG_CONFIG" > /tmp/wg0.conf.tmp
# Check temp file was created successfully
if [ ! -f /tmp/wg0.conf.tmp ]; then
echo "❌ ERROR: Failed to create temporary config file"
exit 1
fi
echo "Temp file size: $(wc -c < /tmp/wg0.conf.tmp) bytes"
echo "Temp file lines: $(wc -l < /tmp/wg0.conf.tmp)"
# Use a more direct approach - write directly with sudo
echo "Writing config directly with sudo..."
sudo tee /etc/wireguard/wg0.conf < /tmp/wg0.conf.tmp > /dev/null
# Clean up temp file
rm -f /tmp/wg0.conf.tmp
# Verify the file was created and check its size
if sudo test -f /etc/wireguard/wg0.conf; then
echo "✅ Config file created successfully"
echo "Final config file size: $(sudo wc -c < /etc/wireguard/wg0.conf) bytes"
else
echo "❌ ERROR: Config file still doesn't exist"
sudo ls -la /etc/wireguard/ || echo "Cannot list /etc/wireguard directory"
exit 1
fi
# Set proper permissions
sudo chmod 600 /etc/wireguard/wg0.conf
sudo chown root:root /etc/wireguard/wg0.conf
echo "Final file permissions: $(sudo ls -la /etc/wireguard/wg0.conf)"
# Verify config file is not empty and has expected content
if ! sudo test -s /etc/wireguard/wg0.conf; then
echo "❌ ERROR: WireGuard config file is empty after writing!"
echo "File size: $(sudo wc -c < /etc/wireguard/wg0.conf) bytes"
exit 1
fi
# Basic validation that it looks like a WireGuard config
if ! sudo grep -q "\[Interface\]" /etc/wireguard/wg0.conf; then
echo "❌ ERROR: WireGuard config doesn't contain [Interface] section"
echo "Please check that TF_WG_CONFIG contains a valid WireGuard configuration"
echo "Config file size: $(sudo wc -c < /etc/wireguard/wg0.conf) bytes"
exit 1
fi
echo "✅ WireGuard configuration applied successfully"
echo "Config file size: $(sudo wc -c < /etc/wireguard/wg0.conf) bytes"
env:
TF_WG_CONFIG: ${{ secrets.TF_WG_CONFIG }}
if: "${{ inputs.TERRAFORM_COMPONENT != 'base-infra' }}"
- name: Start WireGuard
run: |
sudo chmod 600 /etc/wireguard/wg0.conf
sudo chmod 700 /etc/wireguard/
sudo chmod 644 /lib/systemd/system/wg-quick@.service
sudo systemctl daemon-reload
echo "Starting WireGuard..."
if ! sudo wg-quick up wg0; then
echo "❌ ERROR: Failed to start WireGuard VPN"
echo "Config file contents (first 5 lines):"
head -5 /etc/wireguard/wg0.conf
exit 1
fi
echo "✅ WireGuard started successfully"
sudo wg show wg0
if: "${{ inputs.TERRAFORM_COMPONENT != 'base-infra' }}"
- name: Setup Terraform with specified version on the runner
uses: hashicorp/setup-terraform@v3
with:
terraform_version: v1.8.5
- name: Clean Terraform Environment (Prevent Stale Issues)
run: |
echo "=== Cleaning Terraform Environment ==="
# Remove any existing .terraform directory to prevent version conflicts
if [ -d ".terraform" ]; then
echo "🧹 Removing existing .terraform directory..."
rm -rf .terraform
echo "✅ Cleaned .terraform directory"
fi
# Remove any existing .terraform.lock.hcl to allow fresh provider resolution
if [ -f ".terraform.lock.hcl" ]; then
echo "🧹 Removing existing .terraform.lock.hcl..."
rm -f .terraform.lock.hcl
echo "✅ Cleaned .terraform.lock.hcl"
fi
# Remove any temporary Terraform files that might cause conflicts
echo "🧹 Cleaning temporary Terraform files..."
rm -f terraform_*.sh 2>/dev/null || true
rm -f .terraform.tfstate.lock.info 2>/dev/null || true
rm -f terraform.log 2>/dev/null || true
rm -f crash.log 2>/dev/null || true
# Show what we're starting with
echo "✅ Terraform environment cleaned"
echo "Files in directory after cleanup:"
ls -la | head -10
- name: Setup GPG for State File Encryption
run: |
../../../../.github/scripts/setup-gpg.sh \
--backend-type "${{ inputs.BACKEND_TYPE }}" \
--passphrase "${{ secrets.GPG_PASSPHRASE }}"
- name: Decrypt Existing State Files
run: |
../../../../.github/scripts/decrypt-state.sh \
--backend-type "${{ inputs.BACKEND_TYPE }}" \
--passphrase "${{ secrets.GPG_PASSPHRASE }}"
- name: Display Destruction Warning
run: |
echo "DESTRUCTIVE OPERATION: ${{ inputs.CLOUD_PROVIDER }} (${{ inputs.TERRAFORM_COMPONENT }})"
echo "Working directory: $(pwd)"
echo "Available files:"
ls -la
if [ "${{ inputs.TERRAFORM_COMPONENT }}" = "base-infra" ]; then
echo "CRITICAL WARNING: base-infra destruction will remove foundational resources!"
echo " This includes VPCs, subnets, routing tables, security groups, etc."
echo " This is typically a ONE-TIME setup and should rarely be destroyed."
echo " Make sure you understand the impact before proceeding."
else
echo "INFO: infra destruction - removes MOSIP application infrastructure"
echo " This is safe to destroy and recreate as needed."
fi
if [ "${{ inputs.TERRAFORM_DESTROY }}" != "true" ]; then
echo "ERROR: TERRAFORM_DESTROY is not set to true - skipping actual destruction"
else
echo "TERRAFORM_DESTROY confirmed - proceeding with destruction"
fi
- name: Terraform Init with State Validation
run: |
echo "=== Terraform Init with Enhanced State Validation ==="
# Generate the expected state file name with custom pattern
expected_state_file="${{ inputs.CLOUD_PROVIDER }}-${{ inputs.TERRAFORM_COMPONENT }}-${{ github.ref_name }}-terraform.tfstate"
echo "🔍 Looking for state file: $expected_state_file"
# First, verify that any existing state is valid
if [ -f "$expected_state_file" ]; then
echo "🔍 Found existing state file, validating..."
# Basic state file validation
if terraform show > /dev/null 2>&1; then
echo "✅ Existing state is valid"
# Show what resources exist before destroy
echo "📋 Resources in current state:"
terraform state list | head -10 || echo "Unable to list resources"
else
echo "⚠️ WARNING: Existing state may be corrupted or incompatible"
echo "📋 State file size: $(wc -c < "$expected_state_file") bytes"
echo "📋 State file age: $(stat -c %Y "$expected_state_file")"
# For destroy operations, we need to be more careful with bad state
echo "🚨 Bad state detected during destroy - manual intervention may be needed"
fi
else
echo "ℹ️ No existing state file found ($expected_state_file) - nothing to destroy"
echo "This could mean:"
echo " 1. No infrastructure was previously deployed"
echo " 2. State file naming pattern has changed"
echo " 3. Different branch/component combination was used"
echo "Available files in directory:"
ls -la *.tfstate* 2>/dev/null || echo "No state files found"
fi
# Initialize with clean environment
echo "🚀 Running terraform init..."
if ! terraform init; then
echo "❌ Initial init failed, trying with -upgrade to resolve provider conflicts..."
if ! terraform init -upgrade; then
echo "❌ Init with upgrade failed, trying with -reconfigure for backend changes..."
terraform init -reconfigure -upgrade
fi
fi
# Verify initialization was successful
if [ -d ".terraform" ] && [ -f ".terraform.lock.hcl" ]; then
echo "✅ Terraform initialization successful"
echo "📋 Provider versions:"
terraform version
echo "📋 Workspace: $(terraform workspace show)"
else
echo "❌ Terraform initialization verification failed"
exit 1
fi
- name: Terraform refresh
run: terraform refresh -var-file="${{ inputs.CLOUD_PROVIDER }}.tfvars" -no-color
if: "${{ inputs.TERRAFORM_DESTROY == true }}"
- name: Terraform Destroy Plan
id: destroy-plan
run: |
echo "Showing what will be destroyed..."
terraform plan -destroy -var-file="${{ inputs.CLOUD_PROVIDER }}.tfvars" -no-color
if: "${{ inputs.TERRAFORM_DESTROY == true }}"
continue-on-error: true
- name: Terraform Destroy
id: destroy
run: |
echo "Starting Terraform destroy..."
# Standard terraform destroy for all components
echo "Running terraform destroy for ${{ inputs.TERRAFORM_COMPONENT }}..."
terraform destroy -var-file="${{ inputs.CLOUD_PROVIDER }}.tfvars" -no-color -auto-approve
echo "✅ Terraform destroy completed successfully"
if: "${{ inputs.TERRAFORM_DESTROY == true }}"
continue-on-error: true
- name: Emergency State Save on Cancellation
run: |
echo "=== EMERGENCY STATE SAVE ==="
echo "Workflow cancellation detected - attempting emergency state save..."
# Only for local backend
if [ "${{ inputs.BACKEND_TYPE }}" = "local" ]; then
# Quick encrypt and commit whatever state we have
../../../../.github/scripts/encrypt-state.sh \
--backend-type "local" \
--passphrase "${{ secrets.GPG_PASSPHRASE }}" \
--operation "destroy" \
--destroy-success "false" || true
# Emergency commit
git add -f *.tfstate* *.gpg tf-plan* backend.tf 2>/dev/null || true
# Explicitly exclude .terraform directory and its contents
git rm --cached -r .terraform 2>/dev/null || true
git rm --cached .terraform.lock.hcl 2>/dev/null || true
if ! git diff --cached --quiet; then
git config --global user.email ${{ github.actor }}@users.noreply.github.com
git config --global user.name ${{ github.actor }}
git commit -m "EMERGENCY: Destroy state save before cancellation [skip ci]" || true
git push || true
echo "✅ Emergency state saved"
else
echo "⚠️ No state changes to save"
fi
fi
if: cancelled()
continue-on-error: true
- name: Clean up state files and artifacts
run: |
echo "Cleaning up state files and artifacts after successful destruction..."
# Use the encryption script to handle cleanup logic
../../../../.github/scripts/encrypt-state.sh \
--backend-type "${{ inputs.BACKEND_TYPE }}" \
--passphrase "${{ secrets.GPG_PASSPHRASE }}" \
--operation "destroy" \
--destroy-success "true"
rm -f backend.tf
echo "✅ Cleanup completed"
if: "${{ inputs.TERRAFORM_DESTROY == true && steps.destroy.outcome == 'success' }}"
- name: Cleanup State Locking Resources
run: |
echo "Cleaning up state locking resources..."
../../../../.github/scripts/cleanup-state-locking.sh \
--provider "${{ inputs.CLOUD_PROVIDER }}" \
--config "${{ inputs.REMOTE_BACKEND_CONFIG }}" \
--branch "${{ github.ref_name }}" \
--component "${{ inputs.TERRAFORM_COMPONENT }}" \
${{ inputs.ENABLE_STATE_LOCKING == true && '--enable-locking' || '' }}
if: "${{ inputs.TERRAFORM_DESTROY == true && steps.destroy.outcome == 'success' && inputs.BACKEND_TYPE == 'remote' }}"
- name: Add the Terraform state changes
run: |
echo "Current working directory: $(pwd)"
echo "Files in current directory:"
ls -la
echo "Checking for Terraform files to commit..."
# Handle GPG encryption for local backend if state files exist
if [ "${{ steps.destroy.outcome }}" != "success" ]; then
../../../../.github/scripts/encrypt-state.sh \
--backend-type "${{ inputs.BACKEND_TYPE }}" \
--passphrase "${{ secrets.GPG_PASSPHRASE }}" \
--operation "destroy" \
--destroy-success "false"
fi
# Force add Terraform files even if they're in .gitignore - EXCLUDE .terraform
git add -f *.tfstate* 2>/dev/null || echo "No .tfstate files found"
git add -f *.gpg 2>/dev/null || echo "No .gpg files found"
git add -f backend.tf 2>/dev/null || echo "No backend.tf file found"
git add -f *.tf 2>/dev/null || echo "No .tf files to add"
# Explicitly exclude .terraform directory and its contents from being committed
git rm --cached -r .terraform 2>/dev/null || true
git rm --cached .terraform.lock.hcl 2>/dev/null || true
echo "✅ Excluded .terraform directory from commit"
# Add other Terraform-related files but avoid .terraform directory
git add -A
git reset .terraform 2>/dev/null || true # Remove .terraform from staged changes
git reset .terraform.lock.hcl 2>/dev/null || true # Remove lock file from staged changes
echo "Git status after adding files:"
git status
# Check if there are any changes to commit
if git diff --cached --quiet; then
echo "No changes to commit - this is normal for remote backends or when state is managed remotely"
echo "Terraform destroy completed successfully"
else
echo "Committing Terraform changes..."
git config --global user.email ${{ github.actor }}@users.noreply.github.com
git config --global user.name ${{ github.actor }}
# Customize commit message based on backend type and destroy outcome
if [ "${{ inputs.BACKEND_TYPE }}" = "local" ]; then
if [ "${{ steps.destroy.outcome }}" = "success" ]; then
commit_msg="Terraform destroy cleanup GitHub Actions - ${{ inputs.CLOUD_PROVIDER }}-${{ inputs.TERRAFORM_COMPONENT }}"
else
commit_msg="Terraform destroy (encrypted state) GitHub Actions - ${{ inputs.CLOUD_PROVIDER }}-${{ inputs.TERRAFORM_COMPONENT }}"
fi
else
commit_msg="Terraform destroy GitHub Actions - ${{ inputs.CLOUD_PROVIDER }}-${{ inputs.TERRAFORM_COMPONENT }}"
fi
git commit -s -am "$commit_msg"
git push
echo "Changes committed and pushed successfully"
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
if: "${{ inputs.TERRAFORM_DESTROY == true }}"
- name: Terraform destroy status
if: ${{ steps.destroy.outcome == 'failure' }}
run: |
echo "ERROR: Terraform destroy failed"
echo "Check the logs above for detailed error information"
exit 1
- name: Terraform destroy success
if: ${{ steps.destroy.outcome == 'success' }}
run: |
echo "SUCCESS: Terraform destroy completed successfully"
echo "All ${{ inputs.CLOUD_PROVIDER }}-${{ inputs.TERRAFORM_COMPONENT }} resources have been destroyed"
- uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,message,author,commit,workflow,job # selectable (default: repo,message)
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} # required
if: "${{ github.event_name != 'pull_request' && failure() }}" # Pick up events even if the job fails or is canceled.