diff --git a/infra/solidtime/0000_proxmox_playbook.yaml b/infra/solidtime/0000_proxmox_playbook.yaml new file mode 100644 index 0000000..f48b305 --- /dev/null +++ b/infra/solidtime/0000_proxmox_playbook.yaml @@ -0,0 +1,112 @@ +- name: Provision solidtime Proxmox VM + hosts: solidtime + 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: pve2 + 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 + notify: + - Start VM + - Wait + - 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 + - name: Force all notified handlers to run + ansible.builtin.meta: flush_handlers + + # 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: 64 + - name: Update VM + community.general.proxmox_kvm: + update: true + agent: enabled=1 + tags: + - debian-12 + - managed + onboot: true + cores: 4 + memory: 8192 + + - name: Retart VM + community.general.proxmox_kvm: + state: restarted + timeout: 60 + + handlers: + # 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: Start VM + community.general.proxmox_kvm: + state: started + register: start + - name: Wait # Initial apt update, apt upgrade, cloud-init + ansible.builtin.wait_for: + timeout: 90 diff --git a/infra/solidtime/0001_initialise_playbook.yaml b/infra/solidtime/0001_initialise_playbook.yaml new file mode 100644 index 0000000..8a745e1 --- /dev/null +++ b/infra/solidtime/0001_initialise_playbook.yaml @@ -0,0 +1,43 @@ +- name: Initialise VM + hosts: solidtime + 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: /var/lib/docker + fstype: ext4 + opts: rw,errors=remount-ro,x-systemd.growfs + state: mounted + become: true diff --git a/infra/solidtime/0002_docker_playbook.yaml b/infra/solidtime/0002_docker_playbook.yaml new file mode 100644 index 0000000..711488b --- /dev/null +++ b/infra/solidtime/0002_docker_playbook.yaml @@ -0,0 +1,48 @@ +- name: Install docker + hosts: solidtime + 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/solidtime/0003_solidtime_playbook.yaml b/infra/solidtime/0003_solidtime_playbook.yaml new file mode 100644 index 0000000..0b2ea95 --- /dev/null +++ b/infra/solidtime/0003_solidtime_playbook.yaml @@ -0,0 +1,65 @@ +- name: Deploy app + hosts: solidtime + gather_facts: false + vars: + app: solidtime + tasks: + - name: Wait for connection + ansible.builtin.wait_for_connection: + timeout: 300 + + - name: Check if project exists + ansible.builtin.stat: + path: "$HOME/{{ app }}" + register: project + - name: Docker compose down + when: project.stat.exists + community.docker.docker_compose_v2: + project_src: "$HOME/{{ app }}" + state: absent + - name: Copy project + ansible.builtin.copy: + src: "./{{ app }}" + dest: "$HOME" + mode: "0744" + + - name: Replace APP_KEY secret + ansible.builtin.replace: + path: "$HOME/{{ app }}/laravel.env" + regexp: "APP_KEY_VALUE" + replace: "{{ lookup('infisical.vault.read_secrets', project_id=infisical_project, env_slug='prod', + path='/solidtime', secret_name='APP_KEY')['value'] }}" + - name: Replace SMTP Password secret + ansible.builtin.replace: + path: "$HOME/{{ app }}/laravel.env" + regexp: "SMTP_PASSWORD_VALUE" + replace: "{{ lookup('ansible.builtin.env', 'SMTP_PASSWORD') }}" + - name: Replace PASSPORT_PRIVATE_KEY secret + ansible.builtin.replace: + path: "$HOME/{{ app }}/laravel.env" + regexp: "PASSPORT_PRIVATE_KEY_VALUE" + replace: "{{ lookup('infisical.vault.read_secrets', project_id=infisical_project, env_slug='prod', + path='/solidtime', secret_name='PASSPORT_PRIVATE_KEY')['value'] }}" + - name: Replace PASSPORT_PUBLIC_KEY secret + ansible.builtin.replace: + path: "$HOME/{{ app }}/laravel.env" + regexp: "PASSPORT_PUBLIC_KEY_VALUE" + replace: "{{ lookup('infisical.vault.read_secrets', project_id=infisical_project, env_slug='prod', + path='/solidtime', secret_name='PASSPORT_PUBLIC_KEY')['value'] }}" + - name: Replace DB Password secret (app) + ansible.builtin.replace: + path: "$HOME/{{ app }}/laravel.env" + regexp: "DB_PASSWORD_VALUE" + replace: "{{ lookup('infisical.vault.read_secrets', project_id=infisical_project, env_slug='prod', + path='/solidtime', secret_name='DB_PASSWORD')['value'] }}" + + - name: Replace DB Password secret (db) + ansible.builtin.replace: + path: "$HOME/{{ app }}/.env" + regexp: "DB_PASSWORD_VALUE" + replace: "{{ lookup('infisical.vault.read_secrets', project_id=infisical_project, env_slug='prod', + path='/solidtime', secret_name='DB_PASSWORD')['value'] }}" + + - name: Docker compose up + community.docker.docker_compose_v2: + project_src: "$HOME/{{ app }}" diff --git a/infra/solidtime/solidtime/.env b/infra/solidtime/solidtime/.env new file mode 100644 index 0000000..4911438 --- /dev/null +++ b/infra/solidtime/solidtime/.env @@ -0,0 +1,7 @@ +APP_DOMAIN=solidtime.koval.net +DB_DATABASE=solidtime +DB_USERNAME=solidtime +FORWARD_APP_PORT=8000 +FORWARD_DB_PORT=5432 +DB_PASSWORD=DB_PASSWORD_VALUE +SOLIDTIME_IMAGE_TAG=latest diff --git a/infra/solidtime/solidtime/.gitignore b/infra/solidtime/solidtime/.gitignore new file mode 100644 index 0000000..fab02bf --- /dev/null +++ b/infra/solidtime/solidtime/.gitignore @@ -0,0 +1 @@ +!*.env diff --git a/infra/solidtime/solidtime/docker-compose.yml b/infra/solidtime/solidtime/docker-compose.yml new file mode 100644 index 0000000..eebac8b --- /dev/null +++ b/infra/solidtime/solidtime/docker-compose.yml @@ -0,0 +1,96 @@ +services: + app: + restart: always + image: "solidtime/solidtime:${SOLIDTIME_IMAGE_TAG:-latest}" + user: "1000:1000" + ports: + - '${FORWARD_APP_PORT:-8000}:8000' + networks: + - internal + volumes: + - "app-storage:/var/www/html/storage" + - "./logs:/var/www/html/storage/logs" + - "app-storage-app:/var/www/html/storage/app" + environment: + CONTAINER_MODE: http + AUTO_DB_MIGRATE: "true" + healthcheck: + test: [ "CMD-SHELL", "curl --fail http://localhost:8000/health-check/up || exit 1" ] + env_file: + - laravel.env + depends_on: + - database + scheduler: + restart: always + image: "solidtime/solidtime:${SOLIDTIME_IMAGE_TAG:-latest}" + user: "1000:1000" + networks: + - internal + volumes: + - "app-storage:/var/www/html/storage" + - "./logs:/var/www/html/storage/logs" + - "app-storage-app:/var/www/html/storage/app" + environment: + CONTAINER_MODE: scheduler + healthcheck: + test: [ "CMD-SHELL", "supervisorctl status scheduler:scheduler_00" ] + env_file: + - laravel.env + depends_on: + - database + queue: + restart: always + image: "solidtime/solidtime:${SOLIDTIME_IMAGE_TAG:-latest}" + user: "1000:1000" + networks: + - internal + volumes: + - "app-storage:/var/www/html/storage" + - "./logs:/var/www/html/storage/logs" + - "app-storage-app:/var/www/html/storage/app" + environment: + CONTAINER_MODE: worker + WORKER_COMMAND: "php /var/www/html/artisan queue:work" + healthcheck: + test: [ "CMD-SHELL", "supervisorctl status worker:worker_00" ] + env_file: + - laravel.env + depends_on: + - database + database: + restart: always + image: 'postgres:15' +# ports: +# - '${FORWARD_DB_PORT:-5432}:5432' + environment: + PGPASSWORD: '${DB_PASSWORD:-secret}' + POSTGRES_DB: '${DB_DATABASE}' + POSTGRES_USER: '${DB_USERNAME}' + POSTGRES_PASSWORD: '${DB_PASSWORD:-secret}' + volumes: + - 'database-storage:/var/lib/postgresql/data' + networks: + - internal + healthcheck: + test: + - CMD + - pg_isready + - '-q' + - '-d' + - '${DB_DATABASE}' + - '-U' + - '${DB_USERNAME}' + retries: 3 + timeout: 5s + gotenberg: + image: gotenberg/gotenberg:8 + networks: + - internal + healthcheck: + test: [ "CMD", "curl", "--silent", "--fail", "http://localhost:3000/health" ] +networks: + internal: +volumes: + database-storage: + app-storage: + app-storage-app: diff --git a/infra/solidtime/solidtime/laravel.env b/infra/solidtime/solidtime/laravel.env new file mode 100644 index 0000000..4e78e51 --- /dev/null +++ b/infra/solidtime/solidtime/laravel.env @@ -0,0 +1,47 @@ +APP_NAME="solidtime" +VITE_APP_NAME="solidtime" +APP_ENV="production" +APP_DEBUG="false" +APP_URL="https://solidtime.koval.net" +APP_FORCE_HTTPS="true" +APP_ENABLE_REGISTRATION="false" +TRUSTED_PROXIES="10.4.0.1/32" + +# Authentication +APP_KEY="APP_KEY_VALUE" +PASSPORT_PRIVATE_KEY="PASSPORT_PRIVATE_KEY_VALUE" +PASSPORT_PUBLIC_KEY="PASSPORT_PUBLIC_KEY_VALUE" +SUPER_ADMINS="gleb@koval.net" + +# Logging +LOG_CHANNEL="stderr_daily" +LOG_LEVEL="debug" + +# Database +DB_CONNECTION="pgsql" +DB_HOST="database" +DB_PORT="5432" +DB_SSLMODE="require" +DB_DATABASE="solidtime" +DB_USERNAME="solidtime" +DB_PASSWORD="DB_PASSWORD_VALUE" + +# Mail +MAIL_MAILER="smtp" +MAIL_HOST="mx.koval.net" +MAIL_PORT="587" +MAIL_ENCRYPTION="tls" +MAIL_FROM_ADDRESS="no-reply@koval.net" +MAIL_FROM_NAME="solidtime" +MAIL_USERNAME="no-reply@koval.net" +MAIL_PASSWORD="SMTP_PASSWORD_VALUE" + +# Queue +QUEUE_CONNECTION="database" + +# File storage +FILESYSTEM_DISK="local" +PUBLIC_FILESYSTEM_DISK="public" + +# Services +GOTENBERG_URL="http://gotenberg:3000" diff --git a/inventory/proxmox.yaml b/inventory/proxmox.yaml index fa703a5..493a72c 100644 --- a/inventory/proxmox.yaml +++ b/inventory/proxmox.yaml @@ -26,6 +26,9 @@ proxmox: finance: hosts: finance.srv.home.local.koval.net: + solidtime: + hosts: + solidtime.srv.home.local.koval.net: vars: ansible_user: debian ansible_ssh_private_key_file: ~/.ssh/id_rsa