Creating a simple Docker host using Photon OS and VMware Aria Automation

This post goes through the creation of a standalone Docker host based on Photon OS. Then it gets turned in to a catalog item for Aria Automation.

Creating a simple Docker host using Photon OS and VMware Aria Automation
Photo by Philippe Oursel / Unsplash

It's been a while. A long while. But I've been busy. Now though I want to document something that I have been using and developing over the last year or so and share it. Before I get to the good stuff though, there are some foundational things that I need to take care of.

A key element in it is having a simple capability to execute container images as part of an automation pipeline. One of the simplest ways to achieve this is to have a standalone Docker host that I can use as an endpoint for VMware Aria Automation. In this post I will go through the creation of such a host based on Photon OS. Then I'll go one step further and turn it in to a catalog item for Aria Automation.

Photon OS

If you haven't heard of or used Photon OS before, it's an open-source minimalist Linux distribution from VMware that is used as the base for the appliances for solutions such as vCenter, Aria Automation etc. It can be downloaded from Github here, and although there are beta versions of 5.0 available, this article was written against 4.0 Rev 2.

Some might ask why I'm using Photon. Well, aside from the obvious "dogfooding" reason (I work for VMware), it's small and Docker comes pre-installed even if it isn't activated. So you can get to a functioning Docker host fairly easily.

I won't cover the installation process for Photon here. In fact, I can't recall the last time when I did it by hand! I build my images using HashiCorp Packer. In fact, it's Packer that I want to put in to a container to use in my image creation pipeline.

If you want to see the Packer build for Photon, I'll cover it in more detail in a future post. For now however, there are plenty of other guides out there on how to install Photon.

Enabling Docker in Photon

My use-case for Docker involves Aria Automation being able to run containers on the host remotely, so the configuration has a few more steps to it. But it shouldn't be too hard to follow. In brief, those steps are:

  1. Open the firewall ports for remote Docker access
  2. Adding a user to the docker group
  3. Creating service definitions for Docker that include remote access
  4. Enable the Docker services

Firewall

Photon uses iptables to facilitate a firewall service in the OS. If we want Aria Automation to be able to access Docker on the host then we either need to disable the firewall (not recommended) or poke a couple of holes in it. Docker uses ports 2375 and 2376 by default. I'll also open up ICMP so that we can ping the host, but that's not necessary, just a nice-to-have.

Via SSH or on the console for Photon, the following commands are needed to open the ports:

sudo iptables -A INPUT -p tcp --dport 2375 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 2376 -j ACCEPT
sudo iptables -A OUTPUT -p icmp -j ACCEPT
sudo iptables -A INPUT -p icmp -j ACCEPT

But if we want those settings to be persisted across reboots then we need to make them permanent also:

sudo iptables-save > /etc/systemd/scripts/ip4save
sudo systemctl restart iptables

Docker Group

If we want to be able to use Docker, our user will need to be a member of the local group called "docker" on the host. The group already exists, we just need to add our user to it.

As a recommended practice, although there is a root account it's best not to use it directly and make use of sudo to get privileged access. By default I create a non-administrative user account in all of my Linux images that has the ability to use sudo for administrative access. I therefore add this user to the "docker" group.

sudo usermod -a -G docker username

Where "username" is obviously the name of the account I'm using.

Service Definitions

We could just enable Docker at this point, but the service would only accept local connections by default. As I already stated, I want Aria Automation to connect in remotely. Hence I need to make a change to the options used to start Docker.

Now, I could change the default service file directly but such a change could get wiped out by an upgrade / update. Instead, I create and use my own service definitions. The first of these is for the docker.socket service. This involves creating a file called /etc/systemd/system/docker.socket with the following contents:

[Unit]
Description=Docker Socket for the API
PartOf=docker.service

[Socket]
ListenStream=/var/run/docker.sock
SocketMode=0660
SocketUser=root
SocketGroup=docker

[Install]
WantedBy=sockets.target

The file should be owned by root:root and the permissions should match the other files in the directory.

The second service definition file is for the docker.service service. This time the file is called /etc/systemd/system/docker.service and has the following contents:

[Unit]
Description=Docker Application Container Engine
Documentation=https://docs.docker.com
After=network-online.target docker.socket
Requires=docker.socket

[Service]
Type=notify
ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:2375
ExecReload=/bin/kill -s HUP $MAINPID
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity
TimeoutStartSec=0
Delegate=yes
KillMode=process
Restart=on-failure
StartLimitBurst=3
StartLimitInterval=60s

[Install]
WantedBy=multi-user.target

The key thing that is added here is the addition of the tcp://0.0.0.0:2375 option. Without it Docker won't allow remote access.

Start Services

The final step is pretty simple. We just need to enable and start the services.

sudo systemctl daemon-reload
sudo systemctl enable docker.socket
sudo systemctl enable docker
sudo systemctl start docker
sudo systemctl start docker.socket

Job done! Docker is now running and will start automatically at reboot.

There's more...

Wait! What?

Photo by Miguel OrĂ³s / Unsplash

Having gone to that trouble, what if I could capture that and make a catalog item out of it in Aria Automation? Docker hosts on demand!

Thanks to cloud-init it's pretty straightforward. Having a password in clear text isn't a good idea, although it's set to require changing on first login. But this is just an example.

Request for for a Docker host

As you can guess from the form indentation, there are a couple of properties that are part of a property group. However, they're just for selecting a flavor mapping and setting the system disk size. The rest is just a standard Cloud Template.

# ----------------------------------------------------------------------------
# Name:         Docker Host
# Description:  Deploys a standalone Docker host
# Author:       Michael Poore (@mpoore)
# ----------------------------------------------------------------------------name: Docker Host
description: Deploys a standalone Docker Host based on Photon OS
version: 0.0.1
formatVersion: 1
# ----- Inputs -----
inputs:
  linuxSettings:
    type: object
    $ref: /ref/property-groups/defaultLinuxSettings
  photonVersion:
    type: string
    title: OS Version
    description: Select the version of Photon OS to use for this Docker deployment
    default: '4'
    enum:
      - '4'
      - '5'
  addressType:
    type: string
    title: IP Address Type
    description: Select the type of IP address to use for this deployment, Static or DHCP
    default: dynamic
    oneOf:
      - title: DHCP
        const: dynamic
      - title: Static
        const: static
  userAccount:
    type: string
    title: User Account
    description: Enter the name of a user account that will be created on the Docker Host
    default: dockeruser
    minLength: 6
    maxLength: 16
    pattern: \b([a-z0-9]+)\b(?<!root|v12n|docker)
  userPassword:
    type: string
    minLength: 8
    maxLength: 16
    default: null
    pattern: (?=^.{8,}$)(?=.*\d)(?=.*[!@#$%^&*]+)(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$
# ----- Resources -----
resources:
  DockerHost:
    type: Cloud.Machine
    properties:
      image: ${"photon" + input.photonVersion + "-main"}
      flavor: ${input.linuxSettings.instanceSize}
      storage:
        bootDiskCapacityInGB: ${input.linuxSettings.systemDiskSize}
      cloudConfig: |
        #cloud-config
        preserve_hostname: false
        hostname: ${self.resourceName}
        users:
          - name: ${input.userAccount}
            groups: users,docker
            lock_passwd: false
        chpasswd:
          list: |
            ${input.userAccount}:${input.userPassword}
          expire: True
        write_files:
          - owner: root:root
            path: /etc/systemd/system/docker.socket
            permissions: '0755'
            content: |
              [Unit]
              Description=Docker Socket for the API
              PartOf=docker.service

              [Socket]
              ListenStream=/var/run/docker.sock
              SocketMode=0660
              SocketUser=root
              SocketGroup=docker

              [Install]
              WantedBy=sockets.target
          - owner: root:root
            path: /etc/systemd/system/docker.service
            permissions: '0755'
            content: |
              [Unit]
              Description=Docker Application Container Engine
              Documentation=https://docs.docker.com
              After=network-online.target docker.socket
              Requires=docker.socket

              [Service]
              Type=notify
              ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:2375
              ExecReload=/bin/kill -s HUP $MAINPID
              LimitNOFILE=infinity
              LimitNPROC=infinity
              LimitCORE=infinity
              TimeoutStartSec=0
              Delegate=yes
              KillMode=process
              Restart=on-failure
              StartLimitBurst=3
              StartLimitInterval=60s

              [Install]
              WantedBy=multi-user.target
        runcmd:
          - iptables -A INPUT -p tcp --dport 2375 -j ACCEPT
          - iptables -A INPUT -p tcp --dport 2376 -j ACCEPT
          - iptables -A OUTPUT -p icmp -j ACCEPT
          - iptables -A INPUT -p icmp -j ACCEPT
          - iptables-save > /etc/systemd/scripts/ip4save
          - systemctl restart iptables
          - systemctl daemon-reload
          - systemctl enable docker.socket
          - systemctl enable docker
          - systemctl start docker
          - systemctl start docker.socket
      networks:
        - network: ${resource.PrimaryNetwork.id}
          assignment: ${input.addressType}
      role: dkr
  PrimaryNetwork:
    type: Cloud.Network
    properties:
      networkType: existing
# ----------------------------------------------------------------------------
# Changes:
#   0.0.x - Draft versions
# ----------------------------------------------------------------------------

There you have it...