infra, ansible, and required container changes

This commit is contained in:
Jason Ross
2026-04-29 16:39:00 -05:00
parent 5219e3131d
commit 45f2a9a073
35 changed files with 1379 additions and 12 deletions
+14
View File
@@ -0,0 +1,14 @@
# Local state & secrets
*.tfstate
*.tfstate.*
*.tfstate.backup
.terraform/
.terraform.lock.hcl.bak
crash.log
crash.*.log
# User-specific configs (may contain secrets)
backend.hcl
*.auto.tfvars
terraform.tfvars
!terraform.tfvars.example
+71
View File
@@ -0,0 +1,71 @@
# 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:
1. Create an R2 bucket, e.g. `dev-blog-tfstate`.
2. Create an R2 API token (Account → R2 → Manage API tokens) with **Object Read & Write** scoped to that bucket. Note the access key ID + secret.
3. 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
```sh
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`](../.github/workflows/infra.yml) runs:
- **`pull_request`** touching `infra/**` → `tofu plan` (read-only).
- **`workflow_dispatch`** → choose `plan`, `apply`, or `destroy`.
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_instance` resource enables daily backups via
`backups = "enabled"` and a `backups_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. Adjust `os_name_filter` in
`variables.tf` if 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.
+16
View File
@@ -0,0 +1,16 @@
# Example backend configuration for Cloudflare R2.
# Copy to `backend.hcl` (gitignored) and fill in your values, then run:
# tofu init -backend-config=backend.hcl
#
# In CI, these are passed as -backend-config=... flags from secrets instead.
bucket = "dev-blog-tfstate"
endpoints = {
s3 = "https://<ACCOUNT_ID>.r2.cloudflarestorage.com"
}
# AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY env vars supply credentials.
# Alternatively, uncomment:
# access_key = "<R2_ACCESS_KEY_ID>"
# secret_key = "<R2_SECRET_ACCESS_KEY>"
+51
View File
@@ -0,0 +1,51 @@
data "vultr_os" "alma" {
filter {
name = "name"
values = [var.os_name_filter]
}
}
resource "vultr_instance" "blog" {
region = var.region
plan = var.plan
os_id = data.vultr_os.alma.id
hostname = var.hostname
label = var.hostname
tags = var.tags
ssh_key_ids = var.ssh_key_ids
backups = "enabled"
backups_schedule {
type = "daily"
hour = var.backup_hour_utc
}
enable_ipv6 = true
ddos_protection = false
activation_email = false
}
# ---------------------------------------------------------------------------
# Static (Reserved) IPs
#
# Reserved IPs survive instance replacement, so DNS records stay valid even
# if `vultr_instance.blog` is destroyed and recreated.
#
# - v4 reservation is a single /32, so `subnet` is the address itself.
# - v6 reservation is a /64; `subnet` is the network prefix and the instance
# takes an address inside it (exposed as `vultr_instance.blog.v6_main_ip`).
# ---------------------------------------------------------------------------
resource "vultr_reserved_ip" "v4" {
region = var.region
ip_type = "v4"
label = "${var.hostname}-v4"
instance_id = vultr_instance.blog.id
}
resource "vultr_reserved_ip" "v6" {
region = var.region
ip_type = "v6"
label = "${var.hostname}-v6"
instance_id = vultr_instance.blog.id
}
+27
View File
@@ -0,0 +1,27 @@
output "instance_id" {
value = vultr_instance.blog.id
}
# Static IPv4 (Vultr Reserved IP /32).
output "main_ip" {
value = vultr_reserved_ip.v4.subnet
}
# Static IPv6 prefix (Vultr Reserved IP /64). Use a host address inside this
# subnet for AAAA records (the instance's primary v6 is `ipv6_address`).
output "ipv6_subnet" {
value = "${vultr_reserved_ip.v6.subnet}/${vultr_reserved_ip.v6.subnet_size}"
}
output "ipv6_address" {
value = vultr_instance.blog.v6_main_ip
}
output "default_password" {
value = vultr_instance.blog.default_password
sensitive = true
}
output "os" {
value = data.vultr_os.alma.name
}
+6
View File
@@ -0,0 +1,6 @@
vultr_api_key = "REPLACE_ME"
region = "sea"
plan = "vc2-1c-2gb"
os_name_filter = "AlmaLinux 10"
hostname = "dev-blog"
ssh_key_ids = []
+47
View File
@@ -0,0 +1,47 @@
variable "vultr_api_key" {
description = "Vultr API key. Provide via TF_VAR_vultr_api_key env var."
type = string
sensitive = true
}
variable "region" {
description = "Vultr region code."
type = string
default = "sea" # Seattle, WA
}
variable "plan" {
description = "Vultr instance plan."
type = string
default = "vc2-1c-2gb"
}
variable "os_name_filter" {
description = "Substring to match an OS name in the Vultr OS catalog."
type = string
default = "AlmaLinux 10"
}
variable "hostname" {
description = "Hostname / label for the instance."
type = string
default = "dev-blog"
}
variable "ssh_key_ids" {
description = "List of pre-existing Vultr SSH key IDs to inject."
type = list(string)
default = []
}
variable "backup_hour_utc" {
description = "Hour of day (UTC, 0-23) for the daily automated backup."
type = number
default = 8
}
variable "tags" {
description = "Tags to apply to the instance."
type = list(string)
default = ["dev_blog", "managed-by=opentofu"]
}
+32
View File
@@ -0,0 +1,32 @@
terraform {
required_version = ">= 1.8.0"
required_providers {
vultr = {
source = "vultr/vultr"
version = "~> 2.21"
}
}
# Cloudflare R2 is S3-compatible, so we use the s3 backend with a custom
# endpoint. Backend values that depend on secrets/account-specific data are
# supplied at init time via `-backend-config=backend.hcl` (see README).
backend "s3" {
key = "dev_blog/terraform.tfstate"
region = "auto"
# R2 quirks: skip AWS-specific validations and use path-style URLs.
skip_credentials_validation = true
skip_metadata_api_check = true
skip_region_validation = true
skip_requesting_account_id = true
skip_s3_checksum = true
use_path_style = true
}
}
provider "vultr" {
api_key = var.vultr_api_key
rate_limit = 700
retry_limit = 3
}