Skip to content

terraform destroy

terraform destroy #47

# 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: terraform.tfstate.gpg, 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
# 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: 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
run: |
# First try normal init
if ! terraform init; then
echo "Normal init failed, trying with -reconfigure to handle backend configuration changes..."
terraform init -reconfigure
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: terraform destroy -var-file="${{ inputs.CLOUD_PROVIDER }}.tfvars" -no-color -auto-approve
if: "${{ inputs.TERRAFORM_DESTROY == true }}"
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
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"
# Also add any other Terraform-related files that might have been created
git add -A
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.