CI/CD
Terraform
Send Terraform logs and state changes to Parseable. Follow the setup steps and practical guidance for using Terraform in your Parseable observability workflow.
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?