diff --git a/.github/workflows/infra.yaml b/.github/workflows/infra.yaml index 6b1c861..cb0cc9c 100644 --- a/.github/workflows/infra.yaml +++ b/.github/workflows/infra.yaml @@ -4,7 +4,7 @@ on: branches: - main paths: - - infra/**-playbook.yaml + - infra/**playbook.yaml - .github/workflows/infra.yaml push: branches: @@ -23,20 +23,17 @@ jobs: with: fetch-depth: ${{ env.DEPLOY == 'some' && 2 || 1 }} - - name: Setup Python - uses: actions/setup-python@v4 - with: - python-version: "3.11" - - name: Install dependencies run: | + apt update + apt install -y python3-pip pip3 install -r requirements.txt ansible-galaxy collection install community.general - name: Check playbooks run: | - for file in $(find . -wholename "*/infra/*-playbook.yaml" -type f); do - ansible-playbook --inventory ./inventory --check "$file" + for file in $(find . -wholename "*/infra/*playbook.yaml" -type f); do + ansible-playbook --inventory ./inventory --syntax-check "$file" done - name: Get changed playbooks @@ -44,7 +41,7 @@ jobs: if: env.DEPLOY == 'some' uses: tj-actions/changed-files@v38 with: - files: infra/**/*-playbook.yaml + files: infra/**/*playbook.yaml - name: Get playbooks id: playbooks @@ -53,12 +50,19 @@ jobs: if [[ "${{ env.DEPLOY }}" == "some" ]]; then export TO_RUN="${{ steps.files.outputs.all_changed_files }}" else - export TO_RUN="$(find . -wholename './infra/*-playbook.yaml' -type f)" + export TO_RUN="$(find . -wholename './infra/*playbook.yaml' -type f)" fi export TO_RUN="$( echo -n $TO_RUN | tr ' ' '\n' | sort | tr '\n' ' ' )" # run things in order :) echo "will run playbooks: $TO_RUN" echo "to_run=$TO_RUN" >> "$GITHUB_OUTPUT" + - name: Setup environment + if: env.DEPLOY != 'none' && steps.playbooks.outputs.to_run != '' + run: | + mkdir -p -m 700 ~/.ssh + echo "${{ secrets.SSH_PRIVATE }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + - name: Run playbooks if: env.DEPLOY != 'none' && steps.playbooks.outputs.to_run != '' env: @@ -66,4 +70,5 @@ jobs: PROXMOX_USER: ${{ secrets.PROXMOX_USER }} PROXMOX_TOKEN_ID: ${{ secrets.PROXMOX_TOKEN_ID }} PROXMOX_TOKEN_SECRET: ${{ secrets.PROXMOX_TOKEN_SECRET }} - run: ansible-playbook --inventory ./inventory ${{ steps.playbooks.outputs.to_run }} + SSH_PUBLIC: ${{ secrets.SSH_PUBLIC }} + run: ansible-playbook --inventory ./inventory ${{ steps.playbooks.outputs.to_run }} -vv diff --git a/README.md b/README.md index 6c43ecb..0397ccc 100644 --- a/README.md +++ b/README.md @@ -7,4 +7,4 @@ This repository contains any automations used in deploying *.koval.net services. All 'managed' infrastructure is deployed and provisioned with [Ansible](https://www.ansible.com/). However, some 'unmanaged' (manually managed) resources also exist - primarily everything required for this repository to work (I don't want to make a dependency loop). -Ansible playbooks are ran in alphanumerical order and are expected idempotent. +Ansible playbooks are ran in alphanumerical order and are expected to be idempotent. diff --git a/infra/cloud/0000_proxmox_playbook.yaml b/infra/cloud/0000_proxmox_playbook.yaml new file mode 100644 index 0000000..7efb04a --- /dev/null +++ b/infra/cloud/0000_proxmox_playbook.yaml @@ -0,0 +1,110 @@ +- name: Provision cloud Proxmox VM + hosts: cloud + connection: ansible.builtin.local + gather_facts: false + vars: + api_user: "{{ lookup('ansible.builtin.env', 'PROXMOX_USER') }}" + api_host: "{{ lookup('ansible.builtin.env', 'PROXMOX_HOST' ) }}" + api_token_id: "{{ lookup('ansible.builtin.env', 'PROXMOX_TOKEN_ID') }}" + api_token_secret: "{{ lookup('ansible.builtin.env', 'PROXMOX_TOKEN_SECRET') }}" + ssh_public: "{{ lookup('ansible.builtin.env', 'SSH_PUBLIC') }}" + vmname: "{{ inventory_hostname | regex_replace('^([^\\.]+)\\..+$', '\\1') }}" + node: pve + module_defaults: + community.general.proxmox_kvm: + api_user: "{{ api_user }}" + api_host: "{{ api_host }}" + api_token_id: "{{ api_token_id }}" + api_token_secret: "{{ api_token_secret }}" + name: "{{ vmname }}" + node: "{{ node }}" + community.general.proxmox_nic: + api_user: "{{ api_user }}" + api_host: "{{ api_host }}" + api_token_id: "{{ api_token_id }}" + api_token_secret: "{{ api_token_secret }}" + name: "{{ vmname }}" + community.general.proxmox_disk: + api_user: "{{ api_user }}" + api_host: "{{ api_host }}" + api_token_id: "{{ api_token_id }}" + api_token_secret: "{{ api_token_secret }}" + name: "{{ vmname }}" + tasks: + # Initial setup + - name: Create VM + community.general.proxmox_kvm: + clone: "{{ node }}-debian-12" + storage: nvme + register: create + - name: Wait for status + community.general.proxmox_kvm: + state: current + register: vm + retries: 30 + delay: 10 + until: vm.status is defined + + # Networking and initial config + - name: Add PUB NIC + community.general.proxmox_nic: + interface: net0 + firewall: false + bridge: PUB + - name: Add SRV NIC + community.general.proxmox_nic: + interface: net1 + firewall: false + bridge: SRV + - name: Configure cloud-init + community.general.proxmox_kvm: + update: true + ciuser: debian + sshkeys: "{{ ssh_public }}" + ipconfig: + ipconfig0: ip=dhcp,ip6=auto + ipconfig1: ip=dhcp + + # Initial boot + # For some reason debian cloud images don't use + # cloud-init for networking on first boot (cloud-init files + # are regenerated AFTER networking starts). But we need the + # hostname to be registered with DHCP later on so ¯\_(ツ)_/¯ + - name: Initial boot + when: create.changed is true + block: + - name: Start + community.general.proxmox_kvm: + state: started + register: start + - name: Wait 1.5 min # Initial apt update, apt upgrade, cloud-init + ansible.builtin.wait_for: + timeout: 90 + + # VM Configuration + - name: Resize root disk + community.general.proxmox_disk: + disk: scsi0 + size: 16G + state: resized + - name: Create data disk + community.general.proxmox_disk: + disk: scsi1 + backup: true + storage: nvme + size: 2048 + - name: Update VM + community.general.proxmox_kvm: + update: true + agent: enabled=1 + tags: + - debian-12 + - managed + onboot: true + cores: 16 + memory: 32768 + + - name: Retart VM + community.general.proxmox_kvm: + state: restarted + timeout: 60 diff --git a/infra/cloud/0001_initialise_playbook.yaml b/infra/cloud/0001_initialise_playbook.yaml new file mode 100644 index 0000000..579ebcc --- /dev/null +++ b/infra/cloud/0001_initialise_playbook.yaml @@ -0,0 +1,48 @@ +- name: Initialise VM + hosts: cloud + gather_facts: false + tasks: + - name: Wait for connection + ansible.builtin.wait_for_connection: + timeout: 300 + - name: Install system packages + ansible.builtin.apt: + update_cache: true + pkg: + - qemu-guest-agent + - parted + become: true + - name: Enable qemu-guest-agent + ansible.builtin.systemd: + name: qemu-guest-agent + state: started + enabled: true + become: true + + - name: Create data partition + community.general.parted: + device: /dev/disk/by-path/pci-0000:00:05.0-scsi-0:0:0:1 + label: gpt + name: data + number: 1 + state: present + become: true + - name: Create data filesystem + community.general.filesystem: + dev: /dev/disk/by-path/pci-0000:00:05.0-scsi-0:0:0:1-part1 + fstype: ext4 + become: true + - name: Mount data partition + ansible.posix.mount: + src: /dev/disk/by-path/pci-0000:00:05.0-scsi-0:0:0:1-part1 + path: /mnt/data + fstype: ext4 + opts: rw,errors=remount-ro,x-systemd.growfs + state: mounted + become: true + - name: Set data partition permissions + ansible.builtin.file: + path: /mnt/data + owner: debian + group: debian + become: true diff --git a/infra/cloud/0002_docker_playbook.yaml b/infra/cloud/0002_docker_playbook.yaml new file mode 100644 index 0000000..b2a09fe --- /dev/null +++ b/infra/cloud/0002_docker_playbook.yaml @@ -0,0 +1,47 @@ +- name: Install software + hosts: cloud + gather_facts: false + tasks: + - name: Wait for connection + ansible.builtin.wait_for_connection: + timeout: 300 + - name: Install dependencies + ansible.builtin.apt: + update_cache: true + pkg: + - curl + - python3-apt + - gpg + become: true + - name: Add docker key + ansible.builtin.apt_key: + url: https://download.docker.com/linux/debian/gpg + keyring: /etc/apt/keyrings/docker.gpg + become: true + - name: Add docker repo + ansible.builtin.apt_repository: + repo: deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian bookworm stable + become: true + - name: Install docker + ansible.builtin.apt: + update_cache: true + pkg: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-buildx-plugin + - docker-compose-plugin + become: true + - name: Add user to docker group + ansible.builtin.user: + user: debian + groups: + - docker + append: true + become: true + - name: Enable docker + ansible.builtin.systemd: + name: docker + state: started + enabled: true + become: true diff --git a/infra/cloud/0003_nextcloud_playbook.yaml b/infra/cloud/0003_nextcloud_playbook.yaml new file mode 100644 index 0000000..114f215 --- /dev/null +++ b/infra/cloud/0003_nextcloud_playbook.yaml @@ -0,0 +1,22 @@ +- name: Deploy app + hosts: cloud + gather_facts: false + vars: + app: nextcloud + tasks: + - name: Wait for connection + ansible.builtin.wait_for_connection: + timeout: 300 + - name: Get user + ansible.builtin.user: + name: debian + register: user + - name: Copy project + ansible.builtin.copy: + src: ./{{ app }} + dest: "{{ user.home }}" + mode: "0744" + - name: Re-deploy + ansible.builtin.command: bash all-in-one.sh + args: + chdir: "{{ user.home }}/{{ app }}" diff --git a/infra/cloud/0004_immich_playbook.yaml b/infra/cloud/0004_immich_playbook.yaml new file mode 100644 index 0000000..f5ec067 --- /dev/null +++ b/infra/cloud/0004_immich_playbook.yaml @@ -0,0 +1,27 @@ +- name: Deploy app + hosts: cloud + gather_facts: false + vars: + app: immich + tasks: + - name: Wait for connection + ansible.builtin.wait_for_connection: + timeout: 300 + - name: Get user + ansible.builtin.user: + name: debian + register: user + # - name: Docker compose down + # ansible.builtin.command: docker compose down + # args: + # chdir: "{{ user.home }}/{{ app }}" + # ignore_errors: true + # - name: Copy project + # ansible.builtin.copy: + # src: ./{{ app }} + # dest: "{{ user.home }}" + # mode: "0744" + # - name: Docker compose up -d + # ansible.builtin.command: docker compose up -d + # args: + # chdir: "{{ user.home }}/{{ app }}" diff --git a/infra/cloud/immich/docker-compose.yaml b/infra/cloud/immich/docker-compose.yaml new file mode 100644 index 0000000..3caaf6f --- /dev/null +++ b/infra/cloud/immich/docker-compose.yaml @@ -0,0 +1,93 @@ +version: "3" + +services: + immich-server: + container_name: immich_server + image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release} + command: [ "start.sh", "immich" ] + volumes: + - ${UPLOAD_LOCATION}:/usr/src/app/upload + env_file: + - .env + depends_on: + - redis + - database + - typesense + restart: always + + immich-microservices: + container_name: immich_microservices + image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release} + # extends: + # file: hwaccel.yml + # service: hwaccel + command: [ "start.sh", "microservices" ] + volumes: + - ${UPLOAD_LOCATION}:/usr/src/app/upload + env_file: + - .env + depends_on: + - redis + - database + - typesense + restart: always + + immich-machine-learning: + container_name: immich_machine_learning + image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release} + volumes: + - /mnt/data/immich-model-cache:/cache + env_file: + - .env + restart: always + + immich-web: + container_name: immich_web + image: ghcr.io/immich-app/immich-web:${IMMICH_VERSION:-release} + env_file: + - .env + restart: always + + typesense: + container_name: immich_typesense + image: typesense/typesense:0.24.1@sha256:9bcff2b829f12074426ca044b56160ca9d777a0c488303469143dd9f8259d4dd + environment: + - TYPESENSE_API_KEY=${TYPESENSE_API_KEY} + - TYPESENSE_DATA_DIR=/data + # remove this to get debug messages + - GLOG_minloglevel=1 + volumes: + - /mnt/data/immich-typesense:/data + restart: always + + redis: + container_name: immich_redis + image: redis:6.2-alpine@sha256:70a7a5b641117670beae0d80658430853896b5ef269ccf00d1827427e3263fa3 + restart: always + + database: + container_name: immich_postgres + image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441 + env_file: + - .env + environment: + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_USER: ${DB_USERNAME} + POSTGRES_DB: ${DB_DATABASE_NAME} + volumes: + - /mnt/data/immich-pgdata:/var/lib/postgresql/data + restart: always + + immich-proxy: + container_name: immich_proxy + image: ghcr.io/immich-app/immich-proxy:${IMMICH_VERSION:-release} + environment: + # Make sure these values get passed through from the env file + - IMMICH_SERVER_URL + - IMMICH_WEB_URL + ports: + - 2283:8080 + depends_on: + - immich-server + - immich-web + restart: always diff --git a/infra/cloud/nextcloud/all-in-one.sh b/infra/cloud/nextcloud/all-in-one.sh new file mode 100644 index 0000000..b169620 --- /dev/null +++ b/infra/cloud/nextcloud/all-in-one.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +docker stop nextcloud-aio-mastercontainer || true +docker rm nextcloud-aio-mastercontainer || true + +docker run \ +--init \ +--sig-proxy=false \ +--name nextcloud-aio-mastercontainer \ +--restart unless-stopped \ +--publish 8080:8080 \ +--env NEXTCLOUD_DATADIR=/mnt/data/nextcloud \ +--env NEXTCLOUD_UPLOAD_LIMIT=16G \ +--env NEXTCLOUD_MAX_TIME=7200 \ +--env APACHE_PORT=11000 \ +--env APACHE_IP_BINDING=0.0.0.0 \ +--env TZ=Europe/London \ +--env AIO_DISABLE_BACKUP_SECTION=true \ +--volume nextcloud_aio_mastercontainer:/mnt/docker-aio-config \ +--volume /var/run/docker.sock:/var/run/docker.sock:ro \ +-d nextcloud/all-in-one:latest diff --git a/inventory/proxmox.yaml b/inventory/proxmox.yaml index 6b4033d..04520dd 100644 --- a/inventory/proxmox.yaml +++ b/inventory/proxmox.yaml @@ -7,6 +7,13 @@ proxmox: pve.mgmt.home.local.koval.net: pve2.mgmt.home.local.koval.net: managed: - hosts: + children: + cloud: + hosts: + cloud.srv.home.local.koval.net: + vars: + ansible_user: debian + ansible_ssh_private_key_file: ~/.ssh/id_rsa + ansible_ssh_common_args: -o StrictHostKeyChecking=accept-new # TODO: Improve this unmanaged: hosts: