Feat/open tofu #8
+6
-3
@@ -14,6 +14,9 @@ locals {
|
|||||||
bootstrap_script = templatefile("${path.module}/scripts/bootstrap.sh.tftpl", {
|
bootstrap_script = templatefile("${path.module}/scripts/bootstrap.sh.tftpl", {
|
||||||
deploy_user = var.deploy_user
|
deploy_user = var.deploy_user
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# IDs for every named key in var.ssh_key_names - installed on every server.
|
||||||
|
ssh_key_ids = [for k in data.hcloud_ssh_key.main : k.id]
|
||||||
}
|
}
|
||||||
|
|
||||||
module "network" {
|
module "network" {
|
||||||
@@ -30,7 +33,7 @@ module "dev" {
|
|||||||
server_type = var.dev_server_type
|
server_type = var.dev_server_type
|
||||||
image = var.server_image
|
image = var.server_image
|
||||||
location = var.location
|
location = var.location
|
||||||
ssh_key_id = data.hcloud_ssh_key.main.id
|
ssh_key_ids = local.ssh_key_ids
|
||||||
network_id = module.network.id
|
network_id = module.network.id
|
||||||
private_ip = var.dev_private_ip
|
private_ip = var.dev_private_ip
|
||||||
volume_size = var.dev_volume_size
|
volume_size = var.dev_volume_size
|
||||||
@@ -51,7 +54,7 @@ module "prod" {
|
|||||||
server_type = var.prod_server_type
|
server_type = var.prod_server_type
|
||||||
image = var.server_image
|
image = var.server_image
|
||||||
location = var.location
|
location = var.location
|
||||||
ssh_key_id = data.hcloud_ssh_key.main.id
|
ssh_key_ids = local.ssh_key_ids
|
||||||
network_id = module.network.id
|
network_id = module.network.id
|
||||||
private_ip = var.prod_private_ip
|
private_ip = var.prod_private_ip
|
||||||
volume_size = var.prod_volume_size
|
volume_size = var.prod_volume_size
|
||||||
@@ -69,7 +72,7 @@ module "vpn" {
|
|||||||
server_type = var.vpn_server_type
|
server_type = var.vpn_server_type
|
||||||
image = var.server_image
|
image = var.server_image
|
||||||
location = var.location
|
location = var.location
|
||||||
ssh_key_id = data.hcloud_ssh_key.main.id
|
ssh_key_ids = local.ssh_key_ids
|
||||||
allowed_ssh_source_ips = var.allowed_ssh_source_ips
|
allowed_ssh_source_ips = var.allowed_ssh_source_ips
|
||||||
bootstrap_script = local.bootstrap_script
|
bootstrap_script = local.bootstrap_script
|
||||||
ssh_private_key_path = var.ssh_private_key_path
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ resource "hcloud_server" "this" {
|
|||||||
server_type = var.server_type
|
server_type = var.server_type
|
||||||
image = var.image
|
image = var.image
|
||||||
location = var.location
|
location = var.location
|
||||||
ssh_keys = [var.ssh_key_id]
|
ssh_keys = var.ssh_key_ids
|
||||||
firewall_ids = [hcloud_firewall.this.id]
|
firewall_ids = [hcloud_firewall.this.id]
|
||||||
|
|
||||||
network {
|
network {
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ variable "location" {
|
|||||||
type = string
|
type = string
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "ssh_key_id" {
|
variable "ssh_key_ids" {
|
||||||
description = "ID of the Hetzner Cloud SSH key to install on the server."
|
description = "IDs of the Hetzner Cloud SSH keys to install on the server."
|
||||||
type = string
|
type = list(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "network_id" {
|
variable "network_id" {
|
||||||
@@ -49,7 +49,7 @@ variable "bootstrap_script" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
variable "ssh_private_key_path" {
|
variable "ssh_private_key_path" {
|
||||||
description = "Local path to the private key matching var.ssh_key_id, used to run the bootstrap script over SSH."
|
description = "Local path to the private key matching one of var.ssh_key_ids, used to run the bootstrap script over SSH."
|
||||||
type = string
|
type = string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ resource "hcloud_server" "this" {
|
|||||||
server_type = var.server_type
|
server_type = var.server_type
|
||||||
image = var.image
|
image = var.image
|
||||||
location = var.location
|
location = var.location
|
||||||
ssh_keys = [var.ssh_key_id]
|
ssh_keys = var.ssh_key_ids
|
||||||
firewall_ids = [hcloud_firewall.this.id]
|
firewall_ids = [hcloud_firewall.this.id]
|
||||||
|
|
||||||
network {
|
network {
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ variable "location" {
|
|||||||
type = string
|
type = string
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "ssh_key_id" {
|
variable "ssh_key_ids" {
|
||||||
description = "ID of the Hetzner Cloud SSH key to install on the server."
|
description = "IDs of the Hetzner Cloud SSH keys to install on the server."
|
||||||
type = string
|
type = list(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "network_id" {
|
variable "network_id" {
|
||||||
@@ -49,6 +49,6 @@ variable "bootstrap_script" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
variable "ssh_private_key_path" {
|
variable "ssh_private_key_path" {
|
||||||
description = "Local path to the private key matching var.ssh_key_id, used to run the bootstrap script over SSH."
|
description = "Local path to the private key matching one of var.ssh_key_ids, used to run the bootstrap script over SSH."
|
||||||
type = string
|
type = string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ resource "hcloud_server" "this" {
|
|||||||
server_type = var.server_type
|
server_type = var.server_type
|
||||||
image = var.image
|
image = var.image
|
||||||
location = var.location
|
location = var.location
|
||||||
ssh_keys = [var.ssh_key_id]
|
ssh_keys = var.ssh_key_ids
|
||||||
firewall_ids = [hcloud_firewall.this.id]
|
firewall_ids = [hcloud_firewall.this.id]
|
||||||
|
|
||||||
connection {
|
connection {
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ variable "location" {
|
|||||||
type = string
|
type = string
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "ssh_key_id" {
|
variable "ssh_key_ids" {
|
||||||
description = "ID of the Hetzner Cloud SSH key to install on the server."
|
description = "IDs of the Hetzner Cloud SSH keys to install on the server."
|
||||||
type = string
|
type = list(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "allowed_ssh_source_ips" {
|
variable "allowed_ssh_source_ips" {
|
||||||
@@ -29,6 +29,6 @@ variable "bootstrap_script" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
variable "ssh_private_key_path" {
|
variable "ssh_private_key_path" {
|
||||||
description = "Local path to the private key matching var.ssh_key_id, used to run the bootstrap script over SSH."
|
description = "Local path to the private key matching one of var.ssh_key_ids, used to run the bootstrap script over SSH."
|
||||||
type = string
|
type = string
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
data "hcloud_ssh_key" "main" {
|
data "hcloud_ssh_key" "main" {
|
||||||
name = var.ssh_key_name
|
for_each = toset(var.ssh_key_names)
|
||||||
|
name = each.value
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,13 @@
|
|||||||
# The Hetzner API token is NOT set here: export it as HCLOUD_TOKEN in your shell
|
# The Hetzner API token is NOT set here: export it as HCLOUD_TOKEN in your shell
|
||||||
# before running tofu plan/apply.
|
# before running tofu plan/apply.
|
||||||
|
|
||||||
# Name of an SSH key already uploaded to your Hetzner Cloud project
|
# Names of SSH keys already uploaded to your Hetzner Cloud project
|
||||||
# (Console > Security > SSH Keys). Required.
|
# (Console > Security > SSH Keys). All of them are installed on every server. Required.
|
||||||
ssh_key_name = "your-key-name"
|
ssh_key_names = ["your-key-name", "laptop", "phone"]
|
||||||
|
|
||||||
# Local path to the private key matching ssh_key_name above. OpenTofu uses this
|
# Local path to the private key matching ONE of ssh_key_names above (doesn't need
|
||||||
# once per server to upload and run the post-install bootstrap script
|
# to be all of them - just one you hold). OpenTofu uses this once per server to
|
||||||
|
# upload and run the post-install bootstrap script
|
||||||
# (infra/scripts/bootstrap.sh.tftpl) immediately after creation. Required.
|
# (infra/scripts/bootstrap.sh.tftpl) immediately after creation. Required.
|
||||||
ssh_private_key_path = "~/.ssh/id_ed25519"
|
ssh_private_key_path = "~/.ssh/id_ed25519"
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -70,9 +70,9 @@ variable "prod_private_ip" {
|
|||||||
default = "10.0.1.11"
|
default = "10.0.1.11"
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "ssh_key_name" {
|
variable "ssh_key_names" {
|
||||||
description = "Name of an SSH key already uploaded to your Hetzner Cloud project (Console > Security > SSH Keys)."
|
description = "Names of SSH keys already uploaded to your Hetzner Cloud project (Console > Security > SSH Keys). All are installed on every server."
|
||||||
type = string
|
type = list(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "allowed_ssh_source_ips" {
|
variable "allowed_ssh_source_ips" {
|
||||||
@@ -82,7 +82,7 @@ variable "allowed_ssh_source_ips" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
variable "ssh_private_key_path" {
|
variable "ssh_private_key_path" {
|
||||||
description = "Local path to the private key matching var.ssh_key_name. Used once per server by OpenTofu to upload and run the post-install bootstrap script over SSH immediately after creation (see infra/scripts/bootstrap.sh.tftpl)."
|
description = "Local path to the private key matching one of var.ssh_key_names - only needs to be a key you hold yourself, not all of them. Used once per server by OpenTofu to upload and run the post-install bootstrap script over SSH immediately after creation (see infra/scripts/bootstrap.sh.tftpl)."
|
||||||
type = string
|
type = string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,9 +68,9 @@ architecture-beta
|
|||||||
.
|
.
|
||||||
├── infra/ # OpenTofu (Terraform-compatible) config — provisions the 3 servers, network, firewalls, volumes
|
├── infra/ # OpenTofu (Terraform-compatible) config — provisions the 3 servers, network, firewalls, volumes
|
||||||
│ ├── main.tf # root module: wires network + dev/prod/vpn modules together
|
│ ├── main.tf # root module: wires network + dev/prod/vpn modules together
|
||||||
│ ├── variables.tf # shared inputs (sizes, locations, IP ranges, SSH key name)
|
│ ├── variables.tf # shared inputs (sizes, locations, IP ranges, SSH key names)
|
||||||
│ ├── outputs.tf # pass-through outputs from each host module
|
│ ├── outputs.tf # pass-through outputs from each host module
|
||||||
│ ├── ssh.tf # looks up the SSH key already uploaded to Hetzner Cloud
|
│ ├── ssh.tf # looks up each SSH key already uploaded to Hetzner Cloud
|
||||||
│ ├── versions.tf # provider requirements
|
│ ├── versions.tf # provider requirements
|
||||||
│ ├── terraform.tfvars.example
|
│ ├── terraform.tfvars.example
|
||||||
│ ├── scripts/
|
│ ├── scripts/
|
||||||
@@ -98,7 +98,7 @@ Each of `services/dev`, `services/prod`, `services/vpn` follows the same convent
|
|||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- A [Hetzner Cloud](https://console.hetzner.cloud/) project and API token
|
- A [Hetzner Cloud](https://console.hetzner.cloud/) project and API token
|
||||||
- An SSH key uploaded to that project (Console → Security → SSH Keys), and the matching private key available locally (OpenTofu uses it once per server to run the post-install bootstrap — see below)
|
- One or more SSH keys uploaded to that project (Console → Security → SSH Keys) - all are installed on every server - plus the private key matching one of them available locally (OpenTofu uses it once per server to run the post-install bootstrap — see below)
|
||||||
- [OpenTofu](https://opentofu.org/docs/intro/install/) `>= 1.6.0`
|
- [OpenTofu](https://opentofu.org/docs/intro/install/) `>= 1.6.0`
|
||||||
- DNS records for the domains in [Service inventory](#service-inventory) pointed at the relevant server's public IP
|
- DNS records for the domains in [Service inventory](#service-inventory) pointed at the relevant server's public IP
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ Docker + the Compose plugin no longer need installing by hand — the bootstrap
|
|||||||
cd infra
|
cd infra
|
||||||
export HCLOUD_TOKEN=your-hetzner-api-token # never commit this
|
export HCLOUD_TOKEN=your-hetzner-api-token # never commit this
|
||||||
cp terraform.tfvars.example terraform.tfvars
|
cp terraform.tfvars.example terraform.tfvars
|
||||||
$EDITOR terraform.tfvars # set ssh_key_name and ssh_private_key_path at minimum
|
$EDITOR terraform.tfvars # set ssh_key_names and ssh_private_key_path at minimum
|
||||||
|
|
||||||
tofu init
|
tofu init
|
||||||
tofu plan
|
tofu plan
|
||||||
@@ -131,7 +131,7 @@ Useful outputs: `tofu output dev_ipv4`, `tofu output prod_ipv4`, `tofu output vp
|
|||||||
Immediately after each `hcloud_server` is created, OpenTofu uploads and runs [`infra/scripts/bootstrap.sh.tftpl`](infra/scripts/bootstrap.sh.tftpl) over SSH as `root` (`connection` + `file`/`remote-exec` provisioners in each `modules/<host>/main.tf`). It:
|
Immediately after each `hcloud_server` is created, OpenTofu uploads and runs [`infra/scripts/bootstrap.sh.tftpl`](infra/scripts/bootstrap.sh.tftpl) over SSH as `root` (`connection` + `file`/`remote-exec` provisioners in each `modules/<host>/main.tf`). It:
|
||||||
|
|
||||||
- installs Docker Engine + the Compose plugin
|
- installs Docker Engine + the Compose plugin
|
||||||
- creates a non-root sudo user (`var.deploy_user`, default `deploy`) with the same SSH key as root, in the `sudo` and `docker` groups
|
- creates a non-root sudo user (`var.deploy_user`, default `deploy`) with the same SSH keys as root, in the `sudo` and `docker` groups
|
||||||
- hardens `sshd`: password authentication off, root login restricted to key-only (`PermitRootLogin prohibit-password`)
|
- hardens `sshd`: password authentication off, root login restricted to key-only (`PermitRootLogin prohibit-password`)
|
||||||
- installs and enables `unattended-upgrades` for automatic security patches
|
- installs and enables `unattended-upgrades` for automatic security patches
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user