Infrastructure as Code with Terraform¶
This guide walks through the complete workflow for managing b'nerd resources with Terraform: installing the provider, authenticating, writing resource configurations, running plan/apply, and keeping state under control.
Prerequisites: Terraform >= 1.5 installed, a b'nerd account with an API token, and an organization + project already created (see First Project & API Token).
1. Install the Provider¶
Declare the provider in a versions.tf (or any .tf file) in your working directory:
terraform {
required_version = ">= 1.5"
required_providers {
bnerd = {
source = "bnerd-cloud/bnerd"
version = "~> 0.1"
}
}
}
Then initialize:
Terraform downloads the provider binary from the registry and locks the version in .terraform.lock.hcl. Commit the lock file; don't commit .terraform/.
2. Configure Authentication¶
The provider needs four values: API URL, bearer token, organization ID, and project ID. Supply them via environment variables to keep secrets out of source control:
export BNERD_API_URL="https://api.bnerd.cloud"
export BNERD_TOKEN="<your-api-token>"
export BNERD_ORG_ID="<org-uuid>"
export BNERD_PROJECT_ID="<project-uuid>"
Then declare the provider block with no hardcoded values:
Alternatively, pass values in HCL — useful when different workspaces target different projects:
provider "bnerd" {
api_url = var.bnerd_api_url
token = var.bnerd_token
org_id = var.org_id
project_id = var.project_id
}
Keep tokens out of state
API tokens passed via HCL variables end up in terraform.tfstate in plaintext. Use environment variables or a secrets manager (Vault, SOPS, GitLab CI variables) and reference them via TF_VAR_* instead.
3. Define Resources¶
Create a main.tf with the resources you want to manage. A typical starting point manages a project, object storage credentials, and a DNS zone:
# Project (if not already created out-of-band)
resource "bnerd_project" "main" {
name = "my-app"
organization_id = var.org_id
}
# RGW object storage user for the application
resource "bnerd_rgw_user" "app" {
name = "my-app-storage"
project_id = bnerd_project.main.id
}
# DNS zone
resource "bnerd_dns_zone" "primary" {
name = "example.com." # trailing dot required
kind = "Native"
nameservers = ["ns1.bnerd.net.", "ns2.bnerd.net."]
}
# A record for the web endpoint
resource "bnerd_dns_record" "www" {
zone_id = bnerd_dns_zone.primary.id
name = "www.example.com."
type = "A"
ttl = 300
records = ["203.0.113.10"]
}
Expose the S3 credentials as outputs so other systems can consume them:
output "s3_endpoint" { value = var.bnerd_api_url }
output "s3_access_key" { value = bnerd_rgw_user.app.access_key_id }
output "s3_secret_key" {
value = bnerd_rgw_user.app.secret_key
sensitive = true
}
RGW quota and locked fields
quota and locked on bnerd_rgw_user are computed/read-only — they are managed by platform admins and cannot be set via Terraform. If you need quota adjustments, contact your platform team.
4. Read Existing Infrastructure with Data Sources¶
If resources already exist (created via the dashboard or CLI), use data sources to reference them without importing or recreating them:
# Look up an existing network to attach a server to
data "bnerd_network" "mgmt" {
project_id = var.project_id
# filter by name in the consuming resource if the data source returns a list
}
# Look up an existing DNS zone
data "bnerd_dns_zone" "primary" {
name = "example.com."
}
# Add a record to the existing zone
resource "bnerd_dns_record" "api" {
zone_id = data.bnerd_dns_zone.primary.id
name = "api.example.com."
type = "CNAME"
ttl = 300
records = ["www.example.com."]
}
The full data source catalogue is in the Terraform Provider reference.
5. Plan and Apply¶
Preview what Terraform will create, change, or destroy:
Review the output carefully — resources marked + will be created, ~ will be updated in-place, -/+ will be destroyed and recreated. Apply when ready:
Terraform prompts for confirmation. Type yes to proceed. Pass -auto-approve only in CI pipelines where the plan has already been reviewed.
6. Manage State¶
Terraform stores the mapping between your configuration and real resources in terraform.tfstate. Keep this file safe:
- Never commit state to version control. Add
*.tfstateand*.tfstate.backupto.gitignore. - Use a remote backend for team workflows. GitLab's built-in HTTP backend is the simplest option for b'nerd projects:
terraform {
backend "http" {
# GitLab provides the URL, username, and password via CI variables:
# TF_HTTP_ADDRESS, TF_HTTP_USERNAME, TF_HTTP_PASSWORD
}
}
Run terraform init -reconfigure after adding a backend to migrate local state to the remote store.
- State locking is handled automatically by the GitLab backend — concurrent applies are blocked until the lock is released.
7. Import Existing Resources¶
If a resource was created outside Terraform (via the dashboard or CLI) and you want to bring it under management, use terraform import:
# Import an existing RGW user by its ID
terraform import bnerd_rgw_user.app <user-id>
# Import an existing DNS zone (supports org_id/zone_name or just zone_name)
terraform import bnerd_dns_zone.primary example.com.
After importing, run terraform plan to verify the state matches your configuration. Adjust any attributes that differ before committing.
8. Destroy¶
To remove all resources managed by a configuration:
Irreversible
Destroying an RGW user permanently deletes its access keys and all associated buckets and objects. Destroying a DNS zone removes all records. Always review the destroy plan before confirming.
Using Terraform Modules¶
For recurring infrastructure patterns — such as IPsec VPN gateways — use the b'nerd reusable modules:
module "ipsec_gateway" {
source = "git::https://git.bnerd.net/cloud/terraform/common.git//modules/ipsec-gateway?ref=v1.0.0"
prefix = "prod-gw"
router_id = data.openstack_networking_router_v2.router.id
network_id = "<management-network-uuid>"
subnet_id = "<management-subnet-uuid>"
floating_ip = "203.0.113.10"
image_id = data.openstack_images_image_v2.ubuntu.id
ssh_public_key = file("~/.ssh/id_ed25519.pub")
tunnels = [
{
name = "tunnel1"
local_cidr = "10.0.0.0/24"
remote_cidrs = ["192.168.10.0/24"]
},
]
}
See the Terraform Modules reference for the full ipsec-gateway input/output documentation.
Dashboard and CLI alongside Terraform¶
Terraform and the dashboard/CLI work on the same underlying resources. You can:
- Inspect any Terraform-managed resource in the dashboard or via
bnerd <resource> get <id>without affecting Terraform state. - Import resources created via the dashboard or CLI into Terraform management with
terraform import(Step 7 above). - Mix approaches — for example, manage network infrastructure with Terraform but use the dashboard for day-to-day server management. Note that resources managed outside Terraform will show as drift on the next
terraform plan.
Troubleshooting¶
| Symptom | Likely cause | Fix |
|---|---|---|
terraform init fails with provider not found | Wrong source address | Verify source = "bnerd-cloud/bnerd" in required_providers |
403 on terraform apply | Token lacks required permissions | Check errors[0].code in the error output; verify your token has openstack, rgw, or dns permissions as needed |
terraform plan shows unexpected replacement | RequiresReplace attribute changed (e.g. project_id on a server) | Review the plan's # forces replacement lines before applying; these destroy+recreate the resource |
| State drift after manual changes | Resource was modified via dashboard or CLI outside Terraform | Run terraform plan to see the drift; use terraform apply to reconcile or terraform import to bring the current state in |
locked file error on terraform init | Concurrent init or stale lock | Delete .terraform.lock.hcl and re-run terraform init |
RGW quota/locked show as empty in plan | These are admin-only computed fields | This is expected — they are set by platform admins, not the Terraform configuration |
API Reference¶
The Terraform provider calls the same REST API documented in the API Reference. If a Terraform operation fails with an unexpected error, the raw API response is preserved in errors[0].raw (debug only — do not log this field in production). The human-readable message is in errors[0].message.
Next Steps¶
- Terraform Provider reference — all resources and data sources
- Terraform Modules reference — reusable modules
- API Reference — the underlying REST API
- DNS Setup guide — configuring zones and records
- Provision Object Storage guide — RGW users, buckets, and keys