diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..26a2e82 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +/*.tf* +/.git +/.terraform diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b0afff3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.idea +/.terraform +/terraform.tfstate* +/.terraform.tfstate* diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl new file mode 100644 index 0000000..3f59188 --- /dev/null +++ b/.terraform.lock.hcl @@ -0,0 +1,42 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/hashicorp/random" { + version = "3.6.2" + constraints = "~> 3.3" + hashes = [ + "h1:PXvoOj9gj+Or+9k0tQWCQJKxnsVO0GqnQwVahgwRrsU=", + "zh:1f27612f7099441526d8af59f5b4bdcc35f46915df5d243043d7337ea5a3e38a", + "zh:2a58e66502825db8b4b96116c04bd0323bca1cf1f5752bdd8f9c26feb84d3b1e", + "zh:4f0a4fa479e29de0c3c90146fd58799c097f7a55401cb00560dd4e9b1e6fad9d", + "zh:9c93c0fe6ef685513734527e0c8078636b2cc07591427502a7260f4744b1af1d", + "zh:a466ff5219beb77fb3b18a3d7e7fe30e7edd4d95c8e5c87f4f4e3fe3eeb8c2d7", + "zh:ab33e6176d0c757ddb31e40e01a941e6918ad10f7a786c8e8e4f35e5cff81c96", + "zh:b6eabf377a1c12cb3f9ddd97aacdd5b49c1646dc959074124f81d40fcd216d7e", + "zh:ccec5d03d0d1c0f354be299cdd6a417b2700f1a6781df36bcce77246b2f57e50", + "zh:d2a7945eeb691fdd2b1474da76ddc2d1655e2aedbb14b57f06d4f5123d47adf9", + "zh:ed62351f4ad9d1469c6798b77dee5f63b18b29c473620a0046ba3d4f111b621d", + ] +} + +provider "registry.opentofu.org/kreuzwerker/docker" { + version = "3.0.2" + constraints = "~> 3.0" + hashes = [ + "h1:cT2ccWOtlfKYBUE60/v2/4Q6Stk1KYTNnhxSck+VPlU=", + "zh:15b0a2b2b563d8d40f62f83057d91acb02cd0096f207488d8b4298a59203d64f", + "zh:23d919de139f7cd5ebfd2ff1b94e6d9913f0977fcfc2ca02e1573be53e269f95", + "zh:38081b3fe317c7e9555b2aaad325ad3fa516a886d2dfa8605ae6a809c1072138", + "zh:4a9c5065b178082f79ad8160243369c185214d874ff5048556d48d3edd03c4da", + "zh:5438ef6afe057945f28bce43d76c4401254073de01a774760169ac1058830ac2", + "zh:60b7fadc287166e5c9873dfe53a7976d98244979e0ab66428ea0dea1ebf33e06", + "zh:61c5ec1cb94e4c4a4fb1e4a24576d5f39a955f09afb17dab982de62b70a9bdd1", + "zh:a38fe9016ace5f911ab00c88e64b156ebbbbfb72a51a44da3c13d442cd214710", + "zh:c2c4d2b1fd9ebb291c57f524b3bf9d0994ff3e815c0cd9c9bcb87166dc687005", + "zh:d567bb8ce483ab2cf0602e07eae57027a1a53994aba470fa76095912a505533d", + "zh:e83bf05ab6a19dd8c43547ce9a8a511f8c331a124d11ac64687c764ab9d5a792", + "zh:e90c934b5cd65516fbcc454c89a150bfa726e7cf1fe749790c7480bbeb19d387", + "zh:f05f167d2eaf913045d8e7b88c13757e3cf595dd5cd333057fdafc7c4b7fed62", + "zh:fcc9c1cea5ce85e8bcb593862e699a881bd36dffd29e2e367f82d15368659c3d", + ] +} diff --git a/README.md b/README.md index e69de29..6ee48f6 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,39 @@ +Example Dockerised, Terraformed application deployment onto docker on bare metal +=============================================================================== + +We're gonna run Quassel on bare metal in the most low-effort and high-reward enterprisey way possible. +Why? Because we can. + +## Pre-requisites +We're gonna need some tools: + +* OpenTofu, the replacement for Terraform since Hashicorp changed the licence terms: https://opentofu.org/docs/intro/install/. + * As Tofu is a drop in replacement for Terraform, you can use Terraform if you prefer, and I'll refer to them interchangeably. + +## What have we got going on in here? +The main players: + * database.tf - Configures our Quassel instances Postgres + * quassel.tf - Configures our Quassel instance itself + +The bit-actors: + * docker.tf - Configures the Docker Provider for Tofu + * inputs.tf - Where inputs to this deployment are defined and may have defaults set + * outputs.tf - Where outputs from this deployment are defined, which will be output when we run `tofu apply` or `tofu outputs` + * terraform.tf - Telling Terraform/ToFu what packages we need to run this deployment + * .terraform.lock.hcl - Lock file for Terraform/ToFu, this is here to ensure that we're all using the same versions of the same packages + * terraform.tfvars - Where we may set the variables defined in inputs.tf + +## What happens when we run this? +When we run `tofu apply`: + * Tofu will create a Docker network for our Quassel instance + * Tofu will create a Postgres instance for our Quassel instance + * Tofu will compile our Quassel instance image from the Dockerfile in the quassel directory + * Tofu will create a Quassel instance using that image + * Tofu will output the IP address of the Quassel instance and postgres instance + +## How do we run this? +Did you give `tofu apply` a go yet? + +## What we're gonna end up with: + +`DIAGRAM GOES HERE` diff --git a/database.tf b/database.tf new file mode 100644 index 0000000..3496c0a --- /dev/null +++ b/database.tf @@ -0,0 +1,62 @@ +# Find our latest postgres 16 image +data "docker_registry_image" "postgres_quassel" { + name = "postgres:16" +} + +# Generate a random password for our database +resource "random_password" "quassel_db_password" { + length = 32 + special = false +} + +# Create a volume for our database data to live in +resource "docker_volume" "quassel_db" { + name = "${var.docker_prefix}-quassel-db" +} + +# Create our database service +resource "docker_service" "quassel_db" { + name = "${var.docker_prefix}-quassel-db" + task_spec { + container_spec { + # We've got our image from the registry... + image = "${data.docker_registry_image.postgres_quassel.name}@${data.docker_registry_image.postgres_quassel.sha256_digest}" + # And we're going to set some environment variables + env = { + POSTGRES_USER = local.pg_username + POSTGRES_DB = local.pg_database + POSTGRES_PASSWORD = local.pg_password + } + # We're going to define a nice healthcheck that will check that postgres is alive and well + healthcheck { + # Effectively this is running 'pg_isready -d postgres -U postgres' on the commandline inside the container and if it returns 0, the container is healthy, anything else is failure + test = ["CMD-SHELL", "pg_isready", "-d", local.pg_database, "-U", local.pg_username] + interval = "5s" + start_period = "15s" + } + # And we're going to mount our data volume to the container so that the data persists between restarts + mounts { + target = "/var/lib/postgresql/data" + type = "volume" + source = docker_volume.quassel_db.id + } + } + # And attach our network so that the quassel service can talk to the database + networks_advanced { + name = docker_network.quassel.id + } + } + # And we're going to wait for it to be up and running before we move on + converge_config { + delay = "5s" # Wait 5 seconds between checks + timeout = "2m" # Give up after 2 minutes + } + endpoint_spec { + ports { + target_port = local.pg_port_internal + published_port = local.pg_port_external + protocol = "tcp" + publish_mode = "ingress" + } + } +} diff --git a/docker.tf b/docker.tf new file mode 100644 index 0000000..fcad5df --- /dev/null +++ b/docker.tf @@ -0,0 +1,3 @@ +provider "docker" { + host = "ssh://california.ti" +} diff --git a/inputs.tf b/inputs.tf new file mode 100644 index 0000000..e4d3f24 --- /dev/null +++ b/inputs.tf @@ -0,0 +1,7 @@ +variable "docker_prefix" { + description = "Prefix for all docker resources.. We're not alone on this box, so we best avoid collisions" +} +variable "tz" { + description = "Timezone for the server" + default = "Europe/Amsterdam" +} diff --git a/locals.tf b/locals.tf new file mode 100644 index 0000000..b33691d --- /dev/null +++ b/locals.tf @@ -0,0 +1,15 @@ +locals { + # Lets just take a moment to make some ease of life variables + + # Quassel + quassel_port = random_integer.quassel_port.result # Lets just put the port in a variable so we can use it in multiple places + + # database + pg_username = "postgres" + pg_database = "postgres" + pg_password = random_password.quassel_db_password.result + pg_hostname = docker_service.quassel_db.name + # We're going to use the quassel port + 1 for the postgres port because why not.. But this is only for external access, inside the internal network it will still be 5432! + pg_port_internal = 5432 + pg_port_external = local.quassel_port + 1 +} diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..ce4a1df --- /dev/null +++ b/outputs.tf @@ -0,0 +1,15 @@ +output "quassel" { + value = { + hostname = docker_service.quassel_db.name + port = local.quassel_port + } +} +output "postgres" { + value = { + hostname = docker_service.quassel_db.name + port = local.pg_port_external + username = local.pg_username + password = nonsensitive(local.pg_password) + database = local.pg_database + } +} diff --git a/quassel.tf b/quassel.tf new file mode 100644 index 0000000..f45794c --- /dev/null +++ b/quassel.tf @@ -0,0 +1,71 @@ +# Pick a random port to use for our uplink port. +resource "random_integer" "quassel_port" { + max = 65535 + min = 1024 +} + +# Build our latest quassel docker image. +resource "docker_image" "quassel" { + name = "${var.docker_prefix}-quassel" + build { + context = "${path.module}/quassel" + } + triggers = { + dir_sha1 = sha1(join("", [for f in fileset(path.module, "quassel/*") : filesha1(f)])) + } +} + +# Create a network for our quassel service and postgres service to communicate upon +resource "docker_network" "quassel" { + name = "${var.docker_prefix}-quassel" + driver = "overlay" # We're using overlay networking because its fuckin' rad. +} + +# Create our Quassel docker service. +resource "docker_service" "quassel" { + name = "${var.docker_prefix}-quassel" + + # We need the database to be present for this container to work, so we can explicitly tell TF about it here + depends_on = [docker_service.quassel_db] + + # We're going to define the task specification + task_spec { + # Which contains a container specification + container_spec { + # Which has a docker image set + #image = "${data.docker_registry_image.quassel.name}@${data.docker_registry_image.quassel.sha256_digest}" + image = docker_image.quassel.name + env = { + # And a bunch of environment variables as per the upstream documentation. + PUID = 1000 + PGID = 1000 + TZ = var.tz + RUN_OPTS = "--config-from-environment" + DB_BACKEND = "PostgreSQL" + DB_PGSQL_USERNAME = local.pg_username + DB_PGSQL_PASSWORD = local.pg_password + DB_PGSQL_HOSTNAME = local.pg_hostname + DB_PGSQL_PORT = local.pg_port_internal + AUTH_AUTHENTICATOR = "Database" + } + } + # Attach our task to the network we created earlier + networks_advanced { + name = docker_network.quassel.id + } + } + # Setting a converge config means that we will wait for the service to be up and running (and reporting it is healthy) before moving on. + converge_config { + delay = "5s" # Wait 5 seconds between checks + timeout = "2m" # Give up after 2 minutes + } + endpoint_spec { + # Configure our service to listen on a random port on the ingress network (which means any node in the swarm will redirect the traffic to (an instance of) this service) + ports { + target_port = 4242 # default quassel port on the container + published_port = local.quassel_port # Use the random port we generated earlier + protocol = "tcp" + publish_mode = "ingress" # Its that fwicked cool sweet awesome overlay network again, but this time ingress from the outside of the cluster + } + } +} diff --git a/quassel/Dockerfile b/quassel/Dockerfile new file mode 100644 index 0000000..143845f --- /dev/null +++ b/quassel/Dockerfile @@ -0,0 +1,82 @@ +FROM ghcr.io/linuxserver/baseimage-alpine:3.18 as build-stage + +# build time arguements +ARG CXXFLAGS="\ + -D_FORTIFY_SOURCE=2 \ + -Wp,-D_GLIBCXX_ASSERTIONS \ + -fstack-protector-strong \ + -fPIE -pie -Wl,-z,noexecstack \ + -Wl,-z,relro -Wl,-z,now" +ARG QUASSEL_RELEASE +# install build packages +RUN \ + apk add --no-cache \ + boost-dev \ + build-base \ + cmake \ + dbus-dev \ + icu-dev \ + openssl-dev \ + openldap-dev \ + qt5-qtbase-dev \ + qt5-qtscript-dev \ + qt5-qtbase-postgresql \ + qt5-qtbase-sqlite \ + qca-dev \ + zlib-dev + +# fetch source +RUN \ + mkdir -p \ + /tmp/quassel-src/build && \ + if [ -z ${QUASSEL_RELEASE+x} ]; then \ + QUASSEL_RELEASE=$(curl -sX GET "https://api.github.com/repos/quassel/quassel/releases/latest" \ + | jq -r .tag_name); \ + fi && \ + curl -o \ + /tmp/quassel.tar.gz -L \ + "https://github.com/quassel/quassel/archive/${QUASSEL_RELEASE}.tar.gz" && \ + tar xf \ + /tmp/quassel.tar.gz -C \ + /tmp/quassel-src --strip-components=1 + +# build package +RUN \ + cd /tmp/quassel-src/build && \ + cmake \ + -DCMAKE_BUILD_TYPE="Release" \ + -DCMAKE_INSTALL_PREFIX=/usr \ + -DWANT_CORE=ON \ + -DWANT_MONO=OFF \ + -DWANT_QTCLIENT=OFF \ + -DWITH_KDE=OFF \ + /tmp/quassel-src && \ + make -j2 && \ + make DESTDIR=/build/quassel install + +FROM ghcr.io/linuxserver/baseimage-alpine:3.18 + +# set environment variables +ENV HOME /config + +# install runtime packages +RUN \ + apk add --no-cache \ + icu-libs \ + openssl \ + qt5-qtbase \ + qt5-qtbase-postgresql \ + qt5-qtbase-sqlite \ + qt5-qtscript \ + libqca \ + openldap + +# copy artifacts build stage +COPY --from=build-stage /build/quassel/usr/ /usr/ + +# add local files +COPY root/ / + +# ports and volumes +VOLUME /config +EXPOSE 4242 diff --git a/quassel/root/etc/s6-overlay/s6-rc.d/init-config-end/dependencies.d/init-quassel-config b/quassel/root/etc/s6-overlay/s6-rc.d/init-config-end/dependencies.d/init-quassel-config new file mode 100644 index 0000000..e69de29 diff --git a/quassel/root/etc/s6-overlay/s6-rc.d/init-quassel-config/dependencies.d/init-config b/quassel/root/etc/s6-overlay/s6-rc.d/init-quassel-config/dependencies.d/init-config new file mode 100644 index 0000000..e69de29 diff --git a/quassel/root/etc/s6-overlay/s6-rc.d/init-quassel-config/run b/quassel/root/etc/s6-overlay/s6-rc.d/init-quassel-config/run new file mode 100755 index 0000000..912a086 --- /dev/null +++ b/quassel/root/etc/s6-overlay/s6-rc.d/init-quassel-config/run @@ -0,0 +1,13 @@ +#!/usr/bin/with-contenv bash +# shellcheck shell=bash + +# generate key +if [[ ! -f /config/quasselCert.pem ]]; then + openssl req -x509 -nodes -days 3650 \ + -newkey rsa:4096 -keyout /config/quasselCert.pem -out /config/quasselCert.pem \ + -subj "/CN=Quassel-core" +fi + +# permissions +lsiown -R abc:abc \ + /config diff --git a/quassel/root/etc/s6-overlay/s6-rc.d/init-quassel-config/type b/quassel/root/etc/s6-overlay/s6-rc.d/init-quassel-config/type new file mode 100644 index 0000000..bdd22a1 --- /dev/null +++ b/quassel/root/etc/s6-overlay/s6-rc.d/init-quassel-config/type @@ -0,0 +1 @@ +oneshot diff --git a/quassel/root/etc/s6-overlay/s6-rc.d/init-quassel-config/up b/quassel/root/etc/s6-overlay/s6-rc.d/init-quassel-config/up new file mode 100644 index 0000000..61251f0 --- /dev/null +++ b/quassel/root/etc/s6-overlay/s6-rc.d/init-quassel-config/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/init-quassel-config/run diff --git a/quassel/root/etc/s6-overlay/s6-rc.d/svc-quassel/dependencies.d/init-services b/quassel/root/etc/s6-overlay/s6-rc.d/svc-quassel/dependencies.d/init-services new file mode 100644 index 0000000..e69de29 diff --git a/quassel/root/etc/s6-overlay/s6-rc.d/svc-quassel/notification-fd b/quassel/root/etc/s6-overlay/s6-rc.d/svc-quassel/notification-fd new file mode 100644 index 0000000..00750ed --- /dev/null +++ b/quassel/root/etc/s6-overlay/s6-rc.d/svc-quassel/notification-fd @@ -0,0 +1 @@ +3 diff --git a/quassel/root/etc/s6-overlay/s6-rc.d/svc-quassel/run b/quassel/root/etc/s6-overlay/s6-rc.d/svc-quassel/run new file mode 100755 index 0000000..f3679f5 --- /dev/null +++ b/quassel/root/etc/s6-overlay/s6-rc.d/svc-quassel/run @@ -0,0 +1,7 @@ +#!/usr/bin/with-contenv bash +# shellcheck shell=bash + +exec \ + s6-notifyoncheck -d -n 300 -w 1000 -c "nc -z localhost 4242" \ + s6-setuidgid abc quasselcore \ + --configdir /config ${RUN_OPTS} diff --git a/quassel/root/etc/s6-overlay/s6-rc.d/svc-quassel/type b/quassel/root/etc/s6-overlay/s6-rc.d/svc-quassel/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/quassel/root/etc/s6-overlay/s6-rc.d/svc-quassel/type @@ -0,0 +1 @@ +longrun diff --git a/quassel/root/etc/s6-overlay/s6-rc.d/user/contents.d/init-quassel-config b/quassel/root/etc/s6-overlay/s6-rc.d/user/contents.d/init-quassel-config new file mode 100644 index 0000000..e69de29 diff --git a/quassel/root/etc/s6-overlay/s6-rc.d/user/contents.d/svc-quassel b/quassel/root/etc/s6-overlay/s6-rc.d/user/contents.d/svc-quassel new file mode 100644 index 0000000..e69de29 diff --git a/terraform.tf b/terraform.tf new file mode 100644 index 0000000..448ac0b --- /dev/null +++ b/terraform.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + docker = { + source = "kreuzwerker/docker" + version = "~>3.0" + } + random = { + source = "hashicorp/random" + version = "~>3.3" + } + } +} diff --git a/terraform.tfvars b/terraform.tfvars new file mode 100644 index 0000000..5281e56 --- /dev/null +++ b/terraform.tfvars @@ -0,0 +1 @@ +docker_prefix = "example-deployable"