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
-
Navigate to your GitOps repository root:
cd <your-org>-gitops -
Create catalog directory at root level (not inside
catalog/):mkdir <catalog-name>Use a descriptive name like
s3-bucket,rds-postgres, orvpc-network. -
Create required Terraform files:
touch <catalog-name>/provider
touch <catalog-name>/main.tf
touch <catalog-name>/variables.tf
touch <catalog-name>/outputs.tfImportant: The
providerfile has no file extension.
Step 2: Configure Provider File
-
Open
<catalog-name>/provider(no extension) -
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
-
Open
<catalog-name>/main.tf -
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
-
Open
<catalog-name>/variables.tf -
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
-
Open
<catalog-name>/outputs.tf -
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
-
Stage catalog files:
git add <catalog-name>/ -
Commit changes:
git commit -m "feat(catalog): add <catalog-name> IAC catalog" -
Push to remote:
git push origin main
Step 7: Verify Catalog in Konstruct
-
Navigate to Catalogs in the Konstruct UI
-
Your IAC catalog should appear in the catalog list
-
Click the catalog name to view configurable parameters
How IAC Catalogs Work
Understanding the deployment flow helps troubleshoot issues:
-
Token Detokenization: Operator reads
providerfile and replaces<TOKEN>placeholders with actual values -
CamelCase Conversion: User-provided parameter values (camelCase) converted to snake_case for Terraform variables
-
ProviderConfig Creation: Operator creates a Crossplane
ProviderConfigCRD with detokenized provider configuration -
Workspace Creation: Operator creates a Crossplane
WorkspaceCRD containing:- Reference to ProviderConfig
- Terraform module configuration
- Variable values (snake_case)
-
Terraform Execution: Crossplane Terraform provider:
- Initializes Terraform with the backend
- Runs
terraform plan - Runs
terraform apply - Stores state in S3 backend
-
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?
- Deploy your IAC catalog to provision infrastructure
- Create a Hybrid catalog combining IAC and YAML
- View more catalog examples
- Learn about dynamic environment provisioning