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

24
.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# Secrets — never commit
.env
clouds.yaml
*.tfvars
*.pem
*.key
# Terraform local state and cache
terraform/**/.terraform/
terraform/**/terraform.tfstate
terraform/**/terraform.tfstate.backup
terraform/**/.terraform.tfstate.lock.info
# Nix build outputs
result
result-*
# Editor / OS cruft
*.swp
.direnv/
.DS_Store
# Local PDFs in repo root (Infomaniak invoices, etc.)
*.pdf

14
.sops.yaml Normal file
View file

@ -0,0 +1,14 @@
# sops-nix encryption rules.
#
# To get started:
# nix-shell -p age --run 'mkdir -p ~/.config/sops/age && age-keygen -o ~/.config/sops/age/keys.txt'
# Then copy the public key (the line starting with "# public key:") into the
# placeholder below.
keys:
- &laptop age1REPLACE_ME_WITH_YOUR_LAPTOP_PUBLIC_KEY
creation_rules:
- path_regex: hosts/watcher/secrets\.yaml$
key_groups:
- age: [*laptop]

84
README.md Normal file
View file

@ -0,0 +1,84 @@
# remote_server
Personal infrastructure on **Infomaniak Public Cloud** (OpenStack), end-to-end
Swiss. A small always-on **watcher** VM plus, later, on-demand workers using
the same OpenStack APIs.
## Layout
```
.
├── flake.nix # NixOS configs + watcher QCOW2 image package
├── hosts/
│ └── watcher/
│ └── default.nix # services, users, SSH; boot/disk via openstack-* modules
├── terraform/
│ └── infomaniak/ # OpenStack provider, security group, instance
│ ├── main.tf
│ ├── variables.tf
│ ├── watcher.tf
│ ├── outputs.tf
│ ├── clouds.yaml.example
│ └── .env.example
├── scripts/
│ ├── build-image.sh # build QCOW2 → upload to Infomaniak Glance
│ └── deploy.sh # terraform apply → nixos-rebuild switch
└── .sops.yaml # secrets encryption rules
```
## First-time setup
1. **Infomaniak Public Cloud project** — Manager → Public Cloud → create a
project. In the project's API access section, generate an
**application credential** and copy the ID + secret.
2. **Fill `.env`** (gitignored, lives only in the repo — nothing in `~/.config`):
```bash
cp terraform/infomaniak/.env.example terraform/infomaniak/.env
$EDITOR terraform/infomaniak/.env
```
Paste `OS_APPLICATION_CREDENTIAL_ID`, `OS_APPLICATION_CREDENTIAL_SECRET`,
and `TF_VAR_ssh_public_key`.
3. **Enter the dev shell** — brings in `openstack`, `terraform`, `jq`, `sops`
and auto-loads `.env`:
```bash
nix develop
```
4. **Smoke-test auth and confirm catalogue defaults match**:
```bash
openstack token issue
openstack flavor list | grep a2_ram4_disk20_perf1
openstack network list | grep ext-net1
```
5. **Paste your SSH public key** into `hosts/watcher/default.nix`
(`users.users.tyro.openssh.authorizedKeys.keys`).
6. **Build & upload the watcher image** (~5 min the first time):
```bash
./scripts/build-image.sh
```
7. **Provision and deploy**:
```bash
./scripts/deploy.sh
```
After the script finishes: `ssh tyro@<ipv4>`.
## Routine updates
From inside `nix develop`, edit the flake, then push:
```bash
nixos-rebuild switch --flake .#watcher --target-host tyro@<ipv4> --use-remote-sudo
```
You only need to re-run `build-image.sh` if you want **fresh boots** to start
from a current image (e.g. after a major NixOS bump).
## DNS
Managed at Infomaniak (registrar). After the watcher has IPs:
- `tyrolize.ch` A/AAAA → watcher
- `*.tyrolize.ch` A/AAAA → watcher (wildcard for subdomains served by Caddy)
- `lize.ch` A/AAAA → watcher (for the 301 redirect to tyrolize.ch)
- `lize.ch` MX/SPF/DKIM/DMARC → Infomaniak kSuite (auto-configured)
- `tyrolize.ch` empty-SPF + DMARC `p=reject` (anti-spoofing on non-mail domain)

26
dns/lize.ch.zone Normal file
View file

@ -0,0 +1,26 @@
; Domain: lize.ch
; All existing kSuite mail records preserved exactly as-is.
; Adds: A/AAAA for the apex (Caddy on the watcher 301-redirects to
; https://tyrolize.ch), plus CAA restricting TLS to Let's Encrypt.
$TTL 3600
@ IN SOA ns11.infomaniak.ch. hostmaster.infomaniak.ch. (2026061610 10800 3600 605800 3600)
@ 3600 IN NS ns11.infomaniak.ch.
@ 3600 IN NS ns12.infomaniak.ch.
; --- kSuite mail (DO NOT TOUCH) ---
@ 3600 IN MX 5 mta-gw.infomaniak.ch.
@ 3600 IN TXT "v=spf1 include:spf.infomaniak.ch -all"
autoconfig 3600 IN CNAME infomaniak.com.
autodiscover 3600 IN CNAME infomaniak.com.
_dmarc 3600 IN TXT "v=DMARC1; p=reject;"
_domainkey 3600 IN NS ns11.infomaniak.ch.
_domainkey 3600 IN NS ns12.infomaniak.ch.
; --- New: apex points at watcher VM for the redirect to tyrolize.ch ---
@ 300 IN A 195.15.203.200
@ 300 IN AAAA 2001:1600:10:100::b4e
; --- Restrict TLS cert issuance to Let's Encrypt ---
@ 3600 IN CAA 0 issue "letsencrypt.org"

27
dns/tyrolize.ch.zone Normal file
View file

@ -0,0 +1,27 @@
; Domain: tyrolize.ch
; Adds A/AAAA + wildcard pointing at the watcher VM (195.15.203.200),
; plus anti-spoofing (no mail leaves this domain) and CAA restricting
; TLS issuance to Let's Encrypt.
$TTL 3600
@ IN SOA ns11.infomaniak.ch. hostmaster.infomaniak.ch. (2026061646 10800 3600 605800 3600)
@ 3600 IN NS ns11.infomaniak.ch.
@ 3600 IN NS ns12.infomaniak.ch.
; --- Watcher VM (short TTL during bring-up; raise to 3600 later if you want) ---
@ 300 IN A 195.15.203.200
@ 300 IN AAAA 2001:1600:10:100::b4e
* 300 IN A 195.15.203.200
* 300 IN AAAA 2001:1600:10:100::b4e
; --- Anti-spoofing: no mail is sent from tyrolize.ch ---
@ 3600 IN TXT "v=spf1 -all"
_dmarc 3600 IN TXT "v=DMARC1; p=reject; rua=mailto:tyro@lize.ch"
; --- Restrict TLS cert issuance to Let's Encrypt ---
@ 3600 IN CAA 0 issue "letsencrypt.org"
; --- Infomaniak default (harmless on non-mail domain) ---
_domainkey 3600 IN NS ns11.infomaniak.ch.
_domainkey 3600 IN NS ns12.infomaniak.ch.

85
flake.lock generated Normal file
View file

@ -0,0 +1,85 @@
{
"nodes": {
"nixlib": {
"locked": {
"lastModified": 1736643958,
"narHash": "sha256-tmpqTSWVRJVhpvfSN9KXBvKEXplrwKnSZNAoNPf/S/s=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "1418bc28a52126761c02dd3d89b2d8ca0f521181",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"nixos-generators": {
"inputs": {
"nixlib": "nixlib",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1769813415,
"narHash": "sha256-nnVmNNKBi1YiBNPhKclNYDORoHkuKipoz7EtVnXO50A=",
"owner": "nix-community",
"repo": "nixos-generators",
"rev": "8946737ff703382fda7623b9fab071d037e897d5",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixos-generators",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1767313136,
"narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixos-generators": "nixos-generators",
"nixpkgs": "nixpkgs",
"sops-nix": "sops-nix"
}
},
"sops-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1780547341,
"narHash": "sha256-Gq8KNx5A7hBB3uGJaj6eQfLDIz5YdLu92gqBcvHvoUo=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "9ed65852b6257fbeae4355bc24ecfea307ca759a",
"type": "github"
},
"original": {
"owner": "Mic92",
"repo": "sops-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

64
flake.nix Normal file
View file

@ -0,0 +1,64 @@
{
description = "Personal infra on Infomaniak Public Cloud (OpenStack)";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
sops-nix.url = "github:Mic92/sops-nix";
sops-nix.inputs.nixpkgs.follows = "nixpkgs";
nixos-generators.url = "github:nix-community/nixos-generators";
nixos-generators.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, sops-nix, nixos-generators, ... }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in {
# `nix develop` to enter a shell with every tool this repo needs.
devShells.${system}.default = pkgs.mkShell {
packages = with pkgs; [
openstackclient # `openstack` CLI
opentofu # `tofu` — drop-in for terraform, OSS license
jq
age
sops # for sops-nix secrets
];
shellHook = ''
if [ -f "$PWD/terraform/infomaniak/.env" ]; then
set -a; . "$PWD/terraform/infomaniak/.env"; set +a
echo "loaded terraform/infomaniak/.env"
fi
'';
};
# Runtime config — what the watcher box actually IS.
# Push updates with:
# nixos-rebuild switch --flake .#watcher --target-host tyro@<ip> --use-remote-sudo
nixosConfigurations.watcher = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
({ modulesPath, ... }: {
imports = [
"${modulesPath}/virtualisation/openstack-config.nix"
];
})
sops-nix.nixosModules.sops
./hosts/watcher
];
};
# Build a QCOW2 image of the watcher to upload to Infomaniak Glance:
# nix build .#watcher-image
# ls -lh result/nixos.qcow2
packages.${system}.watcher-image = nixos-generators.nixosGenerate {
inherit system pkgs;
format = "openstack";
modules = [
sops-nix.nixosModules.sops
./hosts/watcher
];
};
};
}

65
hosts/watcher/default.nix Normal file
View file

@ -0,0 +1,65 @@
{ config, lib, pkgs, ... }: {
imports = [
../../modules/website.nix
];
# Boot, disk layout, and cloud-init are provided by:
# - openstack-config.nix (for nixos-rebuild on the live box), or
# - openstack-image.nix (when building the QCOW2 image)
# Both are wired in flake.nix.
system.stateVersion = "25.05";
networking.hostName = "watcher";
networking.firewall = {
enable = true;
allowedTCPPorts = [ 22 ]; # services like 80/443 added as they come online
};
time.timeZone = "Europe/Zurich";
i18n.defaultLocale = "en_US.UTF-8";
users.mutableUsers = false;
users.users.tyro = {
isNormalUser = true;
extraGroups = [ "wheel" ];
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHC7oEcIquy/HWSHjA9N62FVKA6js4aOWu9q41Qp3nNj tyrolize@nixos"
];
};
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
KbdInteractiveAuthentication = false;
# openstack-config.nix defaults this to "prohibit-password" so cloud-init can
# inject the OpenStack keypair into root. We don't need it: the same key is
# already in users.users.tyro via the flake. Hard-disable root SSH.
PermitRootLogin = lib.mkForce "no";
};
};
security.sudo.wheelNeedsPassword = false;
environment.systemPackages = with pkgs; [
vim
git
htop
tmux
curl
wget
];
nix.settings = {
experimental-features = [ "nix-command" "flakes" ];
# Allow the tyro user to push pre-built closures from the laptop via
# `nixos-rebuild --target-host` without re-signing every store path.
trusted-users = [ "root" "@wheel" ];
};
nix.gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 14d";
};
}

26
modules/website.nix Normal file
View file

@ -0,0 +1,26 @@
{ config, lib, pkgs, ... }: {
# Caddy serves tyrolize.ch and 301-redirects lize.ch to it.
# TLS certs auto-provisioned via Let's Encrypt (HTTP-01 challenge), which
# requires the apex DNS A/AAAA records to already point at this VM.
networking.firewall.allowedTCPPorts = [ 80 443 ];
services.caddy = {
enable = true;
email = "tyro@lize.ch"; # Let's Encrypt account contact
virtualHosts."tyrolize.ch" = {
extraConfig = ''
root * ${../sites/tyrolize.ch}
file_server
encode gzip zstd
'';
};
virtualHosts."lize.ch" = {
extraConfig = ''
redir https://tyrolize.ch{uri} permanent
'';
};
};
}

46
scripts/build-image.sh Executable file
View file

@ -0,0 +1,46 @@
#!/usr/bin/env bash
# Build the watcher's NixOS QCOW2 image and upload it to Infomaniak Glance.
#
# Prereqs:
# - Run inside `nix develop` (provides openstackclient + nix + flakes).
# - terraform/infomaniak/.env populated and auto-sourced by the devShell.
# - SSH pubkey pasted into hosts/watcher/default.nix.
#
# Re-run any time the watcher's NixOS config changes if you want fresh boots
# from a current image. Routine config updates after boot use nixos-rebuild
# instead (no image rebuild required).
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"
IMAGE_NAME="${TF_VAR_watcher_image_name:-nixos-watcher}"
: "${OS_AUTH_URL:?terraform/infomaniak/.env not loaded — run inside 'nix develop'}"
echo "==> building QCOW2 (this takes a few minutes the first time)"
nix build .#watcher-image
# nixos-generators names the file with the channel revision, e.g.
# nixos-image-openstack-25.05.20260102.ac62194-x86_64-linux.qcow2
QCOW=$(ls "$(readlink -f result)"/*.qcow2 | head -1)
[ -f "$QCOW" ] || { echo "no qcow2 found under result/" >&2; exit 1; }
ls -lh "$QCOW"
echo "==> uploading to Infomaniak Glance as '$IMAGE_NAME'"
# If an older copy exists, delete it first (Glance image names aren't unique).
if openstack image show "$IMAGE_NAME" -f value -c id >/dev/null 2>&1; then
echo " older image found — deleting"
openstack image delete "$IMAGE_NAME"
fi
openstack image create \
--disk-format qcow2 \
--container-format bare \
--file "$QCOW" \
--progress \
"$IMAGE_NAME"
echo "==> done. Image:"
openstack image show "$IMAGE_NAME" -f table -c id -c name -c status -c size

45
scripts/deploy.sh Executable file
View file

@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Provision (or update) the watcher instance via OpenTofu, then push the
# latest flake config with nixos-rebuild.
#
# Prereqs:
# - Watcher image already uploaded to Glance (run scripts/build-image.sh once)
# - Run inside `nix develop` (auto-sources terraform/infomaniak/.env)
#
# Idempotent: safe to re-run.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"
: "${OS_AUTH_URL:?terraform/infomaniak/.env not loaded — run inside 'nix develop'}"
: "${TF_VAR_ssh_public_key:?TF_VAR_ssh_public_key missing — paste pubkey into .env}"
echo "==> tofu apply"
pushd terraform/infomaniak >/dev/null
tofu init -upgrade
tofu apply -auto-approve
IPV4=$(tofu output -raw watcher_ipv4)
popd >/dev/null
echo "==> watcher at $IPV4"
echo "==> waiting for SSH"
for _ in $(seq 1 60); do
if ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=3 \
-o BatchMode=yes tyro@"$IPV4" true 2>/dev/null; then
break
fi
sleep 2
done
echo "==> nixos-rebuild switch"
nixos-rebuild switch \
--flake "$REPO_ROOT#watcher" \
--target-host "tyro@$IPV4" \
--use-remote-sudo
echo
echo "Done."
echo " ssh tyro@$IPV4"

View file

@ -0,0 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>tyrolize</title>
<style>
:root { color-scheme: light dark; }
body {
max-width: 36rem;
margin: 6rem auto;
padding: 0 1.25rem;
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
line-height: 1.5;
}
a { color: inherit; text-underline-offset: 0.2em; }
</style>
</head>
<body>
<h1>tyrolize.ch</h1>
<p>Hello.</p>
<p>Mail: <a href="mailto:tyro@lize.ch">tyro@lize.ch</a></p>
</body>
</html>

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
}
}