Schlagwort: docker stack

  • Docker Networking – Bridge Network

    Default Bridge Network

    Containers, that run on the same machine (share the same Docker daemon) and are not part of a swarm use the so-called bridge network (a virtual network) to communicate with each other.

    Let us create two basic containers

    docker container run -d --name nginx2 -p3001:80 nginx:alpine

    docker container run -d --name nginx2 -p3002:80 nginx:alpine

    You can inspect the bridge-network with

    docker network inspect bridge

    and you will see in the container section of this REST response, that the newly created docker-containers belong to this network.

    "Containers": {
        "9b4e3e3ca279518708ed88a629ba5d40bd1e94d5ed316b74b026b12c1034b497": {
            "Name": "nginx2",
            "EndpointID": "5a65daa2976ff986e2a9b4aeb9df8e990b0bf0a45ac60dd05475af20f15cc5c9",
            "MacAddress": "02:42:ac:11:00:03",
            "IPv4Address": "172.17.0.3/16",
            "IPv6Address": ""
        },
        "c57ee29c8babf8ecf20e5c2a9a66b30c49cc6bb94b22cd4daad79b87f7a33358": {
            "Name": "nginx1",
            "EndpointID": "75dc77f5a8677485aaee91c33734e720ebbb61b94917eacba8f50bd4dc7a88e3",
            "MacAddress": "02:42:ac:11:00:02",
            "IPv4Address": "172.17.0.2/16",
            "IPv6Address": ""
        }
    },

    You also see the IP address of container nginx1 (172.17.0.2) and nginx2 (127.17.0.3). We can also see the IP address an subnet of the bridge:

    "IPAM": {
        "Driver": "default",
        "Options": null,
        "Config": [
            {
                "Subnet": "172.17.0.0/16",
                "Gateway": "172.17.0.1"
            }
        ]
    },

    Now let us see if the nginx-containers can see each other, as well as the bridge.

    docker container exec -it nginx1 ping 172.17.0.3

    docker container exec -it nginx1 ping 172.17.0.1

    docker container exec -it nginx2 ping 172.17.0.2

    docker container exec -it nginx2 ping 172.17.0.1

    In each case, you should be able to reach the destination

    PING 172.17.0.1 (172.17.0.1): 56 data bytes
    64 bytes from 172.17.0.1: seq=0 ttl=64 time=0.092 ms
    64 bytes from 172.17.0.1: seq=1 ttl=64 time=0.167 ms
    64 bytes from 172.17.0.1: seq=2 ttl=64 time=0.126 ms
    Default Bridge Network Docker
    Default Bridge Network Docker

    However, what is not working with the default bridge network is name resolution, e.g.

    docker container exec -it nginx1 ping nginx2

    should not work and lead to a

    ping: bad address 'nginx2'

    Custom Bridge Networks (embedded DNS)

    So to make this work, we have to create our own network

    docker network create --driver bridge nginx-net

    You can see the new network (and other networks) by using

    docker network ls

    NETWORK ID          NAME                DRIVER              SCOPE
    1f3e6ded4f9a        bridge              bridge              local
    20fe2ddb80e6        host                host                local
    6d96ed99b06b        nginx-net           bridge              local
    5866618435c1        none                null                local

    Now let us connect our two containers to this network

    docker network connect nginx-net nginx1

    docker network connect nginx-net nginx2

    If you now do a docker network inspect nginx-net you will see that both containers are connected to this network. You will notice, that the IP-addresses of these containers in this network have changed (the subnet is 172.19.0.0/16 compared to 172.17.0.0/16 as in the default-bridge network).

    "Containers": {
         "a1ce9d1f383cf10b3a0cd27d6ef482719ae4e3ae48cb5b8402b2d3d8b897db03": {
             "Name": "nginx2",
             "EndpointID": "49fc6d33e57e909f0f0ea98e00c8d8f2f2db996d001a3359b1a6dcc968254f91",
             "MacAddress": "02:42:ac:13:00:03",
             "IPv4Address": "172.19.0.3/16",
             "IPv6Address": ""
         },
         "c57ee29c8babf8ecf20e5c2a9a66b30c49cc6bb94b22cd4daad79b87f7a33358": {
             "Name": "nginx1",
             "EndpointID": "fb7e9c28e6a5564e9bba182ada2ce8048d9ab5b8ec3620f7e03a2491b18219b2",
             "MacAddress": "02:42:ac:13:00:02",
             "IPv4Address": "172.19.0.2/16",
             "IPv6Address": ""
         }
     },

    Now let run a ping against the other containers by name again

    docker container exec -it nginx1 ping nginx2

    PING nginx2 (172.19.0.3): 56 data bytes
    64 bytes from 172.19.0.3: seq=0 ttl=64 time=0.139 ms
    64 bytes from 172.19.0.3: seq=1 ttl=64 time=0.152 ms
    c64 bytes from 172.19.0.3: seq=2 ttl=64 time=0.100 ms

    The same should work for the other connections. So basically, the name of the container is its host-name.

    Custom Bridge Network Docker
    Custom Bridge Network Docker

    So why is DNS inside of Docker networks so important? As we have especially seen in the tutorials about Docker Swarm, you can’t rely on the IP address of containers, as they may change, especially when a swarm manager has control over the containers. Hence it is much reliable and easier to reference containers by name than their IP address.

    Round-Robin DNS

    Let us take our dataService from the Swarm tutorial and create two Docker containers on the same host

    docker container run --name dataservice1 -d -p4001:8080 vividbreeze/docker-tutorial:version1

    docker container run --name dataservice2 -d -p4002:8080 vividbreeze/docker-tutorial:version1

    So you call these services you should get a response with an id. This id should be different, depending on the port.

    curl 127.0.0.1:4001

    <?xml version="1.0" ?><result><name>hello</name><id>2128</id></result>

    curl 127.0.0.1:4002

    <?xml version="1.0" ?><result><name>hello</name><id>1976</id></result>

    Now let us create a custom bridge-network and assign both containers to this network.

    docker network create --driver bridge dataservice-net

    docker network connect dataservice-net --alias dataservice dataservice1

    docker network connect dataservice-net --alias dataservice dataservice2

    We used a new parameter called --alias. This alias is an alias for the embedded DNS service, i.e. that when I call dataservice inside of the dataservice-net, the embedded DNS will return the IP address from either dataservice1 or dataservice2. Let us test this by running a new container inside this network and executing curl dataservice:8080 (because inside of dataservice-net both containers listen to port 8080 on different IP-addresses).

    docker container run --rm -it --network dataservice-net centos curl dataservice:8080

    This command runs a new Docker container with CentOS, executes a curl (-it) and deletes itself afterwards (–rm).

    If you repeat this command a couple of time, you see that the either one of the containers returns a result (you can see this on the different ids they return).

    $ docker container run --rm -it --network dataservice-net centos curl dataservice:8080
    <?xml version="1.0" ?><result><name>hello</name><id>1976</id></result>
    $ docker container run --rm -it --network dataservice-net centos curl dataservice:8080
    <?xml version="1.0" ?><result><name>hello</name><id>1976</id></result>
    $ docker container run --rm -it --network dataservice-net centos curl dataservice:8080
    <?xml version="1.0" ?><result><name>hello</name><id>2128</id></result>
    $ docker container run --rm -it --network dataservice-net centos curl dataservice:8080
    <?xml version="1.0" ?><result><name>hello</name><id>2128</id></result>
    $ docker container run --rm -it --network dataservice-net centos curl dataservice:8080
    <?xml version="1.0" ?><result><name>hello</name><id>1976</id></result>
    $ docker container run --rm -it --network dataservice-net centos curl dataservice:8080
    <?xml version="1.0" ?><result><name>hello</name><id>2128</id></result>
    Round Robin DNS - Custom Bridge Network Docker
    Round Robin DNS – Custom Bridge Network Docker

    What is the beauty here? The loads (requests) is balanced between the two containers. If you stop one of the containers, a request to dataservice will still return a result.

    docker container stop dataservice1

    docker container run --rm -it --network dataservice-net centos curl dataservice:8080

    $ docker container run --rm -it --network dataservice-net centos curl dataservice:8080
    <?xml version="1.0" ?><result><name>hello</name><id>1976</id></result>
    $ docker container run --rm -it --network dataservice-net centos curl dataservice:8080
    <?xml version="1.0" ?><result><name>hello</name><id>1976</id></result>
    $ docker container run --rm -it --network dataservice-net centos curl dataservice:8080
    <?xml version="1.0" ?><result><name>hello</name><id>1976</id></result>
    $ docker container run --rm -it --network dataservice-net centos curl dataservice:8080
    <?xml version="1.0" ?><result><name>hello</name><id>1976</id></result>
    

    We will later discuss, how we can use this DNS balancing from outside of the network.

    Further Remarks

    A bridge network can be reached by its IP address from the Docker host, when you are working on Linux (or from inside the Docker VM if you are working on MacOSX or Windows), but not by its name.  The way you can access the containers from the host (also MacOSX and Windows) is by its ports on the local machine, e.g.  curl 127.0.0.1:3000 or curl 192.168.0.6:3001.

    If you are not exposing a port when running the container, the container won’t be accessible from outside of the bridge network (which is fine). This makes sense if you e.g have a service that connects to a database. The service should be accessible from the host network, while the database should only be accessed by the service.

    A container can belong to several networks. You can try this by creating more of our small containers and connect them to our nginx-net or newly created networks.

     

  • 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.