diff --git a/.gitignore b/.gitignore index c496f81..3ea7f9f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ +# Ignore terraform files +.terraform +*.tfvars +*tfstate* + ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## diff --git a/infrastructure/.terraform.lock.hcl b/infrastructure/.terraform.lock.hcl new file mode 100644 index 0000000..a5c96f6 --- /dev/null +++ b/infrastructure/.terraform.lock.hcl @@ -0,0 +1,45 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/hashicorp/hcloud" { + version = "1.52.0" + hashes = [ + "h1:8Juiz/B0XWpSCJmIYLBoGqU14R0W9rudwVInfd7jBt0=", + "zh:1e9bb6b6a2ea5f441638dbae2d60fbe04ff455f58a18c740b8b7913e2197d875", + "zh:29c122e404ba331cfbadacc7f1294de5a31c9dfd60bdfe3e1b402271fc8e419c", + "zh:2bd0ae2f0bb9f16b7753f59a08e57ac7230f9c471278d7882f81406b9426c8c7", + "zh:4383206971873f6b5d81580a9a36e0158924f5816ebb6206b0cf2430e4e6a609", + "zh:47e2ca1cfa18500e4952ab51dc357a0450d00a92da9ea03e452f1f3efe6bbf75", + "zh:8e9fe90e3cea29bb7892b64da737642fc22b0106402df76c228a3cbe99663278", + "zh:a2d69350a69c471ddb63bcc74e105e585319a0fc0f4d1b7f70569f6d2ece5824", + "zh:a97abcc254e21c294e2d6b0fc9068acfd63614b097dda365f1c56ea8b0fd5f6b", + "zh:aba8d72d4fe2e89c922d5446d329e5c23d00b28227b4666e6486ba18ea2ec278", + "zh:ad36c333978c2d9e4bc43dcadcbff42fe771a8c5ef53d028bcacec8287bf78a7", + "zh:cdb1e6903b9d2f0ad8845d4eb390fbe724ee2435fb045baeab38d4319e637682", + "zh:df77b08757f3f36b8aadb33d73362320174047044414325c56a87983f48b5186", + "zh:e07513d5ad387247092b5ae1c87e21a387fc51873b3f38eee616187e38b090a7", + "zh:e2be02bdc59343ff4b9e26c3b93db7680aaf3e6ed13c8c4c4b144c74c2689915", + ] +} + +provider "registry.opentofu.org/hetznercloud/hcloud" { + version = "1.50.0" + constraints = "1.50.0" + hashes = [ + "h1:txFVTG8RI/X3NnyR1RST2cYZiseoDsCVjZt/v2Z6Q/I=", + "zh:0bd650fb52e272f74eda5053a7bb62f0fd92182f57ad3ef742abe165cb8cac98", + "zh:1c36667aa89b672a96c0df3d3c613e80916a2d0944b1a1f9112065f40630b689", + "zh:21f90683890ea7a184b0ac55efd52911694ba86c58898bc8bbe87ee2507bb1eb", + "zh:24349d483a6ff97420d847433553fa031f68f99b9ead4ebb3592fc8955ef521f", + "zh:3fffd83c450bea2b382a986501ae51a4d3e6530eda48ed9ca74d518e4a909c37", + "zh:43d7de1dc4c50fae99d6c4ab4bb394608948091f5b53ddb29bc65deead9dc8a6", + "zh:47a37d5fec79dd8bc9cab2c892bc59e135b86cb51eebe2b01cdb40afac7ed777", + "zh:6efeb9530b8f57618c43f0b294b983d06cce43e9423bdd737eed81db913edb80", + "zh:7511ace4b33baddfc452ef95a634d83b92bfbfaa23cb30403899e95b64727075", + "zh:7bade77104ed8788c9b5171c7daae6ab6c011b3c40b152274fda803bf0bf2707", + "zh:83bce3ff9a1bd52a340a6ebdd2e2b731ec6fb86811ef0ed8a8264daf9d7beb61", + "zh:a09d5fce4c8d33e10b9a19318c965076db2d8ed5f62f5feb3e7502416f66d7bf", + "zh:c942832b80270eb982eeb9cc14f30a437db5fd28faf37d6aa32ec2cd345537d6", + "zh:e2c1812f2e1f9fac17c7551d4ab0efb713b6d751087c18b84b8acd542f587459", + ] +} diff --git a/infrastructure/compose-app/cloud-init.yaml b/infrastructure/compose-app/cloud-init.yaml new file mode 100644 index 0000000..6284ef3 --- /dev/null +++ b/infrastructure/compose-app/cloud-init.yaml @@ -0,0 +1,72 @@ +#cloud-config + +# Upgrade on first boot +package_update: true +package_upgrade: true + +# Add baseline packages +packages: + - git + +write_files: + # Setup the reconciliation data (systemd) + - path: /etc/systemd/system/reconcile.service + permissions: '0644' + content: | + [Unit] + Description=Run reconciliation script + + [Service] + Type=oneshot + ExecStart=/usr/local/bin/reconcile.sh + + - path: /etc/systemd/system/reconcile.timer + permissions: '0644' + content: | + [Unit] + Description=Timer for reconciliation script + + [Timer] + OnBootSec=${boot_delay} + OnUnitActiveSec=${reconciliation_intervall} + Persistent=true + + [Install] + WantedBy=timers.target + + - path: /usr/local/bin/reconcile.sh + permissions: '0755' + content: | + ${indented_reconciliation_script} + +runcmd: + # Add Docker's official GPG key + - install -m 0755 -d /etc/apt/keyrings + - curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + - chmod a+r /etc/apt/keyrings/docker.gpg + + # Add Docker's official repository + - | + echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ + https://download.docker.com/linux/debian \ + $(lsb_release -cs) stable" \ + > /etc/apt/sources.list.d/docker.list + + # Update package index and install Docker components + - apt-get update + # - apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + - apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin + + # Ensure docker is running + - systemctl enable docker + - systemctl start docker + + # Create GitOps repo dir + - mkdir -p /srv/gitops + + # Setup the reconciliation service + - chmod +x /usr/local/bin/reconcile.sh + - systemctl daemon-reload + - systemctl enable reconcile.timer + - systemctl start reconcile.timer diff --git a/infrastructure/compose-app/outputs.tf b/infrastructure/compose-app/outputs.tf new file mode 100644 index 0000000..4e983e0 --- /dev/null +++ b/infrastructure/compose-app/outputs.tf @@ -0,0 +1,12 @@ +output "info" { + description = "Compose application description" + value = hcloud_server.server +} + +output "cloud_init" { + value = local.cloud_init +} + +output "reconciliation_script" { + value = local.reconciliation_script +} diff --git a/infrastructure/compose-app/reconciliation-script.sh b/infrastructure/compose-app/reconciliation-script.sh new file mode 100644 index 0000000..783c24f --- /dev/null +++ b/infrastructure/compose-app/reconciliation-script.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -e + +REPO_DIR="/srv/gitops/repo" +COMPOSE_FILE="$REPO_DIR/${compose_path}" + +if [ ! -d "$REPO_DIR/.git" ]; then + echo "[INFO] Initial clone of repo..." + git clone --branch "${branch}" "${git_remote}" "$REPO_DIR" +else + echo "[INFO] Updating existing repo..." + git -C "$REPO_DIR" pull +fi + +docker compose --file "$COMPOSE_FILE" pull +docker compose --file "$COMPOSE_FILE" up --detach --remove-orphans \ No newline at end of file diff --git a/infrastructure/compose-app/server.tf b/infrastructure/compose-app/server.tf new file mode 100644 index 0000000..8b3bfac --- /dev/null +++ b/infrastructure/compose-app/server.tf @@ -0,0 +1,31 @@ +locals { + reconciliation_script = templatefile("${path.module}/reconciliation-script.sh", { + git_remote = var.source_repository + branch = var.branch + compose_path = var.compose_path + }) + + cloud_init = templatefile("${path.module}/cloud-init.yaml", { + boot_delay = "2min" + reconciliation_intervall = var.reconciliation_interval + indented_reconciliation_script = indent(6, local.reconciliation_script) + }) +} + +resource "hcloud_server" "server" { + name = "compose-app" + + server_type = var.server_type + datacenter = var.datacenter + + labels = { + app = "compose-app" + interval = var.reconciliation_interval + } + + image = "debian-12" + + user_data = local.cloud_init + + ssh_keys = var.ssh_key != null ? [var.ssh_key.id] : [] # HCL ternary syntax +} \ No newline at end of file diff --git a/infrastructure/compose-app/variables.tf b/infrastructure/compose-app/variables.tf new file mode 100644 index 0000000..acbb037 --- /dev/null +++ b/infrastructure/compose-app/variables.tf @@ -0,0 +1,34 @@ +variable "server_type" { + description = "The architecture and size of the server. See Hetzner server type lists for valid values." + type = string +} + +variable "datacenter" { + description = "The datacenter to use. See Hetzners location for valid values." + type = string +} + +variable "source_repository" { + description = "Git repository to use as source for application." + type = string +} + +variable "branch" { + description = "The branch to target." + type = string +} + +variable "compose_path" { + description = "Path to the compose file." + type = string +} + +variable "reconciliation_interval" { + description = "Interval between reconciliation attempts." + type = string +} + +variable "ssh_key" { + description = "Optional SSH key reference on Hetzner. You need to upload this yourself." + default = null +} \ No newline at end of file diff --git a/infrastructure/compose-app/versions.tf b/infrastructure/compose-app/versions.tf new file mode 100644 index 0000000..9ec3946 --- /dev/null +++ b/infrastructure/compose-app/versions.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + hcloud = { + source = "hetznercloud/hcloud" + version = "1.50.0" + } + } +} diff --git a/infrastructure/main.tf b/infrastructure/main.tf new file mode 100644 index 0000000..276b477 --- /dev/null +++ b/infrastructure/main.tf @@ -0,0 +1,35 @@ +variable "debug_key" { + description = "Public SSH key for debugging. Optional." + type = string + default = null +} + +resource "hcloud_ssh_key" "debug_key" { + name = "compose-app-ssh-key" + # Associated Id stored in /secrets/debug_key + public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAaB/mzskgoQF0xcRY79l9FbIzVwK7Vqgowaxk5klJf8" +} + +module "gitops_lite" { + source = "./compose-app" + + # Hardware configuration + server_type = "cpx31" # Hetzner Machine type -> arch=x86, cores=4vCPU, ram=8GB, hdd=160GB + datacenter = "hel1-dc2" # Helsinki + + # Application configuration + source_repository = "https://github.com/jaurund/learning-project-for-RESTAPI-OAuth-SQL-Container.git" + branch = "main" + compose_path = "docker-compose.yaml" + reconciliation_interval = "1min" + + # DEVELOPMENT ONLY + ssh_key = hcloud_ssh_key.debug_key +} + +output "compose_app" { + value = { + ipv4_address = module.gitops_lite.info.ipv4_address + ipv6_address = module.gitops_lite.info.ipv6_address + } +} diff --git a/infrastructure/providers.tf b/infrastructure/providers.tf new file mode 100644 index 0000000..59f35da --- /dev/null +++ b/infrastructure/providers.tf @@ -0,0 +1,18 @@ +terraform { + required_providers { + hcloud = { + source = "hetznercloud/hcloud" + version = "1.50.0" + } + } +} + +provider "hcloud" { + token = var.hetzner_token +} + +variable "hetzner_token" { + description = "Hetzner API token" + type = string + sensitive = true +}