From 900142059774c3213abe95590228257da8d84332 Mon Sep 17 00:00:00 2001 From: uumas Date: Fri, 25 Nov 2022 18:54:56 +0200 Subject: [PATCH] container: support custom built images, mariadb, bind mounts, custom user --- docs/container.md | 23 ++- roles/container/defaults/main.yml | 7 + roles/container/meta/main.yml | 6 - roles/container/tasks/main.yml | 248 ++++++++++++++++++++---- roles/container/templates/Dockerfile.j2 | 9 + roles/container/vars/main.yml | 3 + 6 files changed, 247 insertions(+), 49 deletions(-) create mode 100644 roles/container/templates/Dockerfile.j2 create mode 100644 roles/container/vars/main.yml diff --git a/docs/container.md b/docs/container.md index d733648..6670d54 100644 --- a/docs/container.md +++ b/docs/container.md @@ -27,13 +27,23 @@ docker_vhost_domains: # Other optional variables ``` -docker_database: postgres # Database to set up in a separate container, supports postgres and mongo -database_passwords: # Needed for postgres +docker_service_suffix: production # For running multiple instances of the same service + +docker_host_user: true # Creates a user on the host and makes the docker container use the same uid/gid. Bind mount volume directories will be owned by this user + +docker_database: postgres # Database to set up in a separate container, supports postgres, mariadb and mongo +database_passwords: # Needed for postgres and mariadb gitea: secret +docker_additional_services: + - memcached + docker_volumes: - - gitea_data:/data - - /var/lib/gitea/.ssh:/data/git/.ssh + - name: data + path: /data + - src: /var/lib/gitea/.ssh + path: /data/git/.ssh + docker_published_ports: - "127.0.0.1:{{ ports.gitea.ssh }}:22" docker_env: @@ -42,4 +52,9 @@ docker_env: docker_network_mode: host # Usually you don't want to define this +dockerfile: # For building a custom container image locally + run: + - "apt-get update && apt-get install -y libmemcached-dev zlib1g-dev && pecl install memcached-3.2.0 && docker-php-ext-enable memcached" + + ``` diff --git a/roles/container/defaults/main.yml b/roles/container/defaults/main.yml index 1a277d4..1f2010e 100644 --- a/roles/container/defaults/main.yml +++ b/roles/container/defaults/main.yml @@ -1,5 +1,12 @@ --- +docker_service_name: "{{ docker_service }}" + +docker_host_user: false + reverse_proxy_type: caddy +docker_proxy_target_protocol: http +docker_volume_type: named + docker_additional_env: {} docker_published_ports: [] diff --git a/roles/container/meta/main.yml b/roles/container/meta/main.yml index 8edf377..78053e7 100644 --- a/roles/container/meta/main.yml +++ b/roles/container/meta/main.yml @@ -2,9 +2,3 @@ dependencies: - docker - - role: uumas.general.reverse_proxy - vhost_id: "{{ docker_service }}" - vhost_domains: "{{ docker_vhost_domains[docker_service] }}" - proxy_target_protocol: "{{ docker_proxy_target_protocol | default('http') }}" - proxy_target_port: "{{ ports[docker_service][proxy_target_protocol] }}" - when: reverse_proxy_type != 'none' and reverse_proxy_type != 'traefik' diff --git a/roles/container/tasks/main.yml b/roles/container/tasks/main.yml index fda6a05..523c84c 100644 --- a/roles/container/tasks/main.yml +++ b/roles/container/tasks/main.yml @@ -1,59 +1,220 @@ --- -- name: "{{ docker_service }} docker network" - docker_network: - name: "{{ docker_service }}" - when: docker_network_mode is not defined or docker_network_mode != 'host' +- name: Set docker service full name + set_fact: + docker_service_name: "{{ docker_service }}_{{ docker_service_suffix }}" + when: docker_service_suffix is defined -- name: Set published ports variable - set_fact: - container_published_ports: ["127.0.0.1:{{ ports[docker_service][proxy_target_protocol] }}:{{ docker_image_http_port }}"] - when: reverse_proxy_type != 'traefik' and (docker_network_mode is not defined or docker_network_mode != 'host') +- name: Convert docker_volumes from legacy format + block: + - name: Warn about docker_volumes legacy format + debug: + msg: "docker_volumes is set in a legacy, deprecated format. This support may be removed after december 2022." + + - name: Add legacy docker volumes to docker_volumes_new using the new format + set_fact: + docker_volumes_new: "{{ docker_volumes_new | default([]) + [{ 'name': item.split(':')[0], 'path': item.split(':')[1] }] }}" + when: "'/' not in item.split(':')[0]" + loop: "{{ docker_volumes }}" + - name: Add legacy docker src bind mounts to docker_volumes_new using the new format + set_fact: + docker_volumes_new: "{{ docker_volumes_new | default([]) + [{ 'src': item.split(':')[0], 'path': item.split(':')[1] }] }}" + when: "'/' in item.split(':')[0]" + loop: "{{ docker_volumes }}" + - name: Set final_docker_volumes variable + set_fact: + final_docker_volumes: "{{ docker_volumes_new }}" + when: docker_volumes is defined and docker_volumes[0] is not mapping + +- name: "{{ docker_service_name }} docker network" + docker_network: + name: "{{ docker_service_name }}" + when: docker_network_mode is not defined or docker_network_mode != 'host' - name: Set networks variable set_fact: container_networks: - - name: "{{ docker_service }}" + - name: "{{ docker_service_name }}" when: docker_network_mode is not defined or docker_network_mode != 'host' +- name: Reverse proxy + include_role: + name: uumas.general.reverse_proxy + vars: + vhost_id: "{{ docker_service_name }}" + proxy_target_protocol: "{{ docker_proxy_target_protocol }}" + vhost_domains: "{{ docker_vhost_domains[docker_service_name] }}" + proxy_target_port: "{{ ports[docker_service_name][proxy_target_protocol] }}" + when: reverse_proxy_type != 'none' and reverse_proxy_type != 'traefik' + +- name: Set published ports variable + set_fact: + container_published_ports: ["127.0.0.1:{{ ports[docker_service_name][docker_proxy_target_protocol] }}:{{ docker_image_http_port }}"] + when: reverse_proxy_type != 'traefik' and (docker_network_mode is not defined or docker_network_mode != 'host') + - name: Include traefik vars include_vars: traefik.yml when: reverse_proxy_type == 'traefik' -- name: Set postgres container env - set_fact: - db_container_image: 'postgres:14-alpine' - db_container_env: - POSTGRES_USER: "{{ docker_service }}" - POSTGRES_PASSWORD: "{{ database_passwords[docker_service] }}" - db_container_data: /var/lib/postgresql/data - when: docker_database is defined and docker_database == 'postgres' -- name: Set mongo container env - set_fact: - db_container_image: 'mongo:latest' - db_container_data: /data/db - when: docker_database is defined and docker_database == 'mongo' +- name: Database container + block: + - name: Set postgres container vars + set_fact: + db_container_image: 'postgres:14-alpine' + db_container_env: + POSTGRES_USER: "{{ docker_service_name }}" + POSTGRES_PASSWORD: "{{ database_passwords[docker_service_name] }}" + db_container_data: /var/lib/postgresql/data + when: docker_database == 'postgres' + - name: Set mariadb container vars + set_fact: + db_container_image: mariadb:10 + db_container_env: + MARIADB_USER: "{{ docker_service_name }}" + MARIADB_DATABASE: "{{ docker_service_name }}" + MARIADB_PASSWORD: "{{ database_passwords[docker_service_name] }}" + MARIADB_RANDOM_ROOT_PASSWORD: "{{ database_passwords[docker_service_name + '_root'] is not defined | string }}" + MARIADB_ROOT_PASSOWRD: "{{ database_passwords[docker_service_name + '_root'] | default(omit) }}" + db_container_data: /var/lib/mysql + db_image_port: 3306 + when: docker_database == 'mariadb' + - name: Set mongo container vars + set_fact: + db_container_image: 'mongo:latest' + db_container_data: /data/db + when: docker_database == 'mongo' + - name: Set db published ports var + set_fact: + db_published_ports: ["127.0.0.1:{{ ports[docker_service_name].db }}:{{ db_image_port }}"] + when: ports[docker_service_name].db is defined -- name: "{{ docker_database }} database container for {{ docker_service }}" - docker_container: - name: "{{ docker_service }}_db" - image: "{{ db_container_image }}" - pull: yes - container_default_behavior: no_defaults - env: "{{ db_container_env | default(omit) }}" - restart_policy: always - volumes: - - "{{ docker_service }}_db:{{ db_container_data }}" - networks: "{{ container_networks | default(omit) }}" + - name: "{{ docker_database }} database container for {{ docker_service_name }}" + docker_container: + name: "{{ docker_service_name }}_db" + image: "{{ db_container_image }}" + pull: yes + env: "{{ db_container_env | default(omit) }}" + published_ports: "{{ db_published_ports | default(omit) }}" + restart_policy: always + volumes: + - "{{ docker_service_name }}_db:{{ db_container_data }}" + networks: "{{ container_networks | default(omit) }}" + log_driver: local when: docker_database is defined -- name: "Container for {{ docker_service }}" +- name: Additional services + block: + - name: "Memcached container for {{ docker_service_name }}" + docker_container: + name: "{{ docker_service_name }}_memcached" + image: memcached:alpine + pull: yes + restart_policy: always + networks: "{{ container_networks | default(omit) }}" + log_driver: local + when: "'memcached' in docker_additional_services" + when: docker_additional_services is defined + +- name: "Create /opt/{{ docker_service }} directory" + file: + path: "/opt/{{ docker_service }}" + state: directory + when: (dockerfile is defined and dockerfile | length > 0) or docker_host_user or docker_volume_type == 'bind' + +- name: Image build + block: + - name: Put dockerfile in place + template: + src: Dockerfile.j2 + dest: "/opt/{{ docker_service }}/Dockerfile" + + - name: Build docker image for {{ docker_service }} + docker_image: + name: "local_{{ docker_service }}" + source: build + force_source: true + build: + pull: true + path: "/opt/{{ docker_service }}" + register: docker_built_image + when: dockerfile is defined and dockerfile | length > 0 + +- name: Container user + block: + - name: "Create user for {{ docker_service_name }}" + user: + name: "{{ docker_service_name }}" + home: "/opt/{{ docker_service }}/{{ docker_service_suffix | default('') }}" + create_home: false + system: true + shell: /bin/bash + register: user + + - name: Set docker container user + set_fact: + docker_user: "{{ user.uid }}:{{ user.group }}" + when: docker_host_user + +- name: Bind mounts + block: + - name: "Create /opt/{{ docker_service }}/{{ docker_service_suffix }} directory" + file: + path: "/opt/{{ docker_service }}/{{ docker_service_suffix }}" + state: directory + owner: "{{ user.uid | default(omit) }}" + group: "{{ user.group | default(omit)}}" + when: docker_service_suffix is defined + + - name: Set docker_mounts_dir + set_fact: + docker_mounts_dir: "/opt/{{ docker_service }}/{{ docker_service_suffix }}/mounts" + when: docker_service_suffix is defined + - name: Set docker_mounts_dir + set_fact: + docker_mounts_dir: "/opt/{{ docker_service }}/mounts" + when: docker_service_suffix is not defined + + - name: "Create {{ docker_mounts_dir }} directory" + file: + path: "{{ docker_mounts_dir }}" + state: directory + + - name: "Create docker bind mount directories for {{ docker_service_name }}" + file: + path: "{{ docker_mounts_dir }}/{{ item.name }}" + state: directory + owner: "{{ user.uid if item.set_owner is not defined or item.set_owner else omit | default(omit) }}" + group: "{{ user.group if item.set_group is not defined or item.set_group else omit | default(omit) }}" + mode: 0750 + when: item.name is defined + loop: "{{ docker_volumes }}" + + - name: Set docker_volume_definition for named binds + set_fact: + docker_volume_definition: "{{ docker_volume_definition | default([]) + [docker_mounts_dir + '/' + item.name + ':' + item.path] }}" + when: item.name is defined + loop: "{{ docker_volumes }}" + when: "docker_volume_type == 'bind'" + +- name: Set docker_volume_definition for src binds + set_fact: + docker_volume_definition: "{{ docker_volume_definition | default([]) + [item.src + ':' + item.path] }}" + when: item.src is defined + loop: "{{ final_docker_volumes }}" + +- name: Set docker_volume_definition for named volumes + set_fact: + docker_volume_definition: "{{ docker_volume_definition | default([]) + [item.name + ':' + item.path] }}" + when: docker_volume_type == 'named' and item.name is defined + loop: "{{ final_docker_volumes }}" + +- name: "Container for {{ docker_service_name }}" docker_container: - name: "{{ docker_service }}" - image: "{{ docker_image }}" - pull: true - container_default_behavior: no_defaults - volumes: "{{ docker_volumes | default(omit) }}" + name: "{{ docker_service_name }}" + image: "{{ docker_built_image.image.Id if dockerfile is defined and not ansible_check_mode else docker_image }}" + user: "{{ docker_user | default(omit) }}" + pull: "{{ dockerfile is not defined }}" + volumes: "{{ docker_volume_definition | default(omit) }}" published_ports: "{{ container_published_ports | default([]) + docker_published_ports | default(omit) }}" labels: "{{ traefik_labels | default(omit) }}" env: "{{ docker_env | combine(docker_additional_env) }}" @@ -61,5 +222,14 @@ restart_policy: always network_mode: "{{ docker_network_mode | default(omit) }}" networks: "{{ container_networks | default(omit) }}" + log_driver: local register: container_out +- name: "Reset bind mount directory permissions" + file: + path: "{{ docker_mounts_dir }}/{{ item.name }}" + state: directory + mode: 0750 + when: "docker_volume_type == 'bind' and item.name is defined" + loop: "{{ final_docker_volumes }}" + diff --git a/roles/container/templates/Dockerfile.j2 b/roles/container/templates/Dockerfile.j2 new file mode 100644 index 0000000..74befcd --- /dev/null +++ b/roles/container/templates/Dockerfile.j2 @@ -0,0 +1,9 @@ +# {{ ansible_managed }} + +FROM {{ docker_image }} +{% if dockerfile.run is iterable %} +{% for cmd in dockerfile.run %} +RUN {{ cmd }} +{% endfor %} +{% endif %} + diff --git a/roles/container/vars/main.yml b/roles/container/vars/main.yml new file mode 100644 index 0000000..34fd410 --- /dev/null +++ b/roles/container/vars/main.yml @@ -0,0 +1,3 @@ +--- + +final_docker_volumes: "{{ docker_volumes }}"