mirror of
https://github.com/KevinMidboe/schleppe-pulumi.git
synced 2026-01-09 19:05:51 +00:00
Compare commits
5 Commits
1fd7cfe01d
...
10284ed956
| Author | SHA1 | Date | |
|---|---|---|---|
| 10284ed956 | |||
| 1f4eaae1e7 | |||
| 1bb3e7e21a | |||
| e65aead5f0 | |||
| 2bb876904f |
96
README.md
96
README.md
@@ -2,20 +2,100 @@
|
|||||||
|
|
||||||
Defines code which describes a HA & cached scalable way of serving web applications.
|
Defines code which describes a HA & cached scalable way of serving web applications.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
+-----------------------------------------------------------+
|
||||||
|
| REGION: EU |
|
||||||
|
| |
|
||||||
|
| +-------------- Floating IP ---------+ |
|
||||||
|
| | | |
|
||||||
|
| +----+---------+ +----+---------+ |
|
||||||
|
| | HAProxy #1 | | HAProxy #2 | |
|
||||||
|
| +----+---------+ +----+---------+ |
|
||||||
|
| \__________ active / standby _______/ |
|
||||||
|
| | |
|
||||||
|
| v |
|
||||||
|
| +------+--------+ |
|
||||||
|
| | haproxy (a) | |
|
||||||
|
| +----+----+--+--+ |
|
||||||
|
| | | A |
|
||||||
|
| direct | | | via cache |
|
||||||
|
| | v | |
|
||||||
|
| | +-+--+---------+ |
|
||||||
|
| | | varnish (n) | |
|
||||||
|
| | +------+-------+ |
|
||||||
|
| | | HIT / MISS |
|
||||||
|
| | | |
|
||||||
|
| +---------+ |
|
||||||
|
| | |
|
||||||
|
| v |
|
||||||
|
| +---------+-------+ |
|
||||||
|
| | web server (n) | |
|
||||||
|
| +-----------------+ |
|
||||||
|
| |
|
||||||
|
+-----------------------------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
Where varnish & web server is 2-n number of instances. Currently two regions, EU & US.
|
||||||
|
|
||||||
## infrastructure
|
## infrastructure
|
||||||
|
|
||||||
Configured cloud resources in hezner with Pulumi.
|
Configured cloud resources in hezner with Pulumi.
|
||||||
|
|
||||||
Hetzner has two regions:
|
```bash
|
||||||
- us
|
# first time, init pulumi stack (name optional)
|
||||||
- eu
|
pulumi stack init kevinmidboe/hetzner
|
||||||
|
|
||||||
Each region has:
|
# required configuration values
|
||||||
- haproxy x2
|
pulumi config set sshPublicKey "$(cat ~/.ssh/id_ed25519.pub)"
|
||||||
- varnish x2
|
pulumi config set --secret hcloud:token $HETZNER_API_KEY
|
||||||
- webservers
|
|
||||||
|
# up infrastructure
|
||||||
|
pulumi up
|
||||||
|
|
||||||
|
# (optional w/ adding private IP)
|
||||||
|
# private ips struggle, need to run again to assign correctly
|
||||||
|
pulumi up
|
||||||
|
```
|
||||||
|
|
||||||
## provision
|
## provision
|
||||||
|
|
||||||
Ansible is used to provision software and environments for different software needed.
|
Ansible is used to provision software and environments for software needed and services.
|
||||||
|
|
||||||
|
get ansible configuration values from pulumi output:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# generate inventory (manualy update inventory file)
|
||||||
|
./scripts/generate-inventory.sh | pbcopy
|
||||||
|
|
||||||
|
# following updates config files in place
|
||||||
|
./scripts/update-config_certbot-domains.sh
|
||||||
|
./scripts/update-config_webserver-ips.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
run playbooks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# install, configure & start haproxy
|
||||||
|
ansible-playbook plays/haproxy.yml -i hetzner.ini -l haproxy
|
||||||
|
|
||||||
|
# install, configure & start varnish
|
||||||
|
ansible-playbook plays/varnish.yml -i hetzner.ini -l varnish
|
||||||
|
|
||||||
|
# install web resources & dependencies, pull & starts docker containers
|
||||||
|
ansible-playbook plays/docker.yml -i hetzner.ini -l web
|
||||||
|
ansible-playbook plays/web.yml -i hetzner.ini -l web
|
||||||
|
```
|
||||||
|
|
||||||
|
# Manual steps
|
||||||
|
|
||||||
|
- [x] floating ip DNS registration
|
||||||
|
- [x] extract variables from pulumi stack outputs
|
||||||
|
- [ ] add all cloudflare api keys
|
||||||
|
- `mkdir /root/.ssh/certbot/cloudflare_k9e-no.ini`
|
||||||
|
- [ ] generate certs for appropriate domains
|
||||||
|
- `certbot certonly --agree-tos --dns-cloudflare --dns-cloudflare-credentials /root/.secrets/certbot/cloudflare_k9e-no.ini -d k9e.no`
|
||||||
|
- [ ] combine generated certs into a cert for traefik
|
||||||
|
- `cat /etc/letsencrypt/live/k9e.no/fullchain.pem /etc/letsencrypt/live/k9e.no/privkey.pem > /etc/haproxy/certs/ssl-k9e.no.pem`
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
# CI specific vars
|
|
||||||
|
|
||||||
users:
|
|
||||||
- root
|
|
||||||
ssh_keys_users: ['drone']
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
# Consul server specific
|
|
||||||
consul_is_server: true
|
|
||||||
consul_is_ui: true
|
|
||||||
consul_bootstrap_expect: 1
|
|
||||||
@@ -6,12 +6,3 @@ dns_nameservers:
|
|||||||
- "2606:4700:4700::1001"
|
- "2606:4700:4700::1001"
|
||||||
|
|
||||||
default_user: "kevin"
|
default_user: "kevin"
|
||||||
|
|
||||||
# Consul cluster
|
|
||||||
consul_datacenter: "schleppe"
|
|
||||||
consul_servers:
|
|
||||||
- "10.0.0.140"
|
|
||||||
- "10.0.0.141"
|
|
||||||
- "10.0.0.142"
|
|
||||||
consul_install_dnsmasq: false
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
# python path
|
|
||||||
ansible_python_interpreter: /usr/local/bin/python3
|
|
||||||
|
|
||||||
users:
|
|
||||||
- kevin
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
apt_packages:
|
|
||||||
- git
|
|
||||||
- build-essential
|
|
||||||
- openjdk-21-jdk
|
|
||||||
minecraft_version: 1.20.6
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
---
|
|
||||||
proxmox_install_qemu_guest_agent: true
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
---
|
|
||||||
ssh_keys_users: ['kevin', 'kasper']
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Check if vault is reachable for dynamic config
|
|
||||||
hosts: all
|
|
||||||
connection: local
|
|
||||||
gather_facts: false
|
|
||||||
pre_tasks:
|
|
||||||
- name: Check for vault env variables
|
|
||||||
set_fact:
|
|
||||||
has_vault: "{{ lookup('env', 'VAULT_ADDR') and lookup('env', 'VAULT_TOKEN') and lookup('env', 'HAS_VAULT') != 'FALSE' }}"
|
|
||||||
roles:
|
|
||||||
- { role: roles/vault-config, when: has_vault }
|
|
||||||
|
|
||||||
- name: Install all bind9 service and transfer zone files
|
|
||||||
hosts: all
|
|
||||||
roles:
|
|
||||||
- role: roles/bind9
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Consul
|
|
||||||
hosts: all
|
|
||||||
roles:
|
|
||||||
- role: roles/consul
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Provision git server with gitea
|
|
||||||
hosts: all
|
|
||||||
roles:
|
|
||||||
- role: roles/gitea
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Install and setup immich backup service
|
|
||||||
hosts: all
|
|
||||||
roles:
|
|
||||||
# - role: roles/docker
|
|
||||||
- role: roles/immich
|
|
||||||
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Setup minecraft requirements w/ latest server jar
|
|
||||||
hosts: all
|
|
||||||
roles:
|
|
||||||
- role: roles/apt
|
|
||||||
- role: roles/minecraft
|
|
||||||
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Check if vault is reachable for dynamic config
|
|
||||||
hosts: all
|
|
||||||
connection: local
|
|
||||||
gather_facts: false
|
|
||||||
pre_tasks:
|
|
||||||
- name: Check for vault env variables
|
|
||||||
set_fact:
|
|
||||||
has_vault: "{{ lookup('env', 'VAULT_ADDR') and lookup('env', 'VAULT_TOKEN') }}"
|
|
||||||
TELEGRAF_TOKEN: "{{ lookup('env', 'TELEGRAF_TOKEN') }}"
|
|
||||||
roles:
|
|
||||||
- { role: roles/vault-config, when: has_vault }
|
|
||||||
|
|
||||||
- name: Basic setup for proxmox vm clients
|
|
||||||
hosts: proxmox_nodes
|
|
||||||
roles:
|
|
||||||
# - role: roles/prox-telegraf-metrics
|
|
||||||
- role: roles/prox-templates
|
|
||||||
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Install & configure syncthing
|
|
||||||
hosts: all
|
|
||||||
roles:
|
|
||||||
- role: roles/syncthing
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Install traefik binary & config
|
|
||||||
hosts: all
|
|
||||||
roles:
|
|
||||||
- role: roles/traefik
|
|
||||||
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Install all required packages, built and start service for vault
|
|
||||||
hosts: all
|
|
||||||
roles:
|
|
||||||
- role: roles/vault
|
|
||||||
- role: roles/firewall
|
|
||||||
enable_vault_ufw_port: true
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Install all required packages, built and start service for vinlottis
|
|
||||||
hosts: all
|
|
||||||
roles:
|
|
||||||
- role: roles/vinlottis
|
|
||||||
6
ansible/plays/web.yml
Normal file
6
ansible/plays/web.yml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
- name: copies docker-compose files to all web hosts
|
||||||
|
hosts: web
|
||||||
|
roles:
|
||||||
|
- role: roles/web
|
||||||
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
- name: Setup wireguard
|
|
||||||
hosts: all
|
|
||||||
roles:
|
|
||||||
- role: roles/docker
|
|
||||||
- role: roles/firewall
|
|
||||||
- role: roles/wireguard
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Check if vault is reachable for dynamic config
|
|
||||||
hosts: all
|
|
||||||
connection: local
|
|
||||||
gather_facts: false
|
|
||||||
pre_tasks:
|
|
||||||
- name: Check for vault env variables
|
|
||||||
set_fact:
|
|
||||||
has_vault: "{{ lookup('env', 'VAULT_ADDR') and lookup('env', 'VAULT_TOKEN') }}"
|
|
||||||
XWIKI_DB_USER: "{{ lookup('env', 'XWIKI_DB_USER') }}"
|
|
||||||
XWIKI_DB_PASSWORD: "{{ lookup('env', 'XWIKI_DB_PASSWORD') }}"
|
|
||||||
XWIKI_DB_ROOT_PASSWORD: "{{ lookup('env', 'XWIKI_DB_ROOT_PASSWORD') }}"
|
|
||||||
roles:
|
|
||||||
- { role: roles/vault-config, when: has_vault }
|
|
||||||
|
|
||||||
- name: Setup xwiki working directory and move docker-compose file
|
|
||||||
hosts: all
|
|
||||||
roles:
|
|
||||||
- role: roles/docker
|
|
||||||
- role: roles/firewall
|
|
||||||
- role: roles/xwiki
|
|
||||||
25
ansible/roles/web/tasks/main.yml
Normal file
25
ansible/roles/web/tasks/main.yml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
- name: Ensure remote docker directory exists
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ remote_compose_dir }}"
|
||||||
|
state: directory
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Copy local docker-compose directory to remote
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: "{{ local_compose_dir }}/"
|
||||||
|
dest: "{{ remote_compose_dir }}/"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: '0644'
|
||||||
|
|
||||||
|
- name: Ensure compose-all.sh is executable
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ remote_compose_dir }}/compose-all.sh"
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Run compose-all.sh up
|
||||||
|
ansible.builtin.command: bash compose-all.sh up
|
||||||
|
args:
|
||||||
|
chdir: "{{ remote_compose_dir }}"
|
||||||
3
ansible/roles/web/vars/main.yml
Normal file
3
ansible/roles/web/vars/main.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
local_compose_dir: "{{ playbook_dir }}/../../docker-compose"
|
||||||
|
remote_compose_dir: /opt/docker
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ services:
|
|||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
|
|
||||||
# Router definition
|
# Router definition
|
||||||
- "traefik.http.routers.whoami.rule=Host(`whoami.example.com`)"
|
- "traefik.http.routers.whoami.rule=Host(`whoami.schleppe.cloud`)"
|
||||||
- "traefik.http.routers.whoami.entrypoints=web"
|
- "traefik.http.routers.whoami.entrypoints=web"
|
||||||
|
|
||||||
# Service definition
|
# Service definition
|
||||||
|
|||||||
@@ -2,9 +2,19 @@ import {
|
|||||||
subNetwork,
|
subNetwork,
|
||||||
regionalNetwork,
|
regionalNetwork,
|
||||||
allowHttp,
|
allowHttp,
|
||||||
allowSSH,
|
allowSSHToCurrentIP,
|
||||||
|
floatingIP,
|
||||||
|
attach,
|
||||||
} from "./resources/network";
|
} from "./resources/network";
|
||||||
import { server } from "./resources/compute";
|
import { server } from "./resources/compute";
|
||||||
|
import { dns } from "./resources/cloudflare";
|
||||||
|
import {
|
||||||
|
summarizeServer,
|
||||||
|
summarizeNetwork,
|
||||||
|
summarizeSubNetwork,
|
||||||
|
summarizeFloatingIp,
|
||||||
|
summarizeFirewall,
|
||||||
|
} from "./resources/utils";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
VmSize,
|
VmSize,
|
||||||
@@ -15,15 +25,19 @@ import {
|
|||||||
} from "./resources/types";
|
} from "./resources/types";
|
||||||
|
|
||||||
// regional vnet
|
// regional vnet
|
||||||
const eu = regionalNetwork("ha", "10.24.0.0/18", NetworkRegion.eu);
|
const eu = regionalNetwork("ha-net-eu", "10.24.0.0/18", NetworkRegion.eu);
|
||||||
const usEast = regionalNetwork("ha", "10.25.0.0/18", NetworkRegion.usEast);
|
const usEast = regionalNetwork(
|
||||||
|
"ha-net-us",
|
||||||
|
"10.25.0.0/18",
|
||||||
|
NetworkRegion.usEast,
|
||||||
|
);
|
||||||
|
|
||||||
// subnets for reginal vnets
|
// subnets for reginal vnets
|
||||||
const network = {
|
const network = {
|
||||||
eu: {
|
eu: {
|
||||||
lb: subNetwork(eu, NetworkRole.lb, NetworkRegion.eu, "10.24.1.0/24"),
|
lb: subNetwork(eu, NetworkRole.lb, NetworkRegion.eu, "10.24.1.0/26"),
|
||||||
cache: subNetwork(eu, NetworkRole.cache, NetworkRegion.eu, "10.24.2.0/24"),
|
cache: subNetwork(eu, NetworkRole.cache, NetworkRegion.eu, "10.24.2.0/26"),
|
||||||
web: subNetwork(eu, NetworkRole.web, NetworkRegion.eu, "10.24.3.0/24"),
|
web: subNetwork(eu, NetworkRole.web, NetworkRegion.eu, "10.24.3.0/26"),
|
||||||
// db: subNetwork(eu, NetworkRole.db, "10.24.4.0/24")
|
// db: subNetwork(eu, NetworkRole.db, "10.24.4.0/24")
|
||||||
},
|
},
|
||||||
usEast: {
|
usEast: {
|
||||||
@@ -31,26 +45,26 @@ const network = {
|
|||||||
usEast,
|
usEast,
|
||||||
NetworkRole.lb,
|
NetworkRole.lb,
|
||||||
NetworkRegion.usEast,
|
NetworkRegion.usEast,
|
||||||
"10.25.1.0/24",
|
"10.25.1.0/26",
|
||||||
),
|
),
|
||||||
cache: subNetwork(
|
cache: subNetwork(
|
||||||
usEast,
|
usEast,
|
||||||
NetworkRole.cache,
|
NetworkRole.cache,
|
||||||
NetworkRegion.usEast,
|
NetworkRegion.usEast,
|
||||||
"10.25.2.0/24",
|
"10.25.2.0/26",
|
||||||
),
|
),
|
||||||
web: subNetwork(
|
web: subNetwork(
|
||||||
usEast,
|
usEast,
|
||||||
NetworkRole.web,
|
NetworkRole.web,
|
||||||
NetworkRegion.usEast,
|
NetworkRegion.usEast,
|
||||||
"10.25.3.0/24",
|
"10.25.3.0/26",
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// variable un-maps
|
// variable un-maps
|
||||||
const hel1 = ServerLocations.helsinki;
|
const nbg = ServerLocations.nuremberg;
|
||||||
const hil = ServerLocations.hillsboro;
|
const ash = ServerLocations.ashburn;
|
||||||
const [EU_LB, US_LB, EU_CACHE, US_CACHE, EU_WEB, US_WEB] = [
|
const [EU_LB, US_LB, EU_CACHE, US_CACHE, EU_WEB, US_WEB] = [
|
||||||
network.eu.lb,
|
network.eu.lb,
|
||||||
network.usEast.lb,
|
network.usEast.lb,
|
||||||
@@ -61,32 +75,67 @@ const [EU_LB, US_LB, EU_CACHE, US_CACHE, EU_WEB, US_WEB] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
// compute - server resources
|
// compute - server resources
|
||||||
const haEU1 = server("haproxy-1", VmSize.small, OS.debian, hel1, EU_LB);
|
const haEU1 = server("haproxy-1", VmSize.cx23, OS.debian, nbg, EU_LB, true);
|
||||||
const haEU2 = server("haproxy-2", VmSize.small, OS.debian, hel1, EU_LB);
|
const haEU2 = server("haproxy-2", VmSize.cx23, OS.debian, nbg, EU_LB, true);
|
||||||
const haUS1 = server("haproxy-1", VmSize.small, OS.debian, hil, US_LB);
|
const haUS1 = server("haproxy-1", VmSize.cpx11, OS.debian, ash, US_LB, true);
|
||||||
// const haUS2 = server("haproxy-2", VmSize.small, OS.debian, hil, US_LB);
|
const haUS2 = server("haproxy-2", VmSize.cpx11, OS.debian, ash, US_LB, true);
|
||||||
|
|
||||||
const cacheEU1 = server("varnish-1", VmSize.small, OS.debian, hel1, EU_CACHE);
|
const cacheEU1 = server("varnish-1", VmSize.cx23, OS.debian, nbg, EU_CACHE);
|
||||||
const cacheEU2 = server("varnish-2", VmSize.small, OS.debian, hil, EU_CACHE);
|
const cacheEU2 = server("varnish-2", VmSize.cx23, OS.debian, nbg, EU_CACHE);
|
||||||
// const cacheUS1 = server("varnish-1", VmSize.small, OS.debian, hil, US_CACHE);
|
const cacheUS1 = server("varnish-1", VmSize.cpx11, OS.debian, ash, US_CACHE);
|
||||||
// const cacheUS2 = server("varnish-2", VmSize.small, OS.debian, hil, US_CACHE);
|
const cacheUS2 = server("varnish-2", VmSize.cpx11, OS.debian, ash, US_CACHE);
|
||||||
|
|
||||||
const webEU1 = server("web-1", VmSize.small, OS.debian, hel1, EU_WEB);
|
const webEU1 = server("web-1", VmSize.cx23, OS.debian, nbg, EU_WEB);
|
||||||
// const webEU2 = server("web-2", VmSize.small, OS.debian, hel1, EU_WEB);
|
const webEU2 = server("web-2", VmSize.cx23, OS.debian, nbg, EU_WEB);
|
||||||
// const webUS1 = server("web-1", VmSize.small, OS.debian, hil, US_WEB);
|
const webUS1 = server("web-1", VmSize.cpx11, OS.debian, ash, US_WEB);
|
||||||
|
|
||||||
// firewall & exports
|
// floating IPs
|
||||||
export const firewalls = [allowHttp, allowSSH];
|
const euFloatingIP = floatingIP("schleppe-ha-nbg", haEU1);
|
||||||
|
const usFloatingIP = floatingIP("schleppe-ha-va", haUS1);
|
||||||
|
const floatingIPs = [euFloatingIP, usFloatingIP];
|
||||||
|
const domains = ["k9e.no", "planetposen.no", "whoami.schleppe.cloud"];
|
||||||
|
|
||||||
// exports contd.
|
// Update Cloudflare DNS
|
||||||
export const servers = [haEU1, haEU2, haUS1, cacheEU1, cacheEU2, webEU1];
|
domains.forEach((domain) => {
|
||||||
|
dns(domain, euFloatingIP, "eu-fip");
|
||||||
|
dns(domain, usFloatingIP, "us-fip");
|
||||||
|
});
|
||||||
|
|
||||||
export const networks = [
|
// firewall
|
||||||
eu,
|
const allowSSH = allowSSHToCurrentIP();
|
||||||
usEast,
|
const firewalls = [allowHttp, allowSSH];
|
||||||
|
// DISABLED
|
||||||
|
attach("ssh-fa", allowSSH, [haEU1, haEU2, haUS1, haUS2]);
|
||||||
|
|
||||||
|
// exports
|
||||||
|
const servers = [
|
||||||
|
haEU1,
|
||||||
|
haEU2,
|
||||||
|
haUS1,
|
||||||
|
haUS2,
|
||||||
|
cacheEU1,
|
||||||
|
cacheEU2,
|
||||||
|
cacheUS1,
|
||||||
|
cacheUS2,
|
||||||
|
webEU1,
|
||||||
|
webEU2,
|
||||||
|
webUS1,
|
||||||
|
];
|
||||||
|
|
||||||
|
const networks = [eu, usEast];
|
||||||
|
const subNetworks = [
|
||||||
network.eu.lb,
|
network.eu.lb,
|
||||||
network.eu.cache,
|
network.eu.cache,
|
||||||
network.eu.web,
|
network.eu.web,
|
||||||
network.usEast.lb,
|
network.usEast.lb,
|
||||||
network.usEast.web,
|
network.usEast.web,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const inventory = {
|
||||||
|
vms: servers.map(summarizeServer),
|
||||||
|
networks: networks.map(summarizeNetwork),
|
||||||
|
subnetworks: subNetworks.map(summarizeSubNetwork),
|
||||||
|
firewalls: firewalls.map(summarizeFirewall),
|
||||||
|
floatingIps: floatingIPs.map(summarizeFloatingIp),
|
||||||
|
domains,
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@pulumi/cloudflare": "^6.12.0",
|
||||||
"@pulumi/hcloud": "^1.29.0",
|
"@pulumi/hcloud": "^1.29.0",
|
||||||
"@pulumi/pulumi": "^3.213.0",
|
"@pulumi/pulumi": "^3.213.0",
|
||||||
"@pulumi/random": "^4.18.4",
|
"@pulumi/random": "^4.18.4",
|
||||||
|
|||||||
44
hetzner-pulumi/resources/cloudflare.ts
Normal file
44
hetzner-pulumi/resources/cloudflare.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import * as hcloud from "@pulumi/hcloud";
|
||||||
|
import * as cloudflare from "@pulumi/cloudflare";
|
||||||
|
|
||||||
|
async function getZone(domain: string): Promise<cloudflare.Zone | null> {
|
||||||
|
let match;
|
||||||
|
const zones = await cloudflare.getZones();
|
||||||
|
|
||||||
|
zones.results.forEach((zone) => {
|
||||||
|
if (domain.includes(zone.name)) match = zone;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (match) return match;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dns(
|
||||||
|
domain: string,
|
||||||
|
ipAddress: hcloud.FloatingIp,
|
||||||
|
suffix: string,
|
||||||
|
) {
|
||||||
|
const ip = ipAddress.ipAddress.apply((ip) => ip);
|
||||||
|
const name = `${domain}-${suffix}_dns_record`;
|
||||||
|
const comment = "managed by pulumi - schleppe-ha-project";
|
||||||
|
|
||||||
|
const zone = await getZone(domain);
|
||||||
|
if (!zone)
|
||||||
|
throw new Error(
|
||||||
|
"no matching zone found! check cloudflare token scopes & registration",
|
||||||
|
);
|
||||||
|
|
||||||
|
return new cloudflare.DnsRecord(
|
||||||
|
name,
|
||||||
|
{
|
||||||
|
zoneId: zone.id,
|
||||||
|
name: domain,
|
||||||
|
ttl: 1,
|
||||||
|
type: "A",
|
||||||
|
content: ip,
|
||||||
|
proxied: false,
|
||||||
|
comment,
|
||||||
|
},
|
||||||
|
{ dependsOn: [ipAddress] },
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,51 +1,86 @@
|
|||||||
import * as pulumi from "@pulumi/pulumi";
|
import * as pulumi from "@pulumi/pulumi";
|
||||||
import * as hcloud from "@pulumi/hcloud";
|
import * as hcloud from "@pulumi/hcloud";
|
||||||
import * as random from "@pulumi/random";
|
import * as random from "@pulumi/random";
|
||||||
import { config } from './config';
|
import { config } from "./config";
|
||||||
import { getCheapestServerType } from './utils';
|
import { getCheapestServerType, topicedLabel } from "./utils";
|
||||||
|
|
||||||
import { VmSize, OS, ServerLocations } from "./types";
|
import { VmSize, OS, ServerLocations } from "./types";
|
||||||
|
|
||||||
// “Tag” servers using labels. Hetzner firewalls can target servers by label selectors. :contentReference[oaicite:2]{index=2}
|
// “Tag” servers using labels. Hetzner firewalls can target servers by label selectors. :contentReference[oaicite:2]{index=2}
|
||||||
const serverLabels = {
|
const serverLabels = {
|
||||||
app: "demo",
|
|
||||||
role: "web",
|
|
||||||
env: pulumi.getStack(),
|
env: pulumi.getStack(),
|
||||||
|
managed: "pulumi",
|
||||||
};
|
};
|
||||||
|
|
||||||
const sshPublicKey = config.require("sshPublicKey");
|
const sshPublicKey = config.require("sshPublicKey");
|
||||||
const sshKey = new hcloud.SshKey("ssh-key", {
|
const sshKey = new hcloud.SshKey("ssh-key", {
|
||||||
name: `pulumi-${pulumi.getStack()}-ssh`,
|
name: `pulumi-${pulumi.getStack()}-ssh`,
|
||||||
publicKey: sshPublicKey,
|
publicKey: sshPublicKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const serverName = (name: string, location: string) => {
|
||||||
|
if (name.includes("-")) {
|
||||||
|
const [n, id] = name.split("-");
|
||||||
|
return `${n}-${location}-${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${name}-${location}`;
|
||||||
|
};
|
||||||
|
|
||||||
export function server(
|
export function server(
|
||||||
name: string,
|
name: string,
|
||||||
size: VmSize,
|
size: VmSize,
|
||||||
os: OS = OS.debian,
|
os: OS = OS.debian,
|
||||||
location: ServerLocations,
|
location: ServerLocations,
|
||||||
network: hcloud.NetworkSubnet
|
network: hcloud.NetworkSubnet,
|
||||||
|
ipv4: boolean = false,
|
||||||
): hcloud.Server {
|
): hcloud.Server {
|
||||||
const ceap = getCheapestServerType('eu');
|
const extraLabel = topicedLabel(name)
|
||||||
|
name = serverName(name, location);
|
||||||
|
const networkId = network.networkId.apply((id) => String(id).split("-")[0]);
|
||||||
|
|
||||||
const hexId = new random.RandomId(`${name}-${location}`, {
|
const server = new hcloud.Server(
|
||||||
byteLength: 2, // 2 bytes = 4 hex characters
|
name,
|
||||||
});
|
{
|
||||||
|
|
||||||
name = `${name}-${location}`
|
|
||||||
|
|
||||||
return new hcloud.Server(name, {
|
|
||||||
name,
|
name,
|
||||||
image: os,
|
image: os,
|
||||||
serverType: ceap,
|
serverType: size,
|
||||||
location,
|
location,
|
||||||
backups: false,
|
backups: false,
|
||||||
publicNets: [{
|
publicNets: [
|
||||||
ipv4Enabled: false,
|
{
|
||||||
|
ipv4Enabled: ipv4,
|
||||||
ipv6Enabled: true,
|
ipv6Enabled: true,
|
||||||
}],
|
},
|
||||||
networks: [network],
|
],
|
||||||
|
networks: [
|
||||||
|
{
|
||||||
|
networkId: networkId.apply((nid) => Number(nid)),
|
||||||
|
},
|
||||||
|
],
|
||||||
sshKeys: [sshKey.name],
|
sshKeys: [sshKey.name],
|
||||||
labels: serverLabels
|
labels: {
|
||||||
})
|
...serverLabels,
|
||||||
|
...extraLabel
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ dependsOn: [network] },
|
||||||
|
);
|
||||||
|
|
||||||
|
const serverNet = new hcloud.ServerNetwork(
|
||||||
|
`${name}-servernet-${location}`,
|
||||||
|
{
|
||||||
|
serverId: server.id.apply((id) => Number(id)),
|
||||||
|
subnetId: network.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dependsOn: [network, server],
|
||||||
|
parent: server,
|
||||||
|
deleteBeforeReplace: true,
|
||||||
|
|
||||||
|
ignoreChanges: [ 'serverId', 'ip', 'aliasIps', 'networkId', 'subnetId' ]
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return server;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,24 +2,22 @@ import * as pulumi from "@pulumi/pulumi";
|
|||||||
import * as hcloud from "@pulumi/hcloud";
|
import * as hcloud from "@pulumi/hcloud";
|
||||||
|
|
||||||
import type { NetworkRegion } from "./types";
|
import type { NetworkRegion } from "./types";
|
||||||
|
import { currentIPAddress } from "./utils";
|
||||||
|
|
||||||
// Required
|
// NETWORKS
|
||||||
|
|
||||||
// make sure to have regional parent networks
|
|
||||||
|
|
||||||
const networkName = (name: string, region: NetworkRegion) =>
|
const networkName = (name: string, region: NetworkRegion) =>
|
||||||
`${name}-net-${region}`;
|
`${name}-net-${region}`;
|
||||||
|
|
||||||
export function regionalNetwork(
|
export function regionalNetwork(
|
||||||
prefix: string,
|
name: string,
|
||||||
cidr: string,
|
cidr: string,
|
||||||
region: NetworkRegion,
|
region: NetworkRegion,
|
||||||
) {
|
) {
|
||||||
const name = networkName(prefix, region);
|
const parentNetworkRange = 22;
|
||||||
const parentNetworkRange = 8;
|
|
||||||
const [ip, _] = cidr.split("/");
|
const [ip, _] = cidr.split("/");
|
||||||
|
|
||||||
const net = new hcloud.Network(name, {
|
const net = new hcloud.Network(name, {
|
||||||
|
name,
|
||||||
ipRange: `${ip}/${parentNetworkRange}`,
|
ipRange: `${ip}/${parentNetworkRange}`,
|
||||||
labels: {
|
labels: {
|
||||||
region,
|
region,
|
||||||
@@ -38,16 +36,33 @@ export function subNetwork(
|
|||||||
): hcloud.NetworkSubnet {
|
): hcloud.NetworkSubnet {
|
||||||
const name = `${prefix}-subnet-${region}`;
|
const name = `${prefix}-subnet-${region}`;
|
||||||
|
|
||||||
const net = new hcloud.NetworkSubnet(name, {
|
const net = new hcloud.NetworkSubnet(
|
||||||
networkId: parentNetwork.id.apply(id => Number(id)),
|
name,
|
||||||
|
{
|
||||||
|
networkId: parentNetwork.id.apply((id) => Number(id)),
|
||||||
type: "cloud",
|
type: "cloud",
|
||||||
networkZone: "eu-central",
|
networkZone: region,
|
||||||
ipRange: cidr,
|
ipRange: cidr,
|
||||||
});
|
},
|
||||||
|
{ parent: parentNetwork, dependsOn: [parentNetwork] },
|
||||||
|
);
|
||||||
|
|
||||||
return net;
|
return net;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FLOATING IPs
|
||||||
|
export function floatingIP(name: string, server: hcloud.Server) {
|
||||||
|
return new hcloud.FloatingIp(
|
||||||
|
name,
|
||||||
|
{
|
||||||
|
type: "ipv4",
|
||||||
|
serverId: server.id.apply((i) => Number(i)),
|
||||||
|
},
|
||||||
|
{ dependsOn: [server] },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIREWALL RULES
|
||||||
export const allowHttp = new hcloud.Firewall("allow-http", {
|
export const allowHttp = new hcloud.Firewall("allow-http", {
|
||||||
name: "allow-http",
|
name: "allow-http",
|
||||||
applyTos: [
|
applyTos: [
|
||||||
@@ -80,16 +95,30 @@ export const allowHttp = new hcloud.Firewall("allow-http", {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const allowSSH = new hcloud.Firewall("allow-ssh", {
|
export function allowSSHToCurrentIP() {
|
||||||
|
const ip = currentIPAddress()
|
||||||
|
|
||||||
|
return new hcloud.Firewall("allow-ssh", {
|
||||||
name: "allow-ssh",
|
name: "allow-ssh",
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
direction: "in",
|
direction: "in",
|
||||||
protocol: "tcp",
|
protocol: "tcp",
|
||||||
port: "22",
|
port: "22",
|
||||||
sourceIps: ["127.0.0.0/24"],
|
sourceIps: [ip],
|
||||||
description: "Allow SSH from approved CIDRs only",
|
description: "Allow SSH from approved CIDRs only",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function attach(
|
||||||
|
name: string,
|
||||||
|
firewall: hcloud.Firewall,
|
||||||
|
servers: hcloud.Server[],
|
||||||
|
) {
|
||||||
|
return new hcloud.FirewallAttachment(name, {
|
||||||
|
firewallId: firewall.id.apply((id) => Number(id)),
|
||||||
|
serverIds: servers.map((server) => server.id.apply((id) => Number(id))),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ export enum VmSize {
|
|||||||
small = "small",
|
small = "small",
|
||||||
medium = "medium",
|
medium = "medium",
|
||||||
large = "large",
|
large = "large",
|
||||||
|
cx23 = "cx23",
|
||||||
|
cax11 = "cax11",
|
||||||
|
cpx11 = "cpx11"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum OS {
|
export enum OS {
|
||||||
debian = "debian",
|
debian = "debian-13",
|
||||||
ubuntu = "ubuntu",
|
ubuntu = "ubuntu",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as pulumi from "@pulumi/pulumi";
|
import * as pulumi from "@pulumi/pulumi";
|
||||||
|
import * as hcloud from "@pulumi/hcloud";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import * as crypto from "node:crypto";
|
import * as crypto from "node:crypto";
|
||||||
|
|
||||||
@@ -103,3 +104,62 @@ export function getCheapestServerType(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Label {
|
||||||
|
role?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function topicedLabel(name: string) {
|
||||||
|
let labels: Label = {};
|
||||||
|
if (name.includes("haproxy")) {
|
||||||
|
labels.role = 'load-balancer';
|
||||||
|
} else if (name.includes("web")) {
|
||||||
|
labels.role = 'web'
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const summarizeServer = (s: hcloud.Server) => ({
|
||||||
|
name: s.name,
|
||||||
|
publicIpv4: s.ipv4Address,
|
||||||
|
publicIpv6: s.ipv6Address,
|
||||||
|
privateIp: s.networks.apply(nets => nets?.[0]?.ip ?? 'null'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const summarizeNetwork = (n: hcloud.Network) => ({
|
||||||
|
name: n.name,
|
||||||
|
cidr: n.ipRange
|
||||||
|
});
|
||||||
|
|
||||||
|
export const summarizeSubNetwork = (n: hcloud.NetworkSubnet) => ({
|
||||||
|
gateway: n.gateway,
|
||||||
|
cidr: n.ipRange,
|
||||||
|
zone: n.networkZone,
|
||||||
|
type: n.type
|
||||||
|
});
|
||||||
|
|
||||||
|
export const summarizeFloatingIp = (floatingIp: hcloud.FloatingIp) => ({
|
||||||
|
name: floatingIp.name,
|
||||||
|
address: floatingIp.ipAddress,
|
||||||
|
attachedTo: floatingIp.serverId,
|
||||||
|
location: floatingIp.homeLocation,
|
||||||
|
labels: floatingIp.labels
|
||||||
|
})
|
||||||
|
|
||||||
|
export const summarizeFirewall = (firewall: hcloud.Firewall) => ({
|
||||||
|
name: firewall.name,
|
||||||
|
rules: firewall.rules,
|
||||||
|
labels: firewall.labels
|
||||||
|
})
|
||||||
|
|
||||||
|
export const summarizeDns = (firewall: hcloud.Firewall) => ({
|
||||||
|
name: firewall.name,
|
||||||
|
rules: firewall.rules,
|
||||||
|
labels: firewall.labels
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function currentIPAddress(): Promise<string> {
|
||||||
|
return fetch('https://ifconfig.me/ip')
|
||||||
|
.then(resp => resp.text())
|
||||||
|
}
|
||||||
|
|||||||
@@ -369,6 +369,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
|
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
|
||||||
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
|
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
|
||||||
|
|
||||||
|
"@pulumi/cloudflare@^6.12.0":
|
||||||
|
version "6.12.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@pulumi/cloudflare/-/cloudflare-6.12.0.tgz#edb2c59d1b594a0c64f12ba8cf63b84ed3937fcc"
|
||||||
|
integrity sha512-Evhj9u+ZVCO6Cu9+xon4F3dY4RA7U4ZTWc19MJAIa5GgLeG8X0C84iyo5aruZf2nK7nfJ2d47lyRSaXPBHOzjw==
|
||||||
|
dependencies:
|
||||||
|
"@pulumi/pulumi" "^3.142.0"
|
||||||
|
|
||||||
"@pulumi/hcloud@^1.29.0":
|
"@pulumi/hcloud@^1.29.0":
|
||||||
version "1.29.0"
|
version "1.29.0"
|
||||||
resolved "https://registry.yarnpkg.com/@pulumi/hcloud/-/hcloud-1.29.0.tgz#4c622aa3ae9c4590af783eefbeee1e62e711c7f3"
|
resolved "https://registry.yarnpkg.com/@pulumi/hcloud/-/hcloud-1.29.0.tgz#4c622aa3ae9c4590af783eefbeee1e62e711c7f3"
|
||||||
|
|||||||
Reference in New Issue
Block a user