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:
commit
59d742f4ba
18 changed files with 733 additions and 0 deletions
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
14
.sops.yaml
Normal 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
84
README.md
Normal 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
26
dns/lize.ch.zone
Normal 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
27
dns/tyrolize.ch.zone
Normal 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
85
flake.lock
generated
Normal 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
64
flake.nix
Normal 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
65
hosts/watcher/default.nix
Normal 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
26
modules/website.nix
Normal 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
46
scripts/build-image.sh
Executable 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
45
scripts/deploy.sh
Executable 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"
|
||||||
24
sites/tyrolize.ch/index.html
Normal file
24
sites/tyrolize.ch/index.html
Normal 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>
|
||||||
43
terraform/infomaniak/.env.example
Normal file
43
terraform/infomaniak/.env.example
Normal 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
24
terraform/infomaniak/.terraform.lock.hcl
generated
Normal 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",
|
||||||
|
]
|
||||||
|
}
|
||||||
13
terraform/infomaniak/main.tf
Normal file
13
terraform/infomaniak/main.tf
Normal 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" {}
|
||||||
14
terraform/infomaniak/outputs.tf
Normal file
14
terraform/infomaniak/outputs.tf
Normal 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`)"
|
||||||
|
}
|
||||||
36
terraform/infomaniak/variables.tf
Normal file
36
terraform/infomaniak/variables.tf
Normal 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"
|
||||||
|
}
|
||||||
73
terraform/infomaniak/watcher.tf
Normal file
73
terraform/infomaniak/watcher.tf
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue