feat(infra): make clean deployments reproducible
This commit is contained in:
parent
09c60df3c5
commit
12a395a33c
24 changed files with 525 additions and 32 deletions
|
|
@ -51,6 +51,18 @@ jobs:
|
|||
docker run --rm -v "$PWD:/repo" -w /repo bash:5.3 \
|
||||
bash -ec 'for file in scripts/k3d/lab.sh scripts/provision/bootstrap.sh scripts/provision/lab.sh; do bash -n "$file"; done'
|
||||
|
||||
- name: Validate Terraform
|
||||
run: |
|
||||
for stack in terraform/infra terraform/platform
|
||||
do
|
||||
docker run --rm -v "$PWD:/work" -w "/work/$stack" \
|
||||
hashicorp/terraform:1.15.9 fmt -check
|
||||
docker run --rm -v "$PWD:/work" -w "/work/$stack" \
|
||||
hashicorp/terraform:1.15.9 init -backend=false -lockfile=readonly
|
||||
docker run --rm -v "$PWD:/work" -w "/work/$stack" \
|
||||
hashicorp/terraform:1.15.9 validate
|
||||
done
|
||||
|
||||
- name: Validate Compose and Kubernetes configuration
|
||||
env:
|
||||
CLUSTER_API_UPSTREAM: 192.0.2.1:8080
|
||||
|
|
|
|||
18
.github/workflows/ci.yml
vendored
18
.github/workflows/ci.yml
vendored
|
|
@ -74,3 +74,21 @@ jobs:
|
|||
if: steps.probe.outputs.ready == 'true'
|
||||
with:
|
||||
working-directory: apps/api
|
||||
|
||||
terraform:
|
||||
name: terraform
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: format and validate
|
||||
run: |
|
||||
for stack in terraform/infra terraform/platform
|
||||
do
|
||||
docker run --rm -v "$PWD:/work" -w "/work/$stack" \
|
||||
hashicorp/terraform:1.15.9 fmt -check
|
||||
docker run --rm -v "$PWD:/work" -w "/work/$stack" \
|
||||
hashicorp/terraform:1.15.9 init -backend=false -lockfile=readonly
|
||||
docker run --rm -v "$PWD:/work" -w "/work/$stack" \
|
||||
hashicorp/terraform:1.15.9 validate
|
||||
done
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -85,6 +85,7 @@ CLAUDE.local.md
|
|||
*.tfstate.*
|
||||
*.tfvars
|
||||
!*.tfvars.example
|
||||
terraform/**/inventory.yml
|
||||
crash.log
|
||||
crash.*.log
|
||||
override.tf
|
||||
|
|
|
|||
30
README.md
30
README.md
|
|
@ -342,24 +342,20 @@ pre-promotion analysis fails the pipeline.
|
|||
|
||||
Stated plainly so it does not have to be discovered.
|
||||
|
||||
`terraform/infra/` and `terraform/platform/` are empty. Provisioning is Ansible,
|
||||
and it does the job, but the brief asks for Terraform and this is where it would
|
||||
go. The Ansible path was written to accept hosts from anywhere, so a Terraform
|
||||
module that creates machines and emits an inventory would slot in without
|
||||
touching the roles.
|
||||
`terraform/infra/` creates the two Fedora 44 libvirt machines and emits the
|
||||
inventory consumed by Ansible. `terraform/platform/` installs Argo Rollouts,
|
||||
Sealed Secrets and kube-prometheus-stack. The separation is deliberate:
|
||||
Terraform owns machines and cluster-wide controllers, Ansible owns the hosts,
|
||||
and Kustomize owns the application.
|
||||
|
||||
Sealed Secrets is designed for but not installed. `deploy/secrets/README.md`
|
||||
documents the sealing workflow and `kubectl get crd | grep sealed` still comes
|
||||
back empty, so secrets are currently created out of band. The controller
|
||||
generates a fresh keypair on install, so backing up its private key has to be
|
||||
the first step, not the last.
|
||||
Sealed Secrets and Discord alert routing are configured, but real encrypted
|
||||
values cannot be committed until the owner supplies them. `deploy/secrets/README.md`
|
||||
keeps both plaintext values in local pipelines and commits only encrypted
|
||||
`SealedSecret` resources. The controller key must be backed up outside this
|
||||
repository before it becomes the recovery path.
|
||||
|
||||
Alertmanager routing to Discord is planned and not committed. The alert rules
|
||||
fire; nothing forwards them yet.
|
||||
|
||||
Two items on `docs/roadmap.md` remain open: the QEMU lab has not been tested on a
|
||||
workstation with no dependencies installed, and end-to-end acceptance has not
|
||||
been rerun from two freshly installed Fedora hosts.
|
||||
The remaining acceptance gap is environmental: the reproducible fresh-VM path
|
||||
has not been rerun on a second workstation with no dependencies installed.
|
||||
|
||||
## Fedora traps worth knowing
|
||||
|
||||
|
|
@ -386,7 +382,7 @@ deploy/observability/ Loki, Tempo, collectors, Grafana data sources
|
|||
observability/ dashboards, alert rules, collector configs, local compose
|
||||
scripts/k3d/ portable rollback lab
|
||||
scripts/provision/ Ansible roles, bootstrap, QEMU lab
|
||||
terraform/ empty, see above
|
||||
terraform/ libvirt machines, Ansible inventory and cluster-wide Helm releases
|
||||
docs/ roadmap, evidence, CI/CD and Mini PC notes
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -24,12 +24,43 @@ Postgres only reads `POSTGRES_PASSWORD` when it initialises the data directory.
|
|||
|
||||
## prod (sealed)
|
||||
|
||||
Write the plaintext to `nereus-db.plain.yaml` (gitignored), then:
|
||||
Install the controller through `terraform/platform` first. It is named
|
||||
`sealed-secrets-controller`, which is the default expected by `kubeseal`.
|
||||
|
||||
```fish
|
||||
kubeseal --format yaml < nereus-db.plain.yaml > nereus-db-sealed.yaml
|
||||
Create the database Secret locally and pipe it straight into `kubeseal`. The
|
||||
plaintext exists only in the pipeline and the password variable:
|
||||
|
||||
```bash
|
||||
db_password="$(openssl rand -hex 16)"
|
||||
kubectl create secret generic nereus-db --namespace nereus \
|
||||
--from-literal=POSTGRES_PASSWORD="$db_password" \
|
||||
--from-literal=DATABASE_URL="postgres://nereus:${db_password}@nereus-postgres:5432/nereus?sslmode=disable" \
|
||||
--dry-run=client -o yaml | \
|
||||
kubeseal --format yaml >deploy/secrets/nereus-db-sealed.yaml
|
||||
unset db_password
|
||||
```
|
||||
|
||||
Commit only `nereus-db-sealed.yaml` and add it to the prod overlay. The
|
||||
controller isn't installed yet — `kubectl get crd | grep sealed` comes back
|
||||
empty.
|
||||
Commit only `nereus-db-sealed.yaml` and add it to the prod overlay. Back up the
|
||||
controller key outside the repository before relying on sealed secrets for
|
||||
recovery. Losing that key makes every committed `SealedSecret` undecryptable.
|
||||
|
||||
## Discord alert routing
|
||||
|
||||
Alertmanager reads its Discord webhook from the `nereus-discord` Secret as a
|
||||
mounted file. Create the webhook in Discord, keep it in the shell environment,
|
||||
and seal it without writing plaintext to disk:
|
||||
|
||||
```bash
|
||||
test -n "${DISCORD_WEBHOOK_URL:-}"
|
||||
printf '%s' "$DISCORD_WEBHOOK_URL" | \
|
||||
kubectl create secret generic nereus-discord --namespace observability \
|
||||
--from-file=webhook-url=/dev/stdin --dry-run=client -o yaml | \
|
||||
kubeseal --format yaml >deploy/secrets/nereus-discord-sealed.yaml
|
||||
unset DISCORD_WEBHOOK_URL
|
||||
```
|
||||
|
||||
Apply the sealed secret, confirm it produced the `nereus-discord` Secret, then
|
||||
set `enable_discord = true` in the platform Terraform variables and apply
|
||||
again. Discord routing defaults off so a missing owner-provided webhook cannot
|
||||
stop Alertmanager from starting. The committed opt-in values route warning and
|
||||
critical Nereus alerts to Discord and discard alerts that do not match.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Nereus delivery roadmap
|
||||
|
||||
Updated: 2026-08-27
|
||||
Updated: 2026-08-28
|
||||
|
||||
`[x]` means locally verified. `[~]` means implemented but awaiting a real
|
||||
integration environment. `[ ]` means not implemented. `[H]` is human-owned and
|
||||
|
|
@ -124,6 +124,7 @@ task. Mark a task `[x]` only after its stated verification passes.
|
|||
- [x] Add a deployed-version dashboard variable.
|
||||
- [x] Correlate logs and traces using `trace_id` after cluster log ingestion exists.
|
||||
- [x] Add API error-rate, latency, readiness, and scrape alerts.
|
||||
- [~] Route warning and critical alerts to Discord; configuration validates and awaits the owner-provided sealed webhook.
|
||||
- [x] Load the dashboard and evaluate all six PromQL queries.
|
||||
- [x] Trigger chaos and distinguish the failing version in Grafana.
|
||||
|
||||
|
|
@ -177,7 +178,7 @@ task. Mark a task `[x]` only after its stated verification passes.
|
|||
- [x] Execute `lab.sh check` after a workstation reboot.
|
||||
- [x] Test recovery from an interrupted image download and validate the resumed QCOW2.
|
||||
- [x] Test recovery after node 2 is deleted manually and rejoin it with a fresh identity.
|
||||
- [ ] Test on a workstation with no dependencies installed.
|
||||
- [~] Test on a workstation with no dependencies installed; the reproducible fresh-VM path is implemented and awaits a second host.
|
||||
- [x] Decide whether an aarch64 image path is required; keep the lab x86_64-only.
|
||||
- [ ] After final sign-off, destroy the QEMU lab and remove packages installed only for it with Pacman's dependency-aware cleanup.
|
||||
|
||||
|
|
@ -198,8 +199,8 @@ task. Mark a task `[x]` only after its stated verification passes.
|
|||
|
||||
## Human-owned deployment and infrastructure
|
||||
|
||||
- [H] Define infrastructure modules under `terraform/`.
|
||||
- [H] Export provisioned addresses for Ansible inventory.
|
||||
- [x] Define libvirt infrastructure and Helm platform stacks under `terraform/`.
|
||||
- [x] Export stable node addresses to an Ansible inventory without provisioner hooks.
|
||||
- [H] Create Kubernetes base resources and overlays. Agent-authored 2026-08-21 at the owner's request; builds and server dry-runs pass, not applied.
|
||||
- [H] Create API and load-generator workloads. Agent-authored 2026-08-21; not applied.
|
||||
- [H] Create active and preview Services. Agent-authored 2026-08-21; not applied.
|
||||
|
|
@ -207,7 +208,7 @@ task. Mark a task `[x]` only after its stated verification passes.
|
|||
- [H] Create the real error-rate AnalysisTemplate. Agent-authored 2026-08-21; PromQL never evaluated against real series.
|
||||
- [H] Configure blue-green promotion and automated rollback. Agent-authored 2026-08-21; promotion and abort paths untested with the real API.
|
||||
- [H] Maintain Forgejo Actions and GitHub mirror workflows.
|
||||
- [H] Create sealed secrets from off-repository plaintext inputs.
|
||||
- [~] Install Sealed Secrets and document off-repository sealing; real encrypted values await owner-provided secrets.
|
||||
|
||||
## Forgejo CI/CD
|
||||
|
||||
|
|
|
|||
|
|
@ -37,10 +37,11 @@ can later be managed by Ansible without rebuilding it.
|
|||
## Terraform path
|
||||
|
||||
Terraform is responsible only for creating machines, networks, and addresses.
|
||||
After `terraform apply`, put its resulting addresses into `inventory.yml` and
|
||||
run this playbook. Keeping configuration out of provisioner hooks makes the
|
||||
same Ansible workflow usable for physical hardware, VMs, and manually created
|
||||
hosts.
|
||||
`terraform/infra` creates two Fedora 44 libvirt machines and writes its resulting
|
||||
addresses to `terraform/infra/inventory.yml`. Pass that file to `bootstrap.sh`
|
||||
after `terraform apply`. Keeping configuration out of provisioner hooks makes
|
||||
the same Ansible workflow usable for physical hardware, VMs, and manually
|
||||
created hosts.
|
||||
|
||||
## One-command local QEMU lab
|
||||
|
||||
|
|
|
|||
44
terraform/infra/.terraform.lock.hcl
generated
Normal file
44
terraform/infra/.terraform.lock.hcl
generated
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/dmacvicar/libvirt" {
|
||||
version = "0.8.3"
|
||||
constraints = "0.8.3"
|
||||
hashes = [
|
||||
"h1:Tttxr3E9O75MM+dDmq5sYHQEw29PwtIj+XDj/5drdfE=",
|
||||
"zh:06ff0169beafd1891dc5a30616983abd32004a4f570d1d3dbb5851d84bd1c007",
|
||||
"zh:2dbdd726d0987cda73b56ecdfbcb98a67485e86a7a44aec976c0081b7239d89d",
|
||||
"zh:2e195a7bbdfcc13c45460571a5ba848a5c1e746b477c8381058767560f0ac93b",
|
||||
"zh:3952da13080018c5aec498b73e343c4c22ad884afb8c983138fb7255617aa991",
|
||||
"zh:478841bcf57df938726ddb90f55c7953fad09db4f6348747519afe7fc84b403b",
|
||||
"zh:53bce78b03a82c4782acfe1f32c2b46a68fa5fb2fb90d4a5392c90b436b44244",
|
||||
"zh:5c157f23e9768c67cddf9e847a571adca441607cb5adfb96dbfdd626ceadf92c",
|
||||
"zh:6bc78d631959fb695664966851308e140c38f3f5cf648dd89756320c2d91765d",
|
||||
"zh:8605d7d6915190836802654920a8eea3d751ae437273c4f4476dc0ebb9167a1d",
|
||||
"zh:8b66a22b97331c2a56aed092fd39152d06ad957fd4810aa3f0c4ade0f9b15755",
|
||||
"zh:92586a47a04082f70bb33f722672127a287caeed109beaaca2668e2e1d6a9caf",
|
||||
"zh:99a9ee414f5c4268e287660ce8edec2efcba1f79351f83791b64c7e5ab04f569",
|
||||
"zh:b7cff09fe74b0eb63b5b9aa94de5b33dadbd006d6d5b9578ac476039ea20b062",
|
||||
"zh:d4188a343ff32c0e03ff28c7e84abce0f43cad2fdbcd9046eaafc247429039ff",
|
||||
]
|
||||
}
|
||||
|
||||
provider "registry.terraform.io/hashicorp/local" {
|
||||
version = "2.5.3"
|
||||
constraints = "2.5.3"
|
||||
hashes = [
|
||||
"h1:1Nkh16jQJMp0EuDmvP/96f5Unnir0z12WyDuoR6HjMo=",
|
||||
"zh:284d4b5b572eacd456e605e94372f740f6de27b71b4e1fd49b63745d8ecd4927",
|
||||
"zh:40d9dfc9c549e406b5aab73c023aa485633c1b6b730c933d7bcc2fa67fd1ae6e",
|
||||
"zh:6243509bb208656eb9dc17d3c525c89acdd27f08def427a0dce22d5db90a4c8b",
|
||||
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||
"zh:885d85869f927853b6fe330e235cd03c337ac3b933b0d9ae827ec32fa1fdcdbf",
|
||||
"zh:bab66af51039bdfcccf85b25fe562cbba2f54f6b3812202f4873ade834ec201d",
|
||||
"zh:c505ff1bf9442a889ac7dca3ac05a8ee6f852e0118dd9a61796a2f6ff4837f09",
|
||||
"zh:d36c0b5770841ddb6eaf0499ba3de48e5d4fc99f4829b6ab66b0fab59b1aaf4f",
|
||||
"zh:ddb6a407c7f3ec63efb4dad5f948b54f7f4434ee1a2607a49680d494b1776fe1",
|
||||
"zh:e0dafdd4500bec23d3ff221e3a9b60621c5273e5df867bc59ef6b7e41f5c91f6",
|
||||
"zh:ece8742fd2882a8fc9d6efd20e2590010d43db386b920b2a9c220cfecc18de47",
|
||||
"zh:f4c6b3eb8f39105004cf720e202f04f57e3578441cfb76ca27611139bc116a82",
|
||||
]
|
||||
}
|
||||
19
terraform/infra/README.md
Normal file
19
terraform/infra/README.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Libvirt infrastructure
|
||||
|
||||
This stack creates a dedicated NAT network and two Fedora 44 x86_64 machines.
|
||||
It stops at the machine boundary. Ansible remains responsible for Fedora and
|
||||
k3s configuration.
|
||||
|
||||
Keep real values in an untracked `terraform.tfvars` file:
|
||||
|
||||
```bash
|
||||
cp terraform.tfvars.example terraform.tfvars
|
||||
terraform init
|
||||
terraform apply
|
||||
K3S_TOKEN="$(openssl rand -hex 32)" \
|
||||
../../scripts/provision/bootstrap.sh inventory.yml
|
||||
```
|
||||
|
||||
The generated `inventory.yml`, Terraform state, and variable file are ignored.
|
||||
The SSH public key is safe to place in Terraform input; the matching private key
|
||||
is referenced by absolute path and is never read into Terraform state.
|
||||
12
terraform/infra/cloud-init.yaml.tftpl
Normal file
12
terraform/infra/cloud-init.yaml.tftpl
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#cloud-config
|
||||
hostname: ${hostname}
|
||||
manage_etc_hosts: true
|
||||
users:
|
||||
- name: fedora
|
||||
groups: [wheel]
|
||||
sudo: ALL=(ALL) NOPASSWD:ALL
|
||||
shell: /bin/bash
|
||||
ssh_authorized_keys:
|
||||
- ${ssh_public_key}
|
||||
ssh_pwauth: false
|
||||
disable_root: true
|
||||
24
terraform/infra/inventory.yml.tftpl
Normal file
24
terraform/infra/inventory.yml.tftpl
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
all:
|
||||
vars:
|
||||
ansible_user: fedora
|
||||
ansible_ssh_private_key_file: ${private_key_file}
|
||||
ansible_ssh_common_args: "-o StrictHostKeyChecking=accept-new"
|
||||
k3s_version: v1.33.4+k3s1
|
||||
k3s_cluster_cidr: 10.42.0.0/16
|
||||
k3s_service_cidr: 10.43.0.0/16
|
||||
k3s_node_cidr: ${node_cidr}
|
||||
k3s_operator_cidrs:
|
||||
%{ for cidr in operator_cidrs ~}
|
||||
- ${cidr}
|
||||
%{ endfor ~}
|
||||
children:
|
||||
k3s_server:
|
||||
hosts:
|
||||
node1:
|
||||
ansible_host: ${server_address}
|
||||
k3s_node_ip: ${server_address}
|
||||
k3s_agent:
|
||||
hosts:
|
||||
node2:
|
||||
ansible_host: ${agent_address}
|
||||
k3s_node_ip: ${agent_address}
|
||||
97
terraform/infra/main.tf
Normal file
97
terraform/infra/main.tf
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
locals {
|
||||
nodes = {
|
||||
nereus-node1 = {
|
||||
address = var.node_addresses["nereus-node1"]
|
||||
role = "server"
|
||||
}
|
||||
nereus-node2 = {
|
||||
address = var.node_addresses["nereus-node2"]
|
||||
role = "agent"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "libvirt_network" "nereus" {
|
||||
name = "nereus"
|
||||
mode = "nat"
|
||||
domain = "nereus.test"
|
||||
addresses = [var.network_cidr]
|
||||
|
||||
dhcp {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
dns {
|
||||
enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "libvirt_volume" "fedora" {
|
||||
name = "nereus-fedora-44-base.qcow2"
|
||||
pool = "default"
|
||||
source = var.fedora_image_url
|
||||
format = "qcow2"
|
||||
}
|
||||
|
||||
resource "libvirt_volume" "node" {
|
||||
for_each = local.nodes
|
||||
name = "${each.key}.qcow2"
|
||||
pool = "default"
|
||||
base_volume_id = libvirt_volume.fedora.id
|
||||
size = 30 * 1024 * 1024 * 1024
|
||||
}
|
||||
|
||||
resource "libvirt_cloudinit_disk" "node" {
|
||||
for_each = local.nodes
|
||||
name = "${each.key}-cloud-init.iso"
|
||||
pool = "default"
|
||||
user_data = templatefile("${path.module}/cloud-init.yaml.tftpl", {
|
||||
hostname = each.key
|
||||
ssh_public_key = var.ssh_public_key
|
||||
})
|
||||
}
|
||||
|
||||
resource "libvirt_domain" "node" {
|
||||
for_each = local.nodes
|
||||
|
||||
name = each.key
|
||||
memory = 3072
|
||||
vcpu = 2
|
||||
autostart = true
|
||||
cloudinit = libvirt_cloudinit_disk.node[each.key].id
|
||||
|
||||
disk {
|
||||
volume_id = libvirt_volume.node[each.key].id
|
||||
}
|
||||
|
||||
network_interface {
|
||||
network_id = libvirt_network.nereus.id
|
||||
hostname = each.key
|
||||
addresses = [each.value.address]
|
||||
wait_for_lease = true
|
||||
}
|
||||
|
||||
console {
|
||||
type = "pty"
|
||||
target_type = "serial"
|
||||
target_port = "0"
|
||||
}
|
||||
|
||||
graphics {
|
||||
type = "spice"
|
||||
listen_type = "address"
|
||||
autoport = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "local_sensitive_file" "inventory" {
|
||||
filename = "${path.module}/inventory.yml"
|
||||
file_permission = "0600"
|
||||
content = templatefile("${path.module}/inventory.yml.tftpl", {
|
||||
private_key_file = var.ssh_private_key_file
|
||||
operator_cidrs = var.operator_cidrs
|
||||
node_cidr = var.network_cidr
|
||||
server_address = local.nodes["nereus-node1"].address
|
||||
agent_address = local.nodes["nereus-node2"].address
|
||||
})
|
||||
}
|
||||
9
terraform/infra/outputs.tf
Normal file
9
terraform/infra/outputs.tf
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
output "node_addresses" {
|
||||
description = "Addresses passed to Ansible."
|
||||
value = { for name, node in local.nodes : name => node.address }
|
||||
}
|
||||
|
||||
output "ansible_inventory" {
|
||||
description = "Generated inventory consumed by scripts/provision/bootstrap.sh."
|
||||
value = local_sensitive_file.inventory.filename
|
||||
}
|
||||
4
terraform/infra/terraform.tfvars.example
Normal file
4
terraform/infra/terraform.tfvars.example
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
ssh_public_key = "replace with one public key"
|
||||
ssh_private_key_file = "/absolute/path/to/the/matching/private/key"
|
||||
|
||||
operator_cidrs = ["192.168.123.1/32"]
|
||||
42
terraform/infra/variables.tf
Normal file
42
terraform/infra/variables.tf
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
variable "libvirt_uri" {
|
||||
description = "Libvirt connection used to create the Fedora machines."
|
||||
type = string
|
||||
default = "qemu:///system"
|
||||
}
|
||||
|
||||
variable "fedora_image_url" {
|
||||
description = "Fedora 44 Cloud Base Generic x86_64 image URL."
|
||||
type = string
|
||||
default = "https://download.fedoraproject.org/pub/fedora/linux/releases/44/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-44-1.7.x86_64.qcow2"
|
||||
}
|
||||
|
||||
variable "ssh_public_key" {
|
||||
description = "Public SSH key installed for the fedora user."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "ssh_private_key_file" {
|
||||
description = "Absolute private-key path written into the generated Ansible inventory."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "operator_cidrs" {
|
||||
description = "Networks allowed to administer k3s."
|
||||
type = list(string)
|
||||
default = ["192.168.123.1/32"]
|
||||
}
|
||||
|
||||
variable "network_cidr" {
|
||||
description = "Dedicated libvirt network for the two nodes."
|
||||
type = string
|
||||
default = "192.168.123.0/24"
|
||||
}
|
||||
|
||||
variable "node_addresses" {
|
||||
description = "Stable addresses assigned to each node."
|
||||
type = map(string)
|
||||
default = {
|
||||
nereus-node1 = "192.168.123.10"
|
||||
nereus-node2 = "192.168.123.11"
|
||||
}
|
||||
}
|
||||
18
terraform/infra/versions.tf
Normal file
18
terraform/infra/versions.tf
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
terraform {
|
||||
required_version = ">= 1.10.0"
|
||||
|
||||
required_providers {
|
||||
libvirt = {
|
||||
source = "dmacvicar/libvirt"
|
||||
version = "0.8.3"
|
||||
}
|
||||
local = {
|
||||
source = "hashicorp/local"
|
||||
version = "2.5.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "libvirt" {
|
||||
uri = var.libvirt_uri
|
||||
}
|
||||
22
terraform/platform/.terraform.lock.hcl
generated
Normal file
22
terraform/platform/.terraform.lock.hcl
generated
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/hashicorp/helm" {
|
||||
version = "3.1.1"
|
||||
constraints = "3.1.1"
|
||||
hashes = [
|
||||
"h1:5b2ojWKT0noujHiweCds37ZreRFRQLNaErdJLusJN88=",
|
||||
"zh:1a6d5ce931708aec29d1f3d9e360c2a0c35ba5a54d03eeaff0ce3ca597cd0275",
|
||||
"zh:3411919ba2a5941801e677f0fea08bdd0ae22ba3c9ce3309f55554699e06524a",
|
||||
"zh:81b36138b8f2320dc7f877b50f9e38f4bc614affe68de885d322629dd0d16a29",
|
||||
"zh:95a2a0a497a6082ee06f95b38bd0f0d6924a65722892a856cfd914c0d117f104",
|
||||
"zh:9d3e78c2d1bb46508b972210ad706dd8c8b106f8b206ecf096cd211c54f46990",
|
||||
"zh:a79139abf687387a6efdbbb04289a0a8e7eaca2bd91cdc0ce68ea4f3286c2c34",
|
||||
"zh:aaa8784be125fbd50c48d84d6e171d3fb6ef84a221dbc5165c067ce05faab4c8",
|
||||
"zh:afecd301f469975c9d8f350cc482fe656e082b6ab0f677d1a816c3c615837cc1",
|
||||
"zh:c54c22b18d48ff9053d899d178d9ffef7d9d19785d9bf310a07d648b7aac075b",
|
||||
"zh:db2eefd55aea48e73384a555c72bac3f7d428e24147bedb64e1a039398e5b903",
|
||||
"zh:ee61666a233533fd2be971091cecc01650561f1585783c381b6f6e8a390198a4",
|
||||
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
|
||||
]
|
||||
}
|
||||
20
terraform/platform/README.md
Normal file
20
terraform/platform/README.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Cluster platform
|
||||
|
||||
This stack installs Argo Rollouts, Sealed Secrets, and kube-prometheus-stack on
|
||||
an existing k3s cluster. Use it on a fresh cluster, or import matching Helm
|
||||
releases before applying it to a cluster where they already exist.
|
||||
|
||||
```bash
|
||||
cp terraform.tfvars.example terraform.tfvars
|
||||
terraform init
|
||||
terraform apply
|
||||
```
|
||||
|
||||
The kubeconfig stays outside the repository. Terraform records only its path.
|
||||
Application and observability manifests remain under `deploy/` and are applied
|
||||
after the cluster-wide controllers are ready.
|
||||
|
||||
Discord routing is deliberately off by default so a missing webhook cannot
|
||||
stop Alertmanager. After applying `nereus-discord-sealed.yaml` and confirming
|
||||
that it produced the `nereus-discord` Secret, set `enable_discord = true` and
|
||||
apply this stack again.
|
||||
23
terraform/platform/alertmanager-discord.values.yaml
Normal file
23
terraform/platform/alertmanager-discord.values.yaml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
alertmanager:
|
||||
alertmanagerSpec:
|
||||
secrets:
|
||||
- nereus-discord
|
||||
config:
|
||||
global:
|
||||
resolve_timeout: 5m
|
||||
route:
|
||||
receiver: discard
|
||||
group_by: [alertname, namespace]
|
||||
group_wait: 30s
|
||||
group_interval: 5m
|
||||
repeat_interval: 4h
|
||||
routes:
|
||||
- receiver: discord
|
||||
matchers:
|
||||
- severity=~"warning|critical"
|
||||
receivers:
|
||||
- name: discard
|
||||
- name: discord
|
||||
discord_configs:
|
||||
- webhook_url_file: /etc/alertmanager/secrets/nereus-discord/webhook-url
|
||||
send_resolved: true
|
||||
27
terraform/platform/kube-prometheus-stack.values.yaml
Normal file
27
terraform/platform/kube-prometheus-stack.values.yaml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
kubeEtcd:
|
||||
enabled: false
|
||||
kubeProxy:
|
||||
enabled: false
|
||||
|
||||
prometheus:
|
||||
prometheusSpec:
|
||||
retention: 6h
|
||||
serviceMonitorSelectorNilUsesHelmValues: false
|
||||
podMonitorSelectorNilUsesHelmValues: false
|
||||
ruleSelectorNilUsesHelmValues: false
|
||||
storageSpec:
|
||||
volumeClaimTemplate:
|
||||
spec:
|
||||
accessModes: [ReadWriteOnce]
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
|
||||
grafana:
|
||||
defaultDashboardsTimezone: browser
|
||||
|
||||
alertmanager:
|
||||
alertmanagerSpec:
|
||||
resources:
|
||||
requests: {cpu: 25m, memory: 64Mi}
|
||||
limits: {memory: 256Mi}
|
||||
44
terraform/platform/main.tf
Normal file
44
terraform/platform/main.tf
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
resource "helm_release" "argo_rollouts" {
|
||||
name = "argo-rollouts"
|
||||
repository = "https://argoproj.github.io/argo-helm"
|
||||
chart = "argo-rollouts"
|
||||
version = "2.41.1"
|
||||
namespace = "argo-rollouts"
|
||||
create_namespace = true
|
||||
wait = true
|
||||
timeout = 300
|
||||
}
|
||||
|
||||
resource "helm_release" "sealed_secrets" {
|
||||
name = "sealed-secrets"
|
||||
repository = "https://bitnami-labs.github.io/sealed-secrets"
|
||||
chart = "sealed-secrets"
|
||||
version = "2.19.3"
|
||||
namespace = "kube-system"
|
||||
create_namespace = false
|
||||
wait = true
|
||||
timeout = 300
|
||||
|
||||
values = [yamlencode({
|
||||
fullnameOverride = "sealed-secrets-controller"
|
||||
resources = {
|
||||
requests = { cpu = "25m", memory = "64Mi" }
|
||||
limits = { memory = "256Mi" }
|
||||
}
|
||||
})]
|
||||
}
|
||||
|
||||
resource "helm_release" "kube_prometheus_stack" {
|
||||
name = "kube-prometheus-stack"
|
||||
repository = "https://prometheus-community.github.io/helm-charts"
|
||||
chart = "kube-prometheus-stack"
|
||||
version = "88.5.2"
|
||||
namespace = "observability"
|
||||
create_namespace = true
|
||||
wait = true
|
||||
timeout = 600
|
||||
values = concat(
|
||||
[file("${path.module}/kube-prometheus-stack.values.yaml")],
|
||||
var.enable_discord ? [file("${path.module}/alertmanager-discord.values.yaml")] : []
|
||||
)
|
||||
}
|
||||
2
terraform/platform/terraform.tfvars.example
Normal file
2
terraform/platform/terraform.tfvars.example
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
kubeconfig_path = "/absolute/path/to/kubeconfig"
|
||||
enable_discord = false
|
||||
10
terraform/platform/variables.tf
Normal file
10
terraform/platform/variables.tf
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
variable "kubeconfig_path" {
|
||||
description = "Absolute path to the operator kubeconfig. Its contents never enter Terraform state."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "enable_discord" {
|
||||
description = "Enable Discord routing after the nereus-discord SealedSecret has created its Secret."
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
16
terraform/platform/versions.tf
Normal file
16
terraform/platform/versions.tf
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
terraform {
|
||||
required_version = ">= 1.10.0"
|
||||
|
||||
required_providers {
|
||||
helm = {
|
||||
source = "hashicorp/helm"
|
||||
version = "3.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "helm" {
|
||||
kubernetes = {
|
||||
config_path = var.kubeconfig_path
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue