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.
What it manages
Section titled “What it manages”| 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.
Set it up
Section titled “Set it up”-
Create an access key for Terraform. A service account with only the policies it needs is best; see Access keys and the CLI.
-
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 resourcesIf you have run
tebib configure, the provider reads that profile instead (TEBIB_PROFILEpicks another one). Settings in theprovider "tebib"block win over the environment, and the environment wins over the profile. -
Declare the provider:
terraform {required_providers {tebib = { source = "tebibhost/tebib" }}}provider "tebib" {} -
Run
tofu init, thentofu planandtofu apply(or the same withterraform).
A complete example
Section titled “A complete example”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:
tofu apply -var admin_cidr=203.0.113.7/32ssh 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.
How changes are applied
Section titled “How changes are applied”- 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 largerroot_disk_gib, and tags. A volume grows in place. Anything else, like a new image or subnet, replaces the resource: the plan says so withmust 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.
Import existing resources
Section titled “Import existing resources”Resources you made in the console or with the CLI can be brought under Terraform with their IDs:
tofu import tebib_vm.web vm-0123456789abtofu import tebib_security_group_rule.ssh sg-0123456789ab/sgr-0123456789abEach reference page lists its import ID. Two things the API never returns:
- A VM’s
user_data. After importing a VM, addlifecycle { ignore_changes = [user_data] }, or the next apply replaces the VM. - An access key’s secret. An imported
tebib_iam_access_keyhas nosecret, and an importedtebib_storage_keynosecret_access_key.
Load balancer parts and DNS records take their parent’s ID too:
tofu import tebib_load_balancer_listener.http lb-0123456789ab/lbl-0123456789abtofu import tebib_load_balancer_target.web lb-0123456789ab/lbtg-0123456789ab/lbt-0123456789abtofu import tebib_dns_record.www zone-0123456789ab/rec-0123456789abKeep secrets out of the state
Section titled “Keep secrets out of the state”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.
Load balancers, buckets and DNS
Section titled “Load balancers, buckets and DNS”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_nameis published). Eachtebib_dns_recordis one name and type with all its values; the plan compares the values as the nameservers serve them, somail.acme.exampleandmail.acme.example.are the same. tebib_snapshot_schedulekeeps a VM’s snapshots on a schedule; it needsretention_countorretention_days(or both).