feat: Create docker outpost

This commit is contained in:
Tony Du 2025-02-08 18:32:29 -08:00
parent 293e18a821
commit 993a91f380
22 changed files with 448 additions and 191 deletions

View File

@ -1,3 +1,5 @@
dns_server:
admin_username: "{{ secrets.admin_username }}"
admin_password: "{{ secrets.admin_password }}"
portainer_agent_secret: "{{ secrets.portainer_agent_secret }}"

73
ansible/jumper.yml Normal file
View File

@ -0,0 +1,73 @@
---
- name: Install
hosts: jumper
remote_user: ubuntu
vars:
pv_disks:
- /dev/sda
vg_name: vg1
lv_name: pvs
lv_size: +100%FREE
fs_type: ext4
mount_path: /mnt/docker
extra_docker_daemon_options: |
"data-root": "/mnt/docker/docker-root"
tasks:
- import_role: name=dns-client
- import_role: name=lvm
- name: Ensure docker root exists
become: true
file:
path: /mnt/docker/docker-root
state: directory
mode: "0755"
- import_role: name=docker
- name: Run portainer agent
become: true
docker_container:
name: portainer_agent
image: portainer/agent:2.16.2
ports:
- "9001:9001"
env:
AGENT_SECRET: "{{ portainer_agent_secret }}"
restart_policy: unless-stopped
mounts:
- type: bind
source: /var/run/docker.sock
target: /var/run/docker.sock
- type: bind
source: /mnt/docker/docker-root/volumes
target: /var/lib/docker/volumes
- name: Mount extra shares
tags: [extra-shares]
hosts: jumper
vars:
shares:
- src: truenas.local:/mnt/emc14t9/lfs/media
path: /mnt/media
opts: rw,_netdev,hard,intr,nolock
fstype: nfs
state: mounted
tasks:
- name: Ensure mount directory exists
become: true
file:
path: "{{ item.path }}"
state: directory
mode: '0755'
loop: "{{ shares }}"
- name: Mount media share
become: true
mount:
src: "{{ item.src }}"
path: "{{ item.path }}"
opts: "{{ item.opts }}"
state: "{{ item.state | default('mounted') }}"
fstype: "{{ item.fstype }}"
loop: "{{ shares }}"

View File

@ -10,33 +10,5 @@
lv_size: "{{ lvm.lv_size }}"
fs_type: "{{ lvm.fs_type }}"
mount_path: "{{ lvm.mount_path }}"
tasks:
- name: Create a volume group
community.general.lvg:
vg: "{{ vg_name }}"
pvs: "{{ pv_disks }}"
pvresize: yes
- name: Create Logical Volume for data persistence
community.general.lvol:
vg: "{{ vg_name }}"
lv: "{{ lv_name }}"
size: "{{ lv_size }}"
- name: Create filesystem on LV
community.general.filesystem:
fstype: "{{ fs_type }}"
resizefs: true
dev: /dev/mapper/{{ vg_name }}-{{ lv_name }}
- name: Get LV UUID
ansible.builtin.command: lsblk /dev/mapper/{{ vg_name }}-{{ lv_name }} -no UUID
register: lv_uuid
changed_when: false
- name: Mount created filesystem
ansible.posix.mount:
path: "{{ mount_path }}"
src: UUID={{ lv_uuid.stdout }}
state: mounted
fstype: "{{ fs_type }}"
roles:
- lvm

View File

@ -0,0 +1,20 @@
---
- name: Set DNS servers
become: true
tags: [set_dns_servers]
copy:
content: |
[Resolve]
DNS=10.0.123.123
# FallbackDNS=1.1.1.1
dest: /etc/systemd/resolved.conf
register: dns_servers_configuration
- name: Restart systemd-resolved
tags: [set_dns_servers]
service:
name: systemd-resolved
state: restarted
when: dns_servers_configuration.changed
retries: 2

View File

@ -1,52 +1,4 @@
---
- name: Create /etc/docker
become: true
file:
path: /etc/docker
state: directory
mode: '0755'
- name: Set docker daemon settings
become: true
# Otherwise we risk conflicts on the host subnet
copy:
content: |
{
"default-address-pools": [
{
"base": "172.17.0.0/12",
"size": 20
},
{
"base": "172.168.0.0/16",
"size": 24
}
],
"dns": ["10.0.123.123"]
}
dest: /etc/docker/daemon.json
- name: Install dependencies
become: true
apt:
name:
- python3
- python3-pip
- python3-docker
- docker.io
- docker-buildx
- docker-compose-v2
- python3-jsondiff
- apache2-utils
update_cache: yes
- name: Enable docker
become: true
service:
name: docker
state: started
enabled: true
- name: Init swarm
when: inventory_hostname == groups.swarm_managers[0]
become: true

View File

@ -0,0 +1 @@
extra_docker_daemon_options: ""

View File

@ -0,0 +1,51 @@
---
- name: Create /etc/docker
become: true
file:
path: /etc/docker
state: directory
mode: '0755'
- name: Set docker daemon settings
become: true
# Otherwise we risk conflicts on the host subnet
copy:
content: |
{
{{ extra_docker_daemon_options }},
"default-address-pools": [
{
"base": "172.17.0.0/12",
"size": 20
},
{
"base": "172.168.0.0/16",
"size": 24
}
],
"dns": ["10.0.123.123"]
}
dest: /etc/docker/daemon.json
register: docker_daemon_conf
- name: Install dependencies
become: true
apt:
name:
- python3
- python3-pip
- python3-docker
- docker.io
- docker-buildx
- docker-compose-v2
- python3-jsondiff
- apache2-utils
update_cache: yes
- name: Enable docker
become: true
service:
name: docker
state: "{{ 'restarted' if docker_daemon_conf.changed else 'started' }}"
enabled: true

View File

@ -0,0 +1,36 @@
---
- name: Create a volume group
become: true
community.general.lvg:
vg: "{{ vg_name }}"
pvs: "{{ pv_disks }}"
pvresize: yes
- name: Create Logical Volume for data persistence
become: true
community.general.lvol:
vg: "{{ vg_name }}"
lv: "{{ lv_name }}"
size: "{{ lv_size }}"
- name: Create filesystem on LV
become: true
community.general.filesystem:
fstype: "{{ fs_type }}"
resizefs: true
dev: /dev/mapper/{{ vg_name }}-{{ lv_name }}
- name: Get LV UUID
become: true
ansible.builtin.command: lsblk /dev/mapper/{{ vg_name }}-{{ lv_name }} -no UUID
register: lv_uuid
changed_when: false
- name: Mount created filesystem
become: true
ansible.posix.mount:
path: "{{ mount_path }}"
src: UUID={{ lv_uuid.stdout }}
state: mounted
fstype: "{{ fs_type }}"

View File

@ -8,24 +8,6 @@
state: mounted
fstype: nfs
- name: Set DNS servers
become: true
tags: [set_dns_servers]
copy:
content: |
[Resolve]
DNS=10.0.123.123
# FallbackDNS=1.1.1.1
dest: /etc/systemd/resolved.conf
register: dns_servers_configuration
- name: Restart systemd-resolved
tags: [set_dns_servers]
service:
name: systemd-resolved
state: restarted
when: dns_servers_configuration.changed
retries: 2
- import_tasks: traefik.yml
- import_tasks: portainer.yml

View File

@ -3,15 +3,6 @@ networks:
traefik:
external: true
volumes:
portainer_data:
driver: local
driver_opts:
o: bind
type: none
device: {{nfs_mount_path}}/portainer/data
name: portainer_data
services:
agent:
image: portainer/agent:latest
@ -34,7 +25,7 @@ services:
- "9000:9000"
- "8000:8000"
volumes:
- portainer_data:/data
- {{nfs_mount_path}}/portainer/data:/data
networks:
- portainer
- traefik

View File

@ -2,6 +2,9 @@ api:
dashboard: true
insecure: true
# accessLog:
# filePath: /var/log/traefik/access.log
providers:
swarm:
exposedByDefault: false

View File

@ -4,15 +4,6 @@ networks:
attachable: true
name: traefik
volumes:
traefik:
driver: local
driver_opts:
o: bind
type: none
device: {{nfs_mount_path}}/traefik
name: traefik
secrets:
cf_dns_api_token:
file: "{{nfs_mount_path}}/traefik/secrets/cf-dns-api-token.secret"
@ -34,7 +25,7 @@ services:
- "CF_DNS_API_TOKEN_FILE=/run/secrets/cf_dns_api_token"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- traefik:/data
- {{nfs_mount_path}}/traefik:/data
networks:
- traefik
deploy:

View File

@ -3,6 +3,8 @@
hosts: swarm
tags: [swarm-init]
roles:
- dns-client
- docker
- docker-swarm
- swarm-bootstrap

View File

@ -1,16 +1,15 @@
---
version: "3.8"
networks:
traefik:
external: true
media:
# Argh, mounting the configuration on the NFS drive is not a good idea!
# The sqlite databases in these volumes can have pretty high random RW, so
# we should keep these local. But fuck, managing them on a non-network drive
# on Swarm is such a pain...
# Maybe we can get these to connect to a different DB?
volumes:
jellyseerr_config:
transmission_config:
prowlarr_config:
radarr_config:
sonarr_config:
services:
transmission-openvpn:
@ -21,7 +20,7 @@ services:
- traefik
- media
volumes:
- ${TRANSMISSION_CONFIG_DIRECTORY:-/mnt/stingray/media/transmission-openvpn/config}:/config
- transmission_config:/config
- ${MEDIA_DIRECTORY:-/mnt/media}/torrents:${MEDIA_DIRECTORY:-/mnt/media}/torrents
environment:
- PUID=${PUID:-8796}
@ -43,18 +42,14 @@ services:
- TRANSMISSION_WEB_UI=flood-for-transmission
- TZ=America/Vancouver
- LOCAL_NETWORK=10.0.0.0/16
labels:
- "traefik.enable=true"
- "traefik.http.routers.transmission_openvpn.rule=Host(`${TRANSMISSION_HOST:-tovpn.jumper.mnke.org}`)"
- "traefik.http.routers.transmission_openvpn.entrypoints=websecure"
- "traefik.http.routers.transmission_openvpn.tls=true"
- "traefik.http.services.transmission_openvpn.loadbalancer.server.port=9091"
- "traefik.docker.network=traefik"
deploy:
labels:
- "traefik.enable=true"
- "traefik.http.routers.transmission_openvpn.rule=Host(`${TRANSMISSION_HOST:-tovpn.stingray.mnke.org}`)"
- "traefik.http.routers.transmission_openvpn.entrypoints=websecure"
- "traefik.http.routers.transmission_openvpn.tls.certresolver=letsencrypt"
- "traefik.http.services.transmission_openvpn.loadbalancer.server.port=9091"
- "traefik.swarm.network=traefik"
mode: replicated
replicas: 1
placement:
constraints: [node.role != manager]
resources:
limits:
cpus: '0.50'
@ -73,17 +68,15 @@ services:
- media
- traefik
volumes:
- ${PROWLARR_CONFIG_DIRECTORY:-/mnt/stingray/media/prowlarr/config}:/config
- prowlarr_config:/config
labels:
- "traefik.enable=true"
- "traefik.http.routers.prowlarr.rule=Host(`${PROWLARR_HOST:-prowlarr.jumper.mnke.org}`)"
- "traefik.http.routers.prowlarr.entrypoints=websecure"
- "traefik.http.routers.prowlarr.tls=true"
- "traefik.http.services.prowlarr.loadbalancer.server.port=9696"
- "traefik.docker.network=traefik"
deploy:
labels:
- "traefik.enable=true"
- "traefik.http.routers.prowlarr.rule=Host(`${PROWLARR_HOST:-prowlarr.stingray.mnke.org}`)"
- "traefik.http.routers.prowlarr.entrypoints=websecure"
- "traefik.http.routers.prowlarr.tls.certresolver=letsencrypt"
- "traefik.http.services.prowlarr.loadbalancer.server.port=9696"
- "traefik.swarm.network=traefik"
placement:
constraints: [node.role != manager]
resources:
limits:
cpus: '0.5'
@ -102,20 +95,20 @@ services:
- media
- traefik
volumes:
- ${RADARR_CONFIG_DIRECTORY:-/mnt/stingray/media/radarr/config}:/config
- radarr_config:/config
- ${MEDIA_DIRECTORY:-/mnt/media}:${MEDIA_DIRECTORY:-/mnt/media}
depends_on:
transmission-openvpn:
condition: service_healthy
labels:
- "traefik.enable=true"
- "traefik.http.routers.radarr.rule=Host(`${RADARR_HOST:-radarr.jumper.mnke.org}`)"
- "traefik.http.routers.radarr.entrypoints=websecure"
- "traefik.http.routers.radarr.tls=true"
- "traefik.http.services.radarr.loadbalancer.server.port=7878"
- "traefik.docker.network=traefik"
deploy:
labels:
- "traefik.enable=true"
- "traefik.http.routers.radarr.rule=Host(`${RADARR_HOST:-radarr.stingray.mnke.org}`)"
- "traefik.http.routers.radarr.entrypoints=websecure"
- "traefik.http.routers.radarr.tls.certresolver=letsencrypt"
- "traefik.http.services.radarr.loadbalancer.server.port=7878"
- "traefik.swarm.network=traefik"
mode: replicated
replicas: 1
placement:
constraints: [node.role != manager]
resources:
limits:
cpus: '0.5'
@ -134,20 +127,19 @@ services:
- media
- traefik
volumes:
- ${SONARR_CONFIG_DIRECTORY:-/mnt/stingray/media/sonarr/config}:/config
- sonarr_config:/config
- ${MEDIA_DIRECTORY:-/mnt/media}:${MEDIA_DIRECTORY:-/mnt/media}
depends_on:
transmission-openvpn:
condition: service_healthy
labels:
- "traefik.enable=true"
- "traefik.http.routers.sonarr.rule=Host(`${SONARR_HOST:-sonarr.jumper.mnke.org}`)"
- "traefik.http.routers.sonarr.entrypoints=websecure"
- "traefik.http.routers.sonarr.tls=true"
- "traefik.http.services.sonarr.loadbalancer.server.port=8989"
- "traefik.docker.network=traefik"
deploy:
labels:
- "traefik.enable=true"
- "traefik.http.routers.sonarr.rule=Host(`${SONARR_HOST:-sonarr.stingray.mnke.org}`)"
- "traefik.http.routers.sonarr.entrypoints=websecure"
- "traefik.http.routers.sonarr.tls.certresolver=letsencrypt"
- "traefik.http.services.sonarr.loadbalancer.server.port=8989"
- "traefik.swarm.network=traefik"
mode: replicated
replicas: 1
placement:
constraints: [node.role != manager]
resources:
limits:
cpus: '0.5'
@ -168,10 +160,6 @@ services:
# ports:
# - "${PORT:-8191}:8191"
deploy:
mode: replicated
replicas: 1
placement:
constraints: [node.role != manager]
resources:
limits:
cpus: '0.25'
@ -183,25 +171,26 @@ services:
jellyseerr:
image: fallenbagel/jellyseerr
environment:
- LOG_LEVEL=debug
# - LOG_LEVEL=debug
- TZ=America/Vancouver
networks:
- media
- traefik
volumes:
- ${JELLYSEERR_CONFIG_DIRECTORY:-/mnt/stingray/media/jellyseerr/config}:/app/config
- jellyseerr_config:/app/config
depends_on:
radarr:
condition: service_started
sonarr:
condition: service_started
labels:
- "traefik.enable=true"
- "traefik.http.routers.jellyseerr.rule=Host(`${JELLYSEERR_HOST:-seerr.jumper.mnke.org}`)"
- "traefik.http.routers.jellyseerr.entrypoints=websecure"
- "traefik.http.routers.jellyseerr.tls=true"
- "traefik.http.services.jellyseerr.loadbalancer.server.port=5055"
- "traefik.docker.network=traefik"
deploy:
labels:
- "traefik.enable=true"
- "traefik.http.routers.jellyseerr.rule=Host(`${JELLYSEERR_HOST:-seerr.stingray.mnke.org}`)"
- "traefik.http.routers.jellyseerr.entrypoints=websecure"
- "traefik.http.routers.jellyseerr.tls.certresolver=letsencrypt"
- "traefik.http.services.jellyseerr.loadbalancer.server.port=5055"
- "traefik.swarm.network=traefik"
mode: replicated
replicas: 1
placement:
constraints: [node.role != manager]
resources:
limits:
cpus: '0.5'

View File

@ -0,0 +1,68 @@
networks:
traefik:
attachable: true
name: traefik
volumes:
traefik:
services:
traefik:
image: traefik:v3.3
command:
# - "--log.level=DEBUG"
- "--api.dashboard=true"
- "--api.insecure=true"
- "--providers.docker.exposedByDefault=false"
- "--providers.docker.endpoint=unix:///var/run/docker.sock"
# HTTP
- "--entrypoints.web.address=:80"
# Redirect to HTTPS
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--entrypoints.websecure.address=:443"
# TLS
- "--certificatesresolvers.letsencrypt.acme.dnschallenge=true"
- "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare"
- "--certificatesresolvers.letsencrypt.acme.dnschallenge.resolvers[0]=1.1.1.1"
- "--certificatesresolvers.letsencrypt.acme.dnschallenge.resolvers[1]=1.0.0.1"
- "--certificatesresolvers.letsencrypt.acme.caserver=https://acme-v02.api.letsencrypt.org/directory"
- "--certificatesresolvers.letsencrypt.acme.email=${CF_EMAIL}"
- "--certificatesresolvers.letsencrypt.acme.storage=/data/acme.json"
ports:
- "80:80"
- "443:443"
- "8080:8080"
environment:
- "CF_EMAIL=${CF_EMAIL:-tonydu121@hotmail.com}"
- "CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- traefik:/data
networks:
- traefik
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=Host(`traefik.jumper.mnke.org`)"
- "traefik.http.routers.api.entrypoints=websecure"
- "traefik.http.routers.api.service=api@internal"
- "traefik.http.routers.api.middlewares=auth"
- "traefik.http.routers.api.tls.certresolver=letsencrypt"
- "traefik.http.routers.api.tls.domains[0].main=mnke.org"
- "traefik.http.routers.api.tls.domains[0].sans=*.mnke.org"
- "traefik.http.routers.api.tls.domains[1].main=jumper.mnke.org"
- "traefik.http.routers.api.tls.domains[1].sans=*.jumper.mnke.org"
- "traefik.http.middlewares.auth.basicauth.users=${TRAEFIK_HTPASSWD}"
- "traefik.http.services.dummy-svc.loadbalancer.server.port=9999"
whoami:
image: "traefik/whoami"
networks:
- traefik
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami.rule=Host(`whoami.jumper.mnke.org`)"
- "traefik.http.routers.whoami.entrypoints=websecure"
- "traefik.http.routers.whoami.tls=true"
- "traefik.http.services.whoami.loadbalancer.server.port=80"
- "traefik.docker.network=traefik"

View File

@ -4,7 +4,7 @@ locals {
}
module "dns_server" {
source = "${path.module}/modules/dns-server"
source = "./modules/dns-server"
vm_id = "200"

View File

@ -1,10 +1,10 @@
module "docker_swarm_stingray" {
source = "${path.module}/modules/docker-swarm"
source = "./modules/docker-swarm"
swarm_name = "stingray"
vm_id_prefix = "8"
subnet_cidr = "10.0.42.0/24"
gateway = "10.0.0.1"
gateway = var.gateway
manager_count = 3
worker_count = 3
dns_server_ip = local.dns_server_ip

View File

@ -1,11 +1,11 @@
module "k8s_dolo" {
source = "${path.module}/modules/k8s"
source = "./modules/k8s"
started = true
cluster_name = "dolo"
vm_id_prefix = "1"
subnet_cidr = "10.0.185.0/24"
gateway = "10.0.0.1"
gateway = var.gateway
control_plane_count = 3
worker_count = 3
storage_worker_count = 3
@ -20,7 +20,7 @@ module "k8s_dolo" {
}
module "k8s_folly" {
source = "${path.module}/modules/k8s"
source = "./modules/k8s"
started = false

115
tf/outpost.tf Normal file
View File

@ -0,0 +1,115 @@
resource "proxmox_virtual_environment_file" "jumper" {
content_type = "snippets"
datastore_id = var.proxmox_image_storage
node_name = "pve"
source_raw {
data = <<EOF
${local.common_cloud_init}
hostname: jumper
EOF
file_name = "jumper.cloud-config.yaml"
}
}
resource "proxmox_virtual_environment_vm" "jumper_storage_dummy" {
name = "jumper-dummy"
description = "Managed by Terraform"
tags = ["terraform", "disk-dummy"]
node_name = "pve"
vm_id = 7101
started = false
on_boot = false
disk {
datastore_id = var.proxmox_vm_storage
file_format = "qcow2"
interface = "scsi0"
size = 32
}
}
resource "proxmox_virtual_environment_vm" "jumper" {
# Don't forget to change the cloud init file if this is changed
name = "jumper"
description = "Managed by Terraform"
tags = ["terraform", "ubuntu", "outpost"]
node_name = "pve"
vm_id = 7001
cpu {
cores = 4
type = "host"
}
memory {
dedicated = 4096
floating = 4096
}
agent {
enabled = true
}
startup {
order = "1"
up_delay = "60"
down_delay = "60"
}
disk {
datastore_id = var.proxmox_vm_storage
file_id = proxmox_virtual_environment_file.ubuntu_cloud_image.id
interface = "virtio0"
iothread = true
discard = "on"
size = 32
file_format = "qcow2"
}
initialization {
ip_config {
ipv4 {
address = "10.0.44.2/16"
gateway = var.gateway
}
}
datastore_id = var.proxmox_image_storage
user_data_file_id = proxmox_virtual_environment_file.jumper.id
}
dynamic "disk" {
for_each = { for idx, val in proxmox_virtual_environment_vm.jumper_storage_dummy.disk : idx => val }
iterator = data_disk
content {
datastore_id = data_disk.value["datastore_id"]
path_in_datastore = data_disk.value["path_in_datastore"]
file_format = data_disk.value["file_format"]
size = data_disk.value["size"]
# assign from scsi1 and up
interface = "scsi${data_disk.key + 1}"
}
}
network_device {
bridge = "vmbr0"
}
operating_system {
type = "l26"
}
lifecycle {
}
}
resource "ansible_host" "jumper" {
name = "jumper.local"
groups = ["jumper", "portainer_agent"]
}

View File

@ -8,6 +8,10 @@ terraform {
source = "hashicorp/local"
version = "2.5.2"
}
ansible = {
source = "ansible/ansible"
version = "1.3.0"
}
}
}

View File

@ -8,6 +8,10 @@ variable "proxmox_api_token" {
description = "Proxmox API token bpg proxmox provider with ID and token"
}
variable "gateway" {
type = string
}
variable "s3_backend_endpoint" {
type = string
# example = "http://s3.local"

View File

@ -1,5 +1,6 @@
proxmox_image_storage = "proxmox-local-directory"
proxmox_vm_storage = "proxmox-local-directory"
gateway = "10.0.0.1"
username = "tony"
ssh_import_id = "gh:tonyd33"