diff --git a/.github/workflows/apply.yaml b/.github/workflows/apply.yaml new file mode 100644 index 0000000..bad29ed --- /dev/null +++ b/.github/workflows/apply.yaml @@ -0,0 +1,51 @@ +name: Apply Terraform plan + +on: + push: + branches: + - main + +permissions: + contents: read + pull-requests: write + +jobs: + apply: + runs-on: ubuntu-latest + name: Apply Terraform plan + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TF_VAR_hcloud_token: ${{ secrets.TF_HCLOUD_TOKEN }} + STATE_BUCKET_NAME: ${{ secrets.TF_STATE_BUCKET_NAME }} + STATE_BUCKET_KEY: ${{ secrets.TF_STATE_BUCKET_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.TF_STATE_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.TF_STATE_SECRET_KEY }} + AWS_CA_BUNDLE: "" + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Homebrew + run: | + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + echo >> ~/.bashrc + echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"' >> ~/.bashrc + eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)" + env: + NONINTERACTIVE: 1 + + - name: Install Packer + run: | + sudo apt update + sudo apt install packer + packer + locate packer + + - name: Install Talosctl + run: /home/linuxbrew/.linuxbrew/bin/brew install siderolabs/tap/talosctl + + - name: Terraform apply + uses: dflook/terraform-apply@v2 + with: + path: infra + backend_config: bucket=${{ env.STATE_BUCKET_NAME }} key=${{ env.STATE_BUCKET_KEY }} \ No newline at end of file diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..ca695b1 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,31 @@ +name: Lint Terraform plan + +on: + push: + branches-ignore: + - main + +jobs: + validate: + runs-on: ubuntu-latest + name: Validate Terraform + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Terraform validate + uses: dflook/terraform-validate@v2 + with: + path: infra + + fmt-check: + runs-on: ubuntu-latest + name: Terraform formatting + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Terraform fmt + uses: dflook/terraform-fmt-check@v2 + with: + path: infra \ No newline at end of file diff --git a/.github/workflows/plan.yaml b/.github/workflows/plan.yaml new file mode 100644 index 0000000..6f02ed2 --- /dev/null +++ b/.github/workflows/plan.yaml @@ -0,0 +1,61 @@ +name: Create Terraform plan + +on: [pull_request] + +permissions: + contents: read + pull-requests: write + +jobs: + plan: + runs-on: ubuntu-latest + name: Create a Terraform plan + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TF_VAR_hcloud_token: ${{ secrets.TF_HCLOUD_TOKEN }} + STATE_BUCKET_NAME: ${{ secrets.TF_STATE_BUCKET_NAME }} + STATE_BUCKET_KEY: ${{ secrets.TF_STATE_BUCKET_KEY }} + AWS_ACCESS_KEY_ID: ${{ secrets.TF_STATE_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.TF_STATE_SECRET_KEY }} + AWS_CA_BUNDLE: "" + steps: + - name: Checkout + uses: actions/checkout@v4 + + # - name: Setup Homebrew + # run: | + # /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + # echo >> ~/.bashrc + # echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"' >> ~/.bashrc + # eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)" + # env: + # NONINTERACTIVE: 1 + + # - name: Install Packer + # run: | + # sudo wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg + # sudo echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list + # sudo apt update + # sudo apt install packer + + # - name: Install Talosctl + # run: curl -sL https://talos.dev/install | sh + + - name: Terraform plan + uses: dflook/terraform-plan@v2 + with: + path: infra + backend_config: bucket=${{ env.STATE_BUCKET_NAME }} key=${{ env.STATE_BUCKET_KEY }} + env: + TERRAFORM_PRE_RUN: | + # Install prerequisites + apt update + apt install -y lsb-release + + # Install latest Packer + wget -O - https://apt.releases.hashicorp.com/gpg | gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main" | tee /etc/apt/sources.list.d/hashicorp.list + apt install -y packer + + # Install latest Talosctl + curl -sL https://talos.dev/install | sh \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4104e39..86ce1b7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,9 @@ terraform.tfvars *.auto.tfvars # secrets -kubeconfig -talosconfig \ No newline at end of file +.kubeconfig +.kubeconfig.bak +.talosconfig +.talosconfig.bak +.env +.environment diff --git a/README.md b/README.md index 413558f..64209da 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,42 @@ # Cloudijs Platform -Hosting platform based on Kubernetes. This repository contain the IAC used to setup this platform on [Hetzner](https://www.hetzner.com). The platform relies heavily on the amazing [terraform-hcloud-kubernetes](https://github.com/hcloud-k8s/terraform-hcloud-kubernetes) Terraform module. +Hosting platform based on Kubernetes. This repository contain the Terraform code used to setup this platform on [Hetzner](https://www.hetzner.com). The platform relies heavily on the amazing [terraform-hcloud-kubernetes](https://github.com/hcloud-k8s/terraform-hcloud-kubernetes) Tofu/Terraform module. + +## Pre-requisites + +* [Hetzner Account](https://www.hetzner.com) +* [OpenTofu](https://opentofu.org/docs/intro/install/) +* [Taskfile](https://taskfile.dev/docs/installation) ## Deployment -To deploy the platform you will need a Hetzer account and create a [token](https://docs.hetzner.com/cloud/api/getting-started/generating-api-token/). Then run Terraform or Tofu after setting the token variable: -```bash -export TF_VAR_hcloud_token="" -tofu plan -tofy apply +To deploy the platform you will need a Hetzner account, create an S3 bucket and create an [API token](https://docs.hetzner.com/cloud/api/getting-started/generating-api-token/). Make sure to gather the values for the S3 bucket configuration. These can be specified as environment variables or in an `.env` file: +```sh +export HCLOUD_TOKEN="" + +export STATE_BUCKET_NAME="" +export STATE_BUCKET_KEY="" + +export STATE_BUCKET_ACCESS_KEY="" +export STATE_BUCKET_SECRET_KEY="" +``` +``` +# .env + +HCLOUD_TOKEN="" + +STATE_BUCKET_NAME="" +STATE_BUCKET_KEY="" + +STATE_BUCKET_ACCESS_KEY="" +STATE_BUCKET_SECRET_KEY="" +``` +Run Tofu after setting the required variables to setup the platform: +```sh +task create ``` +Other available tasks like destroying the environment can be found using the `task` command. ## Sources @@ -20,7 +46,8 @@ tofy apply * https://registry.terraform.io/providers/hetznercloud/hcloud/latest * https://docs.hetzner.cloud/changelog#2025-04-23-talos-linux-v195-iso-now-available * https://github.com/hetznercloud/hcloud-cloud-controller-manager/tree/main +* https://github.com/dflook/terraform-github-actions ## License -[MIT license](LICENSE) \ No newline at end of file +[MIT license](LICENSE) diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..43e4c56 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,57 @@ +--- +version: "3" + +dotenv: + - .env + - ../.env + +tasks: + default: + desc: List available tasks + cmds: + - task --list-all + + create: + desc: Create all resources + dir: infra/ + cmds: + - tofu init -backend-config="bucket=${STATE_BUCKET_NAME}" -backend-config="key=${STATE_BUCKET_KEY}" -upgrade + - tofu apply -auto-approve + env: + AWS_ACCESS_KEY_ID: + sh: echo "${STATE_BUCKET_ACCESS_KEY}" + AWS_SECRET_ACCESS_KEY: + sh: echo "${STATE_BUCKET_SECRET_KEY}" + TF_VAR_hcloud_token: + sh: echo "${HCLOUD_TOKEN}" + + delete: + desc: Destroy all resources + dir: infra/ + cmds: + - cmd: tofu state rm 'module.kubernetes.talos_machine_configuration_apply.worker' + ignore_error: true + - cmd: tofu state rm 'module.kubernetes.talos_machine_configuration_apply.control_plane' + ignore_error: true + - cmd: tofu state rm 'module.kubernetes.talos_machine_secrets.this' + ignore_error: true + - cmd: tofu destroy -auto-approve + - cmd: rm -rf terraform terraform.lock.hcl ../.kubeconfig ../.kubeconfig.bak ../.talosconfig ../.talosconfig.bak + env: + AWS_ACCESS_KEY_ID: + sh: echo "${STATE_BUCKET_ACCESS_KEY}" + AWS_SECRET_ACCESS_KEY: + sh: echo "${STATE_BUCKET_SECRET_KEY}" + TF_VAR_hcloud_token: + sh: echo "${HCLOUD_TOKEN}" + + validate: + desc: Run syntax and linting checks + cmds: + - cmd: tflint --chdir infra/ + + recreate: + desc: Recreate all resources + cmds: + - task: delete + - task: create diff --git a/infra/backend.tf b/infra/backend.tf new file mode 100644 index 0000000..8dcd10b --- /dev/null +++ b/infra/backend.tf @@ -0,0 +1,11 @@ +terraform { + backend "s3" { + region = "us-east-1" # Required but not used by Hetzner + endpoints = { s3 = "https://fsn1.your-objectstorage.com" } # Falkenstein region + use_path_style = true + skip_credentials_validation = true + skip_region_validation = true + skip_requesting_account_id = true + skip_metadata_api_check = true + } +} diff --git a/infra/main.tf b/infra/main.tf new file mode 100644 index 0000000..34a6233 --- /dev/null +++ b/infra/main.tf @@ -0,0 +1,39 @@ +module "kubernetes" { + source = "hcloud-k8s/kubernetes/hcloud" + version = "3.26.1" + + # General configuration + cluster_name = "platform" + hcloud_token = var.hcloud_token + + cluster_delete_protection = false + + # Export configs for talosctl and kubectl + cluster_kubeconfig_path = var.kubernetes_config_path + cluster_talosconfig_path = var.talos_config_path + + # Firewall configuration + firewall_use_current_ipv4 = true + + # Enable Cilium Gateway API and Cert Manager + cert_manager_enabled = true + cilium_gateway_api_enabled = true + + control_plane_nodepools = [ + { name = "control", type = "cx23", location = "fsn1", count = 1 } + ] + worker_nodepools = [ + { name = "worker", type = "cx33", location = "fsn1", count = 2 } + ] +} + +# Setup Cloudijs System +module "cloudijs-system" { + depends_on = [module.kubernetes] + source = "./modules/cloudijs-system" + + namespace = var.system_namespace + repository_url = var.repository_url + repository_ref = var.repository_ref + repository_path = var.repository_path +} \ No newline at end of file diff --git a/infra/manifests/issuer.yaml b/infra/manifests/issuer.yaml new file mode 100644 index 0000000..e909522 --- /dev/null +++ b/infra/manifests/issuer.yaml @@ -0,0 +1,13 @@ +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: http-issuer +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + privateKeySecretRef: + name: http-issuer-account-key + solvers: + - http01: + ingress: + ingressClassName: nginx diff --git a/infra/modules/cloudijs-system/main.tf b/infra/modules/cloudijs-system/main.tf new file mode 100644 index 0000000..672e247 --- /dev/null +++ b/infra/modules/cloudijs-system/main.tf @@ -0,0 +1,44 @@ +# Create namespace for Cloudijs system deployments +resource "kubernetes_namespace_v1" "platform_system" { + metadata { + name = var.namespace + } +} + +# Deploy and configure Flux Operator +resource "helm_release" "flux_operator" { + depends_on = [kubernetes_namespace_v1.platform_system] + + name = "flux-operator" + namespace = var.namespace + repository = "oci://ghcr.io/controlplaneio-fluxcd/charts" + chart = "flux-operator" +} + +resource "helm_release" "flux_instance" { + depends_on = [helm_release.flux_operator] + + name = "flux" + namespace = var.namespace + repository = "oci://ghcr.io/controlplaneio-fluxcd/charts" + chart = "flux-instance" + + set = [ + { + name = "instance.sync.kind" + value = "GitRepository" + }, + { + name = "instance.sync.url" + value = var.repository_url + }, + { + name = "instance.sync.ref" + value = var.repository_ref + }, + { + name = "instance.sync.path" + value = var.repository_path + } + ] +} \ No newline at end of file diff --git a/infra/modules/cloudijs-system/variables.tf b/infra/modules/cloudijs-system/variables.tf new file mode 100644 index 0000000..0d3a23d --- /dev/null +++ b/infra/modules/cloudijs-system/variables.tf @@ -0,0 +1,15 @@ +variable "namespace" { + type = string +} + +variable "repository_url" { + type = string +} + +variable "repository_ref" { + type = string +} + +variable "repository_path" { + type = string +} \ No newline at end of file diff --git a/infra/provider.tf b/infra/provider.tf new file mode 100644 index 0000000..fec69fc --- /dev/null +++ b/infra/provider.tf @@ -0,0 +1,34 @@ +terraform { + required_version = ">=1.9.0" + + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = "3.0.1" + } + helm = { + source = "hashicorp/helm" + version = "3.1.1" + } + } +} + +provider "kubernetes" { + host = module.kubernetes.kubeconfig_data["server"] + + cluster_ca_certificate = module.kubernetes.kubeconfig_data["ca"] + + client_certificate = module.kubernetes.kubeconfig_data["cert"] + client_key = module.kubernetes.kubeconfig_data["key"] +} + +provider "helm" { + kubernetes = { + host = module.kubernetes.kubeconfig_data["server"] + + cluster_ca_certificate = module.kubernetes.kubeconfig_data["ca"] + + client_certificate = module.kubernetes.kubeconfig_data["cert"] + client_key = module.kubernetes.kubeconfig_data["key"] + } +} diff --git a/infra/variables.tf b/infra/variables.tf new file mode 100644 index 0000000..849f4fd --- /dev/null +++ b/infra/variables.tf @@ -0,0 +1,35 @@ +# Hetzner secrets +variable "hcloud_token" { + sensitive = true + type = string +} + +# Config paths +variable "kubernetes_config_path" { + default = "../.kubeconfig" + type = string +} +variable "talos_config_path" { + default = "../.talosconfig" + type = string +} + +# System variables +variable "system_namespace" { + default = "cloudijs-system" + type = string +} + +# Git repository +variable "repository_url" { + default = "https://github.com/cloudijs/platform" + type = string +} +variable "repository_ref" { + default = "refs/heads/main" + type = string +} +variable "repository_path" { + default = "apps" + type = string +} \ No newline at end of file diff --git a/terraform/main.tf b/terraform/main.tf deleted file mode 100644 index e62ea46..0000000 --- a/terraform/main.tf +++ /dev/null @@ -1,24 +0,0 @@ -module "kubernetes" { - source = "hcloud-k8s/kubernetes/hcloud" - version = "3.1.0" - - cluster_delete_protection = false - - cluster_name = "platform" - hcloud_token = var.hcloud_token - - # Export configs for Talos and Kube API access - cluster_kubeconfig_path = "kubeconfig" - cluster_talosconfig_path = "talosconfig" - - # Addons - cert_manager_enabled = true - ingress_nginx_enabled = true - - control_plane_nodepools = [ - { name = "control", type = "cx22", location = "fsn1", count = 1 } - ] - worker_nodepools = [ - { name = "worker", type = "cpx11", location = "fsn1", count = 2 } - ] -} diff --git a/terraform/variables.tf b/terraform/variables.tf deleted file mode 100644 index 15e218d..0000000 --- a/terraform/variables.tf +++ /dev/null @@ -1,3 +0,0 @@ -variable "hcloud_token" { - sensitive = true -} \ No newline at end of file