Schlagwort: docker-compose

  • Installing Artifactory with Docker and Ansible

    The aim of this tutorial is to provision Artifactory stack in Docker on a Virtual Machine using Ansible for the provisioning. I separated the different concerns so that they can be tested, changed and run separately. Hence, I recommend you run the parts separately, as I explain them here, in case you run into some problems.

    Prerequisites

    On my machine, I created a directory artifactory-install. I will later push this folder to a git repository. The directory structure inside of this folder will look like this.

    artifactory-install
    ├── Vagrantfile
    ├── artifactory
    │   ├── docker-compose.yml
    │   └── nginx-config
    │       ├── reverse_proxy_ssl.conf
    │       ├── cert.pem
    │       └── key.pem 
    ├── artifactory.yml
    ├── docker-compose.yml
    └── docker.yml

    Please create the subfolders artifactory (the folder that we will copy to our VM) and nginx-config subfolder (which contains the nginx-configuration for the reverse-proxy as well as the certificate and key).

    Installing a Virtual Machine with Vagrant

    I use the following Vagrantfile. The details are explained in Vagrant in a Nutshell. You might want to experiment with the virtual box parameters.

    agrant.configure("2") do |config|
    
        config.vm.define "artifactory" do |web|
    
            # Resources for this machine
            web.vm.provider "virtualbox" do |vb|
               vb.memory = "2048"
               vb.cpus = "1"
            end
    
            web.vm.box = "ubuntu/xenial64"
    
            web.vm.hostname = "artifactory"
    
            # Define public network. If not present, Vagrant will ask.
            web.vm.network "public_network", bridge: "en0: Wi-Fi (AirPort)"
    
            # Disable vagrant ssh and log into machine by ssh
            web.vm.provision "file", source: "~/.ssh/id_rsa.pub", destination: "~/.ssh/authorized_keys"
    
            # Install Python to be able to provision machine using Ansible
            web.vm.provision "shell", inline: "which python || sudo apt -y install python"
    
        end
    
    end

    Installing Docker

    As Artifactory will run as a Docker container, we have to install the docker environment first. In my Playbook (docker.yml), I use the Ansible Role to install Docker and docker-compose from Jeff Geerling. The role variables are explained in the README.md. You might have to adopt this yaml-file, e.g. defining different users etc.

    - name: Install Docker
    
      hosts: artifactory
    
      become: yes
      become_method: sudo
    
      tasks:
      - name: Install Docker and docker-compose
        include_role:
           name: geerlingguy.docker
        vars:
           - docker_edition: 'ce'
           - docker_package_state: present
           - docker_install_compose: true
           - docker_compose_version: "1.22.0"
           - docker_users:
              - vagrant

    Before you run this playbook you have to install the Ansible role

    ansible-galaxy install geerlingguy.docker

    In addition, make sure you have added the IP-address of the VM to your Ansible inventory

    sudo vi /etc/ansible/hosts

    Then you can run this Docker Playbook with

    ansible-playbook docker.yml

    Installing Artifactory

    I will show the Artifactory Playbook (artifactory.yml) first and then we go through the different steps.

    - name: Install Docker
    
      hosts: artifactory
    
      become: yes
      become_method: sudo
    
      tasks:
    
      - name: Check is artifactory folder exists
        stat:
          path: artifactory
        register: artifactory_home
    
      - name: Clean up docker-compose
        command: >
           docker-compose down
        args:
           chdir: ./artifactory/
        when: artifactory_home.stat.exists
    
      - name: Delete artifactory working-dir
        file:
           state: absent
           path: artifactory
        when: artifactory_home.stat.exists
    
      - name: Copy artifactory working-dir
        synchronize:
           src: ./artifactory/
           dest: artifactory
      - name: Generate a Self Signed OpenSSL certificate
        command: >
           openssl req -subj '/CN=localhost' -x509 -newkey rsa:4096 -nodes
           -keyout key.pem -out cert.pem -days 365
        args:
           chdir: ./artifactory/nginx-config/
    
      - name: Call docker-compose to run artifactory-stake
        command: >
           docker-compose -f docker-compose.yml up -d
        args:
           chdir: ./artifactory/
    
    
    

    Clean-Up a previous Artifactory Installation

    As we will see later, the magic happens in the ~/artifactory folder on the VM. So first we will clean-up a previous installation, e.g. stopping and removing the running containers. There are different ways to achieve this. I will use a docker-compose down, which will terminate without an error, even if no container is running. In addition, I will delete the artifactory-folder with all subfolders (if they are present).

    Copy nginx-Configuration and docker-compose.yml

    The artifactory-folder includes the docker-compose.yml to install the Artifactory stack (see below) and the nginx-configuration (see below). They will be copied in a directory with the same name to the remote host.

    I use the synchronise module to copy the files, as currently since Python 3.6 there seems to be a problem that doesn’t allow to copy a directory recursively with the copy module. Unfortunately, synchronise demands your SSH password again. There are workarounds that make sense but don’t look elegant to me, so I avoid them ;).

    Set-Up nginx Configuration

    I will use nginx as a reverse-proxy that also allows a secure connection. The configuration-file is static and located in the nginx-config subfolder (reverse_proxy_ssl.conf)

    server {
      listen 443 ssl;
      listen 8080;
    
      ssl_certificate /etc/nginx/conf.d/cert.pem;
      ssl_certificate_key /etc/nginx/conf.d/key.pem;
    
      location / {
         proxy_pass http://artifactory:8081;
      }
    }

    The configuration is described in the nginx-docs. You might have to adopt this file for your needs.

    The proxy_pass is set to the service-name inside of the Docker overlay-network (as defined in the docker-compose.yml). I will open port 443 for an SSL connection and 8080 for a non-SSL connection.

    Create a Self-Signed Certificate

    We will create a self-signed certificate on the remote-host inside of the folder nginx-config

    openssl req -subj '/CN=localhost' -x509 -newkey rsa:4096 -nodes- keyout key.pem -out cert.pem -days 365

    The certificate and key are referenced in the reverse_proxy_ssl.conf, as explained above. You might run into problems, that your browser won’t accept this certificate. A Google search might provide some relief.

    Run Artifactory

    As mentioned above, we will run Artifactory with a reverse-proxy and a PostgreSQL as its datastore.

    version: "3"
    services:
    
      postgresql:
        image: postgres:latest
        deploy:
          replicas: 1
          restart_policy:
            condition: on-failure
        environment:
          - POSTGRES_DB=artifactory
          - POSTGRES_USER=artifactory
          - POSTGRES_PASSWORD=artifactory
        volumes:
          - ./data/postgresql:/var/lib/postgresql/data
    
      artifactory:
        image: docker.bintray.io/jfrog/artifactory-oss:latest
        user: "${UID}:${GID}"
        deploy:
          replicas: 1
          restart_policy:
            condition: on-failure
        environment:
          - DB_TYPE=postgresql
          - DB_USER=artifactory
          - DB_PASSWORD=artifactory
        volumes:
          - ./data/artifactory:/var/opt/jfrog/artifactory
        depends_on:
          - postgresql
    
      nginx:
        image: nginx:latest
        deploy:
          replicas: 1
          restart_policy:
            condition: on-failure
        volumes:
          - ./nginx-config:/etc/nginx/conf.d
        ports:
          - "443:443"
          - "8080:8080"
        depends_on:
          - artifactory
          - postgresql
    

    Artifactory

    I use the image artifactory-oss:latest from JFrog (as found on JFrog BinTray). On GitHub,  you find some examples of how to use the Artifactory Docker image.

    I am not super satisfied, as out-of-the-box I receive a „Mounted directory must be writable by user ‚artifactory‘ (id 1030)“ error when I bind /var/opt/jfrog/artifactory inside of the container to the folder ./data/artifactory on the VM. Inside of the Dockerfile for this image, they use a few tasks with a user „artifactory“. I don’t have such a user on my VM (and don’t want to create one). A workaround seems to be to set the user-id and group-id inside of the docker-compose.yml as described here.

    Alternatively, you can use the Artifactory Docker image from Matt Grüter provided on DockerHub. However, it doesn’t work with PostgreSQL out-of-the-box and you have to use the internal database of Artifactory (Derby). In addition, the latest image from Matt is built on version 3.9.2 (the current version is 6.2.0, 18/08/2018). Hence, you have to build a new image and upload it to your own repository. Sure if we use docker-compose to deploy our services, we could add a build-segment in the docker-compose.yml. But if we use docker stack to run our services, the build-part will be ignored.

    I do not publish a port (default is 8081) as I want users to access Artifactory only by the reverse-proxy.

    PostgreSQL

    I use the official PostgreSQL Docker image from DockerHub. The data-volume inside of the container will be bound to the postgresql folder in ~/artifactory/data/postgresql on the VM. The credentials have to match the credentials for the artifactory-service. I don’t publish a port, as I don’t want to use the database outside of the Docker container.

    The benefits of using a separate database are when you have intensive usage or a high load on your database, as the embedded database (Derby) might then slow things down.

    Nginx

    I use Nginx as described above. The custom configuration in ~/artifactory/nginx-config/reverse_proxy_ssl.conf is bound to /etc/nginx/conf.d inside of the Docker container. I publish port 443 (SSL) and 8080 (non-SSL) to the world outside of the Docker container.

    Summary

    To get the whole thing started, you have to

    1. Create a VM (or have some physical or virtual machine where you want to install Artifactory) with Python (as needed by Ansible)
    2. Register the VM in the Ansible Inventory (/etc/ansible/hosts)
    3. Start the Ansible Playbook docker.yml to install Docker on the VM (as a prerequisite to run Artifactory)
    4. Start the Ansible Playbook artifactory.yml to install Artifactory (plus PostgreSQL and a reverse-proxy).

    I recommend adopting the different parts for your needs. I am sure you could also improve a lot. Of course, you can include the Ansible Playbooks (docker.yml and artifactory.yml) directly in the provision-part of your Vagrantfile. In this case, you have to only run  vagrant up.

    Integrating Artifactory with Maven

    This article describes how to configure Maven with Artifactory. In my case, the automatic generation of the settings.xml in ~/.m2/ for Maven didn’t include the encrypted password. You can retrieve the encrypted password, as described here. Make sure you update your Custom Base URL in the General Settings, as it will be used to generate the settings.xml.

    Possible Error: Broken Pipe

    I ran into an authentification problem when I first tried to deploy a snapshot archive from my project to Artifactory. It appeared when I ran mvn deploy as (use -X parameter for a more verbose output)

    Caused by: org.eclipse.aether.transfer.ArtifactTransferException: Could not transfer artifact com.vividbreeze.springboot.rest:demo:jar:0.0.1-20180818.082818-1 from/to central (http://artifactory.intern.vividbreeze.com:8080/artifactory/ext-release-local): Broken pipe (Write failed)

    A broken pipe can mean everything, and you will find a lot when you google it. A closer look in the access.log on the VM running Artifactory revealed an

    2018-08-18 08:28:19,165 [DENIED LOGIN]  for chris/192.168.0.5.
    

    The reason was that I provided a wrong encrypted password (see above) in ~/.m2/settings. You should be aware, that the encrypted password changes everytime you deploy a new version of Artifactory.

    Possible Error: Request Entity Too Large

    Another error I ran into when I deployed a very large jar (this can happen with Spring Boot apps that carry a lot of luggage): Return code is: 413, ReasonPhrase: Request Entity Too Large.

    [ERROR] Failed to execute goal org.apache.maven.plugins:maven-deploy-plugin:2.8.2:deploy (default-deploy) on project demo: Failed to deploy artifacts: Could not transfer artifact com.vividbreeze.springboot.rest:demo:jar:0.0.1-20180819.092431-3 from/to snapshots (http://artifactory.intern.vividbreeze.com:8080/artifactory/libs-snapshot): Failed to transfer file: http://artifactory.intern.vividbreeze.com:8080/artifactory/libs-snapshot/com/vividbreeze/springboot/rest/demo/0.0.1-SNAPSHOT/demo-0.0.1-20180819.092431-3.jar. Return code is: 413, ReasonPhrase: Request Entity Too Large. -> [Help 1]
    

    I wasn’t able to find anything in the Artifactory logs, nor the STDIN/ERR of the nginx-container. However, I assumed that there might a limit on the maximum request body size. As the was over 20M large, I added the following line to the ~/artifactory/nginx-config/reverse_proxy_ssl.conf:

    server {
      ...
      client_max_body_size 30M;
      ...
    }

    Further Remarks

    So basically you have to run three scripts, to run Artifactory on a VM. Of course, you can add the two playbooks to the provision-part of the Vagrantfile. For the sake of better debugging (something will go probably wrong), I recommend running them separately.

    The set-up here is for a local or small team installation of Artifactory, as Vagrant and docker-compose are tools made for development. However, I added a deploy-part in the docker-compose.yml, so you can easily set up a swarm and run the docker-compose.yml with docker stack without any problems. Instead of Vagrant, you can use Terraform or Apache Mesos or other tools to build an infrastructure in production.

    To further pre-configure Artifactory, you can use the Artifactory REST API or provide custom configuration files in artifactory/data/artifactory/etc/.

     

     

  • Docker Swarm – Single Node

    In the previous tutorial, we created one small service, and let it run in an isolated Docker container. In reality, your application might consist of many of different services. An e-commerce application encompasses services to register new customers, search for products, list products, show recommendations and so on. These services might even exist more than one time when they are heavily requested. So an application can be seen as a composition of different services (that run in containers).

    In this first part of the tutorial, we will work with the simple application of the Docker Basics Tutorial, that contains only one service. We will deploy this service more than one time and let run on only one machine. In part II we will scale this application over many machines.

    Prerequisites

    Before we start, you should have completed the first part of the tutorial series. As a result, you should an image uploaded to the DockerHub registry. In my case, the image name is vividbreeze/docker-tutorial:version1.

    Docker Swarm

    As mentioned above, a real-world application consists of many containers spread over different hosts. Many hosts can be grouped to a so-called swarm (mainly hosts that run Docker in swarm-mode). A swarm is managed by one or more swarm managers and consists of one or many workers. Before we continue, we have to initial a swarm on our machine.

    docker swarm init

    Swarm initialized: current node (pnb2698sy8gw3c82whvwcrd77) is now a manager.
    
    To add a worker to this swarm, run the following command:
    
        docker swarm join --token SWMTKN-1-39y3w3x0iiqppn57pf2hnrtoj867m992xd9fqkd4c3p83xtej0-9mpv98zins5l0ts8j62ociz4w 192.168.65.3:2377
    
    To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

    The swarm was initialised with one node (our machine) as a swarm manager.

    Docker Stack

    We now have to design our application. We do this in a file called docker-compose.yml.  So far, we have just developed one service, and that runs inside one Docker container. In this part of the tutorial, our application will only consist of one service. Now let us assume this service is heavily used and we want to scale it.

    version: "3"
    services:
      dataservice:
        image: vividbreeze/docker-tutorial:version1
        deploy:
          replicas: 3
        ports:
          - "4000:8080"
    

    The file contains the name of our service and the number of instances (or replicas) that should be deployed. We now do the port mapping here. The port 8080 that is used by the service inside of our container will be mapped to the port 4000 on our host.

    To create our application use (you have to invoke this command from the vm-manager node)

    docker stack deploy -c docker-compose.yml dataapp

    Creating network dataapp_default
    Creating service dataapp_dataservice

    Docker now has created a network dataservice_web and a network dataservice_webnet. We will come to networking in the last part of this tutorial. By „stack“, Docker means a stack of (scaled) services that together form an application. A stack can be deployed on one swarm. It has to be called from a Swarm manager.

    Let us now have a look, of how many containers were created

    docker container ls

    ONTAINER ID        IMAGE                                  COMMAND              CREATED                  STATUS              PORTS                    NAMES
    bb18e9d71530        vividbreeze/docker-tutorial:version1   "java DataService"   Less than a second ago   Up 8 seconds                                 dataapp_dataservice.3.whaxlg53wxugsrw292l19gm2b
    441fb80b9476        vividbreeze/docker-tutorial:version1   "java DataService"   Less than a second ago   Up 7 seconds                                 dataapp_dataservice.4.93x7ma6xinyde9jhearn8hjav
    512eedb2ac63        vividbreeze/docker-tutorial:version1   "java DataService"   Less than a second ago   Up 6 seconds                                 dataapp_dataservice.1.v1o32qvqu75ipm8sra76btfo6
    
    

    In Docker terminology, each of these containers is called a task. Now each container cannot be accessed directly through the localhost and the port (they have no port), but through a manager, that listens to port 4000 on the localhost. These five containers, containing the same service, are bundled together and appear as one service. This service is listed by using

    docker service ls
    ID                  NAME                  MODE                REPLICAS            IMAGE                                  PORTS
    zfbbxn0rgksx        dataapp_dataservice   replicated          5/5                 vividbreeze/docker-tutorial:version1   *:4000->8080/tcp

    You can see the tasks (containers) that belong to this services with

    docker service ps dataservice_web

    ID                  NAME                    IMAGE                                  NODE                    DESIRED STATE       CURRENT STATE            ERROR                         PORTS
    lmw0gnxcs57o        dataapp_dataservice.1       vividbreeze/docker-tutorial:version1   linuxkit-025000000001   Running             Running 13 minutes ago
    fozpqkmrmsb3        dataapp_dataservice.2       vividbreeze/docker-tutorial:version1   linuxkit-025000000001   Running             Running 13 minutes ago
    gc6dccwxw53f        dataapp_dataservice.3       vividbreeze/docker-tutorial:version1   linuxkit-025000000001   Running             Running 13 minutes ago
    

    Now let us call the service 10 times

    repeat 10 { curl localhost:4000; echo } (zsh)
    for ((n=0;n<10;n++)); do curl localhost:4000; echo; done (bash)

    <?xml version="1.0" ?><result><name>hello</name><id>2925</id></result>
    <?xml version="1.0" ?><result><name>hello</name><id>1624</id></result>
    <?xml version="1.0" ?><result><name>hello</name><id>2515</id></result>
    <?xml version="1.0" ?><result><name>hello</name><id>2925</id></result>
    <?xml version="1.0" ?><result><name>hello</name><id>1624</id></result>
    <?xml version="1.0" ?><result><name>hello</name><id>2515</id></result>
    <?xml version="1.0" ?><result><name>hello</name><id>2925</id></result>
    <?xml version="1.0" ?><result><name>hello</name><id>1624</id></result>
    <?xml version="1.0" ?><result><name>hello</name><id>2515</id></result>
    <?xml version="1.0" ?><result><name>hello</name><id>2925</id></result>

    Now you can see, that our service is called ten times, each time one of the services running inside of the containers were used to handle the request (you see three different ids). The service manager (dataservice-web) acts as a load-balancer. In this case, the load balancer uses a round-robin strategy.

    To sum it up, in the docker-compose.yml, we defined our desired state (3 replicas of one service). Docker tries to maintain this desired state using the resources that are available. In our case, one host (one node). A swarm-manager manages the service, including the containers, we have just created. The service can be reached at port 4000 on the localhost.

    Restart Policy

    This can be useful for updating the number of replicas or changing other parameters. Let us play with some of the parameters. Let us add a restart policy to our docker-compose.yml

    version: "3"
    services:
      dataservice:
        image: vividbreeze/docker-tutorial:version1
        deploy:
          replicas: 3
          restart_policy:
            condition: on-failure
        ports:
          - "4000:8080"

    and update our configuration

    docker stack deploy -c docker-compose.yml dataapp

    Let us now call our service again 3 times to memorise the ids

    repeat 3 { curl localhost:4000; echo  }
    
    <?xml version="1.0" ?><result><name>hello</name><id>713</id></result>
    <?xml version="1.0" ?><result><name>hello</name><id>1157</id></result>
    <?xml version="1.0" ?><result><name>hello</name><id>3494</id></result>

    Now let us get the names of our containers

    docker container ls -f "name=dataservice_web"
    
    CONTAINER ID        IMAGE                                  COMMAND              CREATED             STATUS              PORTS               NAMES
    953e010ab4e5        vividbreeze/docker-tutorial:version1   "java DataService"   15 minutes ago      Up 15 minutes                           dataapp_dataservice.1.pb0r4rkr8wzacitgzfwr5fcs7
    f732ffccfdad        vividbreeze/docker-tutorial:version1   "java DataService"   15 minutes ago      Up 15 minutes                           dataapp_dataservice.3.rk7seglslg66cegt6nrehzhzi
    8fb716ef0091        vividbreeze/docker-tutorial:version1   "java DataService"   15 minutes ago      Up 15 minutes                           datasapp_dataservice.2.0mdkfpjxpldnezcqvc7gcibs8

    Now let us kill one of these containers, to see if our manager will start a new one again

    docker container rm -f 953e010ab4e5

    It may take a few seconds, but then you will see a newly created container created by the swarm manager (the container-id of the first container is now different).

    docker container ls -f "name=dataservice_web"
    
    CONTAINER ID        IMAGE                                  COMMAND              CREATED             STATUS              PORTS               NAMES
    bc8b6fa861be        vividbreeze/docker-tutorial:version1   "java DataService"   53 seconds ago      Up 48 seconds                           dataapp_dataservice.1.5aminmnu9fx8qnbzoklfbzyj5
    f732ffccfdad        vividbreeze/docker-tutorial:version1   "java DataService"   17 minutes ago      Up 17 minutes                           dataapp_dataservice.3.rk7seglslg66cegt6nrehzhzi
    8fb716ef0091        vividbreeze/docker-tutorial:version1   "java DataService"   18 minutes ago      Up 17 minutes                           dataapp_datavervice.2.0mdkfpjxpldnezcqvc7gcibs8

    The id in the response of one of the replicas of the service has changed

    repeat 3 { curl localhost:4000; echo  }
    
    <?xml version="1.0" ?><result><name>hello</name><id>2701</id></result>
    <?xml version="1.0" ?><result><name>hello</name><id>1157</id></result>
    <?xml version="1.0" ?><result><name>hello</name><id>3494</id></result>

    Resources Allocation

    You can also change the resources, such as CPU-time and memory that will be allocated to a service

    ...
    image: vividbreeze/docker-tutorial:version1
    deploy:
      replicas: 3
      restart_policy:
        condition: on-failure
      resources:
        limits:
          cpus: '0.50'
          memory: 10M
        reservations:
          cpus: '0.25'
          memory: 5M
    ...

    The service will be allocated to at most 50% CPU-time and 50 MBytes of memory, and at least 25% CPU-time and 5 MBytes of memory.

    Docker Compose

    Instead of docker stack, you can also use docker-compose. docker-compose is a program, written in Python, that does the container orchestration for you on a local machine, e.g. it ignores the deploy-part in the docker-compose.yml.

    However, docker-compose uses some nice debugging and clean-up functionality, e.g. if you start our application with

    docker-compose -f docker-compose.yml up

    you will see the logs of all services (we only have one at the moment) colour-coded in your terminal window.

    WARNING: Some services (web) use the 'deploy' key, which will be ignored. Compose does not support 'deploy' configuration - use `docker stack deploy` to deploy to a swarm.
    WARNING: The Docker Engine you're using is running in swarm mode.
    
    Compose does not use swarm mode to deploy services to multiple nodes in a swarm. All containers will be scheduled on the current node.
    
    To deploy your application across the swarm, use `docker stack deploy`.
    
    Creating network "docker_default" with the default driver
    Pulling web (vividbreeze/docker-tutorial:version1)...
    version1: Pulling from vividbreeze/docker-tutorial
    Digest: sha256:39e30edf7fa8fcb15112f861ff6908c17d514af3f9fcbccf78111bc811bc063d
    Status: Downloaded newer image for vividbreeze/docker-tutorial:version1
    Creating docker_web_1 ... done
    Attaching to docker_web_1

    You can see in the warning, that the deploy part of your docker-compose.yml is ignored, as docker-compose focusses on the composition of services on your local machine, and not across a swarm.

    If you want to clean up (containers, volumes, networks and other) just use

    docker-compose down

    docker-compose also allows you to build your images (docker stack won’t) in case it hasn’t been built before, e.g.

    build:
      context: .
      dockerfile: Dockerfile.NewDataservice
    image: dataserviceNew

    You might notice on the output of many commands, that docker-compose is different from the Docker commands. So again, use docker-compose only for Docker deployments on one host or to verify a docker-compose.yml on your local machine before using it in production.

    Further Remarks

    To summarise

    • Use docker swarm to define a cluster that runs our application. In our case the swarm consisted only of one machine (no real cluster). In the next part of the tutorial, we will see that a cluster can span various physical and virtual machines.
    • Define your application in a docker-compose.yml.
    • Use docker stack to deploy your application in the swarm in a production environment or docker-compose to test and deploy your application in a development environment.

    Of course, there is more to Services, than I explained in this tutorial. However, I hope it helped as a starting point to go into more depth.