diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..03cdeec --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +secrets.yaml diff --git a/README.md b/README.md index f1fdad6..8a680bb 100644 --- a/README.md +++ b/README.md @@ -1 +1,98 @@ -# dev-cluster \ No newline at end of file +# Dev-Cluster GitOps Provisioning + +Provisioning, configuration and manifests for my Kubernetes dev cluster on Hetzner Cloud, set up for GitOps with Flux CD. + +## Prerequisites + +- [Terraform](https://www.terraform.io/downloads.html) installed +- [SOPS](https://github.com/mozilla/sops) installed +- [Age](https://github.com/FiloSottile/age) installed (for encryption) +- A Hetzner Cloud account and API token +- A GitHub account and personal access token (for Flux) + +## Setup Steps + +1. **Generate an Age key:** + ``` + age-keygen -o key.txt + ``` + +2. **Create a `.sops.yaml` file in your project root:** + ```yaml + creation_rules: + - path_regex: secrets\.enc\.yaml$ + age: + ``` + Replace `` with the public key from your `key.txt` file. + +3. **Create a `secrets.yaml` file with your sensitive data:** + ```yaml + username: + user_hashed_password: + user_ssh_public_key: + github_username: + github_repo: + ``` + +4. **Encrypt the secrets file:** + ``` + sops -e secrets.yaml > secrets.enc.yaml + ``` + +5. **Create a `terraform.tfvars` file for your Hetzner Cloud token:** + ```hcl + hcloud_token = "your-hetzner-cloud-token" + ``` + +6. **Initialize Terraform:** + ``` + terraform init + ``` + +7. **Plan your Terraform deployment:** + ``` + terraform plan + ``` + +8. **Apply your Terraform configuration:** + ``` + terraform apply + ``` + +## File Structure + +- `main.tf`: Main Terraform configuration file +- `variables.tf`: Terraform variables definition +- `cloud-init.yaml`: Cloud-init configuration template +- `secrets.enc.yaml`: Encrypted secrets file (do not commit to version control) +- `terraform.tfvars`: Terraform variables values (do not commit to version control) +- `.sops.yaml`: SOPS configuration file + +## Usage + +After successful provisioning, you can access your new server using SSH: + +``` +ssh @ +``` + +The server IP will be output by Terraform after successful application. + +## Customization + +- Modify `cloud-init.yaml` to change the initial server setup. +- Adjust `main.tf` to change Hetzner Cloud resources or add additional configurations. + +## Security Notes + +- Never commit `secrets.yaml`, `secrets.enc.yaml`, or `terraform.tfvars` to version control. +- Keep your `key.txt` file secure and backed up. Losing this file means losing access to your encrypted secrets. + +## Troubleshooting + +If you encounter issues: +1. Check Terraform output for errors. +2. Review cloud-init logs on the server: `/var/log/cloud-init-output.log` +3. Ensure all required variables are correctly set in your encrypted secrets file. + +For further assistance, please open an issue in the project repository. \ No newline at end of file diff --git a/apps/.git-keep b/apps/.git-keep new file mode 100644 index 0000000..e69de29 diff --git a/cluster-config/.git-keep b/cluster-config/.git-keep new file mode 100644 index 0000000..e69de29 diff --git a/infrastructure/cloud-init.yaml b/infrastructure/cloud-init.yaml new file mode 100644 index 0000000..81c0973 --- /dev/null +++ b/infrastructure/cloud-init.yaml @@ -0,0 +1,80 @@ +#cloud-config +package_update: true +package_upgrade: true +package_reboot_if_required: true + +users: +- name: ${username} + groups: [ sudo ] + shell: /usr/bin/zsh + hashed_passwd: ${user_hashed_password} + lock_passwd: false + ssh_authorized_keys: + - ${user_ssh_public_key} +- name: git + lock_passwd: true + +packages: + - apt-transport-https + - ca-certificates + - curl + - gnupg2 + - git + - zsh + - ufw + - fail2ban + - tmux + - bat + - unzip + +write_files: + - content: | + #!/bin/sh + GITEA_POD=$(kubectl --kubeconfig /home/git/.kube/config get po -n gitea -l app=gitea -o name --no-headers=true | cut -d'/' -f2) + kubectl --kubeconfig /home/git/.kube/config exec -i -n gitea $GITEA_POD -c gitea -- env SSH_ORIGINAL_COMMAND="$SSH_ORIGINAL_COMMAND" /bin/sh "$@" + path: /usr/local/bin/gitea-shell + permissions: '0755' + - content: | + #!/bin/sh + GITEA_POD=$(kubectl --kubeconfig /home/git/.kube/config get po -n gitea -l app=gitea -o name --no-headers=true | cut -d'/' -f2) + kubectl --kubeconfig /home/git/.kube/config exec -i -n gitea $GITEA_POD -c gitea -- /usr/local/bin/gitea keys -e git -u $1 -t $2 -k $3 + permissions: '0755' + path: /usr/local/bin/gitea-keys + +ssh: + emit_keys_to_console: false +ssh_pwauth: false +disable_root: true + +ssh_config: + Match User git: + AuthorizedKeysCommandUser: git + AuthorizedKeysCommand: /usr/local/bin/gitea-keys %u %t %k + +runcmd: + # UFW + - ufw default deny incoming + - ufw default allow outgoing + - ufw allow ssh + - ufw allow http + - ufw allow https + - ufw logging on + - ufw enable + # SSH key for user + - su ${username} -c 'ssh-keygen -t ed25519 -f /home/${username}/.ssh/id_ed25519 -q -N "" ' + # Expire password for user + - chage -d 0 ${username} + # SSH Passthrough for user git + - usermod -s /usr/local/bin/gitea-shell git + # k3s + - curl -sfL https://get.k3s.io | sh -s - --disable=traefik + # helm + - curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 + - chmod 700 get_helm.sh + - ./get_helm.sh + # Install Flux + - curl -s https://fluxcd.io/install.sh | sudo bash + # Bootstrap Flux (adjust the GitHub details as needed) + - su ${username} -c 'flux bootstrap github --owner=${github_username} --repository=${github_repo} --path=cluster-config --personal' + +final_message: "The system is finally up, after $UPTIME seconds" \ No newline at end of file diff --git a/infrastructure/main.tf b/infrastructure/main.tf new file mode 100644 index 0000000..cf42042 --- /dev/null +++ b/infrastructure/main.tf @@ -0,0 +1,97 @@ +# Configure the Hetzner Cloud Provider +terraform { + required_providers { + hcloud = { + source = "hetznercloud/hcloud" + } + sops = { + source = "carlpett/sops" + version = "~> 0.5" + } + } + required_version = ">= 0.13" +} + +provider "hcloud" { + token = var.hcloud_token +} + +provider "sops" {} + +data "sops_file" "secrets" { + source_file = "secrets.enc.yaml" +} + +data "cloudinit_config" "k8s_node" { + gzip = true + base64_encode = true + + part { + content_type = "text/cloud-config" + content = templatefile("${path.module}/cloud-init.yaml", { + username = data.sops_file.secrets.data["username"] + user_hashed_password = data.sops_file.secrets.data["user_hashed_password"] + user_ssh_public_key = data.sops_file.secrets.data["user_ssh_public_key"] + github_username = data.sops_file.secrets.data["github_username"] + github_repo = data.sops_file.secrets.data["github_repo"] + }) + } +} + +resource "hcloud_server" "cluster" { + name = "auberon" + image = "ubuntu-24.04" + server_type = "cx22" + location = "nbg1" + backups = true + user_data = data.cloudinit_config.k8s_node.rendered +} + +resource "hcloud_firewall" "cluster-firewall" { + name = "cluster-firewall" + apply_to { + server = hcloud_server.cluster.id + } + rule { + direction = "in" + protocol = "icmp" + source_ips = [ + "0.0.0.0/0", + "::/0" + ] + } + + rule { + direction = "in" + protocol = "tcp" + port = "80" + source_ips = [ + "0.0.0.0/0", + "::/0" + ] + } + + rule { + direction = "in" + protocol = "tcp" + port = "443" + source_ips = [ + "0.0.0.0/0", + "::/0" + ] + } + + rule { + direction = "in" + protocol = "tcp" + port = "22" + source_ips = [ + "0.0.0.0/0", + "::/0" + ] + } +} + +output "server_ip" { + value = hcloud_server.cluster.ipv4_address +} \ No newline at end of file diff --git a/infrastructure/variables.tf b/infrastructure/variables.tf new file mode 100644 index 0000000..84c6da0 --- /dev/null +++ b/infrastructure/variables.tf @@ -0,0 +1,5 @@ +variable "hcloud_token" { + description = "Hetzner Cloud API Token" + type = string + sensitive = true +} \ No newline at end of file