Skip to content

Terraform and OpenTofu

The tebib provider lets OpenTofu and Terraform create and change TebibHost resources from configuration files. It uses the same access keys and settings as the tebib CLI.

Area Resources Data sources
Projects tebib_project tebib_project
Network tebib_vpc, tebib_subnet, tebib_security_group, tebib_security_group_rule, tebib_nat_gateway, tebib_public_ip, tebib_public_ip_association tebib_vpc, tebib_subnet, tebib_security_group
Compute tebib_ssh_key, tebib_vm, tebib_volume, tebib_volume_attachment, tebib_snapshot, tebib_snapshot_schedule tebib_images, tebib_instance_types
Load balancers tebib_load_balancer, tebib_load_balancer_target_group, tebib_load_balancer_listener, tebib_load_balancer_target
Object storage tebib_bucket, tebib_storage_key tebib_storage
DNS tebib_dns_zone, tebib_dns_record
IAM tebib_iam_service_account, tebib_iam_access_key, tebib_iam_role, tebib_iam_policy, tebib_iam_policy_attachment, tebib_iam_group, tebib_iam_group_membership
Billing tebib_prices

Every argument is described in the provider’s reference pages.

  1. Create an access key for Terraform. A service account with only the policies it needs is best; see Access keys and the CLI.

  2. Give the provider the key, and the project to work in. Use environment variables, so the secret stays out of your files:

    Terminal window
    export TEBIB_ACCESS_KEY_ID=TBK...
    export TEBIB_SECRET_ACCESS_KEY=...
    export TEBIB_PROJECT=p-...
    export TEBIB_ORGANIZATION=o-... # for projects and IAM resources

    If you have run tebib configure, the provider reads that profile instead (TEBIB_PROFILE picks another one). Settings in the provider "tebib" block win over the environment, and the environment wins over the profile.

  3. Declare the provider:

    terraform {
    required_providers {
    tebib = { source = "tebibhost/tebib" }
    }
    }
    provider "tebib" {}
  4. Run tofu init, then tofu plan and tofu apply (or the same with terraform).

A web server in its own VPC, with a data volume, reachable on a public IP. Public IPs reach a VM through its VPC’s NAT gateway, so the example makes one (with its own address) first.

# A web server in its own VPC, reachable over SSH and HTTP(S) on a public IP.
#
# export TEBIB_ACCESS_KEY_ID=TBK... TEBIB_SECRET_ACCESS_KEY=... TEBIB_PROJECT=p-...
# tofu init && tofu apply # or: terraform init && terraform apply
# ssh ubuntu@$(tofu output -raw public_ip)
terraform {
required_providers {
tebib = {
source = "tebibhost/tebib"
version = "~> 0.1"
}
}
}
# Settings come from the environment (TEBIB_*) or your `tebib configure` profile.
provider "tebib" {}
variable "ssh_public_key" {
description = "Your SSH public key. Default: ~/.ssh/id_ed25519.pub."
type = string
default = null
}
variable "admin_cidr" {
description = "Where SSH is allowed from. Narrow it to your own address."
type = string
default = "0.0.0.0/0"
}
data "tebib_images" "ubuntu" {
os_family = "ubuntu"
}
# -- network ------------------------------------------------------------------------------
resource "tebib_vpc" "web" {
name = "web"
cidr = "10.10.0.0/16"
}
resource "tebib_subnet" "web" {
name = "web"
vpc_id = tebib_vpc.web.id
cidr = "10.10.1.0/24"
}
# Public IPs reach VMs through their VPC's NAT gateway, so the VPC needs one.
resource "tebib_public_ip" "nat" {
name = "web-nat"
}
resource "tebib_nat_gateway" "web" {
name = "web"
subnet_id = tebib_subnet.web.id
public_ip_id = tebib_public_ip.nat.id
}
resource "tebib_security_group" "web" {
name = "web"
vpc_id = tebib_vpc.web.id
description = "SSH from admins, HTTP and HTTPS from anywhere"
}
resource "tebib_security_group_rule" "ssh" {
security_group_id = tebib_security_group.web.id
protocol = "tcp"
port_from = 22
remote_cidr = var.admin_cidr
}
resource "tebib_security_group_rule" "http" {
security_group_id = tebib_security_group.web.id
protocol = "tcp"
port_from = 80
remote_cidr = "0.0.0.0/0"
}
resource "tebib_security_group_rule" "https" {
security_group_id = tebib_security_group.web.id
protocol = "tcp"
port_from = 443
remote_cidr = "0.0.0.0/0"
}
# -- server -------------------------------------------------------------------------------
resource "tebib_ssh_key" "me" {
name = "me"
public_key = var.ssh_public_key != null ? var.ssh_public_key : file(pathexpand("~/.ssh/id_ed25519.pub"))
}
resource "tebib_vm" "web" {
name = "web-1"
image = data.tebib_images.ubuntu.names[0]
instance_type = "b1.small"
subnet_id = tebib_subnet.web.id
security_group_ids = [tebib_security_group.web.id]
ssh_key_ids = [tebib_ssh_key.me.id]
root_disk_gib = 20
user_data = <<-EOT
#cloud-config
packages: [nginx]
EOT
tags = {
role = "web"
}
}
resource "tebib_volume" "data" {
name = "web-1-data"
size_gib = 50
}
resource "tebib_volume_attachment" "data" {
volume_id = tebib_volume.data.id
vm_id = tebib_vm.web.id
}
resource "tebib_public_ip" "web" {
name = "web-1"
}
resource "tebib_public_ip_association" "web" {
public_ip_id = tebib_public_ip.web.id
vm_id = tebib_vm.web.id
depends_on = [tebib_nat_gateway.web]
}
output "public_ip" {
value = tebib_public_ip.web.address
}
output "private_ip" {
value = tebib_vm.web.ipv4_address
}
output "ipv6" {
value = tebib_vm.web.ipv6_address
}

Run it, then connect:

Terminal window
tofu apply -var admin_cidr=203.0.113.7/32
ssh ubuntu@$(tofu output -raw public_ip)

tofu destroy deletes everything again. The VM’s boot volume goes with it; the data volume is detached first and then deleted.

  • Waiting. Most changes are asynchronous: the platform answers at once and works in the background. The provider waits until each resource is ready (or gone) before moving on. Tune how long with a timeouts { create = "30m" } block on the resource.
  • In place or replaced. A VM changes in place for a new instance_type (it is stopped, resized and started again), its security groups, power_state, a larger root_disk_gib, and tags. A volume grows in place. Anything else, like a new image or subnet, replaces the resource: the plan says so with must be replaced.
  • Failures. If a resource ends in an error (for example, no capacity), the apply fails with the platform’s message and the resource is marked tainted: the next apply replaces it.
  • Drift. Each plan reads the real resources. A change made in the console or the CLI shows up in the plan and is put back by the next apply. A resource deleted outside Terraform is created again.

Resources you made in the console or with the CLI can be brought under Terraform with their IDs:

Terminal window
tofu import tebib_vm.web vm-0123456789ab
tofu import tebib_security_group_rule.ssh sg-0123456789ab/sgr-0123456789ab

Each reference page lists its import ID. Two things the API never returns:

  • A VM’s user_data. After importing a VM, add lifecycle { ignore_changes = [user_data] }, or the next apply replaces the VM.
  • An access key’s secret. An imported tebib_iam_access_key has no secret, and an imported tebib_storage_key no secret_access_key.

Load balancer parts and DNS records take their parent’s ID too:

Terminal window
tofu import tebib_load_balancer_listener.http lb-0123456789ab/lbl-0123456789ab
tofu import tebib_load_balancer_target.web lb-0123456789ab/lbtg-0123456789ab/lbt-0123456789ab
tofu import tebib_dns_record.www zone-0123456789ab/rec-0123456789ab

tebib_iam_access_key and tebib_storage_key return the new key’s secret, and Terraform stores it in the state (marked sensitive, so it isn’t printed). Anyone who can read the state can read the secret. Keep the state in an encrypted backend with restricted access, or create keys with the CLI instead.

A public load balancer in front of VMs, a bucket with a key for an application, and a DNS zone pointing at the load balancer:

resource "tebib_load_balancer" "web" {
name = "web"
}
resource "tebib_load_balancer_target_group" "web" {
load_balancer_id = tebib_load_balancer.web.id
name = "web"
port = 80
health_check = {
protocol = "http"
path = "/healthz"
}
}
resource "tebib_load_balancer_listener" "http" {
load_balancer_id = tebib_load_balancer.web.id
port = 80
target_group_id = tebib_load_balancer_target_group.web.id
}
resource "tebib_load_balancer_target" "web" {
load_balancer_id = tebib_load_balancer.web.id
target_group_id = tebib_load_balancer_target_group.web.id
vm_id = tebib_vm.web.id
}
resource "tebib_bucket" "uploads" {
name = "acme-uploads" # unique across the region
}
resource "tebib_storage_key" "app" {
name = "app"
}
resource "tebib_dns_zone" "acme" {
domain = "acme.example"
}
resource "tebib_dns_record" "apex" {
zone_id = tebib_dns_zone.acme.id
type = "A"
values = [tebib_load_balancer.web.public_address]
}
output "nameservers" {
value = tebib_dns_zone.acme.nameservers # delegate the domain to these at your registrar
}
  • The VMs’ security groups must let the load balancer reach the target port (see Load balancers).
  • A bucket that holds objects is only deleted with force_destroy = true.
  • The DNS zone is created at once, but our nameservers only answer for it once the domain is delegated to them (or the TXT record in verification_txt_name is published). Each tebib_dns_record is one name and type with all its values; the plan compares the values as the nameservers serve them, so mail.acme.example and mail.acme.example. are the same.
  • tebib_snapshot_schedule keeps a VM’s snapshots on a schedule; it needs retention_count or retention_days (or both).