feat(provision): Phase-0 throwaway test VM via vendored @olsitec/pulumi-hetzner
- Vendor hetzner module (Stage-1, trimmed to @pulumi/hcloud+js-yaml; dropped unused types.ts + bcrypt/axios/tls/vault deps). GOTCHA documented: cloud-init moves SSH to port 222. - provision/: isolated stack (platformName foundation-test, no collision with olsicloud4-*) — one cx23 in nbg1-dc3 + firewall (222/80/443/2222) + Docker cloud-init. Dedicated throwaway ed25519 key (operator id_rsa already registered → uniqueness_error). - Provisioned + verified: foundation-test @ 91.98.117.152, Docker 29.6.1, docker-over-SSH OK. Token via ENV (pass), never committed; provision/state gitignored. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6a29db386f
commit
80a99c6f7e
13 changed files with 754 additions and 1 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -7,3 +7,4 @@ bootstrap/state/
|
||||||
*.local.*
|
*.local.*
|
||||||
# os
|
# os
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
provision/state/
|
||||||
|
|
|
||||||
35
bun.lock
35
bun.lock
|
|
@ -38,6 +38,20 @@
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"packages/pulumi-hetzner": {
|
||||||
|
"name": "@olsitec/pulumi-hetzner",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@pulumi/hcloud": "^1.21.1",
|
||||||
|
"@pulumi/pulumi": "^3.138.0",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
"@types/node": "^18",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
"packages/pulumi-vault": {
|
"packages/pulumi-vault": {
|
||||||
"name": "@olsitec/pulumi-vault",
|
"name": "@olsitec/pulumi-vault",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
|
|
@ -55,6 +69,19 @@
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"provision": {
|
||||||
|
"name": "@olsitec/foundation-provision",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@olsitec/pulumi-hetzner": "workspace:*",
|
||||||
|
"@pulumi/hcloud": "^1.21.1",
|
||||||
|
"@pulumi/pulumi": "^3.138.0",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^18",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||||
|
|
@ -133,8 +160,12 @@
|
||||||
|
|
||||||
"@olsitec/foundation-bootstrap": ["@olsitec/foundation-bootstrap@workspace:bootstrap"],
|
"@olsitec/foundation-bootstrap": ["@olsitec/foundation-bootstrap@workspace:bootstrap"],
|
||||||
|
|
||||||
|
"@olsitec/foundation-provision": ["@olsitec/foundation-provision@workspace:provision"],
|
||||||
|
|
||||||
"@olsitec/pulumi-docker": ["@olsitec/pulumi-docker@workspace:packages/pulumi-docker"],
|
"@olsitec/pulumi-docker": ["@olsitec/pulumi-docker@workspace:packages/pulumi-docker"],
|
||||||
|
|
||||||
|
"@olsitec/pulumi-hetzner": ["@olsitec/pulumi-hetzner@workspace:packages/pulumi-hetzner"],
|
||||||
|
|
||||||
"@olsitec/pulumi-vault": ["@olsitec/pulumi-vault@workspace:packages/pulumi-vault"],
|
"@olsitec/pulumi-vault": ["@olsitec/pulumi-vault@workspace:packages/pulumi-vault"],
|
||||||
|
|
||||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
|
"@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
|
||||||
|
|
@ -197,6 +228,8 @@
|
||||||
|
|
||||||
"@pulumi/eslint-plugin": ["@pulumi/eslint-plugin@0.2.0", "", { "dependencies": { "@typescript-eslint/type-utils": "^5.33.1", "@typescript-eslint/typescript-estree": "^5.33.1", "@typescript-eslint/utils": "^5.33.1", "tsutils": "^3.21.0", "typescript": "^4.7.4" } }, "sha512-tb2Wo1pO8kmNIt+ECkVd7ykRHgadFJfddjLG8Of002X+qbRkNZNttdt55o7EdCDHGB6Dn1RFo/MJYNuHjYn/Dg=="],
|
"@pulumi/eslint-plugin": ["@pulumi/eslint-plugin@0.2.0", "", { "dependencies": { "@typescript-eslint/type-utils": "^5.33.1", "@typescript-eslint/typescript-estree": "^5.33.1", "@typescript-eslint/utils": "^5.33.1", "tsutils": "^3.21.0", "typescript": "^4.7.4" } }, "sha512-tb2Wo1pO8kmNIt+ECkVd7ykRHgadFJfddjLG8Of002X+qbRkNZNttdt55o7EdCDHGB6Dn1RFo/MJYNuHjYn/Dg=="],
|
||||||
|
|
||||||
|
"@pulumi/hcloud": ["@pulumi/hcloud@1.39.0", "", { "dependencies": { "@pulumi/pulumi": "^3.142.0" } }, "sha512-rrjOZ1bPliOpsuoGBrd6b9GOeM+CoNSLTJrd061JzwAREdztVP6vy8UEROQj7zIUypEI0+eCqXAA1bxYIQSwkQ=="],
|
||||||
|
|
||||||
"@pulumi/pulumi": ["@pulumi/pulumi@3.248.0", "", { "dependencies": { "@grpc/grpc-js": "^1.10.1", "@logdna/tail-file": "^2.0.6", "@npmcli/arborist": "^9.0.0", "@opentelemetry/api": "^1.9", "@opentelemetry/exporter-trace-otlp-grpc": "^0.57", "@opentelemetry/exporter-zipkin": "^1.30", "@opentelemetry/instrumentation": "^0.57", "@opentelemetry/instrumentation-grpc": "^0.57", "@opentelemetry/resources": "^1.30", "@opentelemetry/sdk-trace-base": "^1.30", "@opentelemetry/sdk-trace-node": "^1.30", "@types/google-protobuf": "^3.15.5", "@types/semver": "^7.5.6", "@types/tmp": "^0.2.6", "execa": "^5.1.0", "fdir": "^6.5.0", "google-protobuf": "^3.21.4", "ini": "^2.0.0", "js-yaml": "^4.0.0", "minimist": "^1.2.6", "normalize-package-data": "^6.0.0", "picomatch": "^4.0.0", "require-from-string": "^2.0.1", "semver": "^7.5.2", "source-map-support": "^0.5.6", "tmp": "^0.2.4", "upath": "^1.1.0" }, "peerDependencies": { "ts-node": ">= 7.0.1 < 12", "typescript": ">= 3.8.3 < 7" }, "optionalPeers": ["ts-node", "typescript"] }, "sha512-EqgeHjVIqMS8voAM7F8SOzFAMHnVXUDdKTNF1o3Lg85YwVI0j4/eIlWG0iIVAWJl3DX0KOOM6++X0wLKHWWwmQ=="],
|
"@pulumi/pulumi": ["@pulumi/pulumi@3.248.0", "", { "dependencies": { "@grpc/grpc-js": "^1.10.1", "@logdna/tail-file": "^2.0.6", "@npmcli/arborist": "^9.0.0", "@opentelemetry/api": "^1.9", "@opentelemetry/exporter-trace-otlp-grpc": "^0.57", "@opentelemetry/exporter-zipkin": "^1.30", "@opentelemetry/instrumentation": "^0.57", "@opentelemetry/instrumentation-grpc": "^0.57", "@opentelemetry/resources": "^1.30", "@opentelemetry/sdk-trace-base": "^1.30", "@opentelemetry/sdk-trace-node": "^1.30", "@types/google-protobuf": "^3.15.5", "@types/semver": "^7.5.6", "@types/tmp": "^0.2.6", "execa": "^5.1.0", "fdir": "^6.5.0", "google-protobuf": "^3.21.4", "ini": "^2.0.0", "js-yaml": "^4.0.0", "minimist": "^1.2.6", "normalize-package-data": "^6.0.0", "picomatch": "^4.0.0", "require-from-string": "^2.0.1", "semver": "^7.5.2", "source-map-support": "^0.5.6", "tmp": "^0.2.4", "upath": "^1.1.0" }, "peerDependencies": { "ts-node": ">= 7.0.1 < 12", "typescript": ">= 3.8.3 < 7" }, "optionalPeers": ["ts-node", "typescript"] }, "sha512-EqgeHjVIqMS8voAM7F8SOzFAMHnVXUDdKTNF1o3Lg85YwVI0j4/eIlWG0iIVAWJl3DX0KOOM6++X0wLKHWWwmQ=="],
|
||||||
|
|
||||||
"@pulumi/random": ["@pulumi/random@4.21.0", "", { "dependencies": { "@pulumi/pulumi": "^3.142.0" } }, "sha512-k4JHqQFOKXWkbSLZtX37xBlRsurfgZbLd1Zib0cLSDP4Dt3uTllR3CBWfMLSnXJGAoTr9MlwyHCer/5tmSaF0Q=="],
|
"@pulumi/random": ["@pulumi/random@4.21.0", "", { "dependencies": { "@pulumi/pulumi": "^3.142.0" } }, "sha512-k4JHqQFOKXWkbSLZtX37xBlRsurfgZbLd1Zib0cLSDP4Dt3uTllR3CBWfMLSnXJGAoTr9MlwyHCer/5tmSaF0Q=="],
|
||||||
|
|
@ -225,6 +258,8 @@
|
||||||
|
|
||||||
"@types/google-protobuf": ["@types/google-protobuf@3.15.12", "", {}, "sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ=="],
|
"@types/google-protobuf": ["@types/google-protobuf@3.15.12", "", {}, "sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ=="],
|
||||||
|
|
||||||
|
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
|
||||||
|
|
||||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
|
"@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,21 @@ NOTE: T03 precursor live validation — prove Docker-over-SSH provider creates f
|
||||||
CMD: (above) pulumi up/inspect/destroy — foundation-net on crunchy01
|
CMD: (above) pulumi up/inspect/destroy — foundation-net on crunchy01
|
||||||
EXIT: 0 — created (subnet 172.30.0.0/24, bridge, attachable, verified) then destroyed clean; nothing persisted.
|
EXIT: 0 — created (subnet 172.30.0.0/24, bridge, attachable, verified) then destroyed clean; nothing persisted.
|
||||||
---
|
---
|
||||||
|
--- 2026-06-30T16:51:46Z ---
|
||||||
|
HOST: mac-studio -> hetzner-api (eu-central/nbg1-dc3)
|
||||||
|
CWD: /Users/andiolsi/work/olsitec-foundation/foundation/provision PROJECT: foundation-provision STACK: foundation-test
|
||||||
|
ENVIRONMENT: test(throwaway)
|
||||||
|
CMD: pulumi up --yes (HetznerDeployment cx22 + firewall + docker cloud-init)
|
||||||
|
EXIT: RUNNING
|
||||||
|
NOTE: Phase-0 provision of throwaway foundation TEST VM via vendored @olsitec/pulumi-hetzner. ~14 hcloud resources. Billable (~EUR0.007/hr). Destroyable via 'pulumi destroy' in provision/.
|
||||||
|
---
|
||||||
|
--- 2026-06-30T16:51:49Z UPDATE ---
|
||||||
|
EXIT: 0 — see outputs (publicIp). VM provisioning; docker installs via cloud-init (~1-2 min).
|
||||||
|
---
|
||||||
|
--- 2026-06-30T16:54:41Z UPDATE ---
|
||||||
|
EXIT: 0 — VM created (cx23, nbg1-dc3). publicIp in outputs. Docker installing via cloud-init.
|
||||||
|
---
|
||||||
|
--- 2026-06-30T16:57:30Z UPDATE ---
|
||||||
|
EXIT: 0 — test VM foundation-test @ 91.98.117.152 (cx23, nbg1-dc3), SSH:222, Docker 29.6.1 verified.
|
||||||
|
NOTE: docker-over-SSH provider path needs SSH_PRIVATE_KEY_PATH=~/.ssh/foundation-test_ed25519 + FOUNDATION_DOCKER_HOST=ssh://root@91.98.117.152:222. DESTROY: cd provision && pulumi destroy (stack foundation-test).
|
||||||
|
---
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@
|
||||||
"description": "olsitec-foundation mono-repo — the bootstrap egg (bootstrap/) + vendored shared Pulumi modules (packages/*). One git clone = the full DR unit (000_TOPOLOGY.md, ADR-005).",
|
"description": "olsitec-foundation mono-repo — the bootstrap egg (bootstrap/) + vendored shared Pulumi modules (packages/*). One git clone = the full DR unit (000_TOPOLOGY.md, ADR-005).",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
"bootstrap"
|
"bootstrap",
|
||||||
|
"provision"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
|
|
|
||||||
12
packages/pulumi-hetzner/VENDORED.md
Normal file
12
packages/pulumi-hetzner/VENDORED.md
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# VENDORED — @olsitec/pulumi-hetzner
|
||||||
|
|
||||||
|
Stage-1 vendor (000_TOPOLOGY.md §5; ADR-005), 2026-06-30.
|
||||||
|
|
||||||
|
- **Source**: `/Users/andiolsi/work/olsicloud4/pulumi/modules/hetzner/` (index.ts, cloudinit-config.ts, tsconfig.json).
|
||||||
|
- **Trimmed**: dropped `types.ts` (standalone, unused by `HetznerDeployment`) and the unused deps it pulled
|
||||||
|
(`bcrypt`, `axios`, `@pulumi/{tls,vault,random}`, `deepmerge`, `yaml`). Real import surface is only
|
||||||
|
`@pulumi/hcloud`, `@pulumi/pulumi`, `fs`, `js-yaml`. Logic unchanged.
|
||||||
|
- **GOTCHA — SSH port 222**: `getCloudInitConfig` writes an sshd drop-in that moves SSH to **port 222**
|
||||||
|
and creates `root` + `andiolsi` users. Consumers (the Docker-over-SSH provider, ssh checks) MUST use
|
||||||
|
`:222`. The module creates **no firewall** — the consumer adds one.
|
||||||
|
- Stage-2 (publish to the foundation registry) is a later task.
|
||||||
126
packages/pulumi-hetzner/cloudinit-config.ts
Normal file
126
packages/pulumi-hetzner/cloudinit-config.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
import * as yaml from "js-yaml";
|
||||||
|
|
||||||
|
export interface GetCloudInitConfigArgs {
|
||||||
|
sshAuthorizedKeys?: string[];
|
||||||
|
extraPackages?: string[];
|
||||||
|
additionalFiles?: {
|
||||||
|
path: string;
|
||||||
|
permissions: string;
|
||||||
|
content: string;
|
||||||
|
}[];
|
||||||
|
lateCommands?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCloudInitConfig = (config: GetCloudInitConfigArgs) => {
|
||||||
|
const defaultCloudInitConfig = {
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
name: "root",
|
||||||
|
sudo: "ALL=(ALL) NOPASSWD:ALL",
|
||||||
|
"lock-passwd": false,
|
||||||
|
passwd:
|
||||||
|
"$6$hB5RrvU5$ll99.7zgATrkGEbInSRCF7o8t3TatZEYQK4QWqk6Ri.DT3LgG0l38Dz47CT.nBjLhhIUVgzIF6t2ZrWmDOcVl1",
|
||||||
|
ssh_authorized_keys: config.sshAuthorizedKeys || [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "andiolsi",
|
||||||
|
sudo: "ALL=(ALL) NOPASSWD:ALL",
|
||||||
|
"lock-passwd": false,
|
||||||
|
passwd:
|
||||||
|
"$6$sKcNr2f8$AubrAJ..bZOPxnGVEWKQQrWShW/i7ClAZRvGLstrmeDcIS92/.BVCtYzgvg2tg6ub9rz8agfLRJg.ryDoM7jG/",
|
||||||
|
ssh_authorized_keys: config.sshAuthorizedKeys || [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
disable_root: false,
|
||||||
|
ssh_pwauth: false,
|
||||||
|
locale: "en_US.UTF-8",
|
||||||
|
mounts: [["tmpfs", "/var/log", "tmpfs", "defaults,size=10%", "0", "0"]],
|
||||||
|
package_update: true,
|
||||||
|
package_upgrade: true,
|
||||||
|
packages: [
|
||||||
|
"curl",
|
||||||
|
"apt-transport-https",
|
||||||
|
"ca-certificates",
|
||||||
|
"vim",
|
||||||
|
"jq",
|
||||||
|
"locales",
|
||||||
|
...(config.extraPackages || []),
|
||||||
|
],
|
||||||
|
write_files: [
|
||||||
|
{
|
||||||
|
path: "/etc/ssh/sshd_config.d/99-cloud-init-manual.conf",
|
||||||
|
permissions: "0600",
|
||||||
|
content: `
|
||||||
|
Port 222
|
||||||
|
PermitRootLogin without-password
|
||||||
|
PubkeyAuthentication yes
|
||||||
|
PubkeyAcceptedKeyTypes=+ssh-rsa
|
||||||
|
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/etc/logrotate.d/var_log",
|
||||||
|
permissions: "0644",
|
||||||
|
content: `
|
||||||
|
/var/log/*.log {
|
||||||
|
daily
|
||||||
|
missingok
|
||||||
|
rotate 7
|
||||||
|
compress
|
||||||
|
delaycompress
|
||||||
|
notifempty
|
||||||
|
create 0640 root adm
|
||||||
|
sharedscripts
|
||||||
|
postrotate
|
||||||
|
systemctl reload rsyslog > /dev/null 2>/dev/null || true
|
||||||
|
endscript
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/etc/selinux/config",
|
||||||
|
permissions: "0644",
|
||||||
|
content: "SELINUX=disabled\nSELINUXTYPE=targeted\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/etc/security/limits.conf",
|
||||||
|
permissions: "0644",
|
||||||
|
content: "* soft nofile 32768\n* hard nofile 65536\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/etc/sysctl.d/99-custom.conf",
|
||||||
|
permissions: "0644",
|
||||||
|
content: `
|
||||||
|
net.ipv4.ip_forward=1
|
||||||
|
net.ipv4.conf.all.arp_ignore=1
|
||||||
|
net.ipv4.conf.all.arp_announce=2
|
||||||
|
net.ipv4.ip_nonlocal_bind=1
|
||||||
|
vm.dirty_ratio=10
|
||||||
|
vm.swappiness=0
|
||||||
|
vm.dirty_background_ratio=5
|
||||||
|
net.ipv6.conf.all.disable_ipv6=1
|
||||||
|
fs.nr_open=3000000
|
||||||
|
fs.file-max=3000000
|
||||||
|
fs.inotify.max_user_instances=1048576
|
||||||
|
fs.inotify.max_queued_events=1048576
|
||||||
|
fs.inotify.max_user_watches=1048576
|
||||||
|
vm.max_map_count=262144
|
||||||
|
net.netfilter.nf_conntrack_max=262144
|
||||||
|
net.netfilter.nf_conntrack_tcp_timeout_established=600
|
||||||
|
net.netfilter.nf_conntrack_tcp_timeout_time_wait=30
|
||||||
|
net.netfilter.nf_conntrack_tcp_timeout_close_wait=30
|
||||||
|
net.netfilter.nf_conntrack_tcp_timeout_fin_wait=30
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
...(config.additionalFiles || []),
|
||||||
|
],
|
||||||
|
runcmd: [
|
||||||
|
"echo never > /sys/kernel/mm/transparent_hugepage/enabled",
|
||||||
|
"echo never > /sys/kernel/mm/transparent_hugepage/defrag",
|
||||||
|
"systemctl stop apparmor || true",
|
||||||
|
"systemctl disable apparmor || true",
|
||||||
|
"sysctl --system",
|
||||||
|
...(config.lateCommands || []),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
return `#cloud-config\n` + yaml.dump(defaultCloudInitConfig);
|
||||||
|
};
|
||||||
392
packages/pulumi-hetzner/index.ts
Normal file
392
packages/pulumi-hetzner/index.ts
Normal file
|
|
@ -0,0 +1,392 @@
|
||||||
|
// hetzner/index.ts
|
||||||
|
import * as pulumi from "@pulumi/pulumi";
|
||||||
|
import * as hcloud from "@pulumi/hcloud";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import {
|
||||||
|
getCloudInitConfig,
|
||||||
|
type GetCloudInitConfigArgs,
|
||||||
|
} from "./cloudinit-config";
|
||||||
|
import { LoadBalancerServiceHealthCheck } from "@pulumi/hcloud/types/input";
|
||||||
|
|
||||||
|
interface HetznerDeploymentConfig {
|
||||||
|
platformName: string;
|
||||||
|
datacenter: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HetznerDeploymentArgs {
|
||||||
|
platformName: string;
|
||||||
|
hcloudToken: pulumi.Input<string>;
|
||||||
|
vswitchId?: number;
|
||||||
|
sshKeyPath: string;
|
||||||
|
datacenter: string;
|
||||||
|
servers: {
|
||||||
|
name: string;
|
||||||
|
hostname: string;
|
||||||
|
image?: string;
|
||||||
|
type: string;
|
||||||
|
labels?: Record<string, string>;
|
||||||
|
cloudInitConfig?: GetCloudInitConfigArgs;
|
||||||
|
}[];
|
||||||
|
loadbalancers?: {
|
||||||
|
name: string;
|
||||||
|
services: {
|
||||||
|
name: string;
|
||||||
|
port: number;
|
||||||
|
targetPort: number;
|
||||||
|
proxyprotocol?: boolean;
|
||||||
|
healthCheck?: LoadBalancerServiceHealthCheck;
|
||||||
|
}[];
|
||||||
|
targets: Partial<hcloud.LoadBalancerTargetArgs>[];
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
class HetznerDeployment extends pulumi.ComponentResource {
|
||||||
|
public readonly deploymentConfig: HetznerDeploymentConfig;
|
||||||
|
public readonly servers: hcloud.Server[];
|
||||||
|
public readonly platformNetwork: hcloud.Network;
|
||||||
|
public readonly platformNetworkSubnetCloud: hcloud.NetworkSubnet;
|
||||||
|
public readonly platformNetworkSubnetDedicated?:
|
||||||
|
| hcloud.NetworkSubnet
|
||||||
|
| undefined;
|
||||||
|
public readonly platformNetworkSubnetLoadBalancers: hcloud.NetworkSubnet;
|
||||||
|
public readonly publicIps: hcloud.PrimaryIp[];
|
||||||
|
public readonly placementGroup: hcloud.PlacementGroup;
|
||||||
|
public readonly networkAttachments: hcloud.ServerNetwork[];
|
||||||
|
// public readonly loadBalancers: hcloud.LoadBalancer[];
|
||||||
|
public readonly loadBalancerInstances: {
|
||||||
|
loadBalancer: hcloud.LoadBalancer;
|
||||||
|
networkAttachment: hcloud.LoadBalancerNetwork;
|
||||||
|
services: hcloud.LoadBalancerService[];
|
||||||
|
targets: hcloud.LoadBalancerTarget[];
|
||||||
|
}[];
|
||||||
|
public readonly serverInfo: {
|
||||||
|
name: pulumi.Output<string>;
|
||||||
|
publicIp: pulumi.Output<string>;
|
||||||
|
privateIp: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
name: string,
|
||||||
|
args: HetznerDeploymentArgs,
|
||||||
|
opts?: pulumi.ComponentResourceOptions,
|
||||||
|
) {
|
||||||
|
super("custom:resource:HetznerDeployment", name, {}, opts);
|
||||||
|
this.deploymentConfig = args;
|
||||||
|
|
||||||
|
// Hetzner Provider
|
||||||
|
const hcloudProvider = new hcloud.Provider(
|
||||||
|
`${this.deploymentConfig.platformName}-hcloud-provider`,
|
||||||
|
{
|
||||||
|
token: args.hcloudToken,
|
||||||
|
},
|
||||||
|
{ parent: this },
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Hetzner API has 2 functions to get datacenters:
|
||||||
|
function getDatacenters(args: GetDatacentersArgs, opts?: InvokeOptions): Promise<GetDatacentersResult>
|
||||||
|
function getDatacentersOutput(args: GetDatacentersOutputArgs, opts?: InvokeOptions): Output<GetDatacentersResult>
|
||||||
|
|
||||||
|
We need to check wether this.deploymentConfig.datacenter is a valid datacenter.
|
||||||
|
*/
|
||||||
|
hcloud
|
||||||
|
.getDatacentersOutput({ provider: hcloudProvider, parent: this })
|
||||||
|
.apply((datacenters) => {
|
||||||
|
const isValid = datacenters.datacenters.some(
|
||||||
|
(dc) => dc.name === this.deploymentConfig.datacenter,
|
||||||
|
);
|
||||||
|
if (!isValid) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid datacenter: ${this.deploymentConfig.datacenter}, valid datacenters are: ${datacenters.datacenters
|
||||||
|
.map((dc) => dc.name)
|
||||||
|
.join(", ")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sshPublicKey = fs.readFileSync(args.sshKeyPath, "utf-8");
|
||||||
|
const sshKey = new hcloud.SshKey(
|
||||||
|
`${this.deploymentConfig.platformName}-ssh-key`,
|
||||||
|
{
|
||||||
|
name: `${this.deploymentConfig.platformName}-ssh-key`,
|
||||||
|
publicKey: sshPublicKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parent: this,
|
||||||
|
provider: hcloudProvider,
|
||||||
|
dependsOn: [hcloudProvider],
|
||||||
|
deleteBeforeReplace: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
this.placementGroup = new hcloud.PlacementGroup(
|
||||||
|
`${this.deploymentConfig.platformName}-hcloud-placement-group`,
|
||||||
|
{
|
||||||
|
name: `${this.deploymentConfig.platformName}-hcloud-placement-group`,
|
||||||
|
type: "spread",
|
||||||
|
labels: {
|
||||||
|
platform: this.deploymentConfig.platformName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ parent: this, provider: hcloudProvider, dependsOn: [hcloudProvider] },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Allocate public IPs
|
||||||
|
this.publicIps = args.servers.map((server, serverIndex) => {
|
||||||
|
return new hcloud.PrimaryIp(
|
||||||
|
`${this.deploymentConfig.platformName}-hcloud-primary-ip-${(serverIndex + 1).toString().padStart(2, "0")}`,
|
||||||
|
{
|
||||||
|
type: "ipv4",
|
||||||
|
assigneeType: "server",
|
||||||
|
autoDelete: false,
|
||||||
|
datacenter: this.deploymentConfig.datacenter,
|
||||||
|
labels: {
|
||||||
|
platform: this.deploymentConfig.platformName,
|
||||||
|
role: "controlplane",
|
||||||
|
node: server.hostname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parent: this,
|
||||||
|
provider: hcloudProvider,
|
||||||
|
dependsOn: [hcloudProvider],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a network
|
||||||
|
this.platformNetwork = new hcloud.Network(
|
||||||
|
`${this.deploymentConfig.platformName}-hcloud-network`,
|
||||||
|
{
|
||||||
|
name: `${this.deploymentConfig.platformName}-hcloud-network`,
|
||||||
|
ipRange: "10.254.0.0/16",
|
||||||
|
labels: {
|
||||||
|
platform: this.deploymentConfig.platformName,
|
||||||
|
},
|
||||||
|
deleteProtection: false,
|
||||||
|
},
|
||||||
|
{ parent: this, provider: hcloudProvider, dependsOn: [hcloudProvider] },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a network subnet for cloud vms
|
||||||
|
this.platformNetworkSubnetCloud = new hcloud.NetworkSubnet(
|
||||||
|
`${this.deploymentConfig.platformName}-hcloud-network-subnet-cloud`,
|
||||||
|
{
|
||||||
|
type: "cloud",
|
||||||
|
networkId: this.platformNetwork.id.apply((id) => Number(id)),
|
||||||
|
networkZone: "eu-central",
|
||||||
|
ipRange: "10.254.1.0/24",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parent: this,
|
||||||
|
provider: hcloudProvider,
|
||||||
|
dependsOn: [hcloudProvider, this.platformNetwork],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (args.vswitchId) {
|
||||||
|
// Create a network subnet for dedicated servers
|
||||||
|
this.platformNetworkSubnetDedicated = new hcloud.NetworkSubnet(
|
||||||
|
`${this.deploymentConfig.platformName}-hcloud-network-subnet-dedicated`,
|
||||||
|
{
|
||||||
|
type: "vswitch",
|
||||||
|
networkId: this.platformNetwork.id.apply((id) => Number(id)),
|
||||||
|
networkZone: "eu-central",
|
||||||
|
ipRange: "10.254.2.0/24",
|
||||||
|
vswitchId: args.vswitchId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parent: this,
|
||||||
|
provider: hcloudProvider,
|
||||||
|
dependsOn: [hcloudProvider, this.platformNetwork],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.servers = args.servers.map((server, serverIndex) => {
|
||||||
|
return new hcloud.Server(
|
||||||
|
`${this.deploymentConfig.platformName}-hcloud-server-${(serverIndex + 1).toString().padStart(2, "0")}-${server.name}`,
|
||||||
|
{
|
||||||
|
name: server.name,
|
||||||
|
serverType: server.type,
|
||||||
|
deleteProtection: false,
|
||||||
|
image: server.image || "debian-12",
|
||||||
|
datacenter: this.deploymentConfig.datacenter,
|
||||||
|
placementGroupId: this.placementGroup.id.apply((id) => Number(id)),
|
||||||
|
sshKeys: [sshKey.id],
|
||||||
|
publicNets: [
|
||||||
|
{
|
||||||
|
ipv4Enabled: true,
|
||||||
|
ipv6Enabled: false,
|
||||||
|
ipv4: this.publicIps[serverIndex].id.apply((id) => Number(id)),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
labels: {
|
||||||
|
platform: this.deploymentConfig.platformName,
|
||||||
|
...server.labels,
|
||||||
|
},
|
||||||
|
userData: getCloudInitConfig({
|
||||||
|
...(server.cloudInitConfig || {}),
|
||||||
|
sshAuthorizedKeys: [sshPublicKey],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parent: this,
|
||||||
|
provider: hcloudProvider,
|
||||||
|
ignoreChanges: ["userData"],
|
||||||
|
dependsOn: [
|
||||||
|
this.placementGroup,
|
||||||
|
this.platformNetwork,
|
||||||
|
this.platformNetworkSubnetCloud,
|
||||||
|
this.publicIps[serverIndex],
|
||||||
|
sshKey,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.networkAttachments = this.servers.map((server, serverIndex) => {
|
||||||
|
return new hcloud.ServerNetwork(
|
||||||
|
`${this.deploymentConfig.platformName}-hcloud-server-network-${(serverIndex + 1).toString().padStart(2, "0")}-${args.servers[serverIndex].name}`,
|
||||||
|
{
|
||||||
|
serverId: server.id.apply((id) => Number(id)),
|
||||||
|
networkId: this.platformNetworkSubnetCloud.networkId.apply((id) =>
|
||||||
|
Number(id),
|
||||||
|
),
|
||||||
|
ip: `10.254.1.${serverIndex + 1 + 10}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parent: this,
|
||||||
|
provider: hcloudProvider,
|
||||||
|
dependsOn: [
|
||||||
|
this.platformNetwork,
|
||||||
|
this.publicIps[serverIndex],
|
||||||
|
this.servers[serverIndex],
|
||||||
|
],
|
||||||
|
deletedWith: this.servers[serverIndex],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
// Create a network subnet for dedicated servers
|
||||||
|
this.platformNetworkSubnetLoadBalancers = new hcloud.NetworkSubnet(
|
||||||
|
`${this.deploymentConfig.platformName}-hcloud-network-subnet-loadbalancers`,
|
||||||
|
{
|
||||||
|
type: "cloud",
|
||||||
|
networkId: this.platformNetwork.id.apply((id) => Number(id)),
|
||||||
|
networkZone: "eu-central",
|
||||||
|
ipRange: "10.254.3.0/24",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parent: this,
|
||||||
|
provider: hcloudProvider,
|
||||||
|
dependsOn: [hcloudProvider, this.platformNetwork],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
this.loadBalancerInstances =
|
||||||
|
args.loadbalancers?.map((loadBalancerArg, loadBalancerIndex) => {
|
||||||
|
const loadBalancer = new hcloud.LoadBalancer(
|
||||||
|
`${this.deploymentConfig.platformName}-hcloud-loadbalancer-${loadBalancerIndex}-${loadBalancerArg.name}`,
|
||||||
|
{
|
||||||
|
name: loadBalancerArg.name,
|
||||||
|
loadBalancerType: "lb11",
|
||||||
|
networkZone: "eu-central",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parent: this,
|
||||||
|
provider: hcloudProvider,
|
||||||
|
dependsOn: [
|
||||||
|
this.platformNetwork,
|
||||||
|
this.platformNetworkSubnetLoadBalancers,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const networkAttachment = new hcloud.LoadBalancerNetwork(
|
||||||
|
`${this.deploymentConfig.platformName}-hcloud-loadbalancer-network-${loadBalancerIndex}-${loadBalancerArg.name}`,
|
||||||
|
{
|
||||||
|
subnetId: this.platformNetworkSubnetLoadBalancers.id.apply(
|
||||||
|
(id) => id,
|
||||||
|
),
|
||||||
|
// networkId: this.platformNetwork.id.apply((id) => Number(id)),
|
||||||
|
loadBalancerId: loadBalancer.id.apply((id) => Number(id)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parent: this,
|
||||||
|
provider: hcloudProvider,
|
||||||
|
dependsOn: [
|
||||||
|
loadBalancer,
|
||||||
|
this.platformNetwork,
|
||||||
|
this.platformNetworkSubnetLoadBalancers,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const loadBalancerTargets = loadBalancerArg.targets.map(
|
||||||
|
(target, targetIndex) => {
|
||||||
|
return new hcloud.LoadBalancerTarget(
|
||||||
|
`${this.deploymentConfig.platformName}-hcloud-loadbalancer-target-${loadBalancerIndex}-${loadBalancerArg.name}-${targetIndex}`,
|
||||||
|
{
|
||||||
|
type: target.type || "ip",
|
||||||
|
loadBalancerId: loadBalancer.id.apply((id) => Number(id)),
|
||||||
|
...target,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parent: this,
|
||||||
|
provider: hcloudProvider,
|
||||||
|
dependsOn: [loadBalancer],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const loadBalancerServices = loadBalancerArg.services.map(
|
||||||
|
(service, serviceIndex) => {
|
||||||
|
return new hcloud.LoadBalancerService(
|
||||||
|
`${this.deploymentConfig.platformName}-hcloud-loadbalancer-service-${loadBalancerIndex}-${loadBalancerArg.name}-${serviceIndex}`,
|
||||||
|
{
|
||||||
|
loadBalancerId: loadBalancer.id.apply((id) => id),
|
||||||
|
protocol: "tcp",
|
||||||
|
listenPort: service.port,
|
||||||
|
destinationPort: service.targetPort,
|
||||||
|
proxyprotocol: service.proxyprotocol || false,
|
||||||
|
healthCheck: service.healthCheck || {
|
||||||
|
port: service.targetPort,
|
||||||
|
protocol: "tcp",
|
||||||
|
interval: 15,
|
||||||
|
timeout: 10,
|
||||||
|
retries: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parent: this,
|
||||||
|
provider: hcloudProvider,
|
||||||
|
dependsOn: [loadBalancer],
|
||||||
|
deleteBeforeReplace: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
loadBalancer: loadBalancer,
|
||||||
|
networkAttachment: networkAttachment,
|
||||||
|
targets: loadBalancerTargets,
|
||||||
|
services: loadBalancerServices,
|
||||||
|
};
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
this.serverInfo = this.servers.map((server, serverIndex) => {
|
||||||
|
return {
|
||||||
|
name: server.name,
|
||||||
|
publicIp: this.publicIps[serverIndex].ipAddress,
|
||||||
|
privateIp: `10.254.1.${serverIndex + 1 + 10}`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.registerOutputs({
|
||||||
|
servers: this.servers,
|
||||||
|
platformNetwork: this.platformNetwork,
|
||||||
|
publicIps: this.publicIps,
|
||||||
|
serverInfo: this.serverInfo,
|
||||||
|
networkAttachments: this.networkAttachments,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { HetznerDeployment };
|
||||||
17
packages/pulumi-hetzner/package.json
Normal file
17
packages/pulumi-hetzner/package.json
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"name": "@olsitec/pulumi-hetzner",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"main": "index.ts",
|
||||||
|
"description": "Vendored Hetzner Cloud provisioning module (Stage-1 vendor; see VENDORED.md). Trimmed to the deps HetznerDeployment actually uses.",
|
||||||
|
"dependencies": {
|
||||||
|
"@pulumi/hcloud": "^1.21.1",
|
||||||
|
"@pulumi/pulumi": "^3.138.0",
|
||||||
|
"js-yaml": "^4.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
"@types/node": "^18",
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
27
packages/pulumi-hetzner/tsconfig.json
Normal file
27
packages/pulumi-hetzner/tsconfig.json
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"outDir": "bin",
|
||||||
|
"target": "es2020",
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"sourceMap": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"pretty": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"bin",
|
||||||
|
"build",
|
||||||
|
"out",
|
||||||
|
"_test_"
|
||||||
|
]
|
||||||
|
}
|
||||||
6
provision/Pulumi.yaml
Normal file
6
provision/Pulumi.yaml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
name: foundation-provision
|
||||||
|
description: Phase-0 — provision the throwaway foundation test VM on Hetzner Cloud (isolated stack).
|
||||||
|
runtime:
|
||||||
|
name: nodejs
|
||||||
|
options:
|
||||||
|
packagemanager: bun
|
||||||
98
provision/index.ts
Normal file
98
provision/index.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
// provision/index.ts — Phase 0: provision the throwaway foundation TEST VM.
|
||||||
|
//
|
||||||
|
// Isolated stack (platformName "foundation-test" → no name collision with the
|
||||||
|
// production olsicloud4-* resources). Uses the vendored @olsitec/pulumi-hetzner
|
||||||
|
// HetznerDeployment for one cx22, adds a firewall, and installs Docker via the
|
||||||
|
// module's cloud-init lateCommands.
|
||||||
|
//
|
||||||
|
// GOTCHA (VENDORED.md): the module's cloud-init moves SSH to PORT 222 and creates
|
||||||
|
// root + andiolsi users. The firewall + the Docker-over-SSH provider use :222.
|
||||||
|
//
|
||||||
|
// Token comes from ENV (export HCLOUD_TOKEN="$(pass olsicloud4/HCLOUD_TOKEN)"),
|
||||||
|
// never committed.
|
||||||
|
import * as pulumi from "@pulumi/pulumi";
|
||||||
|
import * as hcloud from "@pulumi/hcloud";
|
||||||
|
import { HetznerDeployment } from "@olsitec/pulumi-hetzner";
|
||||||
|
import * as os from "os";
|
||||||
|
|
||||||
|
const token = process.env.HCLOUD_TOKEN;
|
||||||
|
if (!token) {
|
||||||
|
throw new Error(
|
||||||
|
'HCLOUD_TOKEN env required: export HCLOUD_TOKEN="$(pass olsicloud4/HCLOUD_TOKEN)"',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const platformName = "foundation-test";
|
||||||
|
const datacenter = "nbg1-dc3"; // matches existing cx22 servers (known-good)
|
||||||
|
// Dedicated throwaway key: the operator's id_rsa pubkey is already registered in
|
||||||
|
// the project (olsicloud4-ssh-key) and Hetzner rejects duplicate key CONTENT, so
|
||||||
|
// the test VM uses its own ed25519 key. SSH/Docker-over-SSH use the matching
|
||||||
|
// private key (~/.ssh/foundation-test_ed25519) on port 222.
|
||||||
|
const sshKeyPath =
|
||||||
|
process.env.FOUNDATION_TEST_SSH_PUBKEY ||
|
||||||
|
`${os.homedir()}/.ssh/foundation-test_ed25519.pub`;
|
||||||
|
|
||||||
|
// Install Docker CE on Debian 12 (runs in cloud-init runcmd, after base packages).
|
||||||
|
const dockerInstall = [
|
||||||
|
"install -m 0755 -d /etc/apt/keyrings",
|
||||||
|
"curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc",
|
||||||
|
"chmod a+r /etc/apt/keyrings/docker.asc",
|
||||||
|
'echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable" > /etc/apt/sources.list.d/docker.list',
|
||||||
|
"apt-get update",
|
||||||
|
"apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin",
|
||||||
|
"systemctl enable --now docker",
|
||||||
|
"touch /root/.provision-done",
|
||||||
|
];
|
||||||
|
|
||||||
|
const dep = new HetznerDeployment(platformName, {
|
||||||
|
platformName,
|
||||||
|
hcloudToken: token,
|
||||||
|
sshKeyPath,
|
||||||
|
datacenter,
|
||||||
|
servers: [
|
||||||
|
{
|
||||||
|
name: "foundation-test",
|
||||||
|
hostname: "foundation-test",
|
||||||
|
type: "cx23", // current-gen Intel shared (cx22 is legacy/unavailable for new servers); 2c/4G
|
||||||
|
image: "debian-12",
|
||||||
|
labels: { purpose: "foundation-test", ephemeral: "true" },
|
||||||
|
cloudInitConfig: { lateCommands: dockerInstall },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Own provider for the firewall (the module's provider is internal/private).
|
||||||
|
const provider = new hcloud.Provider("foundation-test-hcloud", { token });
|
||||||
|
|
||||||
|
const firewall = new hcloud.Firewall(
|
||||||
|
"foundation-test-fw",
|
||||||
|
{
|
||||||
|
name: "foundation-test-fw",
|
||||||
|
rules: [
|
||||||
|
// SSH — cloud-init moves sshd to 222 (VENDORED.md gotcha)
|
||||||
|
{ direction: "in", protocol: "tcp", port: "222", sourceIps: ["0.0.0.0/0", "::/0"] },
|
||||||
|
// Caddy HTTP/HTTPS
|
||||||
|
{ direction: "in", protocol: "tcp", port: "80", sourceIps: ["0.0.0.0/0", "::/0"] },
|
||||||
|
{ direction: "in", protocol: "tcp", port: "443", sourceIps: ["0.0.0.0/0", "::/0"] },
|
||||||
|
// Forgejo git-over-SSH (CONTRACT_001 forgeSshPort)
|
||||||
|
{ direction: "in", protocol: "tcp", port: "2222", sourceIps: ["0.0.0.0/0", "::/0"] },
|
||||||
|
{ direction: "in", protocol: "icmp", sourceIps: ["0.0.0.0/0", "::/0"] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ provider },
|
||||||
|
);
|
||||||
|
|
||||||
|
new hcloud.FirewallAttachment(
|
||||||
|
"foundation-test-fw-attach",
|
||||||
|
{
|
||||||
|
firewallId: firewall.id.apply((id) => Number(id)),
|
||||||
|
serverIds: [dep.servers[0].id.apply((id) => Number(id))],
|
||||||
|
},
|
||||||
|
{ provider },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Outputs consumed by the foundation test stack + the operator.
|
||||||
|
export const serverName = dep.serverInfo[0].name;
|
||||||
|
export const publicIp = dep.serverInfo[0].publicIp;
|
||||||
|
export const sshCommand = pulumi.interpolate`ssh -p 222 root@${dep.serverInfo[0].publicIp}`;
|
||||||
|
export const dockerHost = pulumi.interpolate`ssh://root@${dep.serverInfo[0].publicIp}:222`;
|
||||||
12
provision/package.json
Normal file
12
provision/package.json
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"name": "@olsitec/foundation-provision",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"main": "index.ts",
|
||||||
|
"dependencies": {
|
||||||
|
"@olsitec/pulumi-hetzner": "workspace:*",
|
||||||
|
"@pulumi/hcloud": "^1.21.1",
|
||||||
|
"@pulumi/pulumi": "^3.138.0"
|
||||||
|
},
|
||||||
|
"devDependencies": { "@types/node": "^18", "typescript": "^5.0.0" }
|
||||||
|
}
|
||||||
8
provision/tsconfig.json
Normal file
8
provision/tsconfig.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true, "outDir": "bin", "target": "es2020", "module": "commonjs",
|
||||||
|
"moduleResolution": "node", "sourceMap": true, "experimentalDecorators": true,
|
||||||
|
"esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"files": ["index.ts"]
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue