Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions deploy/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
*.auto.tfvars
*.tfstate
*.tfstate.backup
.terraform/
.terraform.lock.hcl
59 changes: 59 additions & 0 deletions deploy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# NethSecurity deploy

Basic OpenTofu config: create DO droplets (each with a public network and a private VPC network), point DNS at them.

## SSH keys

`sshkeys` (SSH key names already registered in your DO account) get attached to every droplet. This is required by the DO API whenever the image has no cloud-init/agent support — as with a custom OpenWrt/NethSecurity image — DO otherwise refuses to create the droplet, since it has no other way to hand you access to it. The image itself never reads or installs the key (no cloud-init to do so); log in with its own baked-in credentials instead.

## Usage

1. Create `tofu.auto.tfvars` (see `tofu.auto.tfvars.example`):

do_token = "secret"
project = "Aldo"
domain = "al.nethserver.net"
sshkeys = ["yourkey"]
nodes = {
"ns1" = {
hostname = "ns1"
region = "ams3"
size = "s-1vcpu-2gb"
custom_image = {
name = "nethsecurity-8.7.2"
url = "https://updates.nethsecurity.nethserver.org/stable/8.7.2/targets/x86/64/nethsecurity-8.7.2-x86-64-generic-squashfs-combined-efi.img.gz"
}
}
}

`nodes` is a map of node key => droplet spec. Each entry creates one droplet (named `<hostname>.<domain>`), an A record for it, and (per region used) a VPC providing its private network. `size` is optional, default `s-1vcpu-2gb`; do not go below 2GB RAM.

2. Init and apply:

tofu init
tofu apply

Outputs the IP and FQDN of every droplet in `nodes`, keyed by node key.

## Image

Each node sets exactly one of:

- `image`: an existing marketplace/snapshot image slug or ID.
- `custom_image`: import a custom image and use it — scoped to that node's own `region` only, even if another node shares the same region.

### Custom image

Imported from a URL (the DO image-import API only accepts a URL, no local upload):

custom_image = {
name = "nethsecurity-8.7.2"
url = "https://updates.nethsecurity.nethserver.org/stable/8.7.2/targets/x86/64/nethsecurity-8.7.2-x86-64-generic-squashfs-combined-efi.img.gz"
}

Default images (x86/64, generic, squashfs-combined-efi):

| Version | URL |
|---|---|
| 8.7.2 | https://updates.nethsecurity.nethserver.org/stable/8.7.2/targets/x86/64/nethsecurity-8.7.2-x86-64-generic-squashfs-combined-efi.img.gz |
| 8.7.1 | https://updates.nethsecurity.nethserver.org/stable/8.7.1/targets/x86/64/nethsecurity-8.7.1-x86-64-generic-squashfs-combined-efi.img.gz |
82 changes: 82 additions & 0 deletions deploy/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
data "digitalocean_project" "default" {
name = var.project
}

data "digitalocean_domain" "default" {
name = var.domain
}

data "digitalocean_ssh_key" "keys" {
for_each = toset(var.sshkeys)
name = each.value
}

locals {
custom_image_nodes = { for k, n in var.nodes : k => n if n.custom_image != null }
}

# Imported into the node's own region only -- not shared across nodes,
# even if two nodes happen to use the same region.
resource "digitalocean_custom_image" "this" {
for_each = local.custom_image_nodes
name = "${each.value.custom_image.name}-${each.key}"
url = each.value.custom_image.url
regions = [each.value.region]
distribution = "Unknown OS"
}

locals {
image = {
for k, n in var.nodes : k => n.custom_image != null ? digitalocean_custom_image.this[k].id : n.image
}
}

resource "digitalocean_vpc" "private_network" {
for_each = toset([for n in var.nodes : n.region])
name = "${terraform.workspace}-${each.key}-net"
region = each.key
}

# Prevent errors during vpc destroy: wait a few seconds after all
# droplets are destroyed before destroying the private networks.
resource "time_sleep" "vpc_destroy_delay" {
depends_on = [digitalocean_vpc.private_network]
destroy_duration = "10s"
}

resource "digitalocean_droplet" "vps" {
depends_on = [time_sleep.vpc_destroy_delay]
for_each = var.nodes
name = "${each.value.hostname}.${var.domain}"
region = each.value.region
size = each.value.size
image = local.image[each.key]
vpc_uuid = digitalocean_vpc.private_network[each.value.region].id
ssh_keys = [
for k in var.sshkeys : data.digitalocean_ssh_key.keys[k].id
]
}

resource "digitalocean_project_resources" "vps" {
project = data.digitalocean_project.default.id
resources = [for k in keys(var.nodes) : digitalocean_droplet.vps[k].urn]
}

resource "digitalocean_record" "vps" {
for_each = var.nodes
domain = data.digitalocean_domain.default.name
type = "A"
name = each.value.hostname
value = digitalocean_droplet.vps[each.key].ipv4_address
ttl = 300
}

output "droplet_ips" {
description = "Public IPv4 address of each droplet, keyed by node key"
value = { for k, d in digitalocean_droplet.vps : k => d.ipv4_address }
}

output "fqdns" {
description = "FQDN of each droplet, keyed by node key"
value = { for k, n in var.nodes : k => "${n.hostname}.${var.domain}" }
}
18 changes: 18 additions & 0 deletions deploy/provider.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
terraform {
required_providers {
digitalocean = {
source = "digitalocean/digitalocean"
version = "~> 2"
}
}
}

variable "do_token" {
description = "DigitalOcean API token"
type = string
sensitive = true
}

provider "digitalocean" {
token = var.do_token
}
16 changes: 16 additions & 0 deletions deploy/tofu.auto.tfvars.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
do_token = "dop_v1_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
project = "Aldo"
domain = "al.nethserver.net"
sshkeys = ["yourkey"]

nodes = {
"ns1" = {
hostname = "ns1"
region = "ams3"
size = "s-1vcpu-2gb"
custom_image = {
name = "nethsecurity-8.7.2"
url = "https://updates.nethsecurity.nethserver.org/stable/8.7.2/targets/x86/64/nethsecurity-8.7.2-x86-64-generic-squashfs-combined-efi.img.gz"
}
}
}
43 changes: 43 additions & 0 deletions deploy/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
variable "project" {
description = "DigitalOcean project name to attach the droplets to"
type = string
}

variable "domain" {
description = "DNS domain to associate with the droplets"
type = string
}

variable "sshkeys" {
description = <<-EOT
DigitalOcean SSH key names (already registered in your account) to
attach to every droplet. Required by the DO API when the image has no
cloud-init/agent support (e.g. a custom OpenWrt/NethSecurity image) --
DO refuses to create the droplet otherwise, since it has no other way
to hand you access. OpenWrt itself never reads this key (no cloud-init
to inject it); log in with the image's own baked-in credentials.
EOT
type = list(string)
default = []
}

variable "nodes" {
description = <<-EOT
Map of node key => droplet spec. size must be 2GB RAM or more.
Set exactly one of `image` / `custom_image`:
- `image`: an existing marketplace/snapshot image slug or ID.
- `custom_image`: import a custom image from a URL into this node's
region only, and use it.
EOT
type = map(object({
hostname = string
region = string
size = optional(string, "s-1vcpu-2gb")
image = optional(string)
custom_image = optional(object({
name = string
url = string
}))
}))
default = {}
}