commit 5c465972615c5035d415336e1381901306b404ae Author: uumas Date: Sun Jul 28 16:13:03 2024 +0300 Initial commit Basic roles for installing podman, creating containers, networks and services diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b9f9e5d --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +MIT License Copyright (c) 2024 uumas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next +paragraph) shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5022a8b --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +Roles for installing services in podman containers diff --git a/galaxy.yaml b/galaxy.yaml new file mode 100644 index 0000000..8317d0e --- /dev/null +++ b/galaxy.yaml @@ -0,0 +1,10 @@ +--- +namespace: uumas +name: podman +description: Roles for installing services in podman containers +readme: README.md +version: 0.1.0 +repository: "https://git.uumas.fi/uumas/ansible-podman" +license_file: LICENSE +authors: + - uumas diff --git a/meta/runtime.yaml b/meta/runtime.yaml new file mode 100644 index 0000000..1e85b01 --- /dev/null +++ b/meta/runtime.yaml @@ -0,0 +1,2 @@ +--- +requires_ansible: ">=2.15.0" diff --git a/roles/container/defaults/main.yaml b/roles/container/defaults/main.yaml new file mode 100644 index 0000000..3b23bce --- /dev/null +++ b/roles/container/defaults/main.yaml @@ -0,0 +1,11 @@ +--- +container_command: [] +container_user: "" +container_mounts: [] +container_publish_ports: [] +container_networks: [] +container_env: {} +container_auto_start: true +container_auto_update: true +container_requires: [] +container_wants: [] diff --git a/roles/container/handlers/main.yaml b/roles/container/handlers/main.yaml new file mode 100644 index 0000000..2d2cd5b --- /dev/null +++ b/roles/container/handlers/main.yaml @@ -0,0 +1,7 @@ +--- +- name: "Restart container service {{ container_name }}" + ansible.builtin.systemd_service: + name: "{{ container_name }}.service" + state: restarted + daemon_reload: true + ignore_errors: '{{ ansible_check_mode }}' diff --git a/roles/container/meta/argument_specs.yaml b/roles/container/meta/argument_specs.yaml new file mode 100644 index 0000000..1d6dc81 --- /dev/null +++ b/roles/container/meta/argument_specs.yaml @@ -0,0 +1,101 @@ +--- +argument_specs: + main: + short_description: Sets up podman container with systemd units (quadlet) + options: + container_name: + description: Name of the container. Must be unique within a host. + type: str + required: true + container_image: + description: "The image to run in the container, in FQIN format (registry/imagename:tag)" + type: str + required: true + container_command: + description: Command to start the container with. + type: list + required: false + default: [] + elements: str + container_user: + description: The UID to run as inside the container + type: str + required: false + default: "" + + container_mounts: + description: List of bind mounts or volumes to be mounted inside the container. + type: list + required: false + default: [] + elements: dict + options: + type: + description: Type of volume + type: str + required: true + choices: + - volume + - bind + source: + description: + - Mount source. + - If mount type is volume, name of the volume. + - If mount type is bind, host path to bind mount inside the container. + type: str + required: true + destination: + description: Path inside the container to mount at + type: str + required: true + readonly: + description: If true, volume will be mounted as read only inside the container + type: bool + required: false + default: false + + container_publish_ports: + description: "A list of published ports in docker format (::)" + type: list + required: false + default: [] + elements: str + container_networks: + description: A list of podman networks for the container. + type: list + required: false + default: [] + elements: str + container_env: + description: A dict of environment variables for the container + type: dict + required: false + default: {} + + container_requires: + description: > + List of systemd units (like other containers) this one depends on. + You should ensure they are created before this one, or at least within + the same play, before handlers are flushed. + type: list + required: false + default: [] + elements: str + container_wants: + description: > + List of systemd units (like other containers) this one wants. + You should ensure they are created within the same play, before handlers are flushed. + type: list + required: false + default: [] + elements: str + container_auto_start: + description: Set to false to not start the container automatically on boot or restart on failure. + type: bool + required: false + default: true + container_auto_update: + description: Whether to let podman automatically update the container whenever the specified image gets updated + type: bool + required: false + default: true diff --git a/roles/container/meta/main.yaml b/roles/container/meta/main.yaml new file mode 100644 index 0000000..d80fa53 --- /dev/null +++ b/roles/container/meta/main.yaml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: podman diff --git a/roles/container/tasks/main.yaml b/roles/container/tasks/main.yaml new file mode 100644 index 0000000..244b582 --- /dev/null +++ b/roles/container/tasks/main.yaml @@ -0,0 +1,16 @@ +--- +- name: Create networks for container {{ container_name }} + ansible.builtin.include_role: + name: network + vars: + network_name: "{{ network }}" + loop: "{{ container_networks }}" + loop_control: + loop_var: network + +- name: Create container service {{ container_name }} + ansible.builtin.template: + src: container.j2 + dest: "/etc/containers/systemd/{{ container_name }}.container" + mode: "0600" + notify: "Restart container service {{ container_name }}" diff --git a/roles/container/templates/container.j2 b/roles/container/templates/container.j2 new file mode 100644 index 0000000..915d038 --- /dev/null +++ b/roles/container/templates/container.j2 @@ -0,0 +1,46 @@ +# {{ ansible_managed }} + +[Unit] +Description=Container {{ container_name }} +{% for requirement in container_requires %} +Requires={{ requirement }} +After={{ requirement }} +{% endfor %} +{% for want in container_wants %} +Requires={{ want }} +Before={{ want }} +{% endfor %} + +[Container] +Image={{ container_image }} +ContainerName={{ container_name }} +{% if container_command | length > 0 %} +Exec="{{ container_command | join('" "') }}" +{% endif %} +{% if container_user | length > 0 %} +User={{ container_user }} +{% endif %} +{% for mount in container_mounts %} +Mount={% for key, value in mount.items() %}{{ key }}={{ value }}{% if not loop.last %},{% endif %}{% endfor %} + +{% endfor %} +{% for network in container_networks %} +Network={{ network }}.network +{% endfor %} +{% for port in container_publish_ports %} +PublishPort={{ port }} +{% endfor %} +{% for key, value in container_env.items() %} +Environment={{ key }}={{ value }} +{% endfor %} +{% if container_auto_update %} +AutoUpdate=registry +{% endif %} + +{% if container_auto_start %} +[Service] +Restart=always + +[Install] +WantedBy=multi-user.target +{% endif %} diff --git a/roles/network/meta/argument_specs.yaml b/roles/network/meta/argument_specs.yaml new file mode 100644 index 0000000..66aea6c --- /dev/null +++ b/roles/network/meta/argument_specs.yaml @@ -0,0 +1,9 @@ +--- +argument_specs: + main: + short_description: Sets up podman network with systemd unit (quadlet) + options: + network_name: + description: Name of the network. Must be unique within a host. + type: str + required: true diff --git a/roles/network/meta/main.yaml b/roles/network/meta/main.yaml new file mode 100644 index 0000000..d80fa53 --- /dev/null +++ b/roles/network/meta/main.yaml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: podman diff --git a/roles/network/tasks/main.yaml b/roles/network/tasks/main.yaml new file mode 100644 index 0000000..897c883 --- /dev/null +++ b/roles/network/tasks/main.yaml @@ -0,0 +1,7 @@ +--- +- name: "Create container network service {{ network_name }}" + ansible.builtin.template: + src: network.j2 + dest: "/etc/containers/systemd/{{ network_name }}.network" + mode: "0644" + notify: Reload systemd daemon diff --git a/roles/network/templates/network.j2 b/roles/network/templates/network.j2 new file mode 100644 index 0000000..e9503cd --- /dev/null +++ b/roles/network/templates/network.j2 @@ -0,0 +1,7 @@ +# {{ ansible_managed }} + +[Unit] +Description=Container network {{ network_name }} + +[Network] +NetworkName={{ network_name }} diff --git a/roles/podman/handlers/main.yaml b/roles/podman/handlers/main.yaml new file mode 100644 index 0000000..8f99013 --- /dev/null +++ b/roles/podman/handlers/main.yaml @@ -0,0 +1,4 @@ +--- +- name: Reload systemd daemon + ansible.builtin.systemd_service: + daemon_reload: true diff --git a/roles/podman/meta/argument_specs.yaml b/roles/podman/meta/argument_specs.yaml new file mode 100644 index 0000000..35143b3 --- /dev/null +++ b/roles/podman/meta/argument_specs.yaml @@ -0,0 +1,5 @@ +--- +argument_specs: + main: + short_description: Installs podman + options: {} diff --git a/roles/podman/tasks/main.yaml b/roles/podman/tasks/main.yaml new file mode 100644 index 0000000..989f2f5 --- /dev/null +++ b/roles/podman/tasks/main.yaml @@ -0,0 +1,16 @@ +--- +- name: Ensure host distribution is supported + ansible.builtin.import_role: + name: uumas.general.compatcheck + vars: + compatcheck_supported_distributions: + - name: debian + version_min: 13 + - name: ubuntu + version_min: 24 + tags: podman + +- name: Install podman + ansible.builtin.apt: + name: podman + tags: podman diff --git a/roles/service/defaults/main.yaml b/roles/service/defaults/main.yaml new file mode 100644 index 0000000..c839bde --- /dev/null +++ b/roles/service/defaults/main.yaml @@ -0,0 +1,11 @@ +--- +service_domains: [] + +service_container_publish_ports: [] +service_container_mounts: [] +service_container_env: {} + +service_additional_containers: [] + +service_requires: [] +service_auto_update: true diff --git a/roles/service/handlers/main.yaml b/roles/service/handlers/main.yaml new file mode 100644 index 0000000..eecdb6e --- /dev/null +++ b/roles/service/handlers/main.yaml @@ -0,0 +1,7 @@ +--- +- name: "Restart socat socket for {{ service_name }}" + ansible.builtin.systemd_service: + name: "{{ service_name }}-socat.socket" + state: restarted + daemon_reload: true + ignore_errors: '{{ ansible_check_mode }}' diff --git a/roles/service/meta/argument_specs.yaml b/roles/service/meta/argument_specs.yaml new file mode 100644 index 0000000..1ccf11b --- /dev/null +++ b/roles/service/meta/argument_specs.yaml @@ -0,0 +1,144 @@ +--- +argument_specs: + main: + short_description: Sets up a service in podman container(s) + options: + service_name: + description: Name of the service. + type: str + required: true + + service_domains: + description: A list of domains which should be proxied to the main service container + type: list + required: false + default: [] + service_container_http_port: + description: + - Port inside the container where http requests will be proxied to. + - Required if service_domains is not empty. + type: int + required: false + + service_container_image: + description: "The image to run in the service container(s), in FQIN format (registry/imagename:tag)." + type: str + required: true + service_container_publish_ports: + description: "A list of published ports in docker format (::)" + type: list + required: false + default: [] + service_container_mounts: + description: List of bind mounts or volumes to be mounted inside the service container(s). + type: list + required: false + default: [] + elements: dict + options: + type: + description: Type of volume + type: str + required: true + choices: + - volume + - bind + source: + description: + - Mount source. + - If mount type is volume, name of the volume. + - If mount type is bind, host path to bind mount inside the container. + type: str + required: true + destination: + description: Path inside the container to mount at + type: str + required: true + readonly: + description: If true, volume will be mounted as read only inside the container + type: bool + required: false + default: false + service_container_env: + description: A dict of environment variables for the service container(s) + type: dict + required: false + default: {} + + service_additional_containers: + description: + - List of additional containers for the sercice. + - > + Will inherit most options from main service container, except for publish_ports. + All options can be overridden per-container. + type: list + required: false + default: [] + elements: dict + options: + name: + description: + - Name of the container. + - > + This will be appended to the service name, so if for example service name is + nextcloud and this variable is cron, the resulting container will be called + nextcloud-cron + type: str + required: true + image: + description: "The image to run in the container, in FQIN format (registry/image:tag)" + type: str + required: false + default: "{{ service_container_image }}" + mounts: + description: List of bind mounts or volumes to be mounted inside the main service container. + type: list + required: false + default: "{{ service_container_mounts }}" + elements: dict + options: + type: + description: Type of volume + type: str + required: true + choices: + - volume + - bind + source: + description: + - Mount source. + - If mount type is volume, name of the volume. + - If mount type is bind, host path to bind mount inside the container. + type: str + required: true + destination: + description: Path inside the container to mount at + type: str + required: true + readonly: + description: If true, volume will be mounted as read only inside the container + type: bool + required: false + default: false + publish_ports: + description: "A list of published ports in docker format (::)" + type: list + required: false + default: [] + env: + description: A dict of environment variables for the container + type: dict + required: false + default: {} + + service_requires: + description: List of systemd units this service container depends on. + type: list + required: false + default: [] + elements: str + service_auto_update: + description: Whether to let podman automatically update the service containers whenever the specified image gets updated + type: bool + required: false + default: true diff --git a/roles/service/tasks/main.yaml b/roles/service/tasks/main.yaml new file mode 100644 index 0000000..5de8420 --- /dev/null +++ b/roles/service/tasks/main.yaml @@ -0,0 +1,23 @@ +--- +- name: Mounts for {{ service_name }} + ansible.builtin.include_tasks: mounts.yaml + when: service_container_mounts | length > 0 + +- name: Main container for {{ service_name }} + ansible.builtin.import_role: + name: container + vars: + container_name: "{{ service_name }}" + container_image: "{{ service_container_image }}" + container_mounts: "{{ _service_container_mounts }}" + container_publish_ports: "{{ service_container_publish_ports }}" + container_networks: + - "{{ service_name }}" + container_env: "{{ service_container_env }}" + container_requires: "{{ service_requires }}" + container_wants: "{{ [service_name + '-socat.socket'] if service_domains | length > 0 else [] }}" + container_auto_update: "{{ service_auto_update }}" + +- name: Reverse proxy for {{ service_name }} + ansible.builtin.include_tasks: proxy.yaml + when: service_domains | length > 0 diff --git a/roles/service/tasks/mounts.yaml b/roles/service/tasks/mounts.yaml new file mode 100644 index 0000000..683c085 --- /dev/null +++ b/roles/service/tasks/mounts.yaml @@ -0,0 +1,22 @@ +--- +- name: Initialize variables + ansible.builtin.set_fact: + _service_container_mounts: [] + +- name: Set container named mounts + ansible.builtin.set_fact: + _service_container_mounts: > + {{ _service_container_mounts + + [mount | combine({'source': service_name + '-' + mount.source})] }} + when: mount.type == 'volume' + loop: "{{ service_container_mounts }}" + loop_control: + loop_var: mount + +- name: Set container named mounts + ansible.builtin.set_fact: + _service_container_mounts: "{{ _service_container_mounts + [mount] }}" + when: mount.type == 'bind' + loop: "{{ service_container_mounts }}" + loop_control: + loop_var: mount diff --git a/roles/service/tasks/proxy.yaml b/roles/service/tasks/proxy.yaml new file mode 100644 index 0000000..900db3e --- /dev/null +++ b/roles/service/tasks/proxy.yaml @@ -0,0 +1,35 @@ +--- +- name: Socat socket for {{ service_name }} + ansible.builtin.template: + src: socat.socket.j2 + dest: /etc/systemd/system/{{ service_name }}-socat.socket + mode: "0644" + notify: Restart socat socket for {{ service_name }} + +- name: Socat container for {{ service_name }} + ansible.builtin.include_role: + name: container + vars: + container_name: "{{ service_name }}-socat" + container_image: "docker.io/alpine/socat:latest" + container_command: + - "ACCEPT-FD:3,fork" + - "TCP:{{ service_name }}:{{ service_container_http_port }}" + container_user: nobody + container_networks: + - "{{ service_name }}" + container_requires: + - "{{ service_name }}-socat.socket" + - "{{ service_name }}.service" + container_auto_start: false + container_auto_update: "{{ service_auto_update }}" + +- name: Reverse proxy for {{ service_name }} + ansible.builtin.import_role: + name: uumas.general.vhost + vars: + vhost_type: reverse_proxy + vhost_id: "{{ service_name }}" + vhost_domains: "{{ service_domains }}" + vhost_proxy_target_netproto: unix + vhost_proxy_target_socket: "/run/{{ service_name }}-socat.sock" diff --git a/roles/service/templates/socat.socket.j2 b/roles/service/templates/socat.socket.j2 new file mode 100644 index 0000000..7cfa659 --- /dev/null +++ b/roles/service/templates/socat.socket.j2 @@ -0,0 +1,6 @@ +# {{ ansible_managed }} +[Unit] +Description={{ service_name }} socat socket + +[Socket] +ListenStream=/run/{{ service_name }}-socat.sock