back to tutorials

How to Scale AI Agents with Reusable Terraform Modules

Learn how to package a ChatBotKit AI agent as a reusable Terraform module and roll it out consistently across many environments and teams - with workspaces, version pinning, and a CI/CD pipeline.

Once you are running more than one or two AI agents, copy-pasting Terraform between projects stops scaling. The fix is the same as for any infrastructure: capture the agent once as a reusable module, parameterise what differs, and roll it out consistently across environments and teams - with version pinning and a CI/CD pipeline doing the applies.

This tutorial shows you how to turn a ChatBotKit agent into a module and deploy it at scale.

What You'll Learn

By the end of this tutorial, you will be able to:

  • Package an AI agent (bot, skillset, abilities) as a reusable Terraform module
  • Parameterise the agent with input variables and expose outputs
  • Deploy the module across environments using Terraform workspaces
  • Stand up many agents from one configuration
  • Pin the provider version and store state remotely
  • Automate plan/apply with GitHub Actions

Prerequisites

Before starting, make sure you have:

  • A ChatBotKit account with an API key
  • Terraform version 1.0 or higher installed
  • Familiarity with Terraform modules, variables, and state

Estimated time: 30-40 minutes

Step 1: Package the Agent as a Module

A module is just a directory of Terraform files with inputs and outputs. Create modules/agent/ and capture the agent there, with everything that varies between deployments exposed as variables.

modules/agent/variables.tf:

variable "name" {
  description = "Display name of the agent"
  type        = string
}

variable "backstory" {
  description = "System prompt that defines the agent's behaviour"
  type        = string
}

variable "model" {
  description = "The model the agent uses"
  type        = string
  default     = "claude-4.5-sonnet"
}

variable "meta" {
  description = "Metadata applied to the agent (environment, team, etc.)"
  type        = map(string)
  default     = {}
}

modules/agent/main.tf:

terraform {
  required_providers {
    chatbotkit = {
      source = "chatbotkit/chatbotkit"
    }
  }
}

resource "chatbotkit_skillset" "abilities" {
  name        = "${var.name} Abilities"
  description = "Toolset for ${var.name}"
}

resource "chatbotkit_skillset_ability" "web_search" {
  skillset_id = chatbotkit_skillset.abilities.id
  name        = "search_web"
  description = "Search the web for current information"
  instruction = <<-EOT
    ```search
    query: $[query! ys|the search query to find information on the web]
    ```
  EOT
}

resource "chatbotkit_bot" "agent" {
  name        = var.name
  backstory   = var.backstory
  model       = var.model
  skillset_id = chatbotkit_skillset.abilities.id
  meta        = var.meta
}

modules/agent/outputs.tf:

output "bot_id" {
  description = "The ID of the agent"
  value       = chatbotkit_bot.agent.id
}

output "skillset_id" {
  description = "The ID of the agent's skillset"
  value       = chatbotkit_skillset.abilities.id
}

Now the agent's shape lives in one place. Anything that should be configurable per deployment is an input; anything other configurations need to reference is an output.

Step 2: Use the Module

A root configuration consumes the module. Pin the provider version and configure a remote backend so a team (and CI) can share state safely.

main.tf:

terraform {
  required_providers {
    chatbotkit = {
      source  = "chatbotkit/chatbotkit"
      version = "~> 1.5"
    }
  }

  backend "s3" {
    bucket = "my-tf-state"
    key    = "chatbotkit/agents.tfstate"
    region = "us-east-1"
  }
}

provider "chatbotkit" {}

variable "environment" {
  type    = string
  default = "development"
}

module "support_agent" {
  source = "./modules/agent"

  name      = "Support Agent (${var.environment})"
  backstory = "You are a helpful customer-support assistant. Be friendly and resolve issues on first contact."

  meta = {
    managed_by  = "terraform"
    environment = var.environment
    team        = "customer-success"
  }
}

output "support_bot_id" {
  value = module.support_agent.bot_id
}

Step 3: Deploy Across Environments with Workspaces

Workspaces give each environment its own state from the same configuration. Combined with the environment variable, you get isolated dev / staging / production agents:

terraform workspace new development
terraform apply -var "environment=development"

terraform workspace new production
terraform apply -var "environment=production"

Switch between them with terraform workspace select <name>. Each workspace tracks its own resources, so a change in development never touches production.

Step 4: Stand Up Many Agents at Once

Because the agent is a module, you can instantiate it many times in one configuration - for example a fleet of department agents. Drive them from a single map so adding one is a data change:

variable "agents" {
  type = map(object({
    backstory = string
  }))
  default = {
    "Sales Agent"   = { backstory = "You are a sales assistant. Qualify leads and answer product questions." }
    "Support Agent" = { backstory = "You are a support assistant. Resolve issues quickly and clearly." }
    "HR Agent"      = { backstory = "You are an HR assistant. Answer policy questions for employees." }
  }
}

module "fleet" {
  source   = "./modules/agent"
  for_each = var.agents

  name      = each.key
  backstory = each.value.backstory
}

output "fleet_bot_ids" {
  value = { for k, m in module.fleet : k => m.bot_id }
}

Note: for_each works for instantiating modules within a single account like this. To deploy into separate accounts (one per customer), use provider aliases and run_as instead - see How to Deploy Multi-Tenant AI Agents with Terraform.

Step 5: Automate with CI/CD

With state remote and the provider pinned, hand applies over to a pipeline so every change is planned, reviewed, and applied consistently. A minimal GitHub Actions workflow:

name: Deploy ChatBotKit Agents

on:
  push:
    branches: [main]
    paths: ['terraform/**']

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: hashicorp/setup-terraform@v3

      - name: Terraform Init
        run: terraform init
        working-directory: terraform

      - name: Terraform Apply
        run: terraform apply -auto-approve
        working-directory: terraform
        env:
          CHATBOTKIT_API_KEY: ${{ secrets.CHATBOTKIT_API_KEY }}

For safety, run terraform plan on pull requests and reserve apply for merges to main. Store CHATBOTKIT_API_KEY as an encrypted CI secret.

Best Practices for Scale

  • Version everything. Pin the provider (version = "~> 1.5") and commit the lock file so every environment resolves the same provider.
  • One module, many callers. Keep the agent definition in the module; let roots only supply inputs. Improving the agent is a single edit.
  • Remote, isolated state. Use a remote backend and a separate state (workspace or key) per environment or tenant, so blast radius stays contained.
  • Tag with meta. Stamp environment, team, and managed_by so resources are easy to find and audit.
  • Review plans. Gate apply behind a reviewed plan in CI.

Next Steps

Capturing an agent as a module turns "deploy an agent" into a repeatable, reviewable operation - the foundation for running many agents, across many environments, without the configuration sprawl.