diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..ed4ca29 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,4 @@ +[defaults] +inventory = ansible/inventory.yml +roles_path = ansible/roles +host_key_checking = False diff --git a/ansible/inventory.yml b/ansible/inventory.yml new file mode 100644 index 0000000..b435bd9 --- /dev/null +++ b/ansible/inventory.yml @@ -0,0 +1,17 @@ +all: + vars: + ansible_user: nik + ansible_ssh_private_key_file: ~/.ssh/id_ed25519-nik-macbookair + + children: + k3s_server: + hosts: + minisforum: + ansible_host: 192.168.7.77 + ansible_port: 430 + + k3s_agents: + hosts: + # debian will be added here in Phase 2 + # debian: + # ansible_host: 192.168.7.X diff --git a/ansible/playbooks/bootstrap-minisforum.yml b/ansible/playbooks/bootstrap-minisforum.yml new file mode 100644 index 0000000..fe50611 --- /dev/null +++ b/ansible/playbooks/bootstrap-minisforum.yml @@ -0,0 +1,18 @@ +--- +# Run: ansible-playbook -i ansible/inventory.yml ansible/playbooks/bootstrap-minisforum.yml +# Requires: SSH access to 192.168.7.7 as root (or a user with NOPASSWD sudo) +# +# What this does: +# - Creates the 'nik' user with sudo access +# - Hardens SSH (no password auth, no root login) +# - Installs base packages +# - Configures UFW firewall +# - Creates /data/* directories for persistent volumes + +- name: Bootstrap Minisforum + hosts: minisforum + become: true + gather_facts: true + + roles: + - common diff --git a/ansible/playbooks/setup-k3s.yml b/ansible/playbooks/setup-k3s.yml new file mode 100644 index 0000000..e5e8707 --- /dev/null +++ b/ansible/playbooks/setup-k3s.yml @@ -0,0 +1,27 @@ +--- +# Run: ansible-playbook -i ansible/inventory.yml ansible/playbooks/setup-k3s.yml +# +# What this does: +# - Installs K3s in server mode (with Traefik disabled) +# - Installs Helm +# - Fetches kubeconfig to /tmp/k3s-minisforum.yaml on your workstation +# - Labels the node as node-role=primary +# +# After this playbook: +# export KUBECONFIG=/tmp/k3s-minisforum.yaml +# kubectl get nodes # should show minisforum as Ready +# +# Then deploy Traefik: +# helm repo add traefik https://helm.traefik.io/traefik +# helm repo update +# helm upgrade --install traefik traefik/traefik \ +# --namespace traefik --create-namespace \ +# -f values/traefik.yml + +- name: Install K3s server + hosts: minisforum + become: true + gather_facts: true + + roles: + - k3s-server diff --git a/ansible/roles/common/defaults/main.yml b/ansible/roles/common/defaults/main.yml new file mode 100644 index 0000000..cb5b45a --- /dev/null +++ b/ansible/roles/common/defaults/main.yml @@ -0,0 +1,31 @@ +--- +username: nik +timezone: Asia/Tokyo + +base_packages: + - curl + - git + - htop + - vim + - wget + - unzip + - ca-certificates + - gnupg + - lsb-release + - nfs-common # needed in Phase 4 for Jellyfin NFS mount from Debian + +ufw_allowed_ports: + - { port: 430, proto: tcp, comment: SSH } + - { port: 80, proto: tcp, comment: HTTP } + - { port: 443, proto: tcp, comment: HTTPS } + - { port: 6443, proto: tcp, comment: K3s API server } + - { port: 10250, proto: tcp, comment: Kubelet } + - { port: 8472, proto: udp, comment: Flannel VXLAN } + +data_dirs: + - /data/gitea + - /data/jellyfin + - /data/pihole + - /data/dashy + - /data/glances + - /data/traefik diff --git a/ansible/roles/common/handlers/main.yml b/ansible/roles/common/handlers/main.yml new file mode 100644 index 0000000..6998953 --- /dev/null +++ b/ansible/roles/common/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: Restart sshd + ansible.builtin.service: + name: sshd + state: restarted diff --git a/ansible/roles/common/tasks/main.yml b/ansible/roles/common/tasks/main.yml new file mode 100644 index 0000000..f134e70 --- /dev/null +++ b/ansible/roles/common/tasks/main.yml @@ -0,0 +1,73 @@ +--- +- name: Set timezone + community.general.timezone: + name: "{{ timezone }}" + +- name: Install base packages + ansible.builtin.apt: + name: "{{ base_packages }}" + state: present + update_cache: true + +- name: Create primary user + ansible.builtin.user: + name: "{{ username }}" + groups: sudo + shell: /bin/bash + create_home: true + state: present + +- name: Set up authorized SSH key for user + ansible.posix.authorized_key: + user: "{{ username }}" + state: present + key: "{{ lookup('file', '~/.ssh/id_ed25519-nik-macbookair.pub') }}" + +- name: Harden SSH — disable password auth + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: "{{ item.regexp }}" + line: "{{ item.line }}" + state: present + loop: + - { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication no' } + - { regexp: '^#?PermitRootLogin', line: 'PermitRootLogin no' } + - { regexp: '^#?PubkeyAuthentication', line: 'PubkeyAuthentication yes' } + - { regexp: '^#?Port ', line: 'Port 430' } + notify: Restart sshd + +- name: Install UFW + ansible.builtin.apt: + name: ufw + state: present + +- name: Set UFW default deny incoming + community.general.ufw: + default: deny + direction: incoming + +- name: Set UFW default allow outgoing + community.general.ufw: + default: allow + direction: outgoing + +- name: Allow required ports + community.general.ufw: + rule: allow + port: "{{ item.port }}" + proto: "{{ item.proto }}" + comment: "{{ item.comment }}" + loop: "{{ ufw_allowed_ports }}" + +- name: Enable UFW + community.general.ufw: + state: enabled + +- name: Create persistent data directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: "{{ username }}" + group: "{{ username }}" + mode: "0755" + loop: "{{ data_dirs }}" diff --git a/ansible/roles/k3s-server/defaults/main.yml b/ansible/roles/k3s-server/defaults/main.yml new file mode 100644 index 0000000..e201f8d --- /dev/null +++ b/ansible/roles/k3s-server/defaults/main.yml @@ -0,0 +1,14 @@ +--- +k3s_version: v1.32.2+k3s1 # pin to a specific version; update deliberately +k3s_server_ip: 192.168.7.77 + +# Written to /etc/rancher/k3s/config.yaml on the server +k3s_server_config: + disable: + - traefik # we deploy Traefik ourselves via Helm + flannel-backend: vxlan + node-ip: "{{ k3s_server_ip }}" + tls-san: + - "{{ k3s_server_ip }}" + - minisforum + - minisforum.local diff --git a/ansible/roles/k3s-server/tasks/main.yml b/ansible/roles/k3s-server/tasks/main.yml new file mode 100644 index 0000000..de1dcdb --- /dev/null +++ b/ansible/roles/k3s-server/tasks/main.yml @@ -0,0 +1,68 @@ +--- +- name: Create K3s config directory + ansible.builtin.file: + path: /etc/rancher/k3s + state: directory + mode: "0755" + +- name: Write K3s server config + ansible.builtin.copy: + dest: /etc/rancher/k3s/config.yaml + content: "{{ k3s_server_config | to_nice_yaml }}" + mode: "0644" + +- name: Download and install K3s + ansible.builtin.shell: + cmd: > + curl -sfL https://get.k3s.io | + INSTALL_K3S_VERSION={{ k3s_version }} + sh - + creates: /usr/local/bin/k3s # skip if already installed + +- name: Wait for K3s to be ready + ansible.builtin.wait_for: + path: /etc/rancher/k3s/k3s.yaml + timeout: 60 + +- name: Ensure K3s service is running + ansible.builtin.service: + name: k3s + state: started + enabled: true + +- name: Read node token + ansible.builtin.slurp: + src: /var/lib/rancher/k3s/server/node-token + register: k3s_token_raw + +- name: Save node token as fact + ansible.builtin.set_fact: + k3s_node_token: "{{ k3s_token_raw['content'] | b64decode | trim }}" + +- name: Print node token (needed for Phase 2 agent join) + ansible.builtin.debug: + msg: "K3s node token: {{ k3s_node_token }}" + +- name: Fetch kubeconfig to workstation + ansible.builtin.fetch: + src: /etc/rancher/k3s/k3s.yaml + dest: /tmp/k3s-minisforum.yaml + flat: true + +- name: Fix kubeconfig server address + ansible.builtin.replace: + path: /tmp/k3s-minisforum.yaml + regexp: 'https://127\.0\.0\.1:6443' + replace: "https://{{ k3s_server_ip }}:6443" + delegate_to: localhost + become: false + +- name: Install Helm + ansible.builtin.shell: + cmd: curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + creates: /usr/local/bin/helm + +- name: Label server node as primary + ansible.builtin.shell: + cmd: k3s kubectl label node minisforum node-role=primary --overwrite + changed_when: false # label is idempotent but shell module always reports changed diff --git a/values/traefik.yml b/values/traefik.yml new file mode 100644 index 0000000..a7e22df --- /dev/null +++ b/values/traefik.yml @@ -0,0 +1,88 @@ +# Traefik Helm values — Phase 1 +# Chart: traefik/traefik +# Deploy: +# helm repo add traefik https://helm.traefik.io/traefik +# helm repo update +# helm upgrade --install traefik traefik/traefik \ +# --namespace traefik --create-namespace \ +# -f values/traefik.yml + +globalArguments: + - "--global.checknewversion=false" + - "--global.sendanonymoususage=false" + +additionalArguments: + - "--certificatesresolvers.letsencrypt.acme.httpchallenge=true" + - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web" + - "--certificatesresolvers.letsencrypt.acme.email=nik@nik4nao.xyz" + - "--certificatesresolvers.letsencrypt.acme.storage=/data/traefik/acme.json" + +entryPoints: + web: + address: ":80" + http: + redirections: + entryPoint: + to: websecure + scheme: https + websecure: + address: ":443" + +ingressClass: + enabled: true + isDefaultIngressClass: true + +service: + type: LoadBalancer + # K3s includes ServiceLB (klipper) — it will bind this to the node's IP automatically + +persistence: + enabled: false + existingClaim: "" + storageClass: "" + path: /data/traefik + size: 128Mi + accessMode: ReadWriteOnce + +volumes: + - name: traefik-data + hostPath: + path: /data/traefik + type: DirectoryOrCreate + +volumeMounts: + - name: traefik-data + mountPath: /data/traefik + +deployment: + replicas: 1 + # Pin to Minisforum (primary node) + # Remove this section in Phase 2 once you have a multi-node cluster + # and only want Traefik on the server node + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: node-role + operator: In + values: + - primary + +dashboard: + enabled: true + # Accessible internally at http://traefik.192.168.7.7.nip.io or via IngressRoute + # Do NOT expose the dashboard externally + ingressRoute: + dashboard: + enabled: true + matchRule: Host(`traefik.home.arpa`) && (PathPrefix(`/dashboard`) || PathPrefix(`/api`)) + entryPoints: + - websecure + # Add BasicAuth middleware here if you want dashboard password protection + +logs: + general: + level: INFO + access: + enabled: true