Initial infra scaffold: watcher live on Infomaniak Public Cloud

- flake.nix exposes a devShell (openstackclient, opentofu, sops, age) plus
  nixosConfigurations.watcher (runtime) and packages.watcher-image (QCOW2
  via nixos-generators / openstack format).
- hosts/watcher/default.nix: SSH-only base, tyro user with key auth, root
  SSH disabled, trusted-users set so laptop closure pushes work.
- modules/website.nix: Caddy serves tyrolize.ch from sites/tyrolize.ch/;
  lize.ch 301-redirects; firewall opens 80/443. Let's Encrypt via HTTP-01.
- terraform/infomaniak/: OpenStack provider, security group (22/80/443),
  keypair, compute instance booted from the uploaded image. Auth via OS_*
  env vars sourced from terraform/infomaniak/.env by the devShell hook.
- scripts/build-image.sh + scripts/deploy.sh.
- dns/{tyrolize,lize}.ch.zone: full BIND zone files for the advanced view
  in Infomaniak DNS Manager; preserves kSuite mail records on lize.ch.

Watcher live at 195.15.203.200 (IPv6 2001:1600:10💯:b4e), NixOS 25.05.
HTTPS confirmed working on both domains.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
tyrolize 2026-06-16 23:40:14 +02:00
commit 59d742f4ba
18 changed files with 733 additions and 0 deletions

View file

@ -0,0 +1,43 @@
# Copy to `.env` (gitignored) and fill in real values from the clouds.yaml
# you can download at: Infomaniak Manager → Public Cloud → your project →
# API access → Download clouds.yaml.
#
# Once filled in, just run `nix develop` from the repo root — the devShell's
# shellHook automatically sources this file. Then `openstack ...` and
# `terraform ...` are authenticated.
# --- OpenStack auth (application credential = modern, recommended) ---
# In your downloaded clouds.yaml these live under:
# clouds.openstack.auth_type
# clouds.openstack.auth.auth_url
# clouds.openstack.auth.application_credential_id
# clouds.openstack.auth.application_credential_secret
export OS_AUTH_TYPE=v3applicationcredential
export OS_AUTH_URL=https://api.pub1.infomaniak.cloud/identity/v3
export OS_APPLICATION_CREDENTIAL_ID=REPLACE_ME
export OS_APPLICATION_CREDENTIAL_SECRET=REPLACE_ME
export OS_REGION_NAME=dc3-a
export OS_INTERFACE=public
export OS_IDENTITY_API_VERSION=3
# --- Alternative: legacy username + password auth ---
# Only if your clouds.yaml uses passwords instead of an application credential.
# Comment out the four OS_AUTH_TYPE / OS_AUTH_URL / OS_APPLICATION_* lines above
# and uncomment these instead:
#
# export OS_AUTH_URL=https://api.pub1.infomaniak.cloud/identity/v3
# export OS_PROJECT_ID=REPLACE_ME
# export OS_PROJECT_NAME=REPLACE_ME
# export OS_USER_DOMAIN_NAME=Default
# export OS_USERNAME=REPLACE_ME
# export OS_PASSWORD=REPLACE_ME
# --- Terraform variables ---
# Contents of ~/.ssh/id_ed25519.pub (one line, quoted).
export TF_VAR_ssh_public_key="ssh-ed25519 AAAA... tyro@lize.ch"
# Override defaults from variables.tf only if you want a different size/network:
# export TF_VAR_flavor_name="a2-ram4-disk50-perf1"
# export TF_VAR_external_network="ext-net1"
# export TF_VAR_watcher_image_name="nixos-watcher"

24
terraform/infomaniak/.terraform.lock.hcl generated Normal file
View file

@ -0,0 +1,24 @@
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/terraform-provider-openstack/openstack" {
version = "2.1.0"
constraints = "~> 2.0"
hashes = [
"h1:2TcmfEzBOGQPALErrXTaL6v+k/WAL40adao4izRYmdw=",
"zh:113661750398bf21c8fe36aade9fb6f5eb82b5bcd3bcd30bd37ac805d83398f4",
"zh:1b3c26347b9cd61e413ee93c2f422cc3278a77f55fd3516eaabb3e2a85f65281",
"zh:1b751bbf1e4152829a643b532fd3f5967a2e89a41fac381257e0b41665be3306",
"zh:1b967bbfd9b344419c0e0df0c3a15fcbd731e91f19a18955a55aace8d9ec039a",
"zh:1bc0fc7c0a21e568db043b654501ce668ba19bf7628d37a7d2aaa512fd6e5aeb",
"zh:425cbf61757d4b503e7bf0f409ea59835ca3afbd2432d56ad552c2e5d234a572",
"zh:67d4f059cb4d73bf6c060313ec32962c4e5bd8dc7be2542a6f2098ab32575cd9",
"zh:7fe841ac5b68a4f52fb3cf45070828f3845de44746679d434e4349f3c23e3ef2",
"zh:ac1ed4c6ef0b6a3410568a05d3f9933d184497f065988503c43da0b2f0786ab2",
"zh:c5c0d14c86fabd9ab6a5d555e6a8d511942665fb5fa948dd452b0d1934068344",
"zh:c9ae5c210192275185d6823566a9421983e8e64c2665a4cae00b92dd0706bd19",
"zh:ee9865ccc053e7f345e532654fb628d1cf1e81cd2e929643c1691bebffcf7b98",
"zh:f3416d2f666095e740522c4964e436470bb9ec17bd53aaae8169ad93297d07bd",
"zh:fbca85457dd49e17168989d64f7cfc4a519d55ef4e00e89cea2859e87ad87f83",
]
}

View file

@ -0,0 +1,13 @@
terraform {
required_version = ">= 1.6"
required_providers {
openstack = {
source = "terraform-provider-openstack/openstack"
version = "~> 2.0"
}
}
}
# Auth comes from OS_* env vars (sourced from terraform/infomaniak/.env
# via the devShell's shellHook). No clouds.yaml file needed.
provider "openstack" {}

View file

@ -0,0 +1,14 @@
output "watcher_ipv4" {
value = openstack_compute_instance_v2.watcher.access_ip_v4
description = "Public IPv4 of the watcher VM"
}
output "watcher_ipv6" {
value = openstack_compute_instance_v2.watcher.access_ip_v6
description = "Public IPv6 of the watcher VM"
}
output "watcher_id" {
value = openstack_compute_instance_v2.watcher.id
description = "OpenStack instance ID (handy for `openstack server show`)"
}

View file

@ -0,0 +1,36 @@
variable "ssh_public_key" {
description = "Contents of ~/.ssh/id_ed25519.pub (one line, starts with 'ssh-ed25519 ...')"
type = string
}
variable "ssh_key_name" {
description = "Display name for the keypair stored at Infomaniak"
type = string
default = "tyro-laptop"
}
# Flavor naming (confirmed from Infomaniak's catalogue, 2026-06):
# a{vCPU}_ram{GB}_disk{GB}[_perf1]
# _perf1 = higher-tier SSD; disk0 = bring-your-own-volume.
# 2 vCPU / 4 GB / 20 GB SSD = a2-ram4-disk20-perf1, ~CHF 7.5/mo.
variable "flavor_name" {
description = "OpenStack flavor for the watcher"
type = string
default = "a2-ram4-disk20-perf1"
}
# ext-net1 is the dual-stack public network VMs get IPv4 + IPv6 directly.
# ext-floating1 is only needed when running instances behind a router.
variable "external_network" {
description = "Name of Infomaniak's public dual-stack network"
type = string
default = "ext-net1"
}
variable "watcher_image_name" {
description = "Name of the NixOS image previously uploaded to Glance"
type = string
default = "nixos-watcher"
}

View file

@ -0,0 +1,73 @@
data "openstack_images_image_v2" "watcher" {
name = var.watcher_image_name
most_recent = true
}
data "openstack_networking_network_v2" "external" {
name = var.external_network
}
resource "openstack_compute_keypair_v2" "primary" {
name = var.ssh_key_name
public_key = var.ssh_public_key
}
resource "openstack_networking_secgroup_v2" "watcher" {
name = "watcher"
description = "watcher: SSH + HTTP(S)"
}
# SSH in (IPv4 + IPv6)
resource "openstack_networking_secgroup_rule_v2" "ssh_v4" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 22
port_range_max = 22
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.watcher.id
}
resource "openstack_networking_secgroup_rule_v2" "ssh_v6" {
direction = "ingress"
ethertype = "IPv6"
protocol = "tcp"
port_range_min = 22
port_range_max = 22
remote_ip_prefix = "::/0"
security_group_id = openstack_networking_secgroup_v2.watcher.id
}
# HTTP(S) for Caddy. Open from day one so cert provisioning works the moment
# we add the website module closing again would just be churn.
resource "openstack_networking_secgroup_rule_v2" "http" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 80
port_range_max = 80
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.watcher.id
}
resource "openstack_networking_secgroup_rule_v2" "https" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 443
port_range_max = 443
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.watcher.id
}
resource "openstack_compute_instance_v2" "watcher" {
name = "watcher"
flavor_name = var.flavor_name
image_id = data.openstack_images_image_v2.watcher.id
key_pair = openstack_compute_keypair_v2.primary.name
security_groups = [openstack_networking_secgroup_v2.watcher.name]
network {
name = data.openstack_networking_network_v2.external.name
}
}