IaC Integration: Cultivator and Agronomist¶
Farm integrates with two companion tools that feed Infrastructure-as-Code visibility data into the portal:
- Cultivator — discovers IaC stacks in a repository and reports Terraform or OpenTofu plan/apply run outcomes to Farm.
- Agronomist — scans pinned Terraform module versions across stacks and reports drift (how many semver versions behind the latest release each module is).
1. Configuring IAC_INGEST_TOKEN¶
All machine-to-machine ingest endpoints use a static bearer token for authentication. Set the IAC_INGEST_TOKEN environment variable on the Farm API server:
# .env / docker-compose.yml / Kubernetes Secret
IAC_INGEST_TOKEN=<a long random string — treat it like a password>
Generate a secure value with:
If IAC_INGEST_TOKEN is empty, every ingest request will be rejected with 401 Unauthorized.
2. Cultivator — Reporting Run Results¶
After each terraform plan or terraform apply step in CI, post the outcome to Farm using the POST /api/v1/iac/runs/ingest endpoint.
GitHub Actions example¶
- name: Report IaC run to Farm
if: always() # run even on failure so Farm sees failed runs
env:
FARM_URL: ${{ vars.FARM_URL }}
IAC_INGEST_TOKEN: ${{ secrets.IAC_INGEST_TOKEN }}
run: |
STATUS="succeeded"
if [ "${{ job.status }}" != "success" ]; then
STATUS="failed"
fi
curl --fail-with-body -s -X POST \
"${FARM_URL}/api/v1/iac/runs/ingest" \
-H "Authorization: Bearer ${IAC_INGEST_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"stackName\": \"${{ inputs.stack_name }}\",
\"environment\": \"${{ inputs.environment }}\",
\"provider\": \"terraform\",
\"type\": \"${{ inputs.run_type }}\",
\"status\": \"${STATUS}\",
\"resourceChanges\": {
\"add\": ${{ steps.tf-plan.outputs.add || 0 }},
\"change\": ${{ steps.tf-plan.outputs.change || 0 }},
\"destroy\": ${{ steps.tf-plan.outputs.destroy || 0 }}
},
\"triggeredBy\": \"${{ github.actor }}\",
\"pipelineUrl\": \"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\",
\"startedAt\": \"${{ steps.start-time.outputs.time }}\",
\"durationMs\": ${{ steps.duration.outputs.ms || 0 }}
}"
Request body fields
| Field | Required | Description |
|---|---|---|
stackName | Yes | Stack identifier — unique within an environment |
environment | Yes | Target environment (production, staging, etc.) |
provider | No | terraform (default) or opentofu |
type | Yes | plan or apply |
status | Yes | succeeded, failed, or cancelled |
resourceChanges | No | { add, change, destroy } from plan/apply output |
triggeredBy | No | GitHub actor or CI system name |
pipelineUrl | No | Link to the CI run |
startedAt | No | ISO 8601 start time |
finishedAt | No | ISO 8601 finish time |
durationMs | No | Total duration in milliseconds |
3. Cultivator — Importing Stacks via Discovery¶
The cultivator discover command scans a repository for IaC stack roots and writes a JSON manifest. Pass that manifest to Farm with POST /api/v1/iac/stacks/import.
- name: Discover stacks with Cultivator
run: cultivator discover --root ./infra --output stacks.json
- name: Import discovered stacks to Farm
env:
FARM_URL: ${{ vars.FARM_URL }}
IAC_INGEST_TOKEN: ${{ secrets.IAC_INGEST_TOKEN }}
run: |
PAYLOAD=$(jq -n --argjson stacks "$(cat stacks.json)" '{"stacks": $stacks}')
curl --fail-with-body -s -X POST \
"${FARM_URL}/api/v1/iac/stacks/import" \
-H "Authorization: Bearer ${IAC_INGEST_TOKEN}" \
-H "Content-Type: application/json" \
-d "${PAYLOAD}"
Each stack entry in the payload:
{
"name": "core-networking",
"environment": "production",
"provider": "terraform",
"repositoryUrl": "https://github.com/acme/infra",
"basePath": "stacks/core-networking",
"externalToolUrl": "https://app.terraform.io/app/acme/workspaces/core-networking"
}
Upsert behaviour: if a stack with the same name + environment already exists, it is updated. The componentId field is always preserved. The externalToolUrl field is preserved unless it is explicitly included in the payload.
4. Agronomist — Reporting Module Drift¶
After running agronomist report --json report.json, post the drift data to Farm using POST /api/v1/iac/module-drift/ingest.
GitHub Actions example¶
- name: Run Agronomist drift scan
run: agronomist report --json agronomist-report.json
- name: Send module drift to Farm
if: always()
env:
FARM_URL: ${{ vars.FARM_URL }}
IAC_INGEST_TOKEN: ${{ secrets.IAC_INGEST_TOKEN }}
run: |
MODULES=$(jq '.modules' agronomist-report.json)
PAYLOAD=$(jq -n --argjson modules "${MODULES}" '{"modules": $modules}')
curl --fail-with-body -s -X POST \
"${FARM_URL}/api/v1/iac/module-drift/ingest" \
-H "Authorization: Bearer ${IAC_INGEST_TOKEN}" \
-H "Content-Type: application/json" \
-d "${PAYLOAD}"
Expected module shape from Agronomist:
{
"modules": [
{
"stackPath": "stacks/networking/main.tf",
"moduleName": "terraform-aws-modules/vpc/aws",
"sourceUrl": "https://registry.terraform.io/terraform-aws-modules/vpc/aws",
"currentRef": "v3.14.0",
"latestRef": "v3.19.0"
}
]
}
Farm automatically computes versionsBehind from the semver distance between currentRef and latestRef. For non-semver refs (e.g., main, commit SHAs), versionsBehind defaults to 1.
5. Viewing IaC Data in Farm¶
Navigate to IaC in the sidebar (Infrastructure section). The dashboard shows:
- Per-environment tabs with stack cards
- Each stack card: provider badge, last run status icon, run type, resource change chips, relative time, and an external tool link if configured
- Failed stacks surfaced at the top within each environment tab
- A Module Drift view tab listing all detected outdated module references
Click a stack card to view its full run timeline with pagination.