infra, ansible, and required container changes
This commit is contained in:
@@ -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
|
||||
@@ -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.
|
||||
@@ -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>"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 = []
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user