Skip to content

Infrastructure as Code with Terraform

This guide walks through the complete workflow for managing b'nerd Cloud 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 Cloud 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 init

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:

provider "bnerd" {}

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:

terraform plan

Review the output carefully — resources marked + will be created, ~ will be updated in-place, -/+ will be destroyed and recreated. Apply when ready:

terraform apply

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 *.tfstate and *.tfstate.backup to .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:

terraform destroy

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.


Next Steps