Infra (OpenTofu → Vultr)
Provisions a single Vultr instance for the dev_blog:
| Setting | Value |
|---|---|
| Region | sea (Seattle) |
| Plan | vc2-1c-2gb |
| OS | AlmaLinux 10 (looked up via data "vultr_os") |
| Backups | Automated, daily |
| IPv6 | Enabled |
State is stored in Cloudflare R2 via OpenTofu's S3-compatible backend.
One-time setup
1. R2 bucket
In the Cloudflare dashboard:
- Create an R2 bucket, e.g.
dev-blog-tfstate. - Create an R2 API token (Account → R2 → Manage API tokens) with Object Read & Write scoped to that bucket. Note the access key ID + secret.
- Note your Cloudflare Account ID (R2 endpoint host).
2. GitHub repository secrets
Add these in Settings → Secrets and variables → Actions:
| Secret | Value |
|---|---|
VULTR_API_KEY |
Vultr API key |
R2_ACCESS_KEY_ID |
R2 token access key ID |
R2_SECRET_ACCESS_KEY |
R2 token secret access key |
R2_ACCOUNT_ID |
Cloudflare account ID |
R2_BUCKET |
dev-blog-tfstate |
Running locally
cd infra
cp backend.hcl.example backend.hcl # fill in bucket + endpoint
cp terraform.tfvars.example terraform.tfvars
export AWS_ACCESS_KEY_ID=<r2-key-id>
export AWS_SECRET_ACCESS_KEY=<r2-secret>
export TF_VAR_vultr_api_key=<vultr-key>
tofu init -backend-config=backend.hcl
tofu plan
tofu apply
CI/CD
The workflow .github/workflows/infra.yml runs:
pull_requesttouchinginfra/**→tofu plan(read-only).workflow_dispatch→ chooseplan,apply, ordestroy.
Backend init uses -backend-config flags so the bucket and R2 endpoint are
injected from secrets at runtime — no account-specific values are committed.
Notes
- The Vultr provider's
vultr_instanceresource enables daily backups viabackups = "enabled"and abackups_schedule { type = "daily" }block. - AlmaLinux 10 is resolved by name through
data "vultr_os"so we don't have to hard-code an OS ID that may change. Adjustos_name_filterinvariables.tfif Vultr renames it. - The R2 backend uses
region = "auto"and skips AWS-specific validations, which is the standard configuration for R2 as an OpenTofu/Terraform S3 backend.