Skip to main content
Version: 0.5 (Next)

Create an IaC Catalog Item

Create IaC catalog items to provision cloud infrastructure resources using Terraform via Crossplane.

Summary

This guide walks you through creating an IaC catalog item that uses Terraform to provision cloud resources through Crossplane's Terraform provider. IaC catalog templates are stored in an external Git repository and registered with Konstruct through the UI.

Prerequisites

  • Catalog item prerequisites completed
  • A Git repository for your catalog templates (public or private)
  • Understanding of Terraform basics (resources, variables, providers)
  • Cloud provider credentials configured in Crossplane on the management cluster

Step 1: Create Template Repository Structure

IaC catalog templates live in an external Git repository, not in your GitOps repository. You can use a dedicated templates repository or a subdirectory of an existing one.

  1. Create a directory for your IaC template:

    mkdir -p gitops-catalog/<catalog-name>

    Use a descriptive name like s3, rds-postgres, or vpc-network.

  2. Create required Terraform files:

    touch gitops-catalog/<catalog-name>/provider
    touch gitops-catalog/<catalog-name>/main.tf
    touch gitops-catalog/<catalog-name>/variables.tf
    touch gitops-catalog/<catalog-name>/outputs.tf

    Important: The provider file has no file extension.

Step 2: Configure Provider File

The provider file contains Terraform backend and provider configuration with token placeholders that the operator replaces during deployment.

  1. Open gitops-catalog/<catalog-name>/provider (no extension)

  2. Add Terraform backend and provider configuration:

terraform {
backend "s3" {
bucket = "<PROJECT_STATE_STORE_BUCKET>"
key = "registry/clusters/<S3_NAME>/infrastructure/terraform.tfstate"
region = "<PROJECT_REGION>"
encrypt = true
}
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.30.0, < 6.0.0"
}
}
}

provider "aws" {
alias = "<PROJECT_REGION>"
region = "<PROJECT_REGION>"
allowed_account_ids = ["<PROJECT_AWS_ACCOUNT_ID>"]
assume_role_with_web_identity {
session_name = "kubefirst-pro"
role_arn = "<PROJECT_ASSUME_ROLE>"
web_identity_token_file = "/var/run/secrets/eks.amazonaws.com/serviceaccount/token"
}
}

Token Types

IaC deployment values use two types of tokens, distinguished by the TF_ prefix:

Provider tokens (no TF_ prefix) — string-replaced directly in the provider file:

TokenDescription
<PROJECT_STATE_STORE_BUCKET>S3 bucket for Terraform state
<PROJECT_REGION>Cloud provider region
<PROJECT_AWS_ACCOUNT_ID>AWS account ID
<PROJECT_ASSUME_ROLE>IAM role ARN for Crossplane authentication
<S3_NAME>Resource instance name

Workspace tokens (TF_ prefix) — become Crossplane Workspace variables passed to Terraform. The TF_ prefix is stripped and the key is converted to snake_case:

TokenTerraform Variable
<TF_NAME>name
<TF_BUCKET_REGION>bucket_region
<TF_INSTANCE_CLASS>instance_class

Example: A deployment with these values:

iacValues:
"<PROJECT_REGION>": "us-west-2"
"<PROJECT_STATE_STORE_BUCKET>": "my-state-bucket"
"<TF_NAME>": "my-s3-bucket"
"<TF_BUCKET_REGION>": "us-west-2"

Results in:

  • <PROJECT_REGION> and <PROJECT_STATE_STORE_BUCKET> are replaced in the provider file
  • name = "my-s3-bucket" and bucket_region = "us-west-2" are passed as Terraform variables via Crossplane Workspace

Step 3: Define Terraform Resources

  1. Open gitops-catalog/<catalog-name>/main.tf

  2. Add your Terraform resources:

resource "aws_s3_bucket" "main" {
bucket = var.name

tags = {
Name = var.name
ManagedBy = "Terraform"
}
}

resource "aws_s3_bucket_versioning" "main" {
bucket = aws_s3_bucket.main.id

versioning_configuration {
status = "Enabled"
}
}

resource "aws_s3_bucket_server_side_encryption_configuration" "main" {
bucket = aws_s3_bucket.main.id

rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}

resource "aws_s3_bucket_public_access_block" "main" {
bucket = aws_s3_bucket.main.id

block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}

Step 4: Define Variables

  1. Open gitops-catalog/<catalog-name>/variables.tf

  2. Define input variables using snake_case naming:

variable "name" {
description = "Name of the S3 bucket"
type = string
}

variable "bucket_region" {
description = "AWS region for the bucket"
type = string
}

Critical: Variable names must match the snake_case conversion of your TF_ prefixed tokens. For example, <TF_BUCKET_REGION> becomes the variable bucket_region.

Step 5: Define Outputs

  1. Open gitops-catalog/<catalog-name>/outputs.tf

  2. Add Terraform outputs for important resource attributes:

output "bucket_id" {
description = "The name of the S3 bucket"
value = aws_s3_bucket.main.id
}

output "bucket_arn" {
description = "The ARN of the S3 bucket"
value = aws_s3_bucket.main.arn
}

output "bucket_region" {
description = "The AWS region of the S3 bucket"
value = aws_s3_bucket.main.region
}

Step 6: Bundled Dependencies

If your Terraform module includes application code with external dependencies (for example, a Lambda function with npm packages), those dependencies must be committed to the repository.

Crossplane Terraform workspaces clone the module from git and run terraform apply directly — there is no build step. Package managers like npm install or pip install do not run during deployment. If your module uses a Terraform resource that packages a source directory (such as terraform-aws-modules/lambda with source_path), all runtime dependencies must be present in the repository.

Example: Node.js Lambda function

If your module provisions a Lambda function with npm dependencies:

cd lambda/handler

# Install production dependencies only
npm install --omit=dev

# Commit node_modules to the repository
git add node_modules/
tip

For AWS Lambda functions using Node.js 18.x or 20.x, the AWS SDK v3 (@aws-sdk/*) is included in the Lambda runtime. Remove it from package.json to reduce package size — your require('@aws-sdk/...') calls will resolve to the runtime-provided version.

Step 7: Commit and Push Template

  1. Commit your template to the external repository:

    git add gitops-catalog/<catalog-name>/
    git commit -m "feat(catalog): add <catalog-name> IaC catalog template"
    git push origin main
  2. Register the catalog in the Konstruct UI:

    • Navigate to Catalogs
    • Create a new CatalogApplication pointing to your template repository URL and path

How IaC Catalog Deployment Works

When you deploy an IaC catalog item through the Konstruct UI, the application-operator on the control plane performs these steps:

  1. Clone template: Clones the IaC template from the catalog source repository
  2. Push module: Commits the Terraform module to the platform GitOps repository at terraform/<catalogAppName>/
  3. Split values: Separates deployment values by TF_ prefix — provider tokens for string replacement, workspace tokens for Terraform variables
  4. Detokenize provider: Replaces <TOKEN> placeholders in the provider file with actual values
  5. Generate Crossplane resources: Creates ProviderConfig and Workspace CRDs
  6. Generate ArgoCD apps: Creates ArgoCD Applications for ordered sync
  7. Commit to platform GitOps: Pushes all generated content to the platform GitOps repository
  8. ArgoCD syncs: Management cluster ArgoCD detects new content and syncs
  9. Crossplane provisions: Crossplane Terraform provider runs terraform plan and terraform apply
CatalogDeployment CR (control plane)
|
Application-operator clones IaC template from catalog source
|
Commits terraform module + Crossplane resources to platform GitOps repo
|
Mgmt cluster ArgoCD syncs --> creates ProviderConfig + Workspace
|
Crossplane runs Terraform --> provisions infrastructure

Deployment Structure

IaC catalog items deploy to the platform GitOps repository (not the application GitOps repository). The operator commits the following structure:

<platformOrg>/<projectName>-gitops/
|-- terraform/<catalogAppName>/ # Cloned Terraform module
| |-- provider
| |-- main.tf
| |-- variables.tf
| +-- outputs.tf
|
|-- registry/iac/<instanceName>/ # Crossplane resources
| |-- provider-config/
| | +-- provider-config.yaml # ProviderConfig (sync-wave -1)
| +-- infrastructure/
| +-- workspace.yaml # Workspace referencing module
|
+-- registry/clusters/<clusterName>/
|-- iac.yaml # App-of-apps for all IaC
+-- components/iac/
|-- <instanceName>.yaml # ArgoCD app -> iac root
|-- <instanceName>-provider.yaml # ArgoCD app -> provider-config
+-- <instanceName>-infra.yaml # ArgoCD app -> infrastructure

Why Three ArgoCD Applications?

The operator creates three separate ArgoCD Applications per IaC deployment to ensure ordered sync:

  1. <instanceName>-provider.yaml — Syncs the ProviderConfig first (sync-wave -1). The ProviderConfig contains cloud credentials and Terraform backend configuration.
  2. <instanceName>-infra.yaml — Syncs the Workspace after the ProviderConfig is ready. The Workspace references the ProviderConfig and triggers Terraform execution.
  3. <instanceName>.yaml — Parent app pointing to the IaC root directory.

A parent iac.yaml at the cluster level serves as the app-of-apps for all IaC deployments on that cluster.

Workspace Module Source

The Crossplane Workspace references the Terraform module from the same platform GitOps repository:

spec:
forProvider:
source: Remote
module: git::https://github.com/<platformOrg>/<projectName>-gitops//terraform/<catalogAppName>?ref=main

Private Template Repositories

IaC templates in public repositories work without additional configuration. For private repositories, the CatalogApplication must include an encrypted personal access token (PAT).

Generate a PAT

Create a personal access token with read-only access to the template repository:

  • GitHub: Fine-grained token with Contents: Read permission on the repository
  • GitLab: Project access token with read_repository scope

Register with PAT via the UI

When registering a CatalogApplication through the Konstruct UI, provide the PAT in the authentication field. The UI encrypts and stores it automatically.

Register with PAT via kubectl

If registering via kubectl, the PAT must be AES-GCM encrypted with a key derived from the CatalogApplication's namespace:name. The iac.pat field accepts the base64-encoded ciphertext:

apiVersion: konstruct.civo.com/beta1
kind: CatalogApplication
metadata:
name: my-private-catalog
namespace: default
spec:
category: infrastructure-as-code
description: "My private IaC catalog item"
type: IAC
iac:
repo_url: https://github.com/my-org/my-templates
path: my-module
branch: main
pat: "<encrypted-pat>"

The encryption key is SHA-256("default:my-private-catalog") — the namespace and name of the CatalogApplication, colon-separated. The Konstruct API provides an encryption endpoint, or you can use the UI which handles encryption automatically.

note

GitHub App installation tokens are not supported in the iac.pat field. The operator resolves the Git username via the GitHub /user API, which requires a user-scoped token.

CatalogApplication Fields

FieldRequiredDescription
iac.repo_urlYesHTTPS URL to the Git repository containing the template
iac.pathYesPath within the repository to the Terraform module directory
iac.branchYesBranch to clone from
iac.patNoEncrypted PAT for private repositories (omit for public repos)

Best Practices

  • Snake case variables: Always use snake_case for Terraform variable names
  • Token naming: Use <UPPERCASE> format — TF_ prefix for Terraform vars, no prefix for provider tokens
  • State management: Configure S3 backend with encryption enabled
  • Provider versions: Pin provider versions for stability (e.g., >= 5.30.0, < 6.0.0)
  • Resource tagging: Include consistent tags (Name, ManagedBy)
  • Security defaults: Enable encryption, block public access, use least privilege
  • Commit runtime dependencies: If your module includes application code (Lambda functions, Cloud Functions), commit all dependencies — Crossplane cannot run package managers during deployment
  • Public repos: IaC templates in public repositories do not require a PAT

Troubleshooting

Workspace Not Available

Check Crossplane resource status:

kubectl get providerconfig
kubectl get workspace

Common issues:

  • ProviderConfig credentials incorrect
  • S3 backend bucket doesn't exist or lacks permissions
  • IAM role ARN invalid or lacks trust relationship

Terraform Plan Failures

View Workspace details:

kubectl describe workspace <instanceName>

Common issues:

  • Variable type mismatch (check that TF_ token names match snake_case variables)
  • Missing required variables
  • Provider authentication failure
  • Resource quota exceeded

IaC Clone Failed: Authentication Required

If the CatalogDeployment shows IACCloneFailed with "authentication required" or "Repository not found":

  1. Verify the repository URL is correct and accessible
  2. For private repositories, ensure the CatalogApplication has a valid iac.pat field
  3. Confirm the PAT has read access to the repository
  4. If using kubectl, verify the PAT is encrypted with the correct key (namespace:name of the CatalogApplication)
kubectl get catalogdeployment <name> -n <namespace> -o jsonpath='{.status.conditions}' | jq .

Token Detokenization Issues

Verify tokens in provider file:

  • Provider tokens must use <UPPERCASE> format without TF_ prefix
  • Workspace tokens must use <TF_UPPERCASE> format
  • Check that all tokens are defined in deployment values
  • Ensure no typos in token names

What's Next?