commit 80b66bdf0c3580be0e9f35357e58831043b430ee Author: Jordan Del Pilar Date: Fri Jun 12 13:02:38 2026 -0700 feat: initial commit Creates foundation for home lab environment - sets system baselines and creates required directories - adds `common` role for system baselines - adds `dns_server` role to set up dns for internal routing - adds `gitea_server` role to set up git and action runner diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f0dd083 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Ansible Vault Password File +.vault-pass.txt \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1c5a53e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "gitea" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5472757 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# johto-infra +The core Ansible project for provisioning and managing the Del Pilar homelab infrastructure + + +## Summary +This contains the primary Ansible configuration-as-code to provision and manage my home infrastructure. +Currently this system manages server baselines, and container deployment for internal DNS (via AdguardHome) and git/ci (via Gitea) using Podman Quadlets + +## Scope and Limitations +**Warning** This project is heavily opinionated and designed to work with one specific home lab architecture. + +The roles, tasks and templates within this repo are best used as a reference for managing services running as podman quadlets. + +If you want to run this repo against your own home lab you will need to rewrite 'inventory.yaml' and all group and host vars to match your specific environment. + +## Repository Structure +Below is the directory map for this repo. +```text +├── ansible.cfg +├── group_vars +│ └── all.yaml +├── host_vars +│ ├── goldenrod.yaml +│ └── new-bark.yaml +├── inventory.yaml +├── README.md +├── roles +│ ├── common +│ │ └── tasks +│ │ └── main.yaml +│ ├── dns_server +│ │ ├── handlers +│ │ │ └── main.yaml +│ │ ├── tasks +│ │ │ └── main.yaml +│ │ └── templates +│ │ └── adguard_quadlet.j2 +│ └── gitea_server +│ ├── handlers +│ │ └── main.yaml +│ ├── tasks +│ │ └── main.yaml +│ └── templates +│ ├── act_runner_quadlet.j2 +│ └── gitea_quadlet.j2 +└── site.yaml +```` + +## Prerequisites + +To run this project locally the following tools are required. +All install commands assume you are running Ubuntu/Debian. + +- Ansible installed on the control machine + ```bash + sudo apt install ansible + ``` +- Ansible Lint (optional but highly recommended if making changes) + ```bash + sudo apt install ansible-lint + ``` +- SSH access to target nodes + +## How to Run +The ultimate goal of this project is a fully automated gitops workflow. +However, until that is fully running below are the instructions to run this project locally. + +- Clone the repo + ```bash + git clone git@git.delpilar.net:jdelpilar/johto-infra.git + cd johto-infra + ``` +- To run all plays in site.yaml and fully initialize the environment run the following command. This will target all nodes. + ```bash + ansible-playbook site.yaml + ``` +- To limit the execution to a single host use the limit flag (-l, --limit) + ```bash + # This will only target new-bark + ansible-playbook site.yaml -l new-bark + ``` +- To limit the execution to only a specific service or tag, use the tag flag (-t, --tags) + ```bash + # This will only run dns tasks, but will target all nodes + ansible-playbook site.yaml -t dns + ``` +- These flags can be combined if needed + ```bash + # This will only run dns tasks and only target new-bark + ansible-playbook site.yaml -t dns -l new-bark + ``` + +## Services Deployed +Below is a list of all services currently deployed by this project. This list will be updated as new services are added + +Unless otherwise stated all services are run via rootless podman quadlets. + +- Internal DNS (dns_server) - AdguardHome for network wide adblocking and local DNS resoultion +- Git Server and CI/CD runner (gitea_server) - Gitea alongside act runner for local git with repo mirroring and local private ci/cd runners diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..3dc4ad5 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,21 @@ +[defaults] +inventory = ./inventory.yaml +host_key_checking = False +vault_password_file = .vault-pass.txt +forks = 5 +stdout_callback = yaml +callbacks_enabled = ansible.posix.profile_tasks +interpreter_python = auto_silent +retry_files_enabled = False +ansible_managed = Ansible managed: modified on %Y-%m-%d %H:%M:%S +gathering = smart +fact_caching = jsonfile +fact_caching_connection = /tmp/ansible_fact_cache +fact_caching_timeout = 7200 + +[ssh_connection] +pipelining = True +ssh_args = -o ControlMaster=auto -o ControlPersist=60s + +[privilege_escalation] +become_method = sudo diff --git a/group_vars/all.yaml b/group_vars/all.yaml new file mode 100644 index 0000000..2b6e02f --- /dev/null +++ b/group_vars/all.yaml @@ -0,0 +1,2 @@ +--- +github_key_url: https://github.com/redjordan2539.keys diff --git a/host_vars/goldenrod.yaml b/host_vars/goldenrod.yaml new file mode 100644 index 0000000..564e2f8 --- /dev/null +++ b/host_vars/goldenrod.yaml @@ -0,0 +1,11 @@ +--- +ansible_user: jdelpilar +ansible_become_password: !vault | + $ANSIBLE_VAULT;1.1;AES256 + 38343539383461386463663730376139636633656434396264626532313465393762323835393931 + 3030333739373033396639643539613235646364656331660a346431323433366239653535396566 + 62353361393734623632316463393931636566613965306262313038616665323466373363653665 + 6434333764636333370a653836343436643631643833333838643264326663333062646235656164 + 31653163653066653561393333343263653732333335633566363136353164666330 +podman_config_base_dir: /home/{{ ansible_user }}/podman +podman_quadlet_base_dir: "/home/{{ ansible_user }}/.config/containers/systemd" diff --git a/host_vars/new-bark.yaml b/host_vars/new-bark.yaml new file mode 100644 index 0000000..2ad3aa8 --- /dev/null +++ b/host_vars/new-bark.yaml @@ -0,0 +1,45 @@ +--- +ansible_user: jdelpilar +ansible_become_password: !vault | + $ANSIBLE_VAULT;1.1;AES256 + 66633539643530363735343838646634366161346239366132643762323465323766353230343836 + 3935666665383830383163363938303239656436333061370a323236303436313836616233333639 + 38333035643338643439393335356436356434343135663464353863343134386633346466333838 + 3264376339343239650a383939393663346231386461653738656330336338656436373866306137 + 3961 + +podman_config_base_dir: /appdata/podman +podman_quadlet_base_dir: "/home/{{ ansible_user }}/.config/containers/systemd" + +dns_services: + - name: adguard + image: docker.io/adguard/adguardhome:latest + category: management + volumes: + - /appdata/podman/adguard/work:/opt/adguardhome/work + - /appdata/podman/adguard/conf:/opt/adguardhome/conf + +gitea_services: + - name: gitea + image: docker.io/gitea/gitea:latest + container_owner: jdelpilar + subdomain: git + domain: delpilar.net + port: 3000 + volumes: + - /appdata/podman/gitea/data:/var/lib/gitea + - /appdata/podman/gitea/config:/etc/gitea + +runner_services: + - name: act_runner + image: docker.io/gitea/act_runner:nightly + container_owner: jdelpilar + gitea_url: https://git.delpilar.net + runner_token: !vault | + $ANSIBLE_VAULT;1.1;AES256 + 35303036303665376330616430373733363066333332323362623237373262383230316431346338 + 3862636465623436343062316662666531623562353164650a313434633865656634366136643430 + 37653263636535656561613663363165313332323164313365306433363036333437373333316632 + 6563356532323964660a626531303962626236363961356639623263373030663462623235393366 + 36343736663063313933306465626634383863366538343930386335353762663866623535636465 + 3461386532333061323435363366653633613035386363633162 diff --git a/inventory.yaml b/inventory.yaml new file mode 100644 index 0000000..4c4b6a8 --- /dev/null +++ b/inventory.yaml @@ -0,0 +1,31 @@ +--- +all: + children: + servers: + hosts: + new-bark: + ansible_host: 100.64.0.2 + goldenrod: + ansible_host: 100.64.0.1 + traefik_servers: + hosts: + new-bark: + goldenrod: + dns_servers: + hosts: + new-bark: + media_servers: + hosts: + new-bark: + arr_servers: + hosts: + new-bark: + headscale_servers: + hosts: + goldenrod: + vaultwarden_servers: + hosts: + goldenrod: + gitea_servers: + hosts: + new-bark: diff --git a/roles/common/tasks/main.yaml b/roles/common/tasks/main.yaml new file mode 100644 index 0000000..d31e43f --- /dev/null +++ b/roles/common/tasks/main.yaml @@ -0,0 +1,57 @@ +--- +- name: Enable systemd Lingering + ansible.builtin.file: + path: "/var/lib/systemd/linger/{{ ansible_user }}" + state: touch + owner: root + group: root + mode: "0644" + become: true + +- name: Deploy workstation public keys from GitHub + ansible.posix.authorized_key: + user: "{{ ansible_user }}" + state: present + key: "{{ github_key_url }}" + exclusive: true + +- name: Install Podman + ansible.builtin.package: + name: podman + state: present + become: true + +- name: Start Podman Timer + ansible.builtin.systemd: + name: podman-auto-update.timer + state: started + enabled: true + scope: user + +- name: Start Podman Socket + ansible.builtin.systemd: + name: podman.socket + state: started + enabled: true + scope: user + +- name: Create Podman Config Directory + ansible.builtin.file: + path: "{{ podman_config_base_dir }}" + state: directory + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0755" + +- name: Create Podman Quadlet Directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0755" + with_items: + - "{{ podman_quadlet_base_dir }}/infrastructure" + - "{{ podman_quadlet_base_dir }}/management" + - "{{ podman_quadlet_base_dir }}/media" + - "{{ podman_quadlet_base_dir }}/services" diff --git a/roles/dns_server/handlers/main.yaml b/roles/dns_server/handlers/main.yaml new file mode 100644 index 0000000..2156eb1 --- /dev/null +++ b/roles/dns_server/handlers/main.yaml @@ -0,0 +1,12 @@ +- name: Reload systemd User Daemon + ansible.builtin.systemd: + daemon_reload: true + scope: user + become_user: "{{ ansible_user }}" + +- name: Restart changed DNS Services + ansible.builtin.systemd: + name: "{{ item.item.name }}" + state: restarted + scope: user + loop: "{{ dns_quadlet_results.results | selectattr('changed', 'equalto', true) | list }}" diff --git a/roles/dns_server/tasks/main.yaml b/roles/dns_server/tasks/main.yaml new file mode 100644 index 0000000..2249761 --- /dev/null +++ b/roles/dns_server/tasks/main.yaml @@ -0,0 +1,36 @@ +- name: Allow Binding Port 53 + ansible.posix.sysctl: + name: net.ipv4.ip_unprivileged_port_start + value: "53" + state: present + reload: true + become: true + +- name: Create DNS Service Directories + ansible.builtin.file: + path: "{{ item.1.split(':')[0] }}" + state: directory + mode: "0755" + loop: "{{ lookup('ansible.builtin.subelements', dns_services, 'volumes', {'skip_missing': true}) }}" + +- name: Create Podman Quadlets + ansible.builtin.template: + src: adguard_quadlet.j2 + dest: "{{ podman_quadlet_base_dir }}/management/{{ item.name }}.container" + owner: "{{ ansible_user }}" + mode: "664" + loop: "{{ dns_services }}" + register: dns_quadlet_results + notify: + - Reload systemd User Daemon + - Restart changed DNS Services + +- name: Flush Handlers + ansible.builtin.meta: flush_handlers + +- name: Start DNS Services + ansible.builtin.systemd: + name: "{{ item.name }}" + state: started + scope: user + loop: "{{ dns_services }}" diff --git a/roles/dns_server/templates/adguard_quadlet.j2 b/roles/dns_server/templates/adguard_quadlet.j2 new file mode 100644 index 0000000..74b61e8 --- /dev/null +++ b/roles/dns_server/templates/adguard_quadlet.j2 @@ -0,0 +1,44 @@ +# {{ ansible_managed }} +[Unit] +After=network-online.target + +StartLimitBurst=10 +StartLimitIntervalSec=120 + +[Container] +ContainerName=adguard +Image={{ item.image }} + +Network=host + +AutoUpdate={{ item.auto_update | default('registry') }} + +Label=category={{ item.category | default('services') }} +Label=owner={{ container_owner | default('jdelpilar') }} + +HealthCmd=nslookup localhost 127.0.0.1 > /dev/null || exit 1 +HealthInterval=30s +HealthRetries=3 +HealthTimeout=5s + +{% for volume in item.volumes | default([]) %} +Volume={{ volume }} +{% endfor %} + +Environment=TZ={{ timezone | default('America/Los_Angeles') }} +{% if item.env is defined %} +{% for key, value in item.env.items() | sort %} +Environment={{ key }}={{ value }} +{% endfor %} +{% endif %} + +{% if item.exec is defined %} +Exec={{ item.exec }} +{% endif %} + +[Service] +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=default.target \ No newline at end of file diff --git a/roles/gitea_server/handlers/main.yaml b/roles/gitea_server/handlers/main.yaml new file mode 100644 index 0000000..97ab9e3 --- /dev/null +++ b/roles/gitea_server/handlers/main.yaml @@ -0,0 +1,19 @@ +- name: Reload systemd User Daemon + ansible.builtin.systemd: + daemon_reload: true + scope: user + become_user: "{{ ansible_user }}" + +- name: Restart changed Gitea Services + ansible.builtin.systemd: + name: "{{ item.item.name }}" + state: restarted + scope: user + loop: "{{ gitea_quadlet_results.results | selectattr('changed', 'equalto', true) | list }}" + +- name: Restart changed Runner Services + ansible.builtin.systemd: + name: "{{ item.item.name }}" + state: restarted + scope: user + loop: "{{ runner_quadlet_results.results | selectattr('changed', 'equalto', true) | list }}" diff --git a/roles/gitea_server/tasks/main.yaml b/roles/gitea_server/tasks/main.yaml new file mode 100644 index 0000000..9a5f35c --- /dev/null +++ b/roles/gitea_server/tasks/main.yaml @@ -0,0 +1,47 @@ +- name: Create Gitea Directories + ansible.builtin.file: + path: "{{ item.1.split(':')[0] }}" + state: directory + mode: "0755" + loop: "{{ lookup('ansible.builtin.subelements', gitea_services, 'volumes', {'skip_missing': true}) }}" + +- name: Create Gitea Quadlets + ansible.builtin.template: + src: gitea_quadlet.j2 + dest: "{{ podman_quadlet_base_dir }}/management/{{ item.name }}.container" + owner: "{{ ansible_user }}" + mode: "664" + loop: "{{ gitea_services }}" + register: gitea_quadlet_results # <--- Unique name + notify: + - Reload systemd User Daemon + - Restart changed Gitea Services + +- name: Create Act Runner Quadlets + ansible.builtin.template: + src: act_runner_quadlet.j2 + dest: "{{ podman_quadlet_base_dir }}/management/{{ item.name }}container" + owner: "{{ ansible_user }}" + mode: "664" + loop: "{{ runner_services }}" + register: runner_quadlet_results # <--- Unique name + notify: + - Reload systemd User Daemon + - Restart changed Runner Services + +- name: Flush Handlers + ansible.builtin.meta: flush_handlers + +- name: Start Gitea Services + ansible.builtin.systemd: + name: "{{ item.name }}" + state: started + scope: user + loop: "{{ gitea_services }}" + +- name: Start Runner Services + ansible.builtin.systemd: + name: "{{ item.name }}" + state: started + scope: user + loop: "{{ runner_services }}" diff --git a/roles/gitea_server/templates/act_runner_quadlet.j2 b/roles/gitea_server/templates/act_runner_quadlet.j2 new file mode 100644 index 0000000..86c3c24 --- /dev/null +++ b/roles/gitea_server/templates/act_runner_quadlet.j2 @@ -0,0 +1,33 @@ +# {{ ansible_managed }} +[Unit] +After=network-online.target +After=adguard.service +Requires=adguard.service + +StartLimitBurst=10 +StartLimitIntervalSec=120 + +[Container] +ContainerName={{ item.name }} +Image={{ item.image }} + +Network=management-net +PodmanArgs=--add-host=git.delpilar.net:172.23.0.2 + +AutoUpdate=registry + +Label=category=management +Label=owner={{ container_owner | default('jdelpilar') }} + +Volume=/run/user/1000/podman/podman.sock:/var/run/docker.sock + +Environment=TZ='America/Los_Angeles' +Environment=GITEA_INSTANCE_URL={{ item.gitea_url }} +Environment=GITEA_RUNNER_REGISTRATION_TOKEN={{ item.runner_token }} + +[Service] +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=default.target diff --git a/roles/gitea_server/templates/gitea_quadlet.j2 b/roles/gitea_server/templates/gitea_quadlet.j2 new file mode 100644 index 0000000..6c527d7 --- /dev/null +++ b/roles/gitea_server/templates/gitea_quadlet.j2 @@ -0,0 +1,56 @@ +# {{ ansible_managed }} +[Unit] +After=network-online.target +After=adguard.service +Requires=adguard.service + +StartLimitBurst=10 +StartLimitIntervalSec=120 + +[Container] +ContainerName={{ item.name }} +Image={{ item.image }} + +Network=management-net +PublishPort=2222:22 + +AutoUpdate=registry + +Label=category=management +Label=owner={{ container_owner | default('jdelpilar') }} + +{% if item.enable_traefik | default(true) %} +Label=traefik.enable=true +Label=traefik.http.routers.{{ item.name }}.rule=Host(`{{ item.subdomain | default(item.name) }}.{{ base_domain | default('delpilar.net') }}`) +Label=traefik.http.routers.{{ item.name }}.entrypoints={{ item.traefik_entrypoint | default('websecure') }} +Label=traefik.http.routers.{{ item.name }}.tls.certresolver={{ item.traefik_resolver | default('cloudflare') }} +Label=traefik.http.services.{{ item.name }}.loadbalancer.server.port={{ item.port | default(80) }} +Label=traefik.docker.network={{ item.network | default('management-net') }} +Label=traefik.http.routers.{{ item.name }}.tls=true +{% endif %} + +HealthCmd=curl -f http://localhost:3000/api/healthz +HealthInterval=30s +HealthTimeout=10s +HealthRetries=3 +HealthStartPeriod=30s + +{% for volume in item.volumes | default([]) %} +Volume={{ volume }} +{% endfor %} +Volume=/etc/timezone:/etc/timezone:ro +Volume=/etc/localtime:/etc/localtime:ro + +Environment=TZ={{ timezone | default('America/Los_Angeles') }} +{% if item.env is defined %} +{% for key, value in item.env.items() | sort %} +Environment={{ key }}={{ value }} +{% endfor %} +{% endif %} + +[Service] +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=default.target diff --git a/site.yaml b/site.yaml new file mode 100644 index 0000000..eb4f981 --- /dev/null +++ b/site.yaml @@ -0,0 +1,21 @@ +--- +- name: Common Setup + hosts: servers + roles: + - common + tags: + - common + +- name: DNS Setup + hosts: dns_servers + roles: + - dns_server + tags: + - dns + +- name: Gitea Setup + hosts: gitea_servers + roles: + - gitea_server + tags: + - gitea