diff --git a/deploy_accounts_svc_plus.yml b/deploy_accounts_svc_plus.yml index e182cec..b13c4fd 100644 --- a/deploy_accounts_svc_plus.yml +++ b/deploy_accounts_svc_plus.yml @@ -1,8 +1,16 @@ - name: Deploy managed accounts.svc.plus service - hosts: accounts + hosts: "{{ accounts_service_hosts | default('accounts') }}" gather_facts: false become: true vars: - accounts_service_image_tag: "{{ lookup('ansible.builtin.env', 'ACCOUNTS_IMAGE_TAG') | default('70c6a3f8', true) }}" + accounts_service_image_repo: >- + {{ lookup('ansible.builtin.env', 'ACCOUNTS_IMAGE_REPO') + | default('ghcr.io/x-evor/accounts', true) }} + accounts_service_image_tag: >- + {{ lookup('ansible.builtin.env', 'ACCOUNTS_IMAGE_TAG') + | default('70c6a3f8', true) }} + accounts_service_pull_image: >- + {{ lookup('ansible.builtin.env', 'ACCOUNTS_PULL_IMAGE') + | default(false, true) | bool }} roles: - roles/vhosts/accounts_service diff --git a/deploy_postgresql_svc_plus.yml b/deploy_postgresql_svc_plus.yml new file mode 100644 index 0000000..b10e1f5 --- /dev/null +++ b/deploy_postgresql_svc_plus.yml @@ -0,0 +1,25 @@ +- name: Deploy managed postgresql.svc.plus service + hosts: "{{ postgresql_service_hosts | default('postgresql') }}" + gather_facts: false + become: true + vars: + postgresql_service_postgres_image_repo: >- + {{ lookup('ansible.builtin.env', 'POSTGRESQL_POSTGRES_IMAGE_REPO') + | default('postgres-extensions', true) }} + postgresql_service_postgres_image_tag: >- + {{ lookup('ansible.builtin.env', 'POSTGRESQL_POSTGRES_IMAGE_TAG') + | default('17', true) }} + postgresql_service_postgres_pull_image: >- + {{ lookup('ansible.builtin.env', 'POSTGRESQL_POSTGRES_PULL_IMAGE') + | default(false, true) | bool }} + postgresql_service_stunnel_image_repo: >- + {{ lookup('ansible.builtin.env', 'POSTGRESQL_STUNNEL_IMAGE_REPO') + | default('ghcr.io/x-evor/stunnel-server', true) }} + postgresql_service_stunnel_image_tag: >- + {{ lookup('ansible.builtin.env', 'POSTGRESQL_STUNNEL_IMAGE_TAG') + | default('2330d36', true) }} + postgresql_service_stunnel_pull_image: >- + {{ lookup('ansible.builtin.env', 'POSTGRESQL_STUNNEL_PULL_IMAGE') + | default(false, true) | bool }} + roles: + - roles/vhosts/postgresql_service diff --git a/inventory.ini b/inventory.ini index 84a25a3..eb3a04c 100644 --- a/inventory.ini +++ b/inventory.ini @@ -7,6 +7,9 @@ jp-xhttp-contabo.svc.plus ansible_host=46.250.251.132 ansible_user=roo [accounts] acp-server.svc.plus ansible_host=46.250.251.132 ansible_user=root +[postgresql] +acp-server.svc.plus ansible_host=46.250.251.132 ansible_user=root + [k3s] jp-k3s-vultr.svc.plus ansible_host=167.179.110.129 ansible_user=root diff --git a/roles/vhosts/postgresql_service/README.md b/roles/vhosts/postgresql_service/README.md new file mode 100644 index 0000000..d713f32 --- /dev/null +++ b/roles/vhosts/postgresql_service/README.md @@ -0,0 +1,22 @@ +# postgresql_service + +Managed single-host deployment for `postgresql.svc.plus`. + +This role reconciles the current production shape: + +- local PostgreSQL container on `127.0.0.1:5432` +- public TLS tunnel via `stunnel` on `0.0.0.0:5433` +- shared Docker network `cn-toolkit-shared` + +Primary entrypoint: + +- `deploy_postgresql_svc_plus.yml` + +Common overrides: + +- `POSTGRESQL_POSTGRES_IMAGE_REPO` +- `POSTGRESQL_POSTGRES_IMAGE_TAG` +- `POSTGRESQL_POSTGRES_PULL_IMAGE` +- `POSTGRESQL_STUNNEL_IMAGE_REPO` +- `POSTGRESQL_STUNNEL_IMAGE_TAG` +- `POSTGRESQL_STUNNEL_PULL_IMAGE` diff --git a/roles/vhosts/postgresql_service/defaults/main.yml b/roles/vhosts/postgresql_service/defaults/main.yml new file mode 100644 index 0000000..fa8b3ae --- /dev/null +++ b/roles/vhosts/postgresql_service/defaults/main.yml @@ -0,0 +1,83 @@ +--- +postgresql_service_base_dir: /opt/cloud-neutral/postgresql.svc.plus/managed +postgresql_service_shared_network: cn-toolkit-shared +postgresql_service_postgres_network: docker_postgres_network + +postgresql_service_postgres_compose_dir: "{{ postgresql_service_base_dir }}/postgres" +postgresql_service_postgres_compose_file: "{{ postgresql_service_postgres_compose_dir }}/docker-compose.yml" +postgresql_service_postgres_env_file: "{{ postgresql_service_postgres_compose_dir }}/env/postgres.env" +postgresql_service_postgres_config_file: "{{ postgresql_service_postgres_compose_dir }}/config/postgresql.conf" +postgresql_service_postgres_legacy_env_file: /opt/cloud-neutral/postgresql.svc.plus/deploy/docker/.env +postgresql_service_postgres_init_scripts_dir: /opt/cloud-neutral/postgresql.svc.plus/deploy/docker/init-scripts +postgresql_service_postgres_data_path: /data +postgresql_service_postgres_container_name: postgresql-svc-plus +postgresql_service_postgres_image_repo: postgres-extensions +postgresql_service_postgres_image_tag: "17" +postgresql_service_postgres_major: "17" +postgresql_service_postgres_pull_image: false +postgresql_service_postgres_port: 5432 +postgresql_service_postgres_health_user: postgres +postgresql_service_postgres_wait_retries: 30 +postgresql_service_postgres_wait_delay: 2 + +postgresql_service_postgres_env_defaults: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: "" + POSTGRES_DB: postgres + PG_LOCAL_PORT: "5432" + PG_MAJOR: "{{ postgresql_service_postgres_major }}" + PG_DATA_PATH: /data + +postgresql_service_postgres_conf: + listen_addresses: "*" + port: 5432 + max_connections: 100 + superuser_reserved_connections: 3 + shared_buffers: 256MB + effective_cache_size: 1GB + maintenance_work_mem: 64MB + work_mem: 16MB + wal_buffers: 16MB + min_wal_size: 1GB + max_wal_size: 4GB + checkpoint_completion_target: 0.9 + wal_compression: "on" + random_page_cost: 1.1 + effective_io_concurrency: 200 + default_statistics_target: 100 + log_destination: stderr + logging_collector: "on" + log_directory: log + log_filename: postgresql-%Y-%m-%d_%H%M%S.log + log_rotation_age: 1d + log_rotation_size: 100MB + log_line_prefix: "%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h " + log_timezone: UTC + log_checkpoints: "on" + log_connections: "on" + log_disconnections: "on" + log_duration: "off" + log_lock_waits: "on" + log_statement: none + log_temp_files: 0 + log_min_duration_statement: 1000 + datestyle: iso, mdy + timezone: UTC + lc_messages: en_US.utf8 + lc_monetary: en_US.utf8 + lc_numeric: en_US.utf8 + lc_time: en_US.utf8 + default_text_search_config: pg_catalog.english + +postgresql_service_stunnel_compose_dir: "{{ postgresql_service_base_dir }}/stunnel" +postgresql_service_stunnel_compose_file: "{{ postgresql_service_stunnel_compose_dir }}/docker-compose.yml" +postgresql_service_stunnel_config_file: "{{ postgresql_service_stunnel_compose_dir }}/conf/stunnel.conf" +postgresql_service_stunnel_container_name: cn-toolkit-stunnel-server +postgresql_service_stunnel_image_repo: ghcr.io/x-evor/stunnel-server +postgresql_service_stunnel_image_tag: "2330d36" +postgresql_service_stunnel_pull_image: false +postgresql_service_stunnel_accept_port: 5433 +postgresql_service_stunnel_service_name: postgres-tls-server +postgresql_service_stunnel_verify_level: 0 +postgresql_service_stunnel_cert_file: /opt/cloud-neutral/stunnel-server/certs/server-cert.pem +postgresql_service_stunnel_key_file: /opt/cloud-neutral/stunnel-server/certs/server-key.pem diff --git a/roles/vhosts/postgresql_service/tasks/main.yml b/roles/vhosts/postgresql_service/tasks/main.yml new file mode 100644 index 0000000..900e42e --- /dev/null +++ b/roles/vhosts/postgresql_service/tasks/main.yml @@ -0,0 +1,198 @@ +--- +- name: Ensure postgresql service base directory exists + ansible.builtin.file: + path: "{{ postgresql_service_base_dir }}" + state: directory + owner: root + group: root + mode: "0755" + +- name: Ensure managed postgresql directories exist + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: root + group: root + mode: "0755" + loop: + - "{{ postgresql_service_postgres_compose_dir }}" + - "{{ postgresql_service_postgres_compose_dir }}/env" + - "{{ postgresql_service_postgres_compose_dir }}/config" + - "{{ postgresql_service_stunnel_compose_dir }}" + - "{{ postgresql_service_stunnel_compose_dir }}/conf" + - "{{ postgresql_service_postgres_data_path }}" + - "{{ postgresql_service_postgres_init_scripts_dir }}" + +- name: Ensure shared Docker network exists for postgresql service + ansible.builtin.command: docker network inspect "{{ postgresql_service_shared_network }}" + changed_when: false + +- name: Ensure postgres Docker network exists for postgresql service + ansible.builtin.command: docker network inspect "{{ postgresql_service_postgres_network }}" + register: postgresql_service_postgres_network_inspect + changed_when: false + failed_when: false + +- name: Create postgres Docker network when missing + ansible.builtin.command: docker network create "{{ postgresql_service_postgres_network }}" + when: postgresql_service_postgres_network_inspect.rc != 0 + +- name: Check for managed postgres env file + ansible.builtin.stat: + path: "{{ postgresql_service_postgres_env_file }}" + register: postgresql_service_postgres_env_stat + +- name: Check for legacy postgres env file + ansible.builtin.stat: + path: "{{ postgresql_service_postgres_legacy_env_file }}" + register: postgresql_service_postgres_legacy_env_stat + +- name: Seed managed postgres env file from legacy deployment + ansible.builtin.copy: + src: "{{ postgresql_service_postgres_legacy_env_file }}" + dest: "{{ postgresql_service_postgres_env_file }}" + remote_src: true + owner: root + group: root + mode: "0600" + when: + - not postgresql_service_postgres_env_stat.stat.exists + - postgresql_service_postgres_legacy_env_stat.stat.exists + +- name: Render managed postgres env file from defaults + ansible.builtin.template: + src: postgres.env.j2 + dest: "{{ postgresql_service_postgres_env_file }}" + owner: root + group: root + mode: "0600" + when: + - not postgresql_service_postgres_env_stat.stat.exists + - not postgresql_service_postgres_legacy_env_stat.stat.exists + +- name: Ensure managed postgres data path is present in env file + ansible.builtin.lineinfile: + path: "{{ postgresql_service_postgres_env_file }}" + regexp: '^PG_DATA_PATH=' + line: "PG_DATA_PATH={{ postgresql_service_postgres_data_path }}" + state: present + +- name: Ensure managed postgres local port is present in env file + ansible.builtin.lineinfile: + path: "{{ postgresql_service_postgres_env_file }}" + regexp: '^PG_LOCAL_PORT=' + line: "PG_LOCAL_PORT={{ postgresql_service_postgres_port }}" + state: present + +- name: Ensure managed postgres major tag is present in env file + ansible.builtin.lineinfile: + path: "{{ postgresql_service_postgres_env_file }}" + regexp: '^PG_MAJOR=' + line: "PG_MAJOR={{ postgresql_service_postgres_major }}" + state: present + +- name: Render managed postgresql.conf + ansible.builtin.template: + src: postgresql.conf.j2 + dest: "{{ postgresql_service_postgres_config_file }}" + owner: root + group: root + mode: "0644" + +- name: Render managed postgres compose file + ansible.builtin.template: + src: postgres-compose.yml.j2 + dest: "{{ postgresql_service_postgres_compose_file }}" + owner: root + group: root + mode: "0644" + +- name: Check stunnel certificate file + ansible.builtin.stat: + path: "{{ postgresql_service_stunnel_cert_file }}" + register: postgresql_service_stunnel_cert_stat + +- name: Check stunnel key file + ansible.builtin.stat: + path: "{{ postgresql_service_stunnel_key_file }}" + register: postgresql_service_stunnel_key_stat + +- name: Fail when stunnel certificate files are missing + ansible.builtin.fail: + msg: >- + stunnel certificate material is missing. Expected + {{ postgresql_service_stunnel_cert_file }} and {{ postgresql_service_stunnel_key_file }}. + when: + - not postgresql_service_stunnel_cert_stat.stat.exists or not postgresql_service_stunnel_key_stat.stat.exists + +- name: Render managed stunnel config + ansible.builtin.template: + src: stunnel.conf.j2 + dest: "{{ postgresql_service_stunnel_config_file }}" + owner: root + group: root + mode: "0644" + +- name: Render managed stunnel compose file + ansible.builtin.template: + src: stunnel-compose.yml.j2 + dest: "{{ postgresql_service_stunnel_compose_file }}" + owner: root + group: root + mode: "0644" + +- name: Pull postgres image when enabled + ansible.builtin.command: docker compose -f "{{ postgresql_service_postgres_compose_file }}" pull postgres + args: + chdir: "{{ postgresql_service_postgres_compose_dir }}" + when: postgresql_service_postgres_pull_image | bool + +- name: Remove existing postgres container before managed recreate + ansible.builtin.shell: | + set -euo pipefail + ids="$(docker ps -aq --filter name=^/{{ postgresql_service_postgres_container_name }}$)" + if [ -n "${ids}" ]; then + docker rm -f ${ids} + fi + args: + executable: /bin/bash + register: postgresql_service_postgres_cleanup + changed_when: postgresql_service_postgres_cleanup.stdout | trim != "" + +- name: Start managed postgres compose target + ansible.builtin.command: docker compose -f "{{ postgresql_service_postgres_compose_file }}" up -d --force-recreate --remove-orphans + args: + chdir: "{{ postgresql_service_postgres_compose_dir }}" + +- name: Wait for postgres container health + ansible.builtin.command: >- + docker inspect --format={{ '{{' }}if .State.Health{{ '}}' }}{{ '{{' }}.State.Health.Status{{ '}}' }}{{ '{{' }}else{{ '}}' }}unknown{{ '{{' }}end{{ '}}' }} + {{ postgresql_service_postgres_container_name }} + register: postgresql_service_postgres_health + changed_when: false + retries: "{{ postgresql_service_postgres_wait_retries }}" + delay: "{{ postgresql_service_postgres_wait_delay }}" + until: postgresql_service_postgres_health.stdout | trim == 'healthy' + +- name: Pull stunnel image when enabled + ansible.builtin.command: docker compose -f "{{ postgresql_service_stunnel_compose_file }}" pull stunnel + args: + chdir: "{{ postgresql_service_stunnel_compose_dir }}" + when: postgresql_service_stunnel_pull_image | bool + +- name: Remove existing stunnel container before managed recreate + ansible.builtin.shell: | + set -euo pipefail + ids="$(docker ps -aq --filter name=^/{{ postgresql_service_stunnel_container_name }}$)" + if [ -n "${ids}" ]; then + docker rm -f ${ids} + fi + args: + executable: /bin/bash + register: postgresql_service_stunnel_cleanup + changed_when: postgresql_service_stunnel_cleanup.stdout | trim != "" + +- name: Start managed stunnel compose target + ansible.builtin.command: docker compose -f "{{ postgresql_service_stunnel_compose_file }}" up -d --force-recreate --remove-orphans + args: + chdir: "{{ postgresql_service_stunnel_compose_dir }}" diff --git a/roles/vhosts/postgresql_service/templates/postgres-compose.yml.j2 b/roles/vhosts/postgresql_service/templates/postgres-compose.yml.j2 new file mode 100644 index 0000000..9405bf0 --- /dev/null +++ b/roles/vhosts/postgresql_service/templates/postgres-compose.yml.j2 @@ -0,0 +1,26 @@ +services: + postgres: + image: {{ postgresql_service_postgres_image_repo }}:{{ postgresql_service_postgres_image_tag }} + container_name: {{ postgresql_service_postgres_container_name }} + restart: unless-stopped + env_file: + - {{ postgresql_service_postgres_env_file }} + ports: + - "127.0.0.1:{{ postgresql_service_postgres_port }}:5432" + volumes: + - {{ postgresql_service_postgres_data_path }}:/var/lib/postgresql/data + - {{ postgresql_service_postgres_init_scripts_dir }}:/docker-entrypoint-initdb.d:ro + - {{ postgresql_service_postgres_config_file }}:/etc/postgresql/postgresql.conf:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-{{ postgresql_service_postgres_health_user }}} -h 127.0.0.1"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + networks: + - postgres_network + +networks: + postgres_network: + external: true + name: {{ postgresql_service_postgres_network }} diff --git a/roles/vhosts/postgresql_service/templates/postgres.env.j2 b/roles/vhosts/postgresql_service/templates/postgres.env.j2 new file mode 100644 index 0000000..0642a0c --- /dev/null +++ b/roles/vhosts/postgresql_service/templates/postgres.env.j2 @@ -0,0 +1,3 @@ +{% for key, value in postgresql_service_postgres_env_defaults.items() %} +{{ key }}={{ value }} +{% endfor %} diff --git a/roles/vhosts/postgresql_service/templates/postgresql.conf.j2 b/roles/vhosts/postgresql_service/templates/postgresql.conf.j2 new file mode 100644 index 0000000..a8fcd42 --- /dev/null +++ b/roles/vhosts/postgresql_service/templates/postgresql.conf.j2 @@ -0,0 +1,47 @@ +# Managed by Ansible: roles/vhosts/postgresql_service + +listen_addresses = '{{ postgresql_service_postgres_conf.listen_addresses }}' +port = {{ postgresql_service_postgres_conf.port }} +max_connections = {{ postgresql_service_postgres_conf.max_connections }} +superuser_reserved_connections = {{ postgresql_service_postgres_conf.superuser_reserved_connections }} + +shared_buffers = '{{ postgresql_service_postgres_conf.shared_buffers }}' +effective_cache_size = '{{ postgresql_service_postgres_conf.effective_cache_size }}' +maintenance_work_mem = '{{ postgresql_service_postgres_conf.maintenance_work_mem }}' +work_mem = '{{ postgresql_service_postgres_conf.work_mem }}' + +wal_buffers = '{{ postgresql_service_postgres_conf.wal_buffers }}' +min_wal_size = '{{ postgresql_service_postgres_conf.min_wal_size }}' +max_wal_size = '{{ postgresql_service_postgres_conf.max_wal_size }}' +checkpoint_completion_target = {{ postgresql_service_postgres_conf.checkpoint_completion_target }} +wal_compression = {{ postgresql_service_postgres_conf.wal_compression }} + +random_page_cost = {{ postgresql_service_postgres_conf.random_page_cost }} +effective_io_concurrency = {{ postgresql_service_postgres_conf.effective_io_concurrency }} +default_statistics_target = {{ postgresql_service_postgres_conf.default_statistics_target }} + +log_destination = '{{ postgresql_service_postgres_conf.log_destination }}' +logging_collector = {{ postgresql_service_postgres_conf.logging_collector }} +log_directory = '{{ postgresql_service_postgres_conf.log_directory }}' +log_filename = '{{ postgresql_service_postgres_conf.log_filename }}' +log_rotation_age = '{{ postgresql_service_postgres_conf.log_rotation_age }}' +log_rotation_size = '{{ postgresql_service_postgres_conf.log_rotation_size }}' +log_line_prefix = '{{ postgresql_service_postgres_conf.log_line_prefix }}' +log_timezone = '{{ postgresql_service_postgres_conf.log_timezone }}' + +log_checkpoints = {{ postgresql_service_postgres_conf.log_checkpoints }} +log_connections = {{ postgresql_service_postgres_conf.log_connections }} +log_disconnections = {{ postgresql_service_postgres_conf.log_disconnections }} +log_duration = {{ postgresql_service_postgres_conf.log_duration }} +log_lock_waits = {{ postgresql_service_postgres_conf.log_lock_waits }} +log_statement = '{{ postgresql_service_postgres_conf.log_statement }}' +log_temp_files = {{ postgresql_service_postgres_conf.log_temp_files }} +log_min_duration_statement = {{ postgresql_service_postgres_conf.log_min_duration_statement }} + +datestyle = '{{ postgresql_service_postgres_conf.datestyle }}' +timezone = '{{ postgresql_service_postgres_conf.timezone }}' +lc_messages = '{{ postgresql_service_postgres_conf.lc_messages }}' +lc_monetary = '{{ postgresql_service_postgres_conf.lc_monetary }}' +lc_numeric = '{{ postgresql_service_postgres_conf.lc_numeric }}' +lc_time = '{{ postgresql_service_postgres_conf.lc_time }}' +default_text_search_config = '{{ postgresql_service_postgres_conf.default_text_search_config }}' diff --git a/roles/vhosts/postgresql_service/templates/stunnel-compose.yml.j2 b/roles/vhosts/postgresql_service/templates/stunnel-compose.yml.j2 new file mode 100644 index 0000000..ef424d6 --- /dev/null +++ b/roles/vhosts/postgresql_service/templates/stunnel-compose.yml.j2 @@ -0,0 +1,25 @@ +services: + stunnel: + image: {{ postgresql_service_stunnel_image_repo }}:{{ postgresql_service_stunnel_image_tag }} + container_name: {{ postgresql_service_stunnel_container_name }} + user: root + restart: unless-stopped + ports: + - "{{ postgresql_service_stunnel_accept_port }}:{{ postgresql_service_stunnel_accept_port }}" + extra_hosts: + - host.docker.internal:host-gateway + volumes: + - {{ postgresql_service_stunnel_config_file }}:/etc/stunnel/stunnel.conf:ro + - {{ postgresql_service_stunnel_cert_file }}:/etc/stunnel/certs/server-cert.pem:ro + - {{ postgresql_service_stunnel_key_file }}:/etc/stunnel/certs/server-key.pem:ro + networks: + - shared_stunnel + - postgres_network + +networks: + shared_stunnel: + external: true + name: {{ postgresql_service_shared_network }} + postgres_network: + external: true + name: {{ postgresql_service_postgres_network }} diff --git a/roles/vhosts/postgresql_service/templates/stunnel.conf.j2 b/roles/vhosts/postgresql_service/templates/stunnel.conf.j2 new file mode 100644 index 0000000..81de4e8 --- /dev/null +++ b/roles/vhosts/postgresql_service/templates/stunnel.conf.j2 @@ -0,0 +1,9 @@ +foreground = yes +debug = 5 + +[{{ postgresql_service_stunnel_service_name }}] +accept = 0.0.0.0:{{ postgresql_service_stunnel_accept_port }} +connect = {{ postgresql_service_postgres_container_name }}:5432 +verify = {{ postgresql_service_stunnel_verify_level }} +cert = /etc/stunnel/certs/server-cert.pem +key = /etc/stunnel/certs/server-key.pem