My Docker

Ok, here goes another attempt (here is the previous one ⏳) of automation of my Docker server. In the previous article I failed, but now it looks much more promising and this article is being updated and expanded as I progress with this project 📈. However, the original reason for the ability to easily migrate my Docker server to more powerful instance is gone, as my kids are not interested to play on my Minecraft server 😭. Never mind, at least it is opportunity to learn something new.

The New Beginning 🌅

This time my semi-automated deployment consists of three main components:

  1. Terraform
  2. Ansible
  3. My Docker files

Here is how this project looks like in the tree listing.

Terraform

I’m using pretty much the same Terraform plan as in the [previous](/terraform-handson/ article and my goal stays the same, provision the infrastructure as much as possible using Terraform. So here goes the current terraform.tf:

### Variables

variable "scaleway_access_key" {}
variable "scaleway_secret_key" {}

variable "swtype" {
  default = "DEV1-M"
}
variable "hostname" {
  default = "docker-sw1"
}
variable "env" {
  default = "test"
}

variable "cloudflare_email" {}
variable "cloudflare_token" {}
variable "base_domain" {}

### Scaleway

provider "scaleway" {
  access_key = var.scaleway_access_key
  secret_key = var.scaleway_secret_key
  zone = "fr-par-1"
  region = "fr-par"
  organization_id = "zyx"
}

resource "scaleway_instance_ip" "public_ip" {}

resource "scaleway_instance_server" "docker" {
  name = format("%s-%s.%s", var.hostname, var.env, var.base_domain)
  type  = var.swtype
  image = "docker"

  tags = [ var.env, "linux", "docker" ]

  # attach the public_ip to instance
  ip_id = scaleway_instance_ip.public_ip.id
}

### Cloudflare

provider "cloudflare" {
  version = "~> 2.0"
  email = var.cloudflare_email
  api_key = var.cloudflare_token
}

resource "cloudflare_record" "vaulttec" {
  zone_id = "xyz"
  name    = format("%s-%s", var.hostname, var.env)
  value   = scaleway_instance_ip.public_ip.address
  type    = "A"
  proxied = false
}

The terraform.tfvars contains basic variables:

scaleway_access_key = "xyz"
scaleway_secret_key = "xyz"

cloudflare_email = "lubos@klokner.sk"
cloudflare_token = "xyz"

base_domain = "vault-tec.sk"

And finally the output.tf:

# Outputs

output "ssh_to_instance" {
  value = [ format("ssh root@%s-%s.%s", var.hostname, var.env, var.base_domain)]
}

If I remember correctly, the only difference is, that I decided to add a variable env which represent the deployment type. TEST or PROD and it generates a subdomain when registering FQDN in the domain at Cloudflare.

To conclude this section, running tf init, tf plan and tf apply (I have created an alias for terraform command) should:

  1. create a Docker instance at ScaleWay with defined resources assigned (swtype)
  2. register a DNS record at Cloudflare under base_domain as combination of variables: hostname and env

Easy

Docker

Moving to my custom Docker files that I host locally on my MBP 👨🏻‍💻 from where I run all this. There are two of them at this moment:

./traefik

Here is corresponding docker-compose.yml:

version: '3.7'
services:
  traefik:
    image: traefik:v1.7
    container_name: traefik
    restart: always
    command: |
        --web \
        --docker \
        --docker.watch \
        --docker.exposedbydefault=false
    ports:
        - 80:80
        - 443:443
        - 8080:8080
    volumes:
        - ./traefik.toml:/traefik.toml
        - ./acme.json:/acme.json
        - /var/run/docker.sock:/var/run/docker.sock
    networks:
        - traefik
networks:
  traefik:
    name: traefik

…and Traefik configuration file. As I’m extremely lazy, I’m still running older version 1.7 as you can see in the docker-compose.yml file.

traefik.toml:

debug = false

logLevel = "DEBUG"
defaultEntryPoints = ["https","http"]

[entryPoints]
    [entryPoints.http]
    address = ":80"
        [entryPoints.http.redirect]
            entryPoint = "https"
    [entryPoints.https]
        address = ":443"
    [entryPoints.https.tls]
        minVersion = "VersionTLS12"
        cipherSuites = [
            "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
            "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
            "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305"
        ]

[retry]

[acme]
    email = "lubos@klokner.sk"
    storage = "acme.json"
    entryPoint = "https"
    onHostRule = true
    [acme.httpChallenge]
        entryPoint = "http"

[web]
    address = ":8080"
    [web.auth.basic]
    users = ["admin:xyz]%

Nothing special here, it just redirect every page published on Docker using traefik labels to https, ask the Let’s Encrypt for the Cert (if needed) and sends traffic to the particular Docker container. Admin Dashboard is publised on port 8080, without any encryption 🤦🏻‍♂️ (please don’t tell anybody).

f5-demo-httpd

Just generic F5 demo application…

Dockerfile:

FROM f5devcentral/f5-demo-httpd
EXPOSE 80

docker-compose.yaml:

version: '3.7'
services:
    f5-demo-httpd:
        build: .
#        container_name:
#        ports:
#            - ${PORT:-80}:80
        restart: always
        labels:
            traefik.enable: true
            traefik.frontend.rule: Host:demo.f5demo.app
            traefik.docker.network: traefik
        networks:
            - traefik
networks:
    traefik:
        external:
            name: traefik

The rest of Docker configuration files is hosted on Github, so the description is available below in the Ansible section.

Ansible

As I’ve learned each tool is better for different task. For customization of a server, Ansible can do better job than Terraform. As you can see on the screenshot below the Ansible project consists of several files.

_environment.sh

Contains basic environment variables that I want to have set before I start Ansible. Right now it is only:

export ANSIBLE_HOST_KEY_CHECKING=False
inventory.ini

Ansible inventory file which contains the hosts referenced in the playbooks. In my case it is only the IP address which Terraform returns in the ssh_to_instance Output section.

[docker_host]
docker-sw1-test.vault-tec.sk ansible_user=root

Then the actual playbooks continue.

0_default.yml

Here customize the system and prepare it also for the tasks I’ll need later. The main steps are:

  • Update apt repo and cache
  • Upgrade all packages on server
  • Install required system packages: apt-transport-https, ca-certificates, curl, software-properties-common, python3-pip, virtualenv, python3-setuptools, ctop
  • Update PIP
  • Install Docker Modules for Python
  • Install my Github private ssh key in order to be able to fetch my private repos later on
  • Get and Deploy my ubuntu-deploy-scripts from Github
  • Reboot the server if needed after the updates
---
- hosts: docker_host
  gather_facts: no
  tasks:
    - name: Update apt repo and cache 
      apt: update_cache=yes force_apt_get=yes cache_valid_time=3600

    - name: Upgrade all packages on server
      apt: upgrade=dist force_apt_get=yes

    - name: Install required system packages
      apt: name= state=latest update_cache=yes
      loop: [ 'apt-transport-https', 'ca-certificates', 'curl', 'software-properties-common', 'python3-pip', 'virtualenv', 'python3-setuptools', 'ctop' ]

    - name: pip self-update
      pip:
        name: pip
        state: latest
        
    - name: Install Docker Modules for Python
      pip: name=
      loop: [ 'docker', 'docker-compose', 'docker-pretty-ps' ]

    - name: Ensure .ssh directory exists
      file:
        path: /root/.ssh
        state: directory
        mode: 0700
        owner: root
        group: root
    - name: Ensure GitHub deploy key is present on the server
      copy:
        src: /Users/erkac/.ssh/id_rsa_github
        dest: /root/.ssh/id_rsa_github
        mode: 0600
        owner: root
        group: root
    - name: Clone the deploy scripts from GitHub
      git:
        repo: git@github.com:erkac/ubuntu-deploy-scripts.git
        dest: /root/
        accept_hostkey: yes
        key_file: /root/.ssh/id_rsa_github
    - name: Run the ubuntu-deploy-scripts
      shell: ubuntu-deploy.sh -s >> ubuntu-deploy.log
      args:
        - chdir: ubuntu-deploy-scripts/

    - name: Check if a reboot is needed on all server
      register: reboot_required_file
      stat: path=/var/run/reboot-required get_md5=no

    - name: Reboot the box if kernel updated
      reboot:
        msg: "Reboot initiated by Ansible for kernel updates"
        connect_timeout: 5
        reboot_timeout: 300
        pre_reboot_delay: 0
        post_reboot_delay: 30
        test_command: uptime
      when: reboot_required_file.stat.exists
1_traeffik.yml
  • Transfers local Docker files
  • Starts Traeffik
---
- hosts: docker_host
  gather_facts: no
  tasks:
    - name: create build directory
      file:
        path: /root/docker
        state: directory
        owner: root
        group: root
        mode: '0755'
    - name: copy container data
      copy:
        src: ../docker/traefik
        dest: /root/docker
        owner: root
        group: root
        mode: '0644'
    - name: create acme.json file
      file:
        path: /root/docker/traefik/acme.json
        state: touch
        owner: root
        group: root
        mode: '600'
    - name: start Traeffik
      docker_compose:
        project_src: /root/docker/traefik
        build: no
      register: output
3_lubos-klokner-sk.yml

Next I would like only add the example how to get the source code from git and start the project. The remaining ones are the same. I’m either copying local data or pulling code from git.

In this example Ansible:

  • creates the directory
  • gets the data from Github
  • Pulls private repo of this web from Github using private SSH key
  • fixes the permissions of the website static content
  • build jekyll-build and start the NGINX to serve web-lubos this site
---
- hosts: docker_host
  gather_facts: no
  tasks:
    - name: create build directory
      file:
        path: /root/docker
        state: directory
        owner: root
        group: root
        mode: '0755'
    - name: Get data from github
      git:
        repo: git@github.com:erkac/web-lubos-klokner-sk.git
        dest: /root/docker/www-lubos-klokner-sk
        accept_hostkey: yes
        key_file: /root/.ssh/id_rsa_github
    - name: fix permissions on site sub-folder
      file:
        path: /root/docker/www-lubos-klokner-sk/site
        state: directory
        owner: root
        group: root
        mode: '0777'

    - name: build the web page content
      docker_compose:
        project_src: /root/docker/www-lubos-klokner-sk
        services: jekyll-build
      register: output
    - name: publish the web site
      docker_compose:
        project_src: /root/docker/www-lubos-klokner-sk
        services: web-lubos
      register: output

…and that’s all folks! Hope someone will read this and find it useful.