- Go 85%
- Markdown 14.1%
- Shell 0.4%
- Yaml 0.4%
terraform-provider-bahriya
The official Terraform provider for Bahriya — manage every Bahriya resource declaratively, from your first container to a multi-region production estate.
Status: published to the Terraform Registry as
bahriya-cloud/bahriya.
What is Bahriya?
Bahriya is a container cloud with a simple contract: bring a container image, pick your regions, and the platform runs it — load balancing, TLS, health checks, autoscaling, a secrets vault and managed datastores included rather than bolted on. Everything the console can do, this provider can do in HCL, so your whole estate lives in version control.
Explore the platform:
| Product | What it gives you |
|---|---|
| HTTP Containers | Deploy an image, get a TLS-terminated, autoscaling, multi-region service with custom hostnames. |
| Workers | Long-running background processes — queue consumers, stream processors — without ingress. |
| Cron Jobs | Scheduled containers on a cron expression you control. |
| Managed Valkey | The Redis-compatible, open-source in-memory datastore — single, highly available or sharded. |
| Managed Memcached | Zero-ceremony caching next to your containers. |
| Vault | Secrets, TLS bundles, SSH/GPG keypairs and encryption keys — versioned, rotatable, mountable. |
| Configs | Env files and YAML/JSON/plain config files, attached to projects and mounted into containers. |
| Network Policies | Declare which workloads may talk to what, ingress and egress. |
| Volume Storage | Persistent volumes for stateful workloads. |
| Reis CLI | The same platform from your terminal — flags or YAML, ideal alongside Terraform in CI. |
| Terraform | This provider's product page — why infrastructure-as-code is a first-class citizen on Bahriya. |
Learn and stay current:
- Terraform knowledgebase — a per-resource guide with a complete, working deploy example for everything in this provider.
- Getting started and quickstarts — from zero to a running service.
- The full knowledgebase — topologies, networking, vault, billing, migrations and more.
- The blog — launch announcements and the developer series.
Table of contents
- What is Bahriya?
- Installation
- Quickstart
- Provider configuration
- Authentication
- Resources
- Data sources
- Important behaviours
- Examples
- Building from source
- Local development
- Architecture
- Repository layout
- Testing
Installation
From the Terraform Registry:
terraform {
required_providers {
bahriya = {
source = "bahriya-cloud/bahriya"
version = "~> 0.3"
}
}
}
To build and run from source instead, see Building from source and Local development below.
Quickstart
terraform {
required_providers {
bahriya = { source = "bahriya-cloud/bahriya" }
}
}
provider "bahriya" {}
data "bahriya_regions" "active" {
status_filter = "active"
}
resource "bahriya_project" "web" {
handle = "web-prod"
name = "Web Production"
regions = [data.bahriya_regions.active.regions[0].id]
}
resource "bahriya_registry" "ghcr" {
handle = "ghcr"
name = "GitHub Container Registry"
server = "ghcr.io"
username = var.ghcr_username
password = var.ghcr_token
}
resource "bahriya_secret" "db_password" {
handle = "db-password"
name = "Database Password"
value = var.db_password
}
resource "bahriya_container" "api" {
handle = "api"
name = "API Server"
image = "ghcr.io/myorg/api:1.2.3"
# Required for HTTP containers — deploy will fail without these
containerport = "3000"
healthcheckpath = "/healthz"
mincpu = "250"
minmemory = "256"
autoscalingminreplicas = "2"
activeregions = [data.bahriya_regions.active.regions[0].id]
project = bahriya_project.web.id
registry = bahriya_registry.ghcr.handle
secretsenvvar {
secret = bahriya_secret.db_password.handle
name = "DATABASE_PASSWORD"
}
hostnames {
hostname = "api.example.com"
}
}
Provider configuration
| Attribute | Required | Env var | Description |
|---|---|---|---|
token | yes | BAHRIYA_TOKEN | Personal access token (PAT). Issued in the console under your profile. |
organisation_id | yes | BAHRIYA_ORGANISATION_ID | The UUID of the org these resources belong to. |
base_url | no | BAHRIYA_API_URL | API root. Defaults to https://api.bahriya.cloud/console/v1. |
Attributes on the provider block override env vars. For multi-org setups, use alias:
provider "bahriya" {
alias = "prod"
organisation_id = var.prod_org_id
}
provider "bahriya" {
alias = "staging"
organisation_id = var.staging_org_id
}
Authentication
The provider uses Bahriya personal access tokens (PATs):
- Sign in to the console at https://console.bahriya.cloud.
- Open your profile, then Tokens.
- Click Create token, copy the
pat_...value.
export BAHRIYA_TOKEN="pat_..."
export BAHRIYA_ORGANISATION_ID="65cc42f1-..."
Resources
| Resource | Description | E2E Verified |
|---|---|---|
bahriya_project | Logical grouping of containers, secrets, registries; carries quota. | Yes |
bahriya_container | A workload: HTTP server, worker, or cronjob. Multi-region by default. | Yes |
bahriya_registry | Private container registry credentials, scoped to an org. | Yes |
bahriya_secret | Encrypted secret value, mountable as env var in a container. | Yes |
bahriya_memcached | Managed Memcached instance, attachable to containers as a cache backend. | Yes |
bahriya_tls_bundle | TLS bundle (CA + cert + key), versioned with rotation history. | Yes |
bahriya_x509_cert | Standalone X.509 certificate (no key), versioned. | Yes |
bahriya_gpg_keypair | GPG public + private keypair (OpenPGP armor), versioned. | Yes |
bahriya_ssh_keypair | SSH public + private keypair, versioned. | Yes |
bahriya_encryption_key | Symmetric encryption key (AES, ChaCha20, etc.), versioned. | Yes |
bahriya_env_file | KEY=VALUE env file. Injected into containers via envFrom. | Yes |
bahriya_yaml_config | YAML config file, mounted into containers as a file. | Yes |
bahriya_json_config | JSON config file, mounted into containers as a file. | Yes |
bahriya_plain_config | Arbitrary text config file, mounted into containers as a file. | Yes |
bahriya_network_policy | Ingress/egress network policy, attachable to projects and workloads. | Yes |
bahriya_valkey | Managed Valkey instance — cache or persistent store; single, HA or sharded. | Yes |
bahriya_path_rule | Path-scoped HTTP control on a container: basic auth, rate limit, IP allow/deny. | Yes |
bahriya_billing_details | The organisation's billing identity printed on invoices (singleton). | Yes |
bahriya_role | Custom organisation role — a named set of permission grants. | Yes |
bahriya_resource_grant | Shares a specific resource instance with an org member (instance ACL). | Yes |
Each vault/config item also has a corresponding bahriya_project_<type>_attachment resource that binds an item to a project. Once attached, the item can be referenced from a bahriya_container block to mount it inside the container.
All resource schemas are code-generated from the OpenAPI specs at packages/bahriya-openapi/specs/v1/.
The bahriya_organisation and bahriya_regions data sources are also E2E verified.
End-to-end coverage
examples/e2e-vault-configs/ is the comprehensive E2E: in a single apply/destroy it
exercises every resource above plus both data sources — registry + secret and all
vault/config items with their project attachments, a network policy, memcached, a
single-tier valkey, and HTTP + worker + cronjob containers wired to the full set of
mounts, env vars, secret env vars, persistent storage and path rules. Run ./gen-fixtures.sh (once) to mint crypto fixtures and
a fresh handle suffix, then terraform apply.
Secret-sync timing. A container that consumes a secret (via
secretsenvvar) or an env file (viaenvfiles/envFrom) will only reachrunningonce the project'sbahriya-secretsbundle has synced into the namespace. On a brand-new project the first container deploy can lose that race and land inerror; re-running the apply (once the sync has completed) brings it torunning. This is a platform deploy-pipeline ordering concern, not a provider behaviour — the provider creates and attaches the secret and orders the container after it.
Minimum required fields
These are the fields you must set for a successful deploy. Omitting any of these will cause errors (API rejection or runtime health-check failures).
bahriya_container (HTTP)
| Field | Type | Example | Notes |
|---|---|---|---|
handle | string | "api" | Immutable. Cannot be reused after delete (soft-delete). |
name | string | "API Server" | |
image | string | "nginx:alpine" | Full image ref including tag. |
containerport | string | "8080" | The port your application listens on. |
healthcheckpath | string | "/healthz" | HTTP path for liveness/readiness probes. Deploy will fail if the container does not respond 200 at this path on containerport. |
mincpu | string | "100" | Millicores. |
minmemory | string | "128" | Megabytes. |
autoscalingminreplicas | string | "1" | |
activeregions | list | ["falkenstein-1"] | At least one region. |
project | string | bahriya_project.x.id | Must be the project UUID (.id), not the handle. |
bahriya_container (worker / cronjob)
Same as HTTP except containerport and healthcheckpath are not required (workers have no ingress).
bahriya_memcached
| Field | Type | Example | Notes |
|---|---|---|---|
handle | string | "session-cache" | Immutable. Cannot be reused after delete. |
name | string | "Session Cache" | |
memorymb | number | 512 | Cache size in megabytes (integer, not string). |
activeregions | list | ["helsinki-1"] | At least one region. |
project | string | bahriya_project.x.id | Must be the project UUID (.id), not the handle. |
bahriya_project
| Field | Type | Example | Notes |
|---|---|---|---|
handle | string | "web-prod" | Immutable. Soft-deleted. |
name | string | "Web Production" | |
regions | list | ["falkenstein-1"] | At least one region. |
bahriya_registry
| Field | Type | Example | Notes |
|---|---|---|---|
handle | string | "ghcr" | Released on delete (reusable). |
name | string | "GHCR" | |
server | string | "ghcr.io" | Registry hostname. |
username | string | "myuser" | |
password | string | (sensitive) |
bahriya_secret
| Field | Type | Example | Notes |
|---|---|---|---|
handle | string | "db-password" | Released on delete (reusable). |
name | string | "DB Password" | |
value | string | (sensitive) |
Vault items
All vault types require handle and name plus the content fields below. Handles are immutable. Items are soft-deleted (handle burned).
| Resource | Required content fields | Notes |
|---|---|---|
bahriya_tls_bundle | ca, cert, key | PEM-encoded. key is sensitive. |
bahriya_x509_cert | cert | PEM-encoded standalone cert (no key). |
bahriya_gpg_keypair | public_key, private_key | OpenPGP armored. Both fields marked sensitive by the spec. |
bahriya_ssh_keypair | public_key, private_key | OpenSSH format. Both fields marked sensitive by the spec. |
bahriya_encryption_key | format, key | format ∈ base64, hex, raw. key is sensitive. |
The API parses content on create / rotate — invalid material is rejected.
Config items
| Resource | Required content fields | Notes |
|---|---|---|
bahriya_env_file | content | KEY=VALUE lines. Injected via envFrom. |
bahriya_yaml_config | content | Valid YAML. |
bahriya_json_config | content | Valid JSON. |
bahriya_plain_config | content | Arbitrary text. Mounted as a file. |
Networking
| Resource | Notable fields | Notes |
|---|---|---|
bahriya_network_policy | ingresspeers, egresscidrs, egressfqdns, ports | All optional. Any egresscidrs flips the target to deny-by-default outbound. egressfqdns is stored/validated now, enforced later. ports is a nested list of { port, protocol }. |
Project attachments
Each vault/config type has a corresponding attachment resource (bahriya_project_tls_bundle_attachment, etc.). All take exactly two required fields:
| Field | Type | Description |
|---|---|---|
project_id | string | UUID of the project (bahriya_project.x.id). |
handle | string | Handle of the item to attach. |
Both attributes force replacement on change. Attachment is also blocked at the API if a container in the project still references the item.
Data sources
| Data source | Description |
|---|---|
bahriya_organisation | Fetches the current organisation (id, name, handle, email, role). |
bahriya_region | Look up a single region by ID (name, status, location). |
bahriya_regions | List all regions, with optional status_filter. |
Important behaviours
Handles cannot be reused (soft-delete)
For projects, containers, and memcached, deleting in Terraform does not free up the handle. These resources are soft-deleted for billing and audit reasons.
- Use distinct handles per environment (
api-prod,api-staging). - Treat handles as permanent identifiers.
For registries and secrets, handles are released on delete and can be reused.
Async termination
Destroying a bahriya_container or bahriya_memcached triggers async termination. The provider polls until the resource reaches terminated status (default timeout: 10 minutes).
Renaming a handle is destroy + recreate
Handles are immutable. Changing the handle attribute forces Terraform to destroy and recreate the resource.
Imports use UUIDs
terraform import bahriya_container.api 065df92e-4e46-436a-a0a0-aaaaaaaaaaaa
Sensitive fields
Registry password and secret value are marked sensitive. The API returns masked sentinels on read; the provider preserves the real value from your Terraform state.
Vault item content fields (bahriya_tls_bundle.key, bahriya_gpg_keypair.private_key / .public_key, bahriya_ssh_keypair.private_key / .public_key, bahriya_encryption_key.key) are also sensitive and follow the same masking pattern.
Removing a nested attachment block must be explicit
Container nested-list attributes (tls_bundles, x509_certs, gpg_keypairs, ssh_keypairs, encryption_keys, env_files, yaml_configs, json_configs, plain_configs, persistentvolumes, hostnames, initjobs, secretsenvvar, newenvvar) are Optional+Computed with the UseStateForUnknown plan modifier — needed for stable plans against the API's PUT-replace semantics. As a consequence, omitting a block in your .tf does NOT remove the attachment; the prior state value is re-injected silently.
To remove a vault/config attachment from a container, set the block to an explicit empty list:
resource "bahriya_container" "api" {
# ...
json_configs = [] # explicit removal — works
# plain_configs # omitting — does NOT remove; prior list is preserved
}
Detach guard
Attempting to detach a vault/config item from a project while a container in that project still references it returns 409 CONFLICT with the offending container's handle. Update the container to remove the reference first.
Container Update waits for status
After a terraform apply that modifies a container, the provider waits for the container to reach running (or terminal error) before completing the apply. This avoids "inconsistent result after apply" errors when the post-PUT fetch would otherwise race the helm rollout.
Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
Container reaches error status after deploy | Missing or wrong healthcheckpath. The platform probes containerport + healthcheckpath to decide if the container is healthy. | Set healthcheckpath to a path your app responds 200 on (e.g. / or /healthz). |
project attribute rejected | Passing the project handle instead of UUID. | Use bahriya_project.x.id, not .handle. |
terraform plan shows unknown field | Using snake_case names (container_port, min_cpu). The provider uses flat lowercase names from the API. | Use containerport, mincpu, minmemory, activeregions, etc. |
| Handle already taken after destroy | Soft-deleted resources don't release handles. | Use a new handle (e.g. api-v2). |
Examples
Runnable configs live under examples/:
| Directory | What it demonstrates |
|---|---|
project-only/ | Minimal project resource |
registry-secret/ | Registry + secret with variable inputs |
data-sources/ | All 3 data sources (organisation, region, regions) |
container/ | Full container deployment with registry, secrets, hostname |
memcached/ | Cache instance inside a project |
complete/ | Everything together |
Building from source
Requires Go 1.23+.
git clone <repo-url>
cd terraform-provider-bahriya
make build # builds ./bin/terraform-provider-bahriya
make test # runs all 63 unit tests
make generate # regenerates resource code from OpenAPI specs
make verify # generate + git diff (CI gate)
Local development
Add a dev_overrides block to ~/.terraformrc:
provider_installation {
dev_overrides {
"bahriya-cloud/bahriya" = "/path/to/terraform-provider-bahriya/bin"
}
direct {}
}
Then just make build and run terraform plan/apply — no terraform init needed between rebuilds.
Set credentials via environment variables:
export BAHRIYA_TOKEN="pat_..."
export BAHRIYA_ORGANISATION_ID="..."
# Optional: override API URL for dev
export BAHRIYA_API_URL="http://localhost:8080/console/v1"
Architecture
This is a code-generated provider. The OpenAPI specs at packages/bahriya-openapi/specs/v1/ are the source of truth. A Go generator reads those specs and emits Terraform resources with full CRUD, ImportState, diff short-circuit, and status polling.
What's generated
- All 5 resource files (
internal/resources/*_resource.go) — ~3,800 LOC total - Resource registry (
internal/resources/registry.go) - Terraform schemas with nested attributes for complex types (Container has 6 nested object types)
- Plan modifiers (RequiresReplace for handles, UseStateForUnknown for computed fields)
- Sensitive field preservation (password, value)
What's hand-written
- Provider scaffold (
internal/provider/) — schema, Configure, env-var fallbacks - HTTP client (
internal/client/) — PAT auth, retries, typed errors - Data sources (
internal/datasources/) — organisation, region, regions - Transforms (
internal/transforms/) — envvar/secret dict-to-array, x-implies - Lifecycle (
internal/lifecycle/) — WaitForStatus with backoff - Helpers (
internal/resources/helpers.go) — small utilities for generated code - Code generator (
cmd/generator/) — parser, model, renderer, template
Generated files are committed to git. make verify fails CI if they drift.
Releasing
The source of truth is on 1x.ax (origin). GitHub (github) is a mirror that feeds the Terraform Registry. Push to both:
# Push code
git push origin && git push github
# Push tags (triggers the GitHub Actions release workflow)
git push origin --tags && git push github --tags
To cut a release:
git tag v0.1.0
git push origin && git push origin --tags && git push github && git push github --tags
The GitHub Actions workflow runs GoReleaser, builds cross-platform binaries, signs the checksums with GPG, and publishes the release. The Terraform Registry picks it up automatically.
Repository layout
terraform-provider-bahriya/
├── cmd/
│ ├── terraform-provider-bahriya/ # plugin entrypoint
│ └── generator/ # codegen tool
│ ├── templates/ # text/template for resources
│ └── parser_test.go # generator unit tests
├── internal/
│ ├── client/ # HTTP client + tests
│ ├── provider/ # provider block + tests
│ ├── resources/ # GENERATED — do not edit by hand
│ ├── datasources/ # data sources + tests
│ ├── transforms/ # envvar, secret, implies + tests
│ └── lifecycle/ # WaitForStatus + tests
├── examples/ # 6 runnable Terraform configs
├── Makefile
├── go.mod
└── .gitignore
Testing
63 unit tests across all packages:
make test # runs go test ./internal/... and cmd/generator
| Package | Tests | Coverage |
|---|---|---|
cmd/generator | 16 | Parser, model helpers, type resolution, nested $ref |
internal/client | 10 | HTTP envelope, auth, retry, error types |
internal/datasources | 17 | Metadata, schema, configure, response parsing |
internal/provider | 5 | Metadata, schema, resource counts, firstNonEmpty |
internal/lifecycle | 5 | Wait-for-status (target, terminal, timeout, cancel) |
internal/transforms | 11 | Envvar, secret, implies round-trips |