Skip to main content
Version: 0.3

Create an IAC Catalog

Create IAC catalogs to provision cloud infrastructure resources using Terraform via Crossplane.

Summary

This guide walks you through creating an IAC catalog that uses Terraform to provision cloud resources through Crossplane's Terraform provider.

Prerequisites

  • Catalog prerequisites completed
  • Organization GitOps repository cloned locally
  • Understanding of Terraform basics (resources, variables, providers)
  • Cloud provider credentials configured in Crossplane

Step 1: Create Catalog Directory

  1. Navigate to your GitOps repository root:

    cd <your-org>-gitops
  2. Create catalog directory at root level (not inside catalog/):

    mkdir <catalog-name>

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

  3. Create required Terraform files:

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

    Important: The provider file has no file extension.

Step 2: Configure Provider File

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

  2. Add Terraform backend and provider configuration:

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

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

Token Placeholders:

Use <TOKEN_NAME> format for values that will be replaced during deployment:

  • <BUCKET_NAME>: S3 bucket for Terraform state
  • <NAME>: Cluster or resource name
  • <REGION>: AWS region (e.g., us-west-2, eu-west-1)
  • <ACCOUNT_ID>: AWS account ID
  • <ROLE_ARN>: IAM role ARN for Crossplane authentication

Tokens will be detokenized by the Konstruct operator during deployment.

Step 3: Define Terraform Resources

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

  2. Add your Terraform resources:

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

tags = {
Name = var.bucket_name
Environment = var.environment
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
}

Best Practices:

  • Reference variables for all configurable values
  • Include sensible security defaults (encryption, access blocks)
  • Add resource tags for identification
  • Use consistent resource naming

Step 4: Define Variables

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

  2. Define input variables using snake_case naming:

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

variable "environment" {
description = "Environment name (dev, staging, prod)"
type = string
default = "dev"
}

variable "enable_versioning" {
description = "Enable bucket versioning"
type = bool
default = true
}

Critical - Naming Convention:

Terraform variables must use snake_case:

  • ✅ Correct: bucket_name, instance_class, enable_versioning
  • ❌ Incorrect: bucketName, instanceClass, enableVersioning

Why: When users deploy the catalog, they provide values in camelCase through the Konstruct UI. The operator automatically converts camelCase to snake_case for Terraform.

Example conversion:

  • User provides: bucketName: "my-bucket"
  • Operator converts to: bucket_name = "my-bucket" for Terraform

Step 5: Define Outputs

  1. Open <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
}

Why Outputs Matter:

  • Used by other Terraform modules
  • Displayed in Konstruct UI after provisioning
  • Can be referenced by Hybrid catalogs for application configuration

Step 6: Commit and Push Catalog

  1. Stage catalog files:

    git add <catalog-name>/
  2. Commit changes:

    git commit -m "feat(catalog): add <catalog-name> IAC catalog"
  3. Push to remote:

    git push origin main

Step 7: Verify Catalog in Konstruct

  1. Navigate to Catalogs in the Konstruct UI

  2. Your IAC catalog should appear in the catalog list

  3. Click the catalog name to view configurable parameters

How IAC Catalogs Work

Understanding the deployment flow helps troubleshoot issues:

  1. Token Detokenization: Operator reads provider file and replaces <TOKEN> placeholders with actual values

  2. CamelCase Conversion: User-provided parameter values (camelCase) converted to snake_case for Terraform variables

  3. ProviderConfig Creation: Operator creates a Crossplane ProviderConfig CRD with detokenized provider configuration

  4. Workspace Creation: Operator creates a Crossplane Workspace CRD containing:

    • Reference to ProviderConfig
    • Terraform module configuration
    • Variable values (snake_case)
  5. Terraform Execution: Crossplane Terraform provider:

    • Initializes Terraform with the backend
    • Runs terraform plan
    • Runs terraform apply
    • Stores state in S3 backend
  6. Status Updates: Workspace status reflects Terraform execution state

Deployment Location

IAC catalogs deploy to the platform GitOps repository:

<org>-gitops/
└── registry/
└── clusters/
└── <project-cluster-name>/
└── components/
└── iac/
├── <catalog-name>.yaml # ProviderConfig + Workspace CRDs
└── iac.yaml # ArgoCD Application for IAC

Complete Example: S3 Bucket Catalog

Here's a complete example for provisioning S3 buckets:

provider:

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

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

main.tf:

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

tags = {
Name = var.bucket_name
Environment = var.environment
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
}

variables.tf:

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

variable "environment" {
description = "Environment tag"
type = string
default = "dev"
}

outputs.tf:

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
}

Best Practices

  • Snake case variables: Always use snake_case for Terraform variable names
  • Token placeholders: Use <UPPERCASE> format for provider file 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, Environment, ManagedBy)
  • Security defaults: Enable encryption, block public access, use least privilege
  • Output values: Export useful resource attributes for reference
  • Descriptive names: Use clear variable and output descriptions

Troubleshooting

Workspace Not Available

Check Crossplane provider status:

kubectl get providerconfig -n crossplane-system
kubectl get workspace -n crossplane-system

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 logs:

kubectl logs -n crossplane-system -l crossplane.io/claim-name=<catalog-name>

Common issues:

  • Variable type mismatch (check snake_case naming)
  • Missing required variables
  • Provider authentication failure
  • Resource quota exceeded

Token Detokenization Issues

Verify tokens in provider file:

  • Tokens must use <UPPERCASE> format
  • Check that all tokens are defined in deployment values
  • Ensure no typos in token names

What's Next?