From ecf99a5c5c52c957d91ba42905c58667ddd17570 Mon Sep 17 00:00:00 2001 From: Sheogorath <sheogorath@shivering-isles.com> Date: Wed, 1 Mar 2023 15:57:14 +0100 Subject: [PATCH] feat(terraform): Add hedgedoc-demo instance terraform resources As I still maintain the demo instance, it's useful to integrate the demo instance into the current terraform configuration, this allows it to benefit from further developement of this repository. The current README and variable descriptions should help anyone interested in creating their own demo instance equivalent, to deploy this to their hetzer cloud account. Even if not, it might provide some nice inspiration regarding the instance handling. --- .sops.yaml | 3 + renovate.json | 8 + terraform/hedgedoc-demo/.terraform.lock.hcl | 104 +++++++++++ terraform/hedgedoc-demo/README.md | 55 ++++++ terraform/hedgedoc-demo/dns.tf | 17 ++ terraform/hedgedoc-demo/firewall.tf | 62 +++++++ terraform/hedgedoc-demo/main.tf | 41 +++++ terraform/hedgedoc-demo/provider.tf | 16 ++ terraform/hedgedoc-demo/ssh.tf | 2 + .../hedgedoc-demo/templates/cloud-init.tpl | 169 ++++++++++++++++++ .../templates/docker-compose.tpl | 165 +++++++++++++++++ terraform/hedgedoc-demo/terraform.enc.tfvars | 21 +++ terraform/hedgedoc-demo/variables.tf | 137 ++++++++++++++ terraform/hedgedoc-demo/versions.tf | 1 + 14 files changed, 801 insertions(+) create mode 100644 terraform/hedgedoc-demo/.terraform.lock.hcl create mode 100644 terraform/hedgedoc-demo/README.md create mode 100644 terraform/hedgedoc-demo/dns.tf create mode 100644 terraform/hedgedoc-demo/firewall.tf create mode 100644 terraform/hedgedoc-demo/main.tf create mode 100644 terraform/hedgedoc-demo/provider.tf create mode 100644 terraform/hedgedoc-demo/ssh.tf create mode 100644 terraform/hedgedoc-demo/templates/cloud-init.tpl create mode 100644 terraform/hedgedoc-demo/templates/docker-compose.tpl create mode 100644 terraform/hedgedoc-demo/terraform.enc.tfvars create mode 100644 terraform/hedgedoc-demo/variables.tf create mode 120000 terraform/hedgedoc-demo/versions.tf diff --git a/.sops.yaml b/.sops.yaml index 83a51dca8..b05213c37 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -4,3 +4,6 @@ creation_rules: pgp: >- 286791FB6648539775DB31B8FCB98C2A3EC6F601, B137EE1549DFAF960DD1E2B15147025FB9F09E07 + - path_regex: terraform/.*\.tfvars + pgp: >- + 286791FB6648539775DB31B8FCB98C2A3EC6F601 diff --git a/renovate.json b/renovate.json index 58190e317..dfe6c3469 100644 --- a/renovate.json +++ b/renovate.json @@ -31,6 +31,14 @@ "datasourceTemplate": "docker", "versioningTemplate": "docker" }, + { + "fileMatch": ["terraform/hedgedoc-demo/templates/docker-compose.tpl$"], + "matchStrings": [ + "# renovate:\\n\\s+image: \"?'?(?<depName>[^:\\s]+?):(?<currentValue>[^\"]*?)\"?'?\\s" + ], + "datasourceTemplate": "docker", + "versioningTemplate": "docker" + }, { "fileMatch": ["Chart\\.yaml$"], "matchStrings": [ diff --git a/terraform/hedgedoc-demo/.terraform.lock.hcl b/terraform/hedgedoc-demo/.terraform.lock.hcl new file mode 100644 index 000000000..dc7101c95 --- /dev/null +++ b/terraform/hedgedoc-demo/.terraform.lock.hcl @@ -0,0 +1,104 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/cloudflare/cloudflare" { + version = "3.34.0" + constraints = "3.34.0" + hashes = [ + "h1:4jOI2ZUyp/EvIVE9rNvCMTqrIaG7j7XxUD1i43xD0Jg=", + "zh:03729b0fcf189e732aca54452a105d82fec839580cb5d0137317af9163e0e4dd", + "zh:121b16a779e9f2fe8c96e98f32514ee9228346fc240ce12c3fb440958b93d127", + "zh:14509f521845eedd57a8791d76958e50bea4928760a152cd853e43f2c81a329b", + "zh:273336ec2bc59ab90916706c074be27f3fe6ab42addc61a354a0ef5e10c2efa5", + "zh:54931c30f71666856c5d749698264c15196103667c87d961f3d293ff8a5c3237", + "zh:58a35eea3b9e1d2f39d7b5b1c6cf107b70eacdf5891017d6667902903db3bd94", + "zh:5ec958afe392a76a1fea262d9070df839c4d811fc6ffd613a37f8b939ab159ef", + "zh:7c24c0572aa9beee20a33cb18ac54d5088a09653e94664a9f74a9af2ae0e3554", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:9248c43f795dbe54e07c6dbc2fb8e2f20aeac8f21ec91373d52b9975f285ba7e", + "zh:b09abd506601b7c3e0b3bfde0b8b9e1aed7f52b5ad629ef2865b8321852409c7", + "zh:e00032df4cd4aad12adf3b7955fca3d1baa8bff9436c775588417da171a4e1d9", + "zh:e4a8812770914d6ce9d1f8399d702e3fb0ecc4bfd6220ba015fcb3884b243c69", + "zh:f2ad0991ef0820b3fc5bd0a500be4dceffe0b5b2ac6c9c5fd17cbb350f2f1209", + "zh:fea3a9dfb1e752dc2864028049a4af05fabf7b62eb57fff26d139a424e3476fd", + ] +} + +provider "registry.terraform.io/hashicorp/local" { + version = "2.3.0" + constraints = "2.3.0" + hashes = [ + "h1:+l9ZTDGmGdwnuYI5ftUjwP8UgoLw4f4V9xoCzal4LW0=", + "zh:1f1920b3f78c31c6b69cdfe1e016a959667c0e2d01934e1a084b94d5a02cd9d2", + "zh:550a3cdae0ddb350942624e7b2e8b31d28bc15c20511553432413b1f38f4b214", + "zh:68d1d9ccbfce2ce56b28a23b22833a5369d4c719d6d75d50e101a8a8dbe33b9b", + "zh:6ae3ad6d865a906920c313ec2f413d080efe32c230aca711fd106b4cb9022ced", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:a0f413d50f54124057ae3dcd9353a797b84e91dc34bcf85c34a06f8aef1f9b12", + "zh:a2ac6d4088ceddcd73d88505e18b8226a6e008bff967b9e2d04254ef71b4ac6b", + "zh:a851010672e5218bdd4c4ea1822706c9025ef813a03da716d647dd6f8e2cffb0", + "zh:aa797561755041ef2fad99ee9ffc12b5e724e246bb019b21d7409afc2ece3232", + "zh:c6afa960a20d776f54bb1fc260cd13ead17280ebd87f05b9abcaa841ed29d289", + "zh:df0975e86b30bb89717b8c8d6d4690b21db66de06e79e6d6cfda769f3304afe6", + "zh:f0d3cc3da72135efdbe8f4cfbfb0f2f7174827887990a5545e6db1981f0d3a7c", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.4.3" + hashes = [ + "h1:xZGZf18JjMS06pFa4NErzANI98qi59SEcBsOcS2P2yQ=", + "zh:41c53ba47085d8261590990f8633c8906696fa0a3c4b384ff6a7ecbf84339752", + "zh:59d98081c4475f2ad77d881c4412c5129c56214892f490adf11c7e7a5a47de9b", + "zh:686ad1ee40b812b9e016317e7f34c0d63ef837e084dea4a1f578f64a6314ad53", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:84103eae7251384c0d995f5a257c72b0096605048f757b749b7b62107a5dccb3", + "zh:8ee974b110adb78c7cd18aae82b2729e5124d8f115d484215fd5199451053de5", + "zh:9dd4561e3c847e45de603f17fa0c01ae14cae8c4b7b4e6423c9ef3904b308dda", + "zh:bb07bb3c2c0296beba0beec629ebc6474c70732387477a65966483b5efabdbc6", + "zh:e891339e96c9e5a888727b45b2e1bb3fcbdfe0fd7c5b4396e4695459b38c8cb1", + "zh:ea4739860c24dfeaac6c100b2a2e357106a89d18751f7693f3c31ecf6a996f8d", + "zh:f0c76ac303fd0ab59146c39bc121c5d7d86f878e9a69294e29444d4c653786f8", + "zh:f143a9a5af42b38fed328a161279906759ff39ac428ebcfe55606e05e1518b93", + ] +} + +provider "registry.terraform.io/hashicorp/template" { + version = "2.2.0" + constraints = "2.2.0" + hashes = [ + "h1:94qn780bi1qjrbC3uQtjJh3Wkfwd5+tTtJHOb7KTg9w=", + "zh:01702196f0a0492ec07917db7aaa595843d8f171dc195f4c988d2ffca2a06386", + "zh:09aae3da826ba3d7df69efeb25d146a1de0d03e951d35019a0f80e4f58c89b53", + "zh:09ba83c0625b6fe0a954da6fbd0c355ac0b7f07f86c91a2a97849140fea49603", + "zh:0e3a6c8e16f17f19010accd0844187d524580d9fdb0731f675ffcf4afba03d16", + "zh:45f2c594b6f2f34ea663704cc72048b212fe7d16fb4cfd959365fa997228a776", + "zh:77ea3e5a0446784d77114b5e851c970a3dde1e08fa6de38210b8385d7605d451", + "zh:8a154388f3708e3df5a69122a23bdfaf760a523788a5081976b3d5616f7d30ae", + "zh:992843002f2db5a11e626b3fc23dc0c87ad3729b3b3cff08e32ffb3df97edbde", + "zh:ad906f4cebd3ec5e43d5cd6dc8f4c5c9cc3b33d2243c89c5fc18f97f7277b51d", + "zh:c979425ddb256511137ecd093e23283234da0154b7fa8b21c2687182d9aea8b2", + ] +} + +provider "registry.terraform.io/hetznercloud/hcloud" { + version = "1.36.2" + constraints = "1.36.2" + hashes = [ + "h1:VO/dl+g5NfJd436hmT+9NOMQk6oRU4Z9TSJJJrNlN0M=", + "zh:0498ef4209924b30ce7b4a232dd6aee08feab2ebbc90064db699adc10c16707e", + "zh:292e3c0c55d320cf164cdd431ee31580dd86f435aec99721597204bab5de3970", + "zh:3ce8558658baa7c4b9f1eeb92427665b4b930e5b157fbf352977778c90e11aaa", + "zh:46abd0bdeeba46b86832ed31338ad837b584f7b2152f8a9bfa6c3802f481a6da", + "zh:5804e71d411577f06abc0986c8c2e475c49042a192efce5936e4d5bdd874fc22", + "zh:7cef0782e8198346bfe7b61601e1cf8f2158280a5cf665140b72838545ca3127", + "zh:be81782af391ff4cc0859d976637aa00e6fe34061fe4f1df1f5ab5d62ef94f82", + "zh:bf2660e70edf758305085698fc9d05306b174b99559cd0f3f61c0b705ba22275", + "zh:caf727b0a378dc8c9c3594bbf176865f87aa732077820ff045eb352f5a48aeed", + "zh:cf95fc3121b358c7b7b667193ab36b8cb6140e2f6dfbf6f1b4c55b7fec1bb6ef", + "zh:d6d3119f8b971e982b6421dfa3b86314ccaeceaf047a3b6505f79e1a30f8301e", + "zh:e6f7f65dced2e88e3082c57ddcd118412595678cf3c7289bc7e12c724b3bd892", + "zh:f41f59ca511ab1a591d5abdc7f6d32d2e03a1d6087d206a741f95b7b0dd2ea17", + "zh:fbe59fbb5f272a6b206a380f6dbf49837b199960dd038afca2e89b11f72fdfda", + ] +} diff --git a/terraform/hedgedoc-demo/README.md b/terraform/hedgedoc-demo/README.md new file mode 100644 index 000000000..2349a9295 --- /dev/null +++ b/terraform/hedgedoc-demo/README.md @@ -0,0 +1,55 @@ +Hedgedoc Demo Terraform Repository +=== + +This directory contains the deployment of the [Hedgedoc](https://hedgedoc.org) [Demo instance](https://demo.hedgedoc.org). + +The demo instance is hosted on [Hetzner Cloud](https://hetzner.cloud) in combination with [Cloudflare](https://cloudflare.com) as DNS, CDN and WAF provider and [Backblaze](https://backblaze.com) as Backup storage. + +The Terraform setup itself is slightly opinionated and allows reproduction of the instance by replacing the existing one, which includes a minor downtime, until the new instance has downloaded all images as started the containers. + +Features +--- + +This setup provides with some features that weren't available or limited before: + +1. Fully reproducible instance creation +2. Automated update installation +3. Automatic nightly deployment +4. Automatic nightly backups including monitoring +5. Fully self-contained setup -> All components (hedgedoc, s3, postgresql, reverse-proxy) of the demo instance run on the host + +To be done +--- + +- [x] Automated installation of Hedgedoc +- [x] Automated docker-compose revisions +- [x] Automated updates to latest nightly image +- [x] Fully restricted network access to backend host +- [x] Automated backup of Hedgedoc +- [x] Automated backup monitoring +- [ ] Automated restore of Hedgedoc +- [ ] Add safeguard preventing volume deletion +- [ ] Add safeguard preventing destruction while backup process in progress + +Usage +--- + +Be aware, that without the `.tfstate` this repository will produce another demo instance, not modify the existing one. + +1. Initialise terraform using `terraform init -upgrade` +2. Decrypt or fill/adjust the `terraform.tfvars` file to your needs. (all variables are described in the `variables.tf` file, if you want to start from scratch) +3. Run `terraform apply` and review the changes +4. Initiate deployment of the changes by typing `yes`. + +Technology reference +--- + +- [terraform](https://developer.hashicorp.com/terraform/intro) +- [rockylinux](https://docs.rockylinux.org/) +- [docker](https://docs.docker.com) +- [restic](https://restic.readthedocs.io/en/stable/) +- [hedgedoc](https://docs.hedgedoc.org) +- [traefik](https://docs.traefik.io/) +- [postgresql](https://www.postgresql.org/docs/) +- [minio](https://min.io/docs/minio/container/index.html) +- [watchtower](https://containrrr.dev/watchtower/) \ No newline at end of file diff --git a/terraform/hedgedoc-demo/dns.tf b/terraform/hedgedoc-demo/dns.tf new file mode 100644 index 000000000..d0fd2d343 --- /dev/null +++ b/terraform/hedgedoc-demo/dns.tf @@ -0,0 +1,17 @@ +resource "cloudflare_record" "demo-instance" { + count = 1 + zone_id = var.dns_zone_id + name = "demo" + value = module.hedgedoc.server_names[0] + type = "CNAME" + proxied = true +} + +resource "cloudflare_record" "s3-storage" { + count = 1 + zone_id = var.dns_zone_id + name = "s3" + value = module.hedgedoc.server_names[0] + type = "CNAME" + proxied = true +} \ No newline at end of file diff --git a/terraform/hedgedoc-demo/firewall.tf b/terraform/hedgedoc-demo/firewall.tf new file mode 100644 index 000000000..43ecaca49 --- /dev/null +++ b/terraform/hedgedoc-demo/firewall.tf @@ -0,0 +1,62 @@ +data "cloudflare_ip_ranges" "cloudflare" {} + + +resource "hcloud_firewall" "ingress-web" { + name = "ingress-web" + apply_to { + label_selector = "firewall.hetzner.si-infra.de/web=true" + } + + # ICMP is always a good idea + # + # Network reachability tests + rule { + direction = "in" + protocol = "icmp" + source_ips = [ + "0.0.0.0/0", + "::/0" + ] + } + + # Allow HTTP and HTTPS traffic in + rule { + direction = "in" + protocol = "tcp" + port = "80" + source_ips = data.cloudflare_ip_ranges.cloudflare.cidr_blocks + } + rule { + direction = "in" + protocol = "tcp" + port = "443" + source_ips = data.cloudflare_ip_ranges.cloudflare.cidr_blocks + } +} + +resource "hcloud_firewall" "ingress-ssh" { + name = "ingress-ssh" + apply_to { + label_selector = "firewall.hetzner.si-infra.de/ssh=true" + } + + # ICMP is always a good idea + # + # Network reachability tests + rule { + direction = "in" + protocol = "icmp" + source_ips = [ + "0.0.0.0/0", + "::/0" + ] + } + + # Allow SSH traffic in + rule { + direction = "in" + protocol = "tcp" + port = "22" + source_ips = var.ssh_firewall_cidr + } +} diff --git a/terraform/hedgedoc-demo/main.tf b/terraform/hedgedoc-demo/main.tf new file mode 100644 index 000000000..845c89820 --- /dev/null +++ b/terraform/hedgedoc-demo/main.tf @@ -0,0 +1,41 @@ +module "hedgedoc" { + source = "../modules/hcloud_instance" + instance_count = 1 + location = var.location + name = "srv" + dns_domain = var.dns_domain + dns_zone_id = var.dns_zone_id + dns_record_aaaa = false + image = "rocky-9" + user_data = templatefile("templates/cloud-init.tpl", { + docker_compose_file = templatefile("templates/docker-compose.tpl", { + hedgedoc_db_user = "${var.hedgedoc_db_user}", + hedgedoc_db_password = "${var.hedgedoc_db_password}", + + hedgedoc_session_secret = "${var.hedgedoc_session_secret}", + + hedgedoc_minio_accesskey = "${var.hedgedoc_minio_accesskey}", + hedgedoc_minio_secretkey = "${var.hedgedoc_minio_secretkey}", + hedgedoc_minio_endpoint = "${var.hedgedoc_minio_endpoint}", + hedgedoc_minio_bucketname = "${var.hedgedoc_minio_bucketname}", + + hedgedoc_github_clientid = "${var.hedgedoc_github_clientid}", + hedgedoc_github_clientsecret = "${var.hedgedoc_github_clientsecret}", + letsencrypt_email = "${var.letsencrypt_email}", + }), + restic_b2_account_id = "${var.restic_b2_account_id}", + restic_b2_account_key = "${var.restic_b2_account_key}", + restic_repo_url = "${var.restic_repo_url}", + restic_repo_password = "${var.restic_repo_password}", + restic_monitoring_url = "${var.restic_monitoring_url}", + hedgedoc_db_user = "${var.hedgedoc_db_user}", + }) + ssh_keys = data.hcloud_ssh_keys.all_keys.ssh_keys.*.name + server_type = "cx11" + labels = { + "firewall.hetzner.si-infra.de/web" = "true", + "firewall.hetzner.si-infra.de/ssh" = "true", + } + volume = true + volume_size = 45 +} diff --git a/terraform/hedgedoc-demo/provider.tf b/terraform/hedgedoc-demo/provider.tf new file mode 100644 index 000000000..657bd3026 --- /dev/null +++ b/terraform/hedgedoc-demo/provider.tf @@ -0,0 +1,16 @@ +provider "cloudflare" { + api_token = var.cloudflare_api_token +} + +provider "hcloud" { + token = var.hcloud_token +} + +provider "template" { +} + +provider "local" { +} + +provider "random" { +} diff --git a/terraform/hedgedoc-demo/ssh.tf b/terraform/hedgedoc-demo/ssh.tf new file mode 100644 index 000000000..db794f29d --- /dev/null +++ b/terraform/hedgedoc-demo/ssh.tf @@ -0,0 +1,2 @@ +data "hcloud_ssh_keys" "all_keys" { +} diff --git a/terraform/hedgedoc-demo/templates/cloud-init.tpl b/terraform/hedgedoc-demo/templates/cloud-init.tpl new file mode 100644 index 000000000..66c3d2de0 --- /dev/null +++ b/terraform/hedgedoc-demo/templates/cloud-init.tpl @@ -0,0 +1,169 @@ +#!/bin/bash + +set -e + +dnf upgrade -y + +# Enable automatic updates +dnf install -y dnf-automatic + +systemctl enable --now dnf-automatic-install.timer + +# Install docker +dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo +dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin +systemctl enable --now docker + +# Setup volume +echo "/dev/sdb /srv xfs discard,nofail,defaults 0 0" >> /etc/fstab +mount /dev/sdb /srv + +mountpoint /srv + +# Create docker-compose backup +if -e /srv/docker-compose.yaml; then + mv /srv/docker-compose.yaml /srv/docker-compose.yaml.1 +fi + +CUR=1 +for i in $(seq 2 10); do + if -e "/srv/docker-compose.yaml.$${CUR}"; then + mv "/srv/docker-compose.yaml.$${CUR}" "/srv/docker-compose.yaml.$${i}" + else + break + fi + CUR="$${i}" +done + +cat > /srv/docker-compose.yaml <<EOF +${docker_compose_file} +EOF + +cd /srv + +docker compose up -d + +dnf install -y epel-release +dnf install -y restic + +mkdir -p /root/.cache/ + +cat > /etc/sysconfig/restic <<EOF +BACKUP_PATHS="/srv" +B2_ACCOUNT_ID=${restic_b2_account_id} +B2_ACCOUNT_KEY=${restic_b2_account_key} +RETENTION_DAYS=7 +RETENTION_WEEKS=2 +RETENTION_MONTHS=3 +RETENTION_YEARS=1 +RESTIC_REPOSITORY=${restic_repo_url} +RESTIC_PASSWORD=${restic_repo_password} +RESTIC_CACHE_DIR=/root/.cache/restic +EOF + +set -a +source /etc/sysconfig/restic +set +a + +if ! restic snapshots; then + restic init +fi + +# Inspired by https://fedoramagazine.org/automate-backups-with-restic-and-systemd/ +# Inspired by https://www.mavjs.org/post/automatic-backup-restic-systemd-service/ +cat > /etc/systemd/system/offsite-backup.service <<EOF +[Unit] +Description=Offsite backup with restic +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +# Low CPU priority +CPUShares=512 +EnvironmentFile=/etc/sysconfig/restic +ExecStart=/usr/local/libexec/restic-backup-hedgedoc +EOF + +# Inspired by https://github.com/mhw/restic-backup-scripts/blob/e3fe81ba25acc5ae3ccdb28807eea8b0be38ce20/postgresql-backup.sh +cat > /usr/local/libexec/restic-backup-hedgedoc <<EOF +#!/bin/bash +set -e +set -o pipefail + +# Run general backup, excluding the database +/usr/bin/restic backup /srv \ + --exclude=/srv/database/* \ + --one-file-system \ + --tag systemd.timer \ + --tag persistentvolume + +cd /srv + +# Run database backup separately by dumping the database for consistency +docker compose exec database pg_dump --create --clean --if-exists --no-owner --no-privileges -U ${hedgedoc_db_user} hedgedoc | \ + gzip --rsyncable | \ + restic backup \ + --stdin --stdin-filename postgresql/hedgedoc.sql.gz \ + --tag "systemd.timer" \ + --tag "hedgedoc-database" + +# Notify monitoring about success +curl --retry-all-errors --retry 10 '${restic_monitoring_url}' +EOF +chmod +x /usr/local/libexec/restic-backup-hedgedoc + +cat > /etc/systemd/system/offsite-backup-prune.service <<EOF +[Unit] +Description=Restic backup service (data pruning) +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +EnvironmentFile=/etc/sysconfig/restic +ExecStart=/usr/bin/restic forget --prune \ + --tag systemd.timer \ + --group-by "paths,tags" \ + --keep-daily \$RETENTION_DAYS \ + --keep-weekly \$RETENTION_WEEKS \ + --keep-monthly \$RETENTION_MONTHS \ + --keep-yearly \$RETENTION_YEARS +EOF + +cat > /etc/systemd/system/offsite-backup.timer <<EOF +[Unit] +Description=Offsite backup with restic +After=network-online.target +Wants=network-online.target + +[Timer] +OnCalendar=daily +Persistent=true +RandomizedDelaySec=600 + +[Install] +WantedBy=timers.target +EOF + +cat > /etc/systemd/system/offsite-backup-prune.timer <<EOF +[Unit] +Description=Prune data from the restic repository monthly +After=network-online.target +Wants=network-online.target + +[Timer] +OnCalendar=*-*-* 4:00:00 +Persistent=true +RandomizedDelaySec=600 + +[Install] +WantedBy=timers.target +EOF + +systemctl daemon-reload +systemctl enable --now offsite-backup.timer offsite-backup-prune.timer + +sleep 60 + +reboot diff --git a/terraform/hedgedoc-demo/templates/docker-compose.tpl b/terraform/hedgedoc-demo/templates/docker-compose.tpl new file mode 100644 index 000000000..67b411ac2 --- /dev/null +++ b/terraform/hedgedoc-demo/templates/docker-compose.tpl @@ -0,0 +1,165 @@ +version: '2' +services: + database: + # renovate: + image: docker.io/library/postgres:13 + mem_limit: 896mb + memswap_limit: 1024mb + read_only: true + tmpfs: + - /run/postgresql:size=512K + - /tmp:size=128K + stop_grace_period: 2m + stop_signal: SIGINT + environment: + - POSTGRES_USER=${hedgedoc_db_user} + - POSTGRES_PASSWORD=${hedgedoc_db_password} + - POSTGRES_DB=hedgedoc + volumes: + - ./database:/var/lib/postgresql/data:z + networks: + backend: + restart: always + + hedgedoc: + # renovate: + image: quay.io/hedgedoc/hedgedoc-nightly:debian + mem_limit: 1gb + memswap_limit: 1536mb + read_only: true + tmpfs: + - /tmp:size=10M + depends_on: + - database + environment: + - "CMD_DB_URL=postgres://${hedgedoc_db_user}:${hedgedoc_db_password}@database:5432/hedgedoc" + - "CMD_DOMAIN=demo.hedgedoc.org" + - "CMD_PROTOCOL_USESSL=true" + - "CMD_URL_ADDPORT=false" + - "CMD_SESSION_SECRET=${hedgedoc_session_secret}" + - "CMD_MINIO_PORT=443" + - "CMD_MINIO_ENDPOINT=${hedgedoc_minio_endpoint}" + - "CMD_MINIO_ACCESS_KEY=${hedgedoc_minio_accesskey}" + - "CMD_MINIO_SECRET_KEY=${hedgedoc_minio_secretkey}" + - "CMD_MINIO_SECURE=true" + - "CMD_IMAGE_UPLOAD_TYPE=minio" + - "CMD_S3_BUCKET=${hedgedoc_minio_bucketname}" + - "CMD_GITHUB_CLIENTID=${hedgedoc_github_clientid}" + - "CMD_GITHUB_CLIENTSECRET=${hedgedoc_github_clientsecret}" + - "CMD_USECDN=false" + - "CMD_ALLOW_FREE_URL=false" + - "CMD_EMAIL=true" + - "CMD_ALLOW_PDF_EXPORT=false" + - "CMD_CSP_REPORTURI=https://hedgedoc.report-uri.com/r/d/csp/enforce" + - "CMD_OPENID=true" + - "CMD_TOOBUSY_LAG=250" + - "CMD_CSP_ALLOW_FRAMING=false" + - "CMD_CSP_ADD_DISQUS=false" + - "CMD_CSP_ADD_GOOGLE_ANALYTICS=false" + + labels: + - "traefik.http.routers.hedgedoc_demo.rule=Host(\`demo.hedgedoc.org\`) && PathPrefix(\`/\`) && ! PathPrefix(\`/metrics\`)" + - "traefik.http.routers.hedgedoc_demo.entrypoints=websecure" + - "traefik.http.routers.hedgedoc_demo.tls=true" + - "traefik.http.routers.hedgedoc_demo.tls.certresolver=letsencrypt_http" + - "traefik.http.routers.hedgedoc_demo.middlewares=hedgedoc_demo" + - "traefik.http.routers.hedgedoc_demo.service=hedgedoc_demo" + - "traefik.http.services.hedgedoc_demo.loadbalancer.server.port=3000" + - "traefik.http.middlewares.hedgedoc_demo.headers.sslredirect=true" + - "traefik.http.middlewares.hedgedoc_demo.headers.stsSeconds=63072000" + - "traefik.http.middlewares.hedgedoc_demo.headers.browserXssFilter=true" + - "traefik.http.middlewares.hedgedoc_demo.headers.contentTypeNosniff=true" + + - "traefik.enable=true" + - "traefik.port=3000" + - "traefik.docker.network=proxy" + + + volumes: + - './privacy.md:/hedgedoc/public/docs/privacy.md:ro,z' + + networks: + backend: + proxy: + restart: always + + minio: + # renovate: + image: docker.io/minio/minio:latest + mem_limit: 256mb + memswap_limit: 512mb + read_only: true + tmpfs: + - /root/.minio:size=128K + environment: + - "MINIO_ACCESS_KEY=${hedgedoc_minio_accesskey}" + - "MINIO_SECRET_KEY=${hedgedoc_minio_secretkey}" + - "MINIO_BROWSER=off" + - "MINIO_DOMAIN=${hedgedoc_minio_endpoint}" + volumes: + - "./s3-storage:/data:z" + command: ["server", "/data"] + restart: always + labels: + - "traefik.http.routers.minio_hedgedoc.rule=Host(\`${hedgedoc_minio_endpoint}\`) && PathPrefix(\`/\`)" + - "traefik.http.routers.minio_hedgedoc.entrypoints=websecure" + - "traefik.http.routers.minio_hedgedoc.tls=true" + - "traefik.http.routers.minio_hedgedoc.tls.certresolver=letsencrypt_http" + - "traefik.http.routers.minio_hedgedoc.middlewares=minio_hedgedoc" + - "traefik.http.routers.minio_hedgedoc.service=minio_hedgedoc" + - "traefik.http.services.minio_hedgedoc.loadbalancer.server.port=9000" + - "traefik.http.middlewares.minio_hedgedoc.headers.sslredirect=true" + - "traefik.http.middlewares.minio_hedgedoc.headers.stsSeconds=63072000" + - "traefik.http.middlewares.minio_hedgedoc.headers.referrerPolicy=no-referrer" + - "traefik.http.middlewares.minio_hedgedoc.headers.contentTypeNosniff=true" + - "traefik.http.middlewares.minio_hedgedoc.headers.browserXssFilter=true" + - "traefik.enable=true" + - "traefik.port=9000" + - "traefik.docker.network=proxy" + + networks: + proxy: + + proxy: + # renovate: + image: docker.io/library/traefik:v2.9 + security_opt: + - "label:disable" + read_only: true + restart: always + + ports: + - "80:80" + - "443:443" + + volumes: + - "./acme:/etc/traefik/acme:z" + - "/var/run/docker.sock:/var/run/docker.sock" + + command: + - "--entryPoints.web.address=:80" + - "--entryPoints.websecure.address=:443" + - "--accesslog=false" + - "--providers.docker=true" + - "--certificatesResolvers.letsencrypt_http.acme.email=${letsencrypt_email}" + - "--certificatesResolvers.letsencrypt_http.acme.storage=/etc/traefik/acme/acme-v2.json" + - "--certificatesResolvers.letsencrypt_http.acme.httpChallenge.entrypoint=web" + + networks: + proxy: + + watchtower: + # renovate: + image: docker.io/containrrr/watchtower:latest + restart: always + security_opt: + - "label:disable" + environment: + - "WATCHTOWER_CLEANUP=true" + volumes: + - "/var/run/docker.sock:/var/run/docker.sock" + +networks: + proxy: + name: proxy + backend: \ No newline at end of file diff --git a/terraform/hedgedoc-demo/terraform.enc.tfvars b/terraform/hedgedoc-demo/terraform.enc.tfvars new file mode 100644 index 000000000..7a71db85a --- /dev/null +++ b/terraform/hedgedoc-demo/terraform.enc.tfvars @@ -0,0 +1,21 @@ +{ + "data": "ENC[AES256_GCM,data:AvfL4Gvv1+/YJi/7Qcq42x24lK9irGWnTmo8cvazsYgMOciIyCz7Jru6LItoCMrALgXSfuT6v2ezgQJ++shTBllfOnGfvyk4nIjNeeJDwellVoardgiwBO+xxUSyHG6exbuODFw0xE0m3N1hSOyyy1wbOpG6pFGDfkZYtzD6F5MVzyGBpgg+py3/nggIMtWwrm03frSxWESggJ+bLQWsOmVqXVq9fWEAsB4tXfsaR3QaALcyAEeyVNB+111abhxuSLn/+jQHuzovQky8ORpLBGRlR7VtWj2Z6xnJ+KsSWtONJn1qdZddi24ybrJ33R8OCDX2ISQx0wV/XG5APAquAxeNsSik3nOBIqhHjWM9gTNrZohyfGc044WO9xF6zKqSKpuZ8zqnEqd/0qcWUvvFeFL4UEXtYugAQm1LbSj21ulYLEkO0KSMwHTLdeysa53KECBL9rkRh2FjXFtxva7ctDjE5uyfE4sqEB3VpdRu3Frd3nDTrKbI5qtf99Ujbv8fwCkrxKMEkYmHXGBRtBndabafknugUPTCfWk0eLLzxAJ/hSWJ/tFciALFPujqEdtM78zpBvYbmr68DuqO3zq8i2r+7tFhZKHYk6Sk/+kWB202I4RoO5d/MV1tx7ZSvFVeYxJJfRb+ZHR7RfEofLv+EE/Ku7YCb8ABzFPvnTZpqaLxc8BLnl5ahoZzvyhrngu5ZIwVjW9s/SkjynslPC0l2PH7gHpVLXH6rz3ggUU8siOW1885Khwp/Par3vG2C7wXGjXSVitzVM+ssRSI55TVr81ar2DJtriujPBR7vUJnQ1yexOOyPMwg0Q804MjXsFE0N4POB0zIOU390LscyEzLKblqUhi843yrZX+Y4n0AbmZpOUMsDgqqa9BhdrHtMGRGWd6xT+bDwc4mcysHS1VHr+oDaAU4oNh/Px4168DFsLhXexPBtneCV3AEZ1LscL0DjGxVlaNKD6hcl1JqIGwIM+6QbDmpAhjIPNFHR2PYBmYJ9Q92WYUkQw3/CubZ7amY7ItANERBPyYQ5uB1Oq/2LniL4QmLiBFZTOse6amunzwJMMkfEaKqHs+ELjPVSl3qCJjGtHVKDOybbRTX+k4Q22xMI8Vbk71ix60zkUNxnxxpwTaOPE/HhNsPWF3KuXJwXC+xlnG4SPjQAgk0qKyIGNBs0YFV8WVw5mKbSUTxNPndbU2Y1KYnU9L6vyKYBsNST9wg0ehiZPpZlmaRdHrnS7hTX6220nXuUM59ef0cBzZqrbGmVVTgxjI1KA+Cw7xJwr9iMf0JY6vb+s3V7gw9XZ5xJY20JRZsgrAzOAMBWBCtFjEt7UqqcloGQ6zDIsGhnMUdJC/Xxaf/PmxuD/RjUBk8mlaSfIAX43xKVJsRpcX96k+3GtF/lRGnu+AOprwsdqtx0mmdi1/GL8ph7/le+WZHvmyTZ00Y5VQ9mgX/BloY2dK+Yem6zCN/cxlwzTX0ZTdOZ93COs=,iv:wl3rX28j9AxiIldid/fnnbLUEW22WI+KpyH7i+GGJnM=,tag:aDwZAuHYWXhA3gpVIHG25g==,type:str]", + "sops": { + "kms": null, + "gcp_kms": null, + "azure_kv": null, + "hc_vault": null, + "age": null, + "lastmodified": "2023-03-01T00:12:49Z", + "mac": "ENC[AES256_GCM,data:0ooMWpnFkMGj2qI+ZrStExZoxRCkKLa00BqQLFIiG09Qq3JKrbixpR2a/AP7zloauB8TtSyl5DvXHSvpRvWMjvMhA61aUk6w3WQ0I3mdTZv7TLzP172lPo6DzvG35e8TAujH+fEUNfFW0zutMff/Z/9zWJJ5H9f6Z6IusrD1qkA=,iv:4YdQREC5g6chGJjFBKLW0RzZXIpsRHS3SxVEKixCgJU=,tag:AVNiRyZLwLmdesdyWwqp1g==,type:str]", + "pgp": [ + { + "created_at": "2023-03-01T00:12:49Z", + "enc": "-----BEGIN PGP MESSAGE-----\n\nwcFMA7kpg2bgzVHcAQ/+MdRgsdX6mCEGY3Tuqy4Kf2HrCpV1eNLJIgEjMJXupTVZ\nqSVhT5TRDt1kSW7D0czHJTS/Nm9nD6Y0+MFY+UMK2VqFkLPatfkyMHli5JIskpR1\nyi+UsZ5o3UQBjccM7m91kaF3me/+92h2EE2monVFPYNsABlXmNjS9yqei2Jm63ry\nXVCWCpKPLmDDOpCZk7CnJggKO2f+gdgT3JGapkSU0j8to3JoleLuszSDxmiE8S7/\nrmMNTzRzOynN6lLZkS6nQQpYBM9fYzpyxN1AexeX2qzKT18oajcu7uws9Rq5Nr7Z\naS4sPmnJfkIwG5PVBlDg/5GKt4QiMIPHXFl86QW64Du2Roe4T01G3af2FzniDrxi\npL3CgXx+HpcievUI9+fs3as77r0DTIpHZOyTmTW4PDczqdNjviTIlCse/2GiYPhJ\ngHlBkGUoY1CWpJgU4bYZjeJ9DFWSVSkYvjLUO146TyiK+AmNCykff9ipXjH5TKto\nktT80Tgd2rgzgq+536aswr7a86X7PuYXjqJ4qXYM3F+7sRJF5ZEVcuOnsWzpZ8Rh\nAi5tuwWUmCR7bfJHShp381iZbkfT6TWwa4hOx9sXNLtebgf8DOynhPMP9itCIUnE\nJxg/1EtxOp4AvyjqLv0LgRv/tZa58HEGVSKBdWyR96Vq4JQqkVNF1S4PLUHuA+TS\nUQEBoS5+5Zo8nJp/LnZ7k1rzodu7MjZnwbnbkaXTjMZKl0gi2n2NLeITghfgWyQE\nqMUiOZAp+wM676u1r8Mnj8wILofk2EejikMK45bDp7BGQw==\n=xjMK\n-----END PGP MESSAGE-----", + "fp": "286791FB6648539775DB31B8FCB98C2A3EC6F601" + } + ], + "unencrypted_suffix": "_unencrypted", + "version": "3.7.3" + } +} \ No newline at end of file diff --git a/terraform/hedgedoc-demo/variables.tf b/terraform/hedgedoc-demo/variables.tf new file mode 100644 index 000000000..5192f852c --- /dev/null +++ b/terraform/hedgedoc-demo/variables.tf @@ -0,0 +1,137 @@ +variable "dns_domain" { + type = string + description = "Name of the Cloudflare domain" + nullable = false +} + +variable "dns_zone_id" { + type = string + description = "Zone ID of the Cloudflare domain" + nullable = false +} + +variable "location" { + type = string + description = "Region used to deploy the Hetzner Cloud instance. See: https://docs.hetzner.cloud/#locations" + default = "nbg1" + nullable = false +} + +variable "ssh_firewall_cidr" { + type = list(string) + description = "List of CIDRs addresses that are allow to SSH into the host" + default = ["127.0.0.1/32"] + nullable = false +} + +variable "hedgedoc_db_user" { + type = string + description = "Username used to authenticate to the PostgreSQL database. See: https://docs.hedgedoc.org/configuration/#hedgedoc-basics" + nullable = false +} + +variable "hedgedoc_db_password" { + type = string + description = "Password used to authenticate to the PostgreSQL database. See: https://docs.hedgedoc.org/configuration/#hedgedoc-basics" + sensitive = true + nullable = false +} + +variable "hedgedoc_session_secret" { + type = string + description = "Secret used for securing the Hedgedoc session token, this ensures after a restart users stay authenticated. See: https://docs.hedgedoc.org/configuration/#users-and-privileges" + sensitive = true + nullable = false +} + +variable "hedgedoc_minio_accesskey" { + type = string + description = "S3/Minio access key/ id for authentication. See: https://docs.hedgedoc.org/guides/minio-image-upload/" + sensitive = true + nullable = false +} + +variable "hedgedoc_minio_secretkey" { + type = string + description = "S3/Minio secret key for authentication. See: https://docs.hedgedoc.org/guides/minio-image-upload/" + sensitive = true + nullable = false +} + +variable "hedgedoc_minio_endpoint" { + type = string + description = "Connection Endpoint used for uploading files to S3/Minio backend. See: https://docs.hedgedoc.org/guides/minio-image-upload/" + nullable = false +} + +variable "hedgedoc_minio_bucketname" { + type = string + description = "Name of the S3/Minio Bucket. See: https://docs.hedgedoc.org/guides/minio-image-upload/" + nullable = false +} + +variable "hedgedoc_github_clientid" { + type = string + description = "GitHub Client ID used for GitHub SSO. See: https://docs.hedgedoc.org/guides/auth/github/" + sensitive = true + nullable = false +} + +variable "hedgedoc_github_clientsecret" { + type = string + description = "GitHub Client Secret used for GitHub SSO. See: https://docs.hedgedoc.org/guides/auth/github/" + sensitive = true + nullable = false +} + +variable "restic_b2_account_id" { + type = string + description = "ID to access B2 backup storage. See: https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#backblaze-b2" + sensitive = true + nullable = false +} + +variable "restic_b2_account_key" { + type = string + description = "Token to access B2 backup storage. See: https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#backblaze-b2" + sensitive = true + nullable = false +} + +variable "restic_repo_url" { + type = string + description = "URL for restric backup location, should start with b2:. See: https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#backblaze-b2" + nullable = false +} + +variable "restic_repo_password" { + type = string + description = "Password used to secure the restic backups. See: https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html" + sensitive = true + nullable = false +} + +variable "restic_monitoring_url" { + type = string + description = "URL that curl will call after finishing a backup, allows push monitoring" + default = "http://example.com" + sensitive = true + nullable = false +} + +variable "letsencrypt_email" { + description = "E-Mail address used for Let's Encrypt certificates" + nullable = false +} + +variable "cloudflare_api_token" { + description = "API token for Cloudflare authentication. See: https://developers.cloudflare.com/fundamentals/api/get-started/create-token/" + sensitive = true + nullable = false +} + +variable "hcloud_token" { + description = "API token for Hetzner Cloud authentication. See: https://docs.hetzner.cloud/#getting-started" + sensitive = true + nullable = false +} diff --git a/terraform/hedgedoc-demo/versions.tf b/terraform/hedgedoc-demo/versions.tf new file mode 120000 index 000000000..8bd0ff140 --- /dev/null +++ b/terraform/hedgedoc-demo/versions.tf @@ -0,0 +1 @@ +../versions.tf \ No newline at end of file -- GitLab