terraform

This commit is contained in:
Sergei Poljanski 2025-11-01 13:43:31 +02:00
commit 33a12b643f
15 changed files with 1021 additions and 0 deletions

50
terraform/.gitignore vendored Normal file
View file

@ -0,0 +1,50 @@
# Terraform state files
*.tfstate
*.tfstate.*
*.tfstate.backup
# Terraform plan files
tfplan
*.tfplan
plan.txt
# Terraform variable files with secrets
terraform.tfvars
*.auto.tfvars
# Terraform CLI configuration
.terraformrc
terraform.rc
# Terraform directories
.terraform/
.terraform.lock.hcl
# Crash log files
crash.log
crash.*.log
# Override files
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# SSH keys
*.pem
*.key
*_rsa
*_ed25519
deploy_key
deploy_key.pub
# IDE and editor files
.idea/
.vscode/
*.swp
*.swo
*~
# OS files
.DS_Store
Thumbs.db

133
terraform/README.md Normal file
View file

@ -0,0 +1,133 @@
# ASXP.IO Infrastructure
Hetzner Cloud infrastructure managed via Terraform.
## Servers
- **asxpio-ops** (CX22: 2 vCPU, 4GB) - Forgejo, Traefik, monitoring, static sites
- **asxpio-services** (CX43: 8 vCPU, 16GB) - Immich, game servers, XMPP, CI/CD
## Storage (S3-compatible)
**Terraform State:**
- asxpio-terraform-main (hel1)
- asxpio-terraform-repl (fsn1)
**Backups:**
- asxpio-backups-main (hel1)
- asxpio-backups-repl (fsn1)
**Immich:**
- asxpio-immich-main (hel1)
- asxpio-immich-repl (fsn1)
## Network
- VPC: 10.10.0.0/20
- WireGuard VPN: 10.0.0.0/24
- IPv4 + IPv6 enabled
## Cost
| Item | Monthly |
|------|---------|
| ops CX22 | €3.71 |
| services CX43 | €11.15 |
| IPv4 (2x) | €1.24 |
| S3 (1TB) | €6.19 |
| S3 1TB Traffic | €1.24 |
| **Total** | **€22.29** |
## Setup
### 1. Initialize
```bash
terraform init
```
### 2. Configure secrets
Edit `terraform.tfvars`:
```hcl
hcloud_token = "your-token"
s3_access_key = "your-key"
s3_secret_key = "your-secret"
s3_endpoint_hel1 = "hel1.your-objectstorage.com"
s3_endpoint_fsn1 = "fsn1.your-objectstorage.com"
```
### 3. Deploy
```bash
terraform plan
terraform apply
```
### 4. Enable S3 backend (optional)
After buckets are created:
```bash
# Set credentials
export AWS_ACCESS_KEY_ID="your-key"
export AWS_SECRET_ACCESS_KEY="your-secret"
# Or use ~/.aws/credentials file
mkdir -p ~/.aws
cat > ~/.aws/credentials <<EOF
[default]
aws_access_key_id = your-key
aws_secret_access_key = your-secret
EOF
# Uncomment backend in providers.tf
# Then migrate state
terraform init -migrate-state
```
## DNS Management
**Current:** Managed manually via `hcloud` CLI
Zones: asxp.io (61604), czsk.it (61606), childfr.ee (61605)
**TODO:** Migrate to Terraform-managed DNS when provider v1.54.0+ is available:
```hcl
# Upgrade provider in providers.tf
hcloud = {
source = "hetznercloud/hcloud"
version = "~> 1.54"
}
# Import existing zones
terraform import hcloud_zone.asxp_io 61604
terraform import hcloud_zone.czsk_it 61606
terraform import hcloud_zone.childfr_ee 61605
# Example DNS resource
resource "hcloud_zone" "asxp_io" {
name = "asxp.io"
mode = "primary"
labels = {
managed_by = "terraform"
}
}
resource "hcloud_zone_rrset" "apex_a" {
zone = hcloud_zone.asxp_io.name
name = "@"
type = "A"
records = [
{ value = "95.217.19.240", comment = "ops-server" },
]
}
```
See: https://github.com/hetznercloud/terraform-provider-hcloud/releases/tag/v1.54.0
## Files
- `servers.tf` - Server definitions + cloud-init
- `s3.tf` - S3 buckets (6 total)
- `vpc.tf` - Private network
- `firewall.tf` - Security rules
- `dns.tf` - DNS docs (manual management)
- `keys.tf` - SSH keys
- `cloud-init/*.yaml` - Server provisioning

View file

@ -0,0 +1,137 @@
#cloud-config
# Cloud-init configuration for asxpio-ops server
# Debian 13 (trixie) - Ops Server
hostname: asxpio-ops
fqdn: asxpio-ops.asxp.io
manage_etc_hosts: true
# Package management
package_update: true
package_upgrade: true
packages:
- fail2ban
- wget
- curl
- gpg
- rsync
- unzip
- htop
- ncdu
- tree
- git
- tmux
- apt-transport-https
- ca-certificates
- gnupg
- lsb-release
# Create users (docker group will be added later in runcmd)
users:
- name: asxpi
groups: sudo
shell: /bin/bash
sudo: ['ALL=(ALL) NOPASSWD:ALL']
lock_passwd: true
ssh_authorized_keys:
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC//ljrf65xbFyGmy/xeZs5GZvkmufCioqIGYguR3c8c/Ff9HiA4oMRT53Qs16KMuXRg62NusVQvJsvyPRSARpBFsoPiZ/YNBteWlqT1aVqeZwPOfUfFycQ2T8a9qtDnZ0WhamSlJMSAHQPkxXNA6S2ZE9y+dMzMc5qPeRwdsQmuvkxjual/35t9qTgCAG7LvKCKPJkLL5YpwLoqWYWTqSJ6RxNLTYNgIhJBZirzB5rNiFDDupnOuTKSpyRJNxyBSapNge5rCnGQYIeoJ4fi+FTi5wzjlC3hC0BN4Oc/zrm1nS9lyPXQS8KV5/vcQlPrvYZmv0LneeQ/gH/jJsSlwRp2qIatoASSfTlz1y1bOOYtPFojJPikXiXeZMCdlam1II6B94z1ybOIO8TqyKDVs4eHRFfYO72zv0sGsTr7t0C7+N9Wrpmhk38zdlLagZ6HO7KSyoAHm+fQc58fXmOhz4vfYrbmYRPgO0VO03fEb7lSFPPLaSseT0tayli05irJpc= asxpi@laptop
- name: deploy
groups: sudo
shell: /bin/bash
sudo: ['ALL=(ALL) NOPASSWD:ALL']
lock_passwd: true
ssh_authorized_keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGTDPg3mV55lO8uBnvpcGRxJ2vMRP0qB3Pxhg+C8rwj9 asxpio-deploy
# SSH hardening
ssh_pwauth: false
disable_root: false
ssh_deletekeys: false
# Write files
write_files:
# Docker daemon configuration
- path: /etc/docker/daemon.json
content: |
{
"live-restore": true,
"storage-driver": "overlay2",
"log-opts": {
"max-size": "20m"
}
}
owner: root:root
permissions: '0644'
# Custom bash prompt for asxpi user
- path: /home/asxpi/.bashrc_custom
content: |
# Custom bash prompt styling for asxpio servers
export PS1="\[$(tput bold)\]\[\033[38;5;4m\][\[$(tput sgr0)\]\t\[$(tput sgr0)\]\[$(tput bold)\]\[\033[38;5;4m\]]\[$(tput sgr0)\] \[$(tput sgr0)\]\[$(tput bold)\]\[\033[38;5;4m\][\[$(tput sgr0)\]\u@\h \[$(tput sgr0)\]\[$(tput bold)\]\[\033[38;5;4m\]\w]\[$(tput sgr0)\] \[$(tput sgr0)\]\[$(tput bold)\]\[\033[38;5;4m\]\\$\[$(tput sgr0)\] \[$(tput sgr0)\]"
owner: root:root
permissions: '0644'
# SSH hardening config
- path: /etc/ssh/sshd_config.d/99-hardening.conf
content: |
PasswordAuthentication no
PermitRootLogin prohibit-password
PubkeyAuthentication yes
owner: root:root
permissions: '0644'
runcmd:
# Install Docker
- curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
- echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
- apt-get update
- apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Enable and start Docker
- systemctl enable docker
- systemctl start docker
# Add users to docker group (after docker is installed)
- usermod -aG docker asxpi
- usermod -aG docker deploy
# Enable and start fail2ban
- systemctl enable fail2ban
- systemctl start fail2ban
# Fix home directory ownership and setup bashrc for asxpi
- chown asxpi:asxpi /home/asxpi
- chown asxpi:asxpi /home/asxpi/.bashrc_custom
- cp /etc/skel/.bashrc /home/asxpi/.bashrc
- cp /etc/skel/.profile /home/asxpi/.profile
- chown asxpi:asxpi /home/asxpi/.bashrc /home/asxpi/.profile
- echo '[ -f ~/.bashrc_custom ] && source ~/.bashrc_custom' >> /home/asxpi/.bashrc
# Fix home directory ownership for deploy user
- chown deploy:deploy /home/deploy
# Create directory structure for services
- mkdir -p /opt/{traefik,forgejo,grafana,prometheus,loki,promtail,static-sites}
- mkdir -p /opt/traefik/config
- mkdir -p /opt/static-sites/{asxp.io,czsk.it,childfr.ee,builds,upload}
- chown -R asxpi:asxpi /opt/*
# Set up weekly Docker image pruning
- echo '0 3 * * 1 docker image prune -af > /var/log/docker-prune.log 2>&1' | crontab -
# Restart SSH to apply hardening
- systemctl restart sshd
final_message: |
==========================================
asxpio-ops server is ready!
==========================================
Hostname: asxpio-ops
OS: Debian 13 (trixie)
User: asxpi (sudo + docker access)
SSH: Key-only authentication enabled
Docker: Installed with compose plugin
You can now SSH: ssh asxpi@$(hostname -I | awk '{print $1}')
==========================================

View file

@ -0,0 +1,138 @@
#cloud-config
# Cloud-init configuration for asxpio-services server
# Debian 13 (trixie) - Services Server
hostname: asxpio-services
fqdn: asxpio-services.asxp.io
manage_etc_hosts: true
# Package management
package_update: true
package_upgrade: true
packages:
- fail2ban
- wget
- curl
- gpg
- rsync
- unzip
- htop
- ncdu
- tree
- git
- tmux
- apt-transport-https
- ca-certificates
- gnupg
- lsb-release
# Create users (docker group will be added later in runcmd)
users:
- name: asxpi
groups: sudo
shell: /bin/bash
sudo: ['ALL=(ALL) NOPASSWD:ALL']
lock_passwd: true
ssh_authorized_keys:
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC//ljrf65xbFyGmy/xeZs5GZvkmufCioqIGYguR3c8c/Ff9HiA4oMRT53Qs16KMuXRg62NusVQvJsvyPRSARpBFsoPiZ/YNBteWlqT1aVqeZwPOfUfFycQ2T8a9qtDnZ0WhamSlJMSAHQPkxXNA6S2ZE9y+dMzMc5qPeRwdsQmuvkxjual/35t9qTgCAG7LvKCKPJkLL5YpwLoqWYWTqSJ6RxNLTYNgIhJBZirzB5rNiFDDupnOuTKSpyRJNxyBSapNge5rCnGQYIeoJ4fi+FTi5wzjlC3hC0BN4Oc/zrm1nS9lyPXQS8KV5/vcQlPrvYZmv0LneeQ/gH/jJsSlwRp2qIatoASSfTlz1y1bOOYtPFojJPikXiXeZMCdlam1II6B94z1ybOIO8TqyKDVs4eHRFfYO72zv0sGsTr7t0C7+N9Wrpmhk38zdlLagZ6HO7KSyoAHm+fQc58fXmOhz4vfYrbmYRPgO0VO03fEb7lSFPPLaSseT0tayli05irJpc= asxpi@laptop
- name: deploy
groups: sudo
shell: /bin/bash
sudo: ['ALL=(ALL) NOPASSWD:ALL']
lock_passwd: true
ssh_authorized_keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGTDPg3mV55lO8uBnvpcGRxJ2vMRP0qB3Pxhg+C8rwj9 asxpio-deploy
# SSH hardening
ssh_pwauth: false
disable_root: false
ssh_deletekeys: false
# Write files
write_files:
# Docker daemon configuration
- path: /etc/docker/daemon.json
content: |
{
"live-restore": true,
"storage-driver": "overlay2",
"log-opts": {
"max-size": "20m"
}
}
owner: root:root
permissions: '0644'
# Custom bash prompt for asxpi user
- path: /home/asxpi/.bashrc_custom
content: |
# Custom bash prompt styling for asxpio servers
export PS1="\[$(tput bold)\]\[\033[38;5;4m\][\[$(tput sgr0)\]\t\[$(tput sgr0)\]\[$(tput bold)\]\[\033[38;5;4m\]]\[$(tput sgr0)\] \[$(tput sgr0)\]\[$(tput bold)\]\[\033[38;5;4m\][\[$(tput sgr0)\]\u@\h \[$(tput sgr0)\]\[$(tput bold)\]\[\033[38;5;4m\]\w]\[$(tput sgr0)\] \[$(tput sgr0)\]\[$(tput bold)\]\[\033[38;5;4m\]\\$\[$(tput sgr0)\] \[$(tput sgr0)\]"
owner: root:root
permissions: '0644'
# SSH hardening config
- path: /etc/ssh/sshd_config.d/99-hardening.conf
content: |
PasswordAuthentication no
PermitRootLogin prohibit-password
PubkeyAuthentication yes
owner: root:root
permissions: '0644'
runcmd:
# Install Docker
- curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
- echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
- apt-get update
- apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Enable and start Docker
- systemctl enable docker
- systemctl start docker
# Add users to docker group (after docker is installed)
- usermod -aG docker asxpi
- usermod -aG docker deploy
# Enable and start fail2ban
- systemctl enable fail2ban
- systemctl start fail2ban
# Fix home directory ownership and setup bashrc for asxpi
- chown asxpi:asxpi /home/asxpi
- chown asxpi:asxpi /home/asxpi/.bashrc_custom
- cp /etc/skel/.bashrc /home/asxpi/.bashrc
- cp /etc/skel/.profile /home/asxpi/.profile
- chown asxpi:asxpi /home/asxpi/.bashrc /home/asxpi/.profile
- echo '[ -f ~/.bashrc_custom ] && source ~/.bashrc_custom' >> /home/asxpi/.bashrc
# Fix home directory ownership for deploy user
- chown deploy:deploy /home/deploy
# Create directory structure for services
- mkdir -p /opt/{forgejo-runner,immich,minecraft,valheim,factorio,prosody,coturn}
- mkdir -p /opt/minecraft/{data,backups}
- mkdir -p /opt/valheim/data
- mkdir -p /opt/factorio/data
- chown -R asxpi:asxpi /opt/*
# Set up weekly Docker image pruning
- echo '0 3 * * 1 docker image prune -af > /var/log/docker-prune.log 2>&1' | crontab -
# Restart SSH to apply hardening
- systemctl restart sshd
final_message: |
==========================================
asxpio-services server is ready!
==========================================
Hostname: asxpio-services
OS: Debian 13 (trixie)
User: asxpi (sudo + docker access)
SSH: Key-only authentication enabled
Docker: Installed with compose plugin
You can now SSH: ssh asxpi@$(hostname -I | awk '{print $1}')
==========================================

34
terraform/dns.tf Normal file
View file

@ -0,0 +1,34 @@
# Hetzner Cloud DNS Management
# =================================
#
# DNS zones and records are now managed by Terraform.
#
# Existing Zones:
# - asxp.io (ID: 61604) - 34 records
# - czsk.it (ID: 61606) - 7 records
# - childfr.ee (ID: 61605) - 13 records
#
# To manage DNS records:
# Use the 'hcloud_record' resource in Terraform.
#
# Key records to update during migration:
# - asxp.io: @, git, builds, upload, onitama, proxy, xmpp, turn, photos
# - czsk.it: @
# - childfr.ee: @
resource "hcloud_zone" "czsk_it" {
name = "czsk.it"
mode = "primary"
labels = {
managed_by = "terraform"
}
}
resource "hcloud_zone_rrset" "apex_a" {
zone = hcloud_zone.czsk_it.name
name = "test"
type = "A"
records = [
{ value = "127.0.0.1", comment = "test" },
]
}

278
terraform/firewall.tf Normal file
View file

@ -0,0 +1,278 @@
resource "hcloud_firewall" "ops_fw" {
name = "asxpio-ops-fw"
# VPC
rule {
direction = "in"
protocol = "tcp"
port = "any"
source_ips = [
"10.10.0.0/20"
]
}
rule {
direction = "in"
protocol = "udp"
port = "any"
source_ips = [
"10.10.0.0/20"
]
}
rule {
direction = "in"
protocol = "icmp"
source_ips = [
"10.10.0.0/20"
]
}
# WireGuard
rule {
direction = "in"
protocol = "tcp"
port = "any"
source_ips = [
"10.20.0.0/24"
]
}
rule {
direction = "in"
protocol = "udp"
port = "any"
source_ips = [
"10.20.0.0/24"
]
}
rule {
direction = "in"
protocol = "icmp"
source_ips = [
"10.20.0.0/24"
]
}
# SSH TODO: Remove
rule {
direction = "in"
protocol = "tcp"
port = "22"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
rule {
direction = "in"
protocol = "tcp"
port = "80"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
rule {
direction = "in"
protocol = "tcp"
port = "443"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
# WireGuard VPN
rule {
direction = "in"
protocol = "udp"
port = "60101"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
}
resource "hcloud_firewall" "services_fw" {
name = "asxpio-services-fw"
# VPC
rule {
direction = "in"
protocol = "tcp"
port = "any"
source_ips = [
"10.10.0.0/20"
]
}
rule {
direction = "in"
protocol = "udp"
port = "any"
source_ips = [
"10.10.0.0/20"
]
}
rule {
direction = "in"
protocol = "icmp"
source_ips = [
"10.10.0.0/20"
]
}
# WireGuard
rule {
direction = "in"
protocol = "tcp"
port = "any"
source_ips = [
"10.20.0.0/24"
]
}
rule {
direction = "in"
protocol = "udp"
port = "any"
source_ips = [
"10.20.0.0/24"
]
}
rule {
direction = "in"
protocol = "icmp"
source_ips = [
"10.20.0.0/24"
]
}
# SSH TODO:Remove
rule {
direction = "in"
protocol = "tcp"
port = "22"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
# Minecraft Server
rule {
direction = "in"
protocol = "tcp"
port = "25565"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
# Minecraft Geyser
rule {
direction = "in"
protocol = "udp"
port = "19132"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
# Valheim Server (UDP port range)
rule {
direction = "in"
protocol = "udp"
port = "2456-2458"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
# Factorio Server
rule {
direction = "in"
protocol = "udp"
port = "34197"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
# Prosody XMPP - Client connections
rule {
direction = "in"
protocol = "tcp"
port = "5222"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
# Prosody XMPP - Server-to-server
rule {
direction = "in"
protocol = "tcp"
port = "5269"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
# Prosody XMPP - HTTP/BOSH
rule {
direction = "in"
protocol = "tcp"
port = "5280"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
# Prosody XMPP - HTTPS
rule {
direction = "in"
protocol = "tcp"
port = "5281"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
# TURN/STUN Server (UDP)
rule {
direction = "in"
protocol = "udp"
port = "3478"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
# TURN/STUN Server (TCP)
rule {
direction = "in"
protocol = "tcp"
port = "3478"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
}

9
terraform/keys.tf Normal file
View file

@ -0,0 +1,9 @@
resource "hcloud_ssh_key" "asxpi" {
name = "asxpi"
public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC//ljrf65xbFyGmy/xeZs5GZvkmufCioqIGYguR3c8c/Ff9HiA4oMRT53Qs16KMuXRg62NusVQvJsvyPRSARpBFsoPiZ/YNBteWlqT1aVqeZwPOfUfFycQ2T8a9qtDnZ0WhamSlJMSAHQPkxXNA6S2ZE9y+dMzMc5qPeRwdsQmuvkxjual/35t9qTgCAG7LvKCKPJkLL5YpwLoqWYWTqSJ6RxNLTYNgIhJBZirzB5rNiFDDupnOuTKSpyRJNxyBSapNge5rCnGQYIeoJ4fi+FTi5wzjlC3hC0BN4Oc/zrm1nS9lyPXQS8KV5/vcQlPrvYZmv0LneeQ/gH/jJsSlwRp2qIatoASSfTlz1y1bOOYtPFojJPikXiXeZMCdlam1II6B94z1ybOIO8TqyKDVs4eHRFfYO72zv0sGsTr7t0C7+N9Wrpmhk38zdlLagZ6HO7KSyoAHm+fQc58fXmOhz4vfYrbmYRPgO0VO03fEb7lSFPPLaSseT0tayli05irJpc= asxpi@meow"
}
resource "hcloud_ssh_key" "deploy" {
name = "asxpio-deploy"
public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGTDPg3mV55lO8uBnvpcGRxJ2vMRP0qB3Pxhg+C8rwj9 asxpio-deploy"
}

29
terraform/outputs.tf Normal file
View file

@ -0,0 +1,29 @@
# Server IPs
output "ops_ipv4" {
value = hcloud_server.ops.ipv4_address
description = "Ops server IPv4"
}
output "ops_ipv6" {
value = hcloud_server.ops.ipv6_address
description = "Ops server IPv6"
}
output "services_ipv4" {
value = hcloud_server.services.ipv4_address
description = "Services server IPv4"
}
output "services_ipv6" {
value = hcloud_server.services.ipv6_address
description = "Services server IPv6"
}
# VPC Network
output "vpc_network" {
value = {
id = hcloud_network.main.id
ip_range = hcloud_network.main.ip_range
}
description = "VPC network info"
}

51
terraform/providers.tf Normal file
View file

@ -0,0 +1,51 @@
terraform {
required_version = ">= 1.0.0"
# S3 backend - comment this section if bucket not yet created
backend "s3" {
bucket = "asxpio-terraform-main"
key = "terraform/state/terraform.tfstate"
region = "hel1"
endpoint = "https://hel1.your-objectstorage.com"
skip_credentials_validation = true
skip_metadata_api_check = true
skip_region_validation = true
skip_requesting_account_id = true
force_path_style = true
}
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "~> 1.54"
}
minio = {
source = "aminueza/minio"
version = "~> 3.11"
}
}
}
provider "hcloud" {
token = var.hcloud_token
}
# MinIO provider for Helsinki (hel1) region
provider "minio" {
alias = "hel1"
minio_server = var.s3_endpoint_hel1
minio_user = var.s3_access_key
minio_password = var.s3_secret_key
minio_region = "hel1"
minio_ssl = true
}
# MinIO provider for Falkenstein (fsn1) region
provider "minio" {
alias = "fsn1"
minio_server = var.s3_endpoint_fsn1
minio_user = var.s3_access_key
minio_password = var.s3_secret_key
minio_region = "fsn1"
minio_ssl = true
}

35
terraform/s3.tf Normal file
View file

@ -0,0 +1,35 @@
resource "minio_s3_bucket" "terraform_main" {
provider = minio.hel1
bucket = "asxpio-terraform-main"
acl = "private"
}
resource "minio_s3_bucket" "terraform_repl" {
provider = minio.fsn1
bucket = "asxpio-terraform-repl"
acl = "private"
}
resource "minio_s3_bucket" "backups_main" {
provider = minio.hel1
bucket = "asxpio-backups-main"
acl = "private"
}
resource "minio_s3_bucket" "backups_repl" {
provider = minio.fsn1
bucket = "asxpio-backups-repl"
acl = "private"
}
resource "minio_s3_bucket" "immich_main" {
provider = minio.hel1
bucket = "asxpio-immich-main"
acl = "private"
}
resource "minio_s3_bucket" "immich_repl" {
provider = minio.fsn1
bucket = "asxpio-immich-repl"
acl = "private"
}

73
terraform/servers.tf Normal file
View file

@ -0,0 +1,73 @@
# Ops Server - CX22 (2 vCPU, 4GB RAM)
resource "hcloud_server" "ops" {
name = "asxpio-ops"
image = "debian-13"
location = "hel1"
server_type = "cx23"
ssh_keys = [
hcloud_ssh_key.asxpi.id,
hcloud_ssh_key.deploy.id
]
backups = false
public_net {
ipv4_enabled = true
ipv6_enabled = true
}
network {
network_id = hcloud_network.main.id
ip = "10.10.0.10"
}
firewall_ids = [hcloud_firewall.ops_fw.id]
labels = {
role = "ops"
env = "production"
managed_by = "terraform"
project = "asxpio"
}
# Cloud-init for automated server setup
user_data = file("${path.module}/cloud-init/ops.yaml")
}
# Services Server - CX43 (8 vCPU, 16GB RAM)
resource "hcloud_server" "services" {
name = "asxpio-services"
image = "debian-13"
location = "hel1"
server_type = "cx43"
ssh_keys = [
hcloud_ssh_key.asxpi.id,
hcloud_ssh_key.deploy.id
]
backups = false
public_net {
ipv4_enabled = true
ipv6_enabled = true
}
network {
network_id = hcloud_network.main.id
ip = "10.10.0.20"
}
firewall_ids = [hcloud_firewall.services_fw.id]
labels = {
role = "services"
env = "production"
managed_by = "terraform"
project = "asxpio"
}
# Cloud-init for automated server setup
user_data = file("${path.module}/cloud-init/services.yaml")
}

View file

@ -0,0 +1,13 @@
# Hetzner Cloud API Token
# Get from: https://console.hetzner.cloud/ → Project → Security → API Tokens
hcloud_token = "your-hetzner-cloud-api-token-here"
# S3 Access Credentials
# Create buckets first in Hetzner Cloud Console, then get credentials
s3_access_key = "your-s3-access-key-here"
s3_secret_key = "your-s3-secret-key-here"
# S3 Endpoint (update after creating buckets)
# Format: https://<bucket-name>.fsn1.your-objectstorage.com
s3_endpoint_hel1 = "hel1.your-objectstorage.com"
s3_endpoint_fsn1 = "fsn1.your-objectstorage.com"

29
terraform/variables.tf Normal file
View file

@ -0,0 +1,29 @@
variable "hcloud_token" {
description = "Hetzner Cloud API Token"
type = string
sensitive = true
}
variable "s3_access_key" {
description = "S3 Access Key"
type = string
sensitive = true
}
variable "s3_secret_key" {
description = "S3 Secret Key"
type = string
sensitive = true
}
variable "s3_endpoint_hel1" {
description = "S3 Endpoint URL for Helsinki region"
type = string
default = "hel1.your-objectstorage.com"
}
variable "s3_endpoint_fsn1" {
description = "S3 Endpoint URL for Falkenstein region"
type = string
default = "fsn1.your-objectstorage.com"
}

11
terraform/vpc.tf Normal file
View file

@ -0,0 +1,11 @@
resource "hcloud_network" "main" {
name = "asxpio-vpc"
ip_range = "10.10.0.0/20"
}
resource "hcloud_network_subnet" "eu-central" {
network_id = hcloud_network.main.id
type = "cloud"
network_zone = "eu-central"
ip_range = "10.10.0.0/23"
}