Lead Image © Franck Boston, Fotolia.com

Lead Image © Franck Boston, Fotolia.com

Manage containerized setups with Ansible

Put a Bow on It

Article from ADMIN 77/2023
By
The Ansible automation tool not only controls virtual machines in cloud environments, it manages containerized setups simply and easily.

Cloud rollouts with Ansible, in which administrators create virtual machines (VMs) in clouds and set up their applications there, have changed as companies have increasingly started using containerized applications instead of VMs. Containers can greatly simplify managing, maintaining, and updating applications and infrastructures. However, the prerequisite remains that the application in question is suitable for containerized use. The platform of choice is, of course, Kubernetes, and in the concluding part of this article I introduce you to managing Kubernetes applications with Ansible.

However, sometimes Kubernetes is too large and complicated. In branch installations, remote offices, and at the network edge, Kubernetes might not be needed, and a simple container setup with Podman will suffice. Kubernetes-style functionality for small installations can be implemented quite easily with the "LANP" stack.

LANP Instead of Kubernetes

LAMP is a common term for a web application stack comprising Linux, Apache, MySQL, and PHP; however, an invention for this article is "LANP," made up of Linux, Ansible, Nginx, and Podman. The idea is simple and coherent. Kubernetes helps you roll out applications in containers, grouped by namespaces and route traffic to HTTP/ HTTPS through routers from the host to the pod. LANP does the same thing, but without Kubernetes. The Linux host runs Podman for the containers and Nginx as a reverse proxy. The host has an IP address and an fully qualified domain name (FQDN).

Depending on the flexibility of the DNS setup of this environment, the Nginx proxy then forwards to the containers either C domains in the style <http or https>://<app>.fqdn or subdirectories following the pattern <http or https>://fqdn/<app> . If needed, Nginx also handles secure socket layer (SSL) termination. You only need an SSL certificate for the host, and Nginx sends the traffic to the containers internally over HTTP. The "ingress" route through a virtual subdirectory is theoretically easier to implement, but it requires the application in the container to handle this form of URL rewrite. If in doubt, adjust the configuration of the web application in the container. The wildcard route by way of the C domain is a more reliable option.

The logical flow of an application rollout by Ansible on this platform is as follows: The playbook first creates a Podman pod, in which it groups all the containers of the desired application. This pod only opens up the HTTP port of the application to the outside by port mapping. Internal ports (e.g., the database container) remain invisible outside the pod. This way, you could easily run multiple pods, each with its own MariaDB container, without any of them blocking the host's port on 3306.

Within the pod, Ansible then rolls out the required containers with matching configurations. Finally, it creates the reverse proxy configuration matching the application in /etc/nginx/conf.d/ on the Podman/Nginx host and activates it.

In previous articles, I always worked with bridge networks and custom IP addresses for containers in articles with Podman. Although I could also do that here, the pod is assigned the IP address and not an individual container. For this example, I instead use a setup with port mappings without a bridge network.

Required Preparations

To begin, install a Linux machine or VM with a distribution of your choice and set it up with Podman and Nginx. In this example, I use RHEL, but the setup also works with any other distribution. Only the configuration of the Nginx server differs for enterprise Linux- and Debian-based distributions. You might need to adjust the templates and directories. On a system with SELinux enabled, do not forget to use the command:

setsebool httpd_can_network_connect=trueOtherwise, Nginx is not allowed to route traffic to nonstandard ports.

A Raspberry Pi will be fine for simple applications. Make sure the machine has a valid name on the local area network (LAN) and that your DNS resolves it correctly. Depending on whether you want to run the proxy service with C domains or subdirectories, your DNS server will need to resolve wildcards on the Podman host. For this example, I used a RHEL-8 VM named pod.mynet.ip with C domain routing. The DNS server (dnsmasq) of the network therefore contains the matching entry:

address=/.pod.mynet.ip/192.168.2.41

DNS requests for names such as app1.pod.mynet.ip or wp01.pod.mynet.ip always prompt a response of 192.168.2.41 . I used a Raspberry Pi 4 with 8GB of RAM and parallel RHEL 9 (pi8.mynet.ip) to use Nginx with subdirectory routing. The Ansible code ran on a Fedora 36 workstation, the alternative being a Windows subsystem for Linux (WSL) environment running Fedora 36 with Ansible 2.13. In addition to the basic installation, you need the containers.podman collection:

ansible-galaxy collection install containers.podman

Alternatively, launch the playbooks from an AWX environment (web-based user interface) with the appropriate execution environment.

In this example, I used a typical WordPress setup comprising a MariaDB container for the database and the application container with Apache, PHP, and WordPress (Figures 1 and 2).

Figure 1: Thanks to isolation in pods, multiple identical applications can run in parallel on the same host. The reverse proxy controls access by routing on the basis of name or subdirectory.
Figure 2: The WordPress environment also runs in the usual way in a compartmentalized pod. The application automatically adopts the external URL from the reverse proxy.

Separating What from How

To use Ansible playbooks as flexibly as possible for different scenarios, the automation logic is separated from the application parameters. The example is not yet fully generalized but can be used for several scenarios. First, store all the required parameters in a configuration file named config.yml; it uses vars_file to include the playbook. The setup procedure starts with the host configuration for the setup (i.e., the URL of the Podman server, the base directory of the persistent storage for the containers, and the name to be assigned to the application pod),

domain_name: pod.mynet.ip
pod_dir: /var/pods
pod_name: wp01_pod

followed by the database pod parameters, which are largely self-explanatory. Podman later masks the local host directory into the container with the variables db_local_dir and db_pod_dir so that the database is not lost, even if the MariaDB port stops or is updated:

db_name: db01
db_user: wpuser
db_pwd: wp01pwd
db_root_pwd: DBrootPWD
db_image: docker.io/mariadb:latest
db_local_dir: db01
db_pod_dir: /var/lib/mysql

The parameters for the application pod look very similar. Only later does Podman make app_port available to the outside world and therefore to the reverse proxy. If you run multiple application pods, they simply need different port numbers:

app_name: wp01
app_port: 18000
app_image: docker.io/wordpress
app_local_dir: wp01
app_pod_dir: /var/www/html

For the reverse proxy in router mode, define a simple Nginx configuration file as a Jinja2 template, which Ansible later simply adds to /etc/nginx/conf.d/ (Listing 1). The configuration file sets up a reverse proxy from the application name to its application port on the Podman host.

Listing 1

Jinja2 Template for Reverse Proxy

server {
   Listen 80;
   server_name {{ app_name }}.{{ domain_name }};
   access_log /var/log/nginx/{{ app_name }}.access.log;
   error_log /var/log/nginx/{{ app_name }}.error.log;
   client_max_body_size 65536M;
   location / {
      proxy_pass http://127.0.0.1:{{ app_port }};
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection 'upgrade';
      proxy_set_header Host $host;
      proxy_cache_bypass $http_upgrade;
   }
}

If you use Nginx to terminate SSL security, specify port 443 with SSL in the Listen section accordingly and add the required SSL parameters and certificate details in front of the location section.

In subdirectory mode, on the other hand, the proxy configuration is not integrated by the internal server configuration file, but by the internal location configuration, which Ansible stores in /etc/nginx/default.d. From there, it integrates nginx.conf within the regular server statement (Listing 2). I took the rewrite and proxy rules provided there from the WordPress documentation, so they might not work for other web applications. The Ansible playbook that rolls out the containers and configures the proxy starts with the default parameters:

- name: Roll Out WordPress
   hosts: pod
   become: true
   gather_facts: false
vars_files:
      - config.yml

Listing 2

Jinja2 Template for Router Mode Proxy

location /{{ app_name }}/ {
   rewrite ^([^\?#]*/)([^\?#\./]+)([\?#].*)?$ $1$2/$3 permanent;
   proxy_pass http://127.0.0.1:{{ app_port }}/;
   proxy_read_timeout 90;
   proxy_connect_timeout 90;
   proxy_redirect off;
   proxy_set_header X-Real-IP $remote_addr;
   proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
   proxy_set_header X-Forwarded-Proto $scheme;
   proxy_set_header Host $host;
   proxy_set_header X-NginX-Proxy true;
   proxy_set_header Connection "";
}

You do not need the data (facts) of the remote system in this context. Ansible takes the configuration from the previously declared file. It first creates the pod and only redirects external port 18000 to internal port 80. Because all web applications usually listen on port 80, I hard-coded it in this example:

tasks:
   - name: Create Application Pod
      containers.podman.podman_pod:
         name: "{{ pod_name }}"
         state: started
         ports:
            - "{{ app_port }}:80"

If you use applications with other ports (e.g., Kibana on port 5601), you also need to declare this specification as a variable in config.yml as app_internal_ port, for example. After that, the entry in Listing 3 creates the subdirectories, in which the applications later store their files on the Podman host . The code in Listing 4 rolls out the database container, followed by the sections of Listing 5, which create the application container. In this case, the connection to the database container is simply defined as 127.0. 0.1:3306, because the database port is open inside the pod, but cannot be seen from the outside.

Listing 3

Creating Subdirectories

- name: Create Container Directory DB
   ansible.builtin.file:
      path: "{{ pod_dir }}/{{ db_local_dir }}"
      state: directory
- name: Create Container Directory WP
   ansible.builtin.file:
      path: "{{ pod_dir }}/{{ app_local_dir }}"
      state: directory

Listing 4

Rolling Out Database Container

- name: Create MySQL Database for WP
   containers.podman.podman_container:
      name: "{{ db_name }}"
      image: "{{ db_image }}"
      state: started
      pod: "{{ pod_name }}"
      volumes:
         - "{{ pod_dir }}/{{ db_local_dir }}: {{ db_pod_dir }}:Z"
      env:
         MARIADB_ROOT_PASSWORD: "{{ db_root_pwd }}"
         MARIADB_DATABASE: "{{ db_name }}"
         MARIADB_USER: "{{ db_user }}"
         MARIADB_PASSWORD: "{{ db_pwd }}"

Listing 5

Creating Application Container

- name: Create and run WP Container
   containers.podman.podman_container:
      pod: "{{ pod_name }}"
      name: "{{ app_name }}"
      image: "{{ app_image }}"
      state: started
      volumes:
         - "{{ pod_dir }}/{{ app_local_dir }}: {{ app_pod_dir }}:Z"
      env:
      WORDPRESS_DB_HOST: "127.0.0.1:3306"
      WORDPRESS_DB_NAME: "{{ db_name }}"
      WORDPRESS_DB_USER: "{{ db_user }}"
      WORDPRESS_DB_PASSWORD: "{{ db_pwd }}"

Finally, I'll look at the reverse proxy configuration for the proxy in C domain routing mode:

- name: Set Reverse Proxy
   ansible.builtin.template:
      src: proxy.j2
      dest: /etc/nginx/conf.d/{{ app_name }}.conf

For subdirectory routing, the matching entry is:

dest: /etc/nginx/default.d/{{ app_name }}.conf

In both cases, the code continues:

owner: root
group: root
mode: '0644'
- name: Reload Reverse Proxy
   ansible.builtin.service:
      name: nginx
      state: reloaded

That completes the task. With this playbook template and individual config.yml files, you can quite easily roll out most applications with an app container and a database container. The next evolution of this playbook moves the env variables from the playbook into the configuration file.

The next stage then declares the variables in the config.yml file as a hierarchical "dictionary" and therefore only needs a Create Container task in the playbook that iterates over the dictionary with an arbitrary number of containers. To do this, you create a cleanup playbook that deletes the entire installation – including the containers, pod, and reverse proxy – but preserves the application data. You can also update the application easily with a cleanup and re-rollup, assuming your container images are pointing to the :latest tag.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy ADMIN Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

comments powered by Disqus