Example: Create and register an AWS collection agent with Terraform and Claude
Claude writes one Terraform configuration that creates the Monte Carlo deployment, deploys the agent in your AWS account and registers it. You run terraform apply; integrations are connected to the agent later.
A collection agent runs in your AWS account, so connections originate from your network and credentials can stay in your own secret store. In this example Claude writes a single Terraform configuration that does everything: it creates the deployment in Monte Carlo, deploys the agent with Monte Carlo's mcd-agent module, and registers it. You run terraform apply once. No integration is connected yet; the other examples connect integrations to an agent like this one.
Don't use Terraform? See Create and register an AWS collection agent with the AWS CLI: Claude creates the deployment and you deploy the agent with Monte Carlo's CloudFormation template.
| Who | What | How |
|---|---|---|
| Claude | Read your account and write the Terraform | MCP tools, then files in your working directory |
| You | Create the deployment, the agent and its registration | terraform apply |
| Claude | Confirm the deployment is enabled | get_deployment |
Before you start
- Claude with the Agent Toolkit plugin, signed in to the Monte Carlo MCP server. See Agent Toolkit.
- Terraform 1.11 or later, with the Monte Carlo provider (
monte-carlo-data/montecarlo) authenticated through your Monte Carlo CLI profile (~/.mcd/profiles.ini) orMCD_DEFAULT_API_ID/MCD_DEFAULT_API_TOKEN.montecarlo whoamishows which account the profile reaches. - AWS credentials for the account the agent runs in, able to create IAM roles, Lambda functions, security groups and S3 buckets.
- The Collection AWS account ID from Settings → Account information (Monte Carlo's account, not yours).
- For databases inside a VPC: at least two private subnets that can reach them and that have outbound internet access through NAT. Without subnets, the agent reaches only publicly accessible endpoints.
1. Ask Claude
/monte-carlo-onboarding we need a Monte Carlo collection agent in our AWS account, all with Terraform; we'll connect databases to it later
Claude reads your account first. If you already have an enabled AWS agent, it asks whether to reuse it: a second agent is only needed for a different AWS account, region, or a network the first one can't reach. It proposes one configuration with one apply, and asks for the region, the Collection AWS account ID, a deployment name, the private subnets, and where your Terraform state lives.
2. Answer Claude's questions
Answers:
- AWS account <account-id>, region <region>. Collection AWS account ID: <collection-aws-account-id>.
- We need a new agent for the databases in VPC <vpc-id>; run it in the private subnets <subnet-a> and <subnet-b> (they have a NAT route).
- Deployment name: <deployment-name>.
- I run Terraform myself; the Monte Carlo provider uses my default CLI profile. Write the files into the current directory.
- State: <your state backend>.
3. The Terraform Claude writes
Claude writes the files and checks them with terraform validate. No variable has a default, so a missing value stops terraform plan and names it. Nothing in these files, the plan or the state is secret.
versions.tf:
terraform {
# Write-only arguments need Terraform 1.11 or later.
required_version = ">= 1.11"
required_providers {
montecarlo = { source = "monte-carlo-data/montecarlo" }
# 6.50 is the first with `secret_string_wo`, used when databases are connected later.
aws = { source = "hashicorp/aws", version = ">= 6.50" }
}
}
# Endpoint from the profile's mcd_api_endpoint when set; otherwise pass it in (no default).
variable "montecarlo_endpoint" {
type = string
default = null # null means "use the profile's endpoint"; never a URL here
}
provider "montecarlo" {
endpoint = var.montecarlo_endpoint
}
provider "aws" {
region = var.aws_region
}variables.tf:
variable "aws_region" { type = string }
# The AWS account the agent is deployed into; checked against the caller's identity.
variable "aws_account_id" { type = string }
# Account information → Collection → AWS account ID: Monte Carlo's account, not yours.
variable "monte_carlo_collection_account_id" { type = string }
variable "deployment_name" { type = string }
# Private subnets with a route to the databases (and a NAT route out).
variable "private_subnets" { type = list(string) }main.tf:
data "aws_caller_identity" "current" {}
resource "montecarlo_deployment" "agent" {
name = var.deployment_name
type = "COLLECTION_AGENT"
runtime_platform = "AWS"
lifecycle {
precondition {
condition = data.aws_caller_identity.current.account_id == var.aws_account_id
error_message = "The AWS credentials in use are not for aws_account_id."
}
precondition {
condition = var.monte_carlo_collection_account_id != var.aws_account_id
error_message = "monte_carlo_collection_account_id must be Monte Carlo's Collection AWS account ID, not the account the agent runs in."
}
}
}
module "mcd_agent" {
source = "monte-carlo-data/mcd-agent/aws"
version = "1.0.7"
region = var.aws_region
cloud_account_id = var.monte_carlo_collection_account_id
external_id = montecarlo_deployment.agent.aws_external_id
private_subnets = var.private_subnets
}
resource "montecarlo_aws_collection_agent" "agent" {
deployment_id = montecarlo_deployment.agent.id
name = var.deployment_name
lambda_function_arn = module.mcd_agent.mcd_agent_function_arn
role_arn = module.mcd_agent.mcd_agent_invoker_role_arn
}
# Non-secret ids and names only.
output "created_ids" {
value = {
deployment = montecarlo_deployment.agent.id
agent = montecarlo_aws_collection_agent.agent.id
}
}
output "agent" {
value = {
lambda_function_arn = module.mcd_agent.mcd_agent_function_arn
invoker_role_arn = module.mcd_agent.mcd_agent_invoker_role_arn
security_group_name = module.mcd_agent.mcd_agent_security_group_name
storage_bucket_name = module.mcd_agent.mcd_agent_storage_bucket_name
}
}The deployment's External ID goes straight from montecarlo_deployment into the module, so you never copy it by hand. The two preconditions stop the run before anything is created if your AWS credentials are for the wrong account, or if the Collection AWS account ID is your own.
Your values go in terraform.tfvars:
aws_region = "<region>"
aws_account_id = "<account-id>"
monte_carlo_collection_account_id = "<collection-aws-account-id>"
deployment_name = "<deployment-name>"
private_subnets = ["<subnet-a>", "<subnet-b>"]4. Apply
montecarlo whoami
aws sts get-caller-identity --query Account --output text
terraform init
terraform plan -out=agent.tfplan
terraform apply agent.tfplan
terraform output created_idsIn this example the plan created 21 resources: the deployment, the agent's Lambda function, IAM roles, security group and S3 bucket, and the registration. The apply took about five minutes, most of it creating the Lambda function inside the VPC. The registration took about a minute, because Monte Carlo checks it can reach the agent before registering it. Send Claude the created_ids output; the ids aren't secret.
5. Claude confirms
Claude reads the deployment and agent and confirms both are enabled. The run ends with:
Created in this run (by your terraform apply, 21 resources)
deployment <id> <deployment-name> COLLECTION_AGENT/AWS enabled=true
agent <id> <deployment-name> enabled=true
credentials / warehouse / connection none (databases to be connected later)
Pending on your side
- Later: Secrets Manager secret ARNs for the databases to connect
- Later: database security groups allow inbound traffic from the agent's security group
Cleanup if you abandon this: Terraform manages all of it, so don't use the delete tools.
To connect a database later, ask Claude to add it to this same configuration: a reference to its secret in your Secrets Manager, a read grant for the agent, then the warehouse and connection. See Onboard Snowflake through a new AWS agent, all with Terraform for what that looks like. Each database's security group also needs an inbound rule from the agent's security group (terraform output agent names it).
Removing the agent
Terraform manages everything here, so remove it with Terraform, not with Claude's delete tools. The agent's S3 bucket is versioned, and terraform destroy fails while it still holds old object versions: empty it first, then destroy. The steps are in Remove an integration and its agent. Deleting a Lambda function that runs inside a VPC can take 15–40 minutes while AWS releases its network interfaces.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| The plan stops at a precondition | The AWS credentials are for another account, or monte_carlo_collection_account_id is your own account ID. Fix the value and plan again. |
montecarlo_deployment fails with User is not authorized … explicit deny (sometimes reported as no value given for required property type) | The provider is calling a different Monte Carlo environment than the one your API key belongs to. Leave montecarlo_endpoint unset so the profile's endpoint is used, or set it to your environment's API URL. |
| Registration fails with "Could not connect to Agent" | The Collection AWS account ID is wrong, so Monte Carlo can't assume the invoker role. Check it against Settings → Account information. |
| The apply stops partway | What it created stays in the Terraform state: fix the cause and run terraform apply again. |
Updated 1 day ago
