From 34ff7cefd7ff842aa975f440565a437fdfad5bff Mon Sep 17 00:00:00 2001 From: Lars Kaiser Date: Fri, 8 Oct 2021 11:14:51 +0200 Subject: [PATCH] feat: new postgresql role --- roles/postgresql/README.md | 64 ++++++++ roles/postgresql/defaults/main.yml | 31 ++++ roles/postgresql/handlers/main.yml | 8 + roles/postgresql/tasks/initialize.yml | 51 ++++++ roles/postgresql/tasks/main.yml | 146 ++++++++++++++++++ .../postgresql/templates/postgresql-passwd.j2 | 29 ++++ roles/postgresql/vars/main.yml | 37 +++++ 7 files changed, 366 insertions(+) create mode 100644 roles/postgresql/README.md create mode 100644 roles/postgresql/defaults/main.yml create mode 100644 roles/postgresql/handlers/main.yml create mode 100644 roles/postgresql/tasks/initialize.yml create mode 100644 roles/postgresql/tasks/main.yml create mode 100644 roles/postgresql/templates/postgresql-passwd.j2 create mode 100644 roles/postgresql/vars/main.yml diff --git a/roles/postgresql/README.md b/roles/postgresql/README.md new file mode 100644 index 0000000..68942fc --- /dev/null +++ b/roles/postgresql/README.md @@ -0,0 +1,64 @@ +# `famedly.base.postgresql` ansible role for deploying PostgreSQL inside a docker container +This role supports both PostgreSQL listening on a +- UNIX socket on the host (mountable into other containers) +- a TCP socket on a configurable port on the host +at the same time. + +It does not support changing the listening TCP port inside the container to anything other than the standard `5432`. + +If both UNIX and TCP sockets are activated, the configuration of global options is done via UNIX socket. + +Since the container sets the permissions of the UNIX socket to `0777`, we do not allow `trust` as auth method for local connections in `pg_hba.conf`. + +## Requirements +- psycopg2 + +## Role Variables +See `defaults/main.yml`. + +The `postgresql_superuser_password` variable is required and sets the password for the default user `postgres`. + +In `postgresql_global_config_options` you can specify global config options in the form of `{ option: "listen_addresses", value: "*" }. + +In the optional `postgresql_host_port` variable you can provide an unused port on the host which will be forwarded to the container port 5432. + +By default `postgresql_connect_socket` is set to `true`, which will mount `postgresql_socket_path` into the container and thus provide the PostgreSQL UNIX socket on the host. +You can disable this by setting the variable to `false`, PostgreSQL will still listen on the UNIX socket inside the container. + +### Alternative docker image +You can specify an alternative container image to use, for example if you want PostGIS support. +If this image is based on the default `docker.io/postgres` image, this role should work as expected. +However, this is not guaranteed. + +This is an example for specifying the official PostGIS image: +```yaml +postgresql_version: "13" +postgis_version: "3.1" +postgresql_container_image_name: "docker.io/postgis/postgis" +postgresql_container_image_tag: "{{ postgresql_version }}-{{ postgis_version }}-{{ postgresql_container_distro }}" +``` + +You could also override the entire `postgresql_container_image` variable. + +## Dependencies +Docker needs to be installed and configured. + +## Example Playbook +```yaml +--- +- name: Install PostgreSQL in a docker container + hosts: [ all ] + become: true + roles: + - famedly.base.postgresql + vars: + postgresql_host_port: "2345" + postgresql_superuser_password: "{{ vault_postgresql_superuser_password }}" + postgresql_connect_socket: "false" +``` + +## License +GNU Affero General Public License v3 + +## Author Information +Famedly GmbH, famedly.de diff --git a/roles/postgresql/defaults/main.yml b/roles/postgresql/defaults/main.yml new file mode 100644 index 0000000..7d8368b --- /dev/null +++ b/roles/postgresql/defaults/main.yml @@ -0,0 +1,31 @@ +--- +postgresql_user: postgresql +postgresql_base_path: /opt/postgresql +postgresql_data_path: "{{ postgresql_base_path }}/data" +postgresql_socket_path: "{{ postgresql_base_path }}/sockets" +postgresql_config_path: "{{ postgresql_base_path }}/config" +postgresql_connect_socket: true + +postgresql_container_image_name: "docker.io/postgres" +postgresql_container_version: "13.4" +postgresql_container_distro: alpine +postgresql_container_image_tag: "{{ postgresql_container_version }}{{ '-' + postgresql_container_distro if postgresql_container_distro else '' }}" +postgresql_container_image: "{{ postgresql_container_image_name }}:{{ postgresql_container_image_tag }}" +postgresql_container_name: postgresql +postgresql_container_labels: {} +postgresql_container_ports: >- + {{ + [] if postgresql_host_port is undefined else + [ postgresql_host_port | string + ':5432' ] + }} +postgresql_container_networks: [] +postgresql_container_etc_hosts: {} +postgresql_container_pull: true +postgresql_container_recreate: false + +postgresql_container_fd_soft_limit: "8192" +postgresql_container_fd_hard_limit: "8192" +postgresql_container_ulimits: ["nofile:{{ postgresql_container_fd_soft_limit }}:{{ postgresql_container_fd_hard_limit }}"] +postgresql_container_memory_reservation: "256M" +postgresql_container_memory: "512M" +postgresql_container_shm_size: "128M" diff --git a/roles/postgresql/handlers/main.yml b/roles/postgresql/handlers/main.yml new file mode 100644 index 0000000..7303171 --- /dev/null +++ b/roles/postgresql/handlers/main.yml @@ -0,0 +1,8 @@ +--- + +- name: Restart PostgreSQL docker container + community.docker.docker_container: + name: postgresql + state: started + restart: true + listen: postgresql_container_restart diff --git a/roles/postgresql/tasks/initialize.yml b/roles/postgresql/tasks/initialize.yml new file mode 100644 index 0000000..94086b7 --- /dev/null +++ b/roles/postgresql/tasks/initialize.yml @@ -0,0 +1,51 @@ +--- + +- name: Create directories for PostgreSQL + file: + path: "{{ item }}" + state: directory + owner: "{{ postgresql_user_res.uid }}" + group: "{{ postgresql_user_res.group }}" + mode: 0755 + loop: + - "{{ postgresql_base_path }}" + - "{{ postgresql_data_path }}" + - "{{ postgresql_socket_path }}" + - "{{ postgresql_config_path }}" + tags: [ 'prepare', 'prepare-postgresql' ] + +- name: Template fake /etc/passwd for postgres + ansible.builtin.template: + src: postgresql-passwd.j2 + dest: "{{ postgresql_config_path }}/postgresql-passwd" + owner: "{{ postgresql_user_res.uid }}" + group: "{{ postgresql_user_res.group }}" + mode: 0640 + tags: [ 'prepare', 'prepare-postgresql' ] + +- name: Initialize PostgreSQL container + community.docker.docker_container: + name: "{{ postgresql_container_name }}" + image: "{{ postgresql_container_image }}" + ports: "{{ postgresql_container_ports }}" + volumes: "{{ postgresql_container_init_volumes }}" + labels: "{{ postgresql_container_labels_complete }}" + networks: "{{ postgresql_container_networks }}" + etc_hosts: "{{ postgresql_container_etc_hosts }}" + user: "{{ postgresql_user_res.uid }}:{{ postgresql_user_res.group }}" + state: started + env: + POSTGRES_PASSWORD: "{{ postgresql_superuser_password }}" + register: postgresql_container + tags: [ 'prepare', 'prepare-postgresql' ] + +- name: Wait for container to be initialized + wait_for: + path: "{{ postgresql_socket_path }}/.s.PGSQL.5432" + tags: [ 'prepare', 'prepare-postgresql' ] + +- name: Stop initialized container + community.docker.docker_container: + name: "{{ postgresql_container_name }}" + state: absent + tags: [ 'prepare', 'prepare-postgresql' ] diff --git a/roles/postgresql/tasks/main.yml b/roles/postgresql/tasks/main.yml new file mode 100644 index 0000000..6a9a4a3 --- /dev/null +++ b/roles/postgresql/tasks/main.yml @@ -0,0 +1,146 @@ +--- + +- name: Create system user to run postgresql as + user: + name: "{{ postgresql_user }}" + state: present + system: yes + register: postgresql_user_res + tags: ['prepare', 'prepare-postgresql', + 'deploy', 'deploy-postgresql'] + +- name: Ensure PostgreSQL container image is pulled + community.docker.docker_image: + name: "{{ postgresql_container_image }}" + force_source: "{{ postgresql_container_pull }}" + source: pull + state: present + tags: [ 'prepare', 'prepare-postgresql' ] + +- name: Check if postgresql_data_path exists + stat: + path: "{{ postgresql_data_path }}" + register: stat_postgresql_data_path + tags: [ 'prepare', 'prepare-postgresql' ] + +- name: Check if postgresql_data_path is empty + find: + paths: "{{ postgresql_data_path }}" + file_type: any + when: stat_postgresql_data_path.stat.exists + register: find_postgresql_data_path + tags: [ 'prepare', 'prepare-postgresql' ] + +- name: Initialize + include_tasks: initialize.yml + when: (not stat_postgresql_data_path.stat.exists) or (find_postgresql_data_path is defined and find_postgresql_data_path.examined == 0) + +- name: Template fake /etc/passwd for postgres + ansible.builtin.template: + src: postgresql-passwd.j2 + dest: "{{ postgresql_config_path }}/postgresql-passwd" + owner: "{{ postgresql_user_res.uid }}" + group: "{{ postgresql_user_res.group }}" + mode: 0640 + tags: [ 'prepare', 'prepare-postgresql' ] + +- name: Insert pg_hba.conf header + ansible.builtin.lineinfile: + path: "{{ postgresql_config_path }}/pg_hba.conf" + insertbefore: BOF + line: "# Ansible managed" + create: true + mode: 0640 + owner: "{{ postgresql_user_res.uid }}" + group: "{{ postgresql_user_res.group }}" + tags: [ 'prepare', 'prepare-postgresql' ] + +- name: Allow peer login for postgres user on socket + community.postgresql.postgresql_pg_hba: + dest: "{{ postgresql_config_path }}/pg_hba.conf" + contype: local + users: postgres + method: peer + options: map=root_postgres + tags: [ 'prepare', 'prepare-postgresql' ] + +- name: Insert pg_ident.conf header + ansible.builtin.lineinfile: + path: "{{ postgresql_config_path }}/pg_ident.conf" + insertbefore: BOF + line: "# Ansible managed" + create: true + mode: 0640 + owner: "{{ postgresql_user_res.uid }}" + group: "{{ postgresql_user_res.group }}" + tags: [ 'prepare', 'prepare-postgresql' ] + +- name: Insert pg_ident.conf root_postgres user map + ansible.builtin.lineinfile: + path: "{{ postgresql_config_path }}/pg_ident.conf" + insertafter: "# Ansible managed" + line: "root_postgres\troot\tpostgres" # noqa no-tabs + tags: [ 'prepare', 'prepare-postgresql' ] + +- name: Ensure PostgreSQL container is started + community.docker.docker_container: + name: "{{ postgresql_container_name }}" + image: "{{ postgresql_container_image }}" + ports: "{{ postgresql_container_ports }}" + volumes: "{{ postgresql_container_volumes_complete }}" + labels: "{{ postgresql_container_labels_complete }}" + networks: "{{ postgresql_container_networks }}" + etc_hosts: "{{ postgresql_container_etc_hosts }}" + ulimits: "{{ postgresql_container_ulimits }}" + memory_reservation: "{{ postgresql_container_memory_reservation }}" + memory: "{{ postgresql_container_memory }}" + shm_size: "{{ postgresql_container_shm_size }}" + recreate: "{{ postgresql_container_recreate }}" + user: "{{ postgresql_user_res.uid }}:{{ postgresql_user_res.group }}" + restart_policy: unless-stopped + state: started + register: postgresql_container + tags: [ 'deploy', 'deploy-postgresql' ] + +- name: Wait for container startup + wait_for: + path: "{{ postgresql_socket_path }}/.s.PGSQL.5432" + when: postgresql_connect_socket + tags: [ 'deploy', 'deploy-postgresql' ] + +- name: Wait for container startup + wait_for: + port: "{{ postgresql_host_port }}" + when: not postgresql_connect_socket + tags: [ 'deploy', 'deploy-postgresql' ] + +- name: Set superuser password + community.postgresql.postgresql_user: + name: postgres + password: "{{ postgresql_superuser_password }}" + login_host: "{{ postgresql_connection.login_host }}" + tags: [ 'deploy', 'deploy-postgresql' ] + +- name: Set global configuration options + community.postgresql.postgresql_set: + name: "{{ item.option }}" + value: "{{ item.value }}" + login_host: "{{ postgresql_connection.login_host }}" + login_port: "{{ postgresql_connection.login_port }}" + login_password: "{{ postgresql_connection.login_password }}" + register: postgresql_set_result + loop: "{{ postgresql_global_config_options }}" + tags: [ 'deploy', 'deploy-postgresql' ] + +# The above task sets global options, but only some of them require a restart +# The below task notifies the restart handler only in these cases, preventing unnecessary downtime +- name: Check and notify handler if restart is required + debug: + msg: "{{ item.item.option }} changed. Restart required: {{ item.restart_required }}" + when: item.changed # noqa no-handler + changed_when: item.restart_required + notify: postgresql_container_restart + loop: "{{ postgresql_set_result.results }}" + loop_control: + label: "{{ item.item.option }}" + tags: [ 'deploy', 'deploy-postgresql' ] diff --git a/roles/postgresql/templates/postgresql-passwd.j2 b/roles/postgresql/templates/postgresql-passwd.j2 new file mode 100644 index 0000000..9c854c2 --- /dev/null +++ b/roles/postgresql/templates/postgresql-passwd.j2 @@ -0,0 +1,29 @@ +root:x:0:0:root:/root:/bin/ash +bin:x:1:1:bin:/bin:/sbin/nologin +daemon:x:2:2:daemon:/sbin:/sbin/nologin +adm:x:3:4:adm:/var/adm:/sbin/nologin +lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin +sync:x:5:0:sync:/sbin:/bin/sync +shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown +halt:x:7:0:halt:/sbin:/sbin/halt +mail:x:8:12:mail:/var/mail:/sbin/nologin +news:x:9:13:news:/usr/lib/news:/sbin/nologin +uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin +operator:x:11:0:operator:/root:/sbin/nologin +man:x:13:15:man:/usr/man:/sbin/nologin +postmaster:x:14:12:postmaster:/var/mail:/sbin/nologin +cron:x:16:16:cron:/var/spool/cron:/sbin/nologin +ftp:x:21:21::/var/lib/ftp:/sbin/nologin +sshd:x:22:22:sshd:/dev/null:/sbin/nologin +at:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologin +squid:x:31:31:Squid:/var/cache/squid:/sbin/nologin +xfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin +games:x:35:35:games:/usr/games:/sbin/nologin +cyrus:x:85:12::/usr/cyrus:/sbin/nologin +vpopmail:x:89:89::/var/vpopmail:/sbin/nologin +ntp:x:123:123:NTP:/var/empty:/sbin/nologin +smmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologin +guest:x:405:100:guest:/dev/null:/sbin/nologin +nobody:x:65534:65534:nobody:/:/sbin/nologin +{{ postgresql_user_res.name }}:x:{{ postgresql_user_res.uid }}:{{ postgresql_user_res.group }}::{{ postgresql_user_res.home }}:{{ postgresql_user_res.shell }} +utmp:x:100:406:utmp:/home/utmp:/bin/false diff --git a/roles/postgresql/vars/main.yml b/roles/postgresql/vars/main.yml new file mode 100644 index 0000000..09fb79a --- /dev/null +++ b/roles/postgresql/vars/main.yml @@ -0,0 +1,37 @@ +--- +postgresql_connection: + login_host: >- + {{ + postgresql_socket_path if postgresql_connect_socket + else (postgresql_container.container.NetworkSettings.IPAddress if postgresql_host_port is undefined + else "127.0.0.1") + }} + login_port: >- + {{ + postgresql_host_port if postgresql_host_port is defined and not postgresql_connect_socket + else "5432" + }} + login_password: "{{ postgresql_superuser_password }}" + +postgresql_container_data_dir: /var/lib/postgresql/data +postgresql_container_socket_dir: /var/run/postgresql + +postgresql_container_volumes_base: + - "{{ postgresql_data_path }}:{{ postgresql_container_data_dir }}:z" + - "{{ postgresql_config_path }}/pg_hba.conf:{{ postgresql_container_data_dir }}/pg_hba.conf:ro" + - "{{ postgresql_config_path }}/pg_ident.conf:{{ postgresql_container_data_dir }}/pg_ident.conf:ro" + - "{{ postgresql_config_path }}/postgresql-passwd:/etc/passwd:ro" +postgresql_container_volumes_complete: >- + {{ postgresql_container_volumes_base + + ([postgresql_socket_path + ':' + postgresql_container_socket_dir + ':z'] + if postgresql_connect_socket else []) + }} + +postgresql_container_init_volumes: + - "{{ postgresql_data_path }}:{{ postgresql_container_data_dir }}:z" + - "{{ postgresql_socket_path }}:{{ postgresql_container_socket_dir }}:z" + - "{{ postgresql_config_path }}/postgresql-passwd:/etc/passwd:ro" + +postgresql_container_labels_base: + version: "{{ postgresql_container_version }}" +postgresql_container_labels_complete: "{{ postgresql_container_labels_base | combine(postgresql_container_labels) }}"