Terraform
Send Terraform logs and state changes to Parseable
Collect Terraform execution logs and state changes in Parseable.
Overview
Integrate Terraform with Parseable to:
- Execution Logs - Track plan and apply operations
- State Changes - Monitor infrastructure changes
- Audit Trail - Compliance and change history
- Cost Tracking - Monitor resource changes
Prerequisites
- Terraform CLI or Terraform Cloud
- Parseable instance accessible
- CI/CD pipeline (recommended)
Method 1: CI/CD Integration
Send Terraform logs from your CI/CD pipeline.
GitHub Actions
name: Terraform
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_wrapper: false
- name: Terraform Init
id: init
run: terraform init
- name: Terraform Plan
id: plan
run: terraform plan -out=tfplan -json > plan.json 2>&1
continue-on-error: true
- name: Send Plan to Parseable
run: |
# Parse plan output
ADDS=$(jq '[.resource_changes[]? | select(.change.actions | contains(["create"]))] | length' plan.json 2>/dev/null || echo 0)
CHANGES=$(jq '[.resource_changes[]? | select(.change.actions | contains(["update"]))] | length' plan.json 2>/dev/null || echo 0)
DESTROYS=$(jq '[.resource_changes[]? | select(.change.actions | contains(["delete"]))] | length' plan.json 2>/dev/null || echo 0)
curl -X POST "${{ secrets.PARSEABLE_URL }}/api/v1/ingest" \
-H "Authorization: Basic ${{ secrets.PARSEABLE_AUTH }}" \
-H "X-P-Stream: terraform-logs" \
-H "Content-Type: application/json" \
-d "[{
\"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",
\"event\": \"plan\",
\"repository\": \"${{ github.repository }}\",
\"ref\": \"${{ github.ref }}\",
\"sha\": \"${{ github.sha }}\",
\"actor\": \"${{ github.actor }}\",
\"status\": \"${{ steps.plan.outcome }}\",
\"resources_to_add\": ${ADDS},
\"resources_to_change\": ${CHANGES},
\"resources_to_destroy\": ${DESTROYS}
}]"
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
id: apply
run: terraform apply -auto-approve tfplan
- name: Send Apply to Parseable
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: |
curl -X POST "${{ secrets.PARSEABLE_URL }}/api/v1/ingest" \
-H "Authorization: Basic ${{ secrets.PARSEABLE_AUTH }}" \
-H "X-P-Stream: terraform-logs" \
-H "Content-Type: application/json" \
-d "[{
\"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",
\"event\": \"apply\",
\"repository\": \"${{ github.repository }}\",
\"sha\": \"${{ github.sha }}\",
\"actor\": \"${{ github.actor }}\",
\"status\": \"${{ steps.apply.outcome }}\"
}]"GitLab CI
stages:
- plan
- apply
variables:
TF_ROOT: ${CI_PROJECT_DIR}
plan:
stage: plan
image: hashicorp/terraform:latest
script:
- terraform init
- terraform plan -out=tfplan -json > plan.json
- |
ADDS=$(jq '[.resource_changes[]? | select(.change.actions | contains(["create"]))] | length' plan.json)
CHANGES=$(jq '[.resource_changes[]? | select(.change.actions | contains(["update"]))] | length' plan.json)
DESTROYS=$(jq '[.resource_changes[]? | select(.change.actions | contains(["delete"]))] | length' plan.json)
curl -X POST "${PARSEABLE_URL}/api/v1/ingest" \
-H "Authorization: Basic ${PARSEABLE_AUTH}" \
-H "X-P-Stream: terraform-logs" \
-H "Content-Type: application/json" \
-d "[{
\"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",
\"event\": \"plan\",
\"project\": \"${CI_PROJECT_PATH}\",
\"ref\": \"${CI_COMMIT_REF_NAME}\",
\"sha\": \"${CI_COMMIT_SHA}\",
\"actor\": \"${GITLAB_USER_LOGIN}\",
\"resources_to_add\": ${ADDS},
\"resources_to_change\": ${CHANGES},
\"resources_to_destroy\": ${DESTROYS}
}]"
artifacts:
paths:
- tfplan
apply:
stage: apply
image: hashicorp/terraform:latest
script:
- terraform init
- terraform apply -auto-approve tfplan
- |
curl -X POST "${PARSEABLE_URL}/api/v1/ingest" \
-H "Authorization: Basic ${PARSEABLE_AUTH}" \
-H "X-P-Stream: terraform-logs" \
-H "Content-Type: application/json" \
-d "[{
\"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",
\"event\": \"apply\",
\"project\": \"${CI_PROJECT_PATH}\",
\"sha\": \"${CI_COMMIT_SHA}\",
\"status\": \"success\"
}]"
only:
- mainMethod 2: Terraform Cloud Webhooks
Use Terraform Cloud run notifications.
Webhook Receiver
const express = require('express');
const axios = require('axios');
const app = express();
app.use(express.json());
const PARSEABLE_URL = process.env.PARSEABLE_URL;
const PARSEABLE_AUTH = process.env.PARSEABLE_AUTH;
app.post('/webhook', async (req, res) => {
const notification = req.body;
const logEntry = {
timestamp: new Date().toISOString(),
event: 'terraform_cloud',
run_id: notification.run_id,
run_status: notification.run_status,
workspace: notification.workspace_name,
organization: notification.organization_name,
message: notification.run_message,
created_by: notification.run_created_by,
is_destroy: notification.run_is_destroy,
plan_resource_additions: notification.plan_resource_additions,
plan_resource_changes: notification.plan_resource_changes,
plan_resource_destructions: notification.plan_resource_destructions
};
try {
await axios.post(`${PARSEABLE_URL}/api/v1/ingest`, [logEntry], {
headers: {
'Authorization': `Basic ${PARSEABLE_AUTH}`,
'X-P-Stream': 'terraform-cloud',
'Content-Type': 'application/json'
}
});
} catch (error) {
console.error('Error:', error);
}
res.status(200).json({ status: 'received' });
});
app.listen(3000);Configure in Terraform Cloud
- Go to Workspace Settings → Notifications
- Click Create Notification
- Select Webhook
- Enter your webhook URL
- Select triggers (Run completed, needs attention, etc.)
Querying Terraform Logs
-- Recent Terraform operations
SELECT timestamp, event, repository, actor, status,
resources_to_add, resources_to_change, resources_to_destroy
FROM "terraform-logs"
ORDER BY timestamp DESC
LIMIT 100
-- Failed applies
SELECT timestamp, repository, actor, status
FROM "terraform-logs"
WHERE event = 'apply' AND status != 'success'
ORDER BY timestamp DESC
-- Resource change summary
SELECT
DATE_TRUNC('day', timestamp) as day,
SUM(resources_to_add) as total_added,
SUM(resources_to_change) as total_changed,
SUM(resources_to_destroy) as total_destroyed
FROM "terraform-logs"
WHERE event = 'plan'
GROUP BY day
ORDER BY day DESC
-- Most active repositories
SELECT
repository,
COUNT(*) as operations,
SUM(resources_to_add + resources_to_change + resources_to_destroy) as total_changes
FROM "terraform-logs"
WHERE timestamp > NOW() - INTERVAL '30 days'
GROUP BY repository
ORDER BY operations DESCBest Practices
- Log Plans and Applies - Track both operations
- Include Resource Counts - Monitor change volume
- Track Actors - Know who made changes
- Alert on Destroys - Notify on resource deletions
- Store Plan Files - Keep for audit purposes
Troubleshooting
Missing Logs
- Check CI/CD pipeline logs
- Verify Parseable endpoint
- Check authentication
Incorrect Counts
- Verify JSON parsing
- Check plan output format
- Handle empty plans
Next Steps
- Set up alerts for infrastructure changes
- Create dashboards for IaC metrics
- Configure ArgoCD for GitOps
Was this page helpful?