mirror of
https://github.com/jpetazzo/container.training.git
synced 2026-03-04 10:20:39 +00:00
2675 lines
52 KiB
HTML
2675 lines
52 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<base target="_blank">
|
|
<title>Docker Orchestration Workshop</title>
|
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
|
<style type="text/css">
|
|
@import url(https://fonts.googleapis.com/css?family=Yanone+Kaffeesatz);
|
|
@import url(https://fonts.googleapis.com/css?family=Droid+Serif:400,700,400italic);
|
|
@import url(https://fonts.googleapis.com/css?family=Ubuntu+Mono:400,700,400italic);
|
|
|
|
body { font-family: 'Droid Serif'; }
|
|
|
|
h1, h2, h3 {
|
|
font-family: 'Yanone Kaffeesatz';
|
|
font-weight: normal;
|
|
margin-top: 0.5em;
|
|
}
|
|
a {
|
|
text-decoration: none;
|
|
color: blue;
|
|
}
|
|
.remark-slide-content { padding: 1em 2.5em 1em 2.5em; }
|
|
|
|
.remark-slide-content { font-size: 25px; }
|
|
.remark-slide-content h1 { font-size: 50px; }
|
|
.remark-slide-content h2 { font-size: 50px; }
|
|
.remark-slide-content h3 { font-size: 25px; }
|
|
.remark-code { font-size: 25px; }
|
|
.small .remark-code { font-size: 16px; }
|
|
|
|
.remark-code, .remark-inline-code { font-family: 'Ubuntu Mono'; }
|
|
.red { color: #fa0000; }
|
|
.gray { color: #ccc; }
|
|
.small { font-size: 70%; }
|
|
.big { font-size: 140%; }
|
|
.underline { text-decoration: underline; }
|
|
.pic {
|
|
vertical-align: middle;
|
|
text-align: center;
|
|
padding: 0 0 0 0 !important;
|
|
}
|
|
img {
|
|
max-width: 100%;
|
|
max-height: 550px;
|
|
}
|
|
.title {
|
|
vertical-align: middle;
|
|
text-align: center;
|
|
}
|
|
.title h1 { font-size: 100px; }
|
|
.title p { font-size: 100px; }
|
|
.quote {
|
|
background: #eee;
|
|
border-left: 10px solid #ccc;
|
|
margin: 1.5em 10px;
|
|
padding: 0.5em 10px;
|
|
quotes: "\201C""\201D""\2018""\2019";
|
|
font-style: italic;
|
|
}
|
|
.quote:before {
|
|
color: #ccc;
|
|
content: open-quote;
|
|
font-size: 4em;
|
|
line-height: 0.1em;
|
|
margin-right: 0.25em;
|
|
vertical-align: -0.4em;
|
|
}
|
|
.quote p {
|
|
display: inline;
|
|
}
|
|
.warning {
|
|
background-image: url("warning.png");
|
|
background-size: 1.5em;
|
|
background-repeat: no-repeat;
|
|
padding-left: 2em;
|
|
}
|
|
.exercise {
|
|
background-color: #eee;
|
|
background-image: url("keyboard.png");
|
|
background-size: 1.4em;
|
|
background-repeat: no-repeat;
|
|
background-position: 0.2em 0.2em;
|
|
border: 2px dotted black;
|
|
}
|
|
.exercise::before {
|
|
content: "Exercise";
|
|
margin-left: 1.8em;
|
|
}
|
|
li p { line-height: 1.25em; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<textarea id="source">
|
|
|
|
class: title
|
|
|
|
Docker <br/> Orchestration <br/> Workshop
|
|
|
|
---
|
|
|
|
## Logistics
|
|
|
|
<!--
|
|
- Hello! We're `jerome at docker dot com` and `aj at soulshake dot net`
|
|
-->
|
|
|
|
- Hello! I'm `jerome at docker dot com`
|
|
|
|
<!--
|
|
Reminder, when updating the agenda: when people are told to show
|
|
up at 9am, they usually trickle in until 9:30am (except for paid
|
|
training sessions). If you're not sure that people will be there
|
|
on time, it's a good idea to have a breakfast with the attendees
|
|
at e.g. 9am, and start at 9:30.
|
|
|
|
- Agenda:
|
|
|
|
.small[
|
|
- 08:00-09:00 hello and breakfast
|
|
- 09:00:10:25 part 1
|
|
- 10:25-10:35 coffee break
|
|
- 10:35-12:00 part 2
|
|
- 12:00-13:00 lunch break
|
|
- 13:00-14:25 part 3
|
|
- 14:25-14:35 coffee break
|
|
- 14:35-16:00 part 4
|
|
]
|
|
|
|
-->
|
|
|
|
- The tutorial will run from 1pm to 5pm
|
|
|
|
- There will be a break at 2:45pm (stop me if I don't!)
|
|
|
|
- All the content is publicly available (slides, code samples, scripts)
|
|
|
|
<!--
|
|
Remember to change:
|
|
- the link below
|
|
- the "tweet my speed" hashtag in DockerCoins HTML
|
|
-->
|
|
|
|
- Live feedback, questions, help on
|
|
[Gitter](http://container.training/chat)
|
|
|
|
---
|
|
|
|
|
|
<!--
|
|
grep '^# ' index.html | grep -v '<br' | tr '#' '-'
|
|
-->
|
|
|
|
## Chapter 1: getting started
|
|
|
|
- Pre-requirements
|
|
- VM environment
|
|
- Our sample application
|
|
- Running the application
|
|
- Identifying bottlenecks
|
|
|
|
---
|
|
|
|
## Chapter 2: Scaling out
|
|
|
|
- SwarmKit
|
|
- Creating our first Swarm
|
|
- Running our first Swarm service
|
|
|
|
---
|
|
|
|
## Chapter 3: our app on Swarm
|
|
|
|
- Deploying a local registry
|
|
- Overlay networks
|
|
|
|
---
|
|
|
|
## Chapter 4: operating the Swarm
|
|
|
|
- Breaking into an overlay network
|
|
- Rolling updates
|
|
- Centralized logging
|
|
- Setting up ELK to store container logs
|
|
|
|
---
|
|
|
|
# Pre-requirements
|
|
|
|
- Computer with network connection and SSH client
|
|
|
|
- on Linux, OS X, FreeBSD... you are probably all set
|
|
|
|
- on Windows, get [putty](http://www.putty.org/),
|
|
[Git BASH](https://git-for-windows.github.io/), or
|
|
[MobaXterm](http://mobaxterm.mobatek.net/)
|
|
|
|
- Some Docker knowledge
|
|
|
|
(If you're here, you definitely qualify ☺)
|
|
|
|
<!--
|
|
(but that's OK if you're not a Docker expert!)
|
|
-->
|
|
|
|
|
|
---
|
|
|
|
## Nice-to-haves
|
|
|
|
- [GitHub](https://github.com/join) account
|
|
<br/>(if you want to fork the repo)
|
|
|
|
<br/>(if you want to fork the repo; also used to join Gitter)
|
|
|
|
- [Gitter](https://gitter.im/) account
|
|
<br/>(to join the conversation during the workshop)
|
|
|
|
- [Docker Hub](https://hub.docker.com) account
|
|
<br/>(it's one way to distribute images on your Swarm cluster)
|
|
|
|
---
|
|
|
|
## Hands-on sections
|
|
|
|
- The whole workshop is hands-on
|
|
|
|
- I will show Docker 1.12 in action
|
|
|
|
- I invite you to reproduce what I do
|
|
|
|
- All hands-on sections are clearly identified, like the gray rectangle below
|
|
|
|
.exercise[
|
|
|
|
- This is the stuff you're supposed to do!
|
|
- Go to [container.training](http://container.training/) to view these slides
|
|
- Join the chat room on
|
|
[Gitter](http://container.training/chat)
|
|
|
|
]
|
|
|
|
---
|
|
|
|
# VM environment
|
|
|
|
- Each person gets 5 private VMs (not shared with anybody else)
|
|
- They'll be up until tonight
|
|
- You have a little card with login+password+IP addresses
|
|
- You can automatically SSH from one VM to another
|
|
|
|
.exercise[
|
|
|
|
<!--
|
|
```bash
|
|
for N in $(seq 1 5); do
|
|
ssh -o StrictHostKeyChecking=no node$N true
|
|
done
|
|
for N in $(seq 1 5); do
|
|
(.
|
|
docker-machine rm -f node$N
|
|
ssh node$N "docker ps -aq | xargs -r docker rm -f"
|
|
ssh node$N sudo rm -f /etc/systemd/system/docker.service
|
|
ssh node$N sudo systemctl daemon-reload
|
|
echo Restarting node$N.
|
|
ssh node$N sudo systemctl restart docker
|
|
echo Restarted node$N.
|
|
) &
|
|
done
|
|
wait
|
|
```
|
|
-->
|
|
|
|
- Log into the first VM (`node1`)
|
|
- Check that you can SSH (without password) to `node2`:
|
|
```bash
|
|
ssh node2
|
|
```
|
|
- Type `exit` or `^D` to come back to node1
|
|
|
|
<!--
|
|
```meta
|
|
^D
|
|
```
|
|
-->
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## We will (mostly) interact with node1 only
|
|
|
|
- Unless instructed, **all commands must be run from the first VM, `node1`**
|
|
|
|
- We will only checkout/copy the code on `node1`
|
|
|
|
- When we will use the other nodes, we will do it mostly through the Docker API
|
|
|
|
- We will use SSH only for the initial setup and a few "out of band" operations (checking internal logs, debugging...)
|
|
|
|
---
|
|
|
|
## Terminals
|
|
|
|
Once in a while, the instructions will say:
|
|
<br/>"Open a new terminal."
|
|
|
|
There are multiple ways to do this:
|
|
|
|
- create a new window or tab on your machine, and SSH into the VM;
|
|
|
|
- use screen or tmux on the VM and open a new window from there.
|
|
|
|
You are welcome to use the method that you feel the most comfortable with.
|
|
|
|
---
|
|
|
|
## Tmux cheatsheet
|
|
|
|
- Ctrl-b c → creates a new window
|
|
- Ctrl-b n → go to next window
|
|
- Ctrl-b p → go to previous window
|
|
- Ctrl-b " → split window top/bottom
|
|
- Ctrl-b % → split window left/right
|
|
- Ctrl-b Alt-1 → rearrange windows in columns
|
|
- Ctrl-b Alt-2 → rearrange windows in rows
|
|
- Ctrl-b arrows → navigate to other windows
|
|
- Ctrl-b d → detach session
|
|
- tmux attach → reattach to session
|
|
|
|
---
|
|
|
|
## Brand new versions!
|
|
|
|
- Engine 1.12
|
|
- Compose 1.8
|
|
|
|
.exercise[
|
|
|
|
- Check all installed versions:
|
|
```bash
|
|
docker version
|
|
docker-compose -v
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
# Our sample application
|
|
|
|
- Visit the GitHub repository with all the materials of this workshop:
|
|
<br/>https://github.com/jpetazzo/orchestration-workshop
|
|
|
|
- The application is in the [dockercoins](
|
|
https://github.com/jpetazzo/orchestration-workshop/tree/master/dockercoins)
|
|
subdirectory
|
|
|
|
- Let's look at the general layout of the source code:
|
|
|
|
there is a Compose file [docker-compose.yml](
|
|
https://github.com/jpetazzo/orchestration-workshop/blob/master/dockercoins/docker-compose.yml) ...
|
|
|
|
... and 4 other services, each in its own directory:
|
|
|
|
- `rng` = web service generating random bytes
|
|
- `hasher` = web service computing hash of POSTed data
|
|
- `worker` = background process using `rng` and `hasher`
|
|
- `webui` = web interface to watch progress
|
|
|
|
---
|
|
|
|
## Compose file format version
|
|
|
|
*Particularly relevant if you have used Compose before...*
|
|
|
|
- Compose 1.6 introduced support for a new Compose file format (aka "v2")
|
|
|
|
- Services are no longer at the top level, but under a `services` section
|
|
|
|
- There has to be a `version` key at the top level, with value `"2"` (as a string, not an integer)
|
|
|
|
- Containers are placed on a dedicated network, making links unnecessary
|
|
|
|
- There are other minor differences, but upgrade is easy and straightforward
|
|
|
|
---
|
|
|
|
## Links, naming, and service discovery
|
|
|
|
- Containers can have network aliases (resolvable through DNS)
|
|
|
|
- Compose file version 2 makes each container reachable through its service name
|
|
|
|
- Compose file version 1 requires "links" sections
|
|
|
|
- Our code can connect to services using their short name
|
|
|
|
(instead of e.g. IP address or FQDN)
|
|
|
|
---
|
|
|
|
## Example in `worker/worker.py`
|
|
|
|

|
|
|
|
---
|
|
|
|
## What's this application?
|
|
|
|
---
|
|
|
|
class: pic
|
|
|
|

|
|
|
|
(DockerCoins 2016 logo courtesy of @XtlCnslt and @ndeloof. Thanks!)
|
|
|
|
---
|
|
|
|
## What's this application?
|
|
|
|
- It is a DockerCoin miner! 💰🐳📦🚢
|
|
|
|
- No, you can't buy coffee with DockerCoins
|
|
|
|
- How DockerCoins works:
|
|
|
|
- `worker` asks to `rng` to give it random bytes
|
|
- `worker` feeds those random bytes into `hasher`
|
|
- each hash starting with `0` is a DockerCoin
|
|
- DockerCoins are stored in `redis`
|
|
- `redis` is also updated every second to track speed
|
|
- you can see the progress with the `webui`
|
|
|
|
---
|
|
|
|
## Getting the application source code
|
|
|
|
- We will clone the GitHub repository
|
|
|
|
- The repository also contains scripts and tools that we will use through the workshop
|
|
|
|
.exercise[
|
|
|
|
<!--
|
|
```bash
|
|
[ -d orchestration-workshop ] && mv orchestration-workshop orchestration-workshop.$$
|
|
```
|
|
-->
|
|
|
|
- Clone the repository on `node1`:
|
|
```bash
|
|
git clone git://github.com/jpetazzo/orchestration-workshop
|
|
```
|
|
|
|
]
|
|
|
|
(You can also fork the repository on GitHub and clone your fork if you prefer that.)
|
|
|
|
---
|
|
|
|
# Running the application
|
|
|
|
Without further ado, let's start our application.
|
|
|
|
.exercise[
|
|
|
|
- Go to the `dockercoins` directory, in the cloned repo:
|
|
```bash
|
|
cd ~/orchestration-workshop/dockercoins
|
|
```
|
|
|
|
- Use Compose to build and run all containers:
|
|
```bash
|
|
docker-compose up
|
|
```
|
|
|
|
]
|
|
|
|
Compose tells Docker to build all container images (pulling
|
|
the corresponding base images), then starts all containers,
|
|
and displays aggregated logs.
|
|
|
|
---
|
|
|
|
## Lots of logs
|
|
|
|
- The application continuously generates logs
|
|
|
|
- We can see the `worker` service making requests to `rng` and `hasher`
|
|
|
|
- Let's put that in the background
|
|
|
|
.exercise[
|
|
|
|
- Stop the application by hitting `^C`
|
|
|
|
<!--
|
|
```meta
|
|
^C
|
|
```
|
|
-->
|
|
|
|
]
|
|
|
|
- `^C` stops all containers by sending them the `TERM` signal
|
|
|
|
- Some containers exit immediately, others take longer
|
|
<br/>(because they don't handle `SIGTERM` and end up being killed after a 10s timeout)
|
|
|
|
---
|
|
|
|
## Restarting in the background
|
|
|
|
- Many flags and commands of Compose are modeled after those of `docker`
|
|
|
|
.exercise[
|
|
|
|
- Start the app in the background with the `-d` option:
|
|
```bash
|
|
docker-compose up -d
|
|
```
|
|
|
|
- Check that our app is running with the `ps` command:
|
|
```bash
|
|
docker-compose ps
|
|
```
|
|
|
|
]
|
|
|
|
`docker-compose ps` also shows the ports exposed by the application.
|
|
|
|
---
|
|
|
|
## Viewing logs
|
|
|
|
- The `docker-compose logs` command works like `docker logs`
|
|
|
|
.exercise[
|
|
|
|
- View all logs since container creation and exit when done:
|
|
```bash
|
|
docker-compose logs
|
|
```
|
|
|
|
- Stream container logs, starting at the last 10 lines for each container:
|
|
```bash
|
|
docker-compose logs --tail 10 --follow
|
|
```
|
|
|
|
<!--
|
|
```meta
|
|
^C
|
|
```
|
|
-->
|
|
|
|
]
|
|
|
|
Tip: use `^S` and `^Q` to pause/resume log output.
|
|
|
|
???
|
|
|
|
## Upgrading from Compose 1.6
|
|
|
|
.warning[The `logs` command has changed between Compose 1.6 and 1.7!]
|
|
|
|
- Up to 1.6
|
|
|
|
- `docker-compose logs` is the equivalent of `logs --follow`
|
|
|
|
- `docker-compose logs` must be restarted if containers are added
|
|
|
|
- Since 1.7
|
|
|
|
- `--follow` must be specified explicitly
|
|
|
|
- new containers are automatically picked up by `docker-compose logs`
|
|
|
|
---
|
|
|
|
## Connecting to the web UI
|
|
|
|
- The `webui` container exposes a web dashboard; let's view it
|
|
|
|
.exercise[
|
|
|
|
- Open http://[yourVMaddr]:8000/ (from a browser)
|
|
|
|
]
|
|
|
|
- The app actually has a constant, steady speed (3.33 coins/second)
|
|
|
|
- The speed seems not-so-steady because:
|
|
|
|
- the worker doesn't update the counter after every loop, but up to once per second
|
|
|
|
- the speed is computed by the browser, checking the counter about once per second
|
|
|
|
- between two consecutive updates, the counter will increase either by 4, or by 0
|
|
|
|
---
|
|
|
|
## Scaling up the application
|
|
|
|
- Our goal is to make that performance graph go up (without changing a line of code!)
|
|
|
|
- Before trying to scale the application, we'll figure out if we need more resources
|
|
|
|
(CPU, RAM...)
|
|
|
|
- For that, we will use good old UNIX tools on our Docker node
|
|
|
|
---
|
|
|
|
## Looking at resource usage
|
|
|
|
- Let's look at CPU, memory, and I/O usage
|
|
|
|
.exercise[
|
|
|
|
- run `top` to see CPU and memory usage (you should see idle cycles)
|
|
|
|
- run `vmstat 3` to see I/O usage (si/so/bi/bo)
|
|
<br/>(the 4 numbers should be almost zero, except `bo` for logging)
|
|
|
|
]
|
|
|
|
We have available resources.
|
|
|
|
- Why?
|
|
- How can we use them?
|
|
|
|
---
|
|
|
|
## Scaling workers on a single node
|
|
|
|
- Docker Compose supports scaling
|
|
- Let's scale `worker` and see what happens!
|
|
|
|
.exercise[
|
|
|
|
- Start one more `worker` container:
|
|
```bash
|
|
docker-compose scale worker=2
|
|
```
|
|
|
|
- Look at the performance graph (it should show a x2 improvement)
|
|
|
|
- Look at the aggregated logs of our containers (`worker_2` should show up)
|
|
|
|
- Look at the impact on CPU load with e.g. top (it should be negligible)
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Adding more workers
|
|
|
|
- Great, let's add more workers and call it a day, then!
|
|
|
|
.exercise[
|
|
|
|
- Start eight more `worker` containers:
|
|
```bash
|
|
docker-compose scale worker=10
|
|
```
|
|
|
|
- Look at the performance graph: does it show a x10 improvement?
|
|
|
|
- Look at the aggregated logs of our containers
|
|
|
|
- Look at the impact on CPU load and memory usage
|
|
|
|
<!--
|
|
```bash
|
|
sleep 5
|
|
killall docker-compose
|
|
```
|
|
-->
|
|
|
|
]
|
|
|
|
---
|
|
|
|
# Identifying bottlenecks
|
|
|
|
- You should have seen a 3x speed bump (not 10x)
|
|
|
|
- Adding workers didn't result in linear improvement
|
|
|
|
- *Something else* is slowing us down
|
|
|
|
--
|
|
|
|
- ... But what?
|
|
|
|
--
|
|
|
|
- The code doesn't have instrumentation
|
|
|
|
- Let's use state-of-the-art HTTP performance analysis!
|
|
<br/>(i.e. good old tools like `ab`, `httping`...)
|
|
|
|
---
|
|
|
|
## Measuring latency under load
|
|
|
|
We will use `httping`.
|
|
|
|
.exercise[
|
|
|
|
- Check the latency of `rng`:
|
|
```bash
|
|
httping -c 10 localhost:8001
|
|
```
|
|
|
|
- Check the latency of `hasher`:
|
|
```bash
|
|
httping -c 10 localhost:8002
|
|
```
|
|
|
|
]
|
|
|
|
`rng` has a much higher latency than `hasher`.
|
|
|
|
---
|
|
|
|
## Let's draw hasty conclusions
|
|
|
|
- The bottleneck seems to be `rng`
|
|
|
|
- *What if* we don't have enough entropy and can't generate enough random numbers?
|
|
|
|
- We need to scale out the `rng` service on multiple machines!
|
|
|
|
Note: this is a fiction! We have enough entropy. But we need a pretext to scale out.
|
|
<br/>(In fact, the code of `rng` uses `/dev/urandom`, which doesn't need entropy.)
|
|
|
|
---
|
|
|
|
## Clean up
|
|
|
|
- Before moving on, let's remove those containers
|
|
|
|
.exercise[
|
|
|
|
- Tell Compose to remove everything:
|
|
```bash
|
|
docker-compose down
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
class: title
|
|
|
|
# Scaling out
|
|
|
|
---
|
|
|
|
# SwarmKit
|
|
|
|
- [SwarmKit](https://github.com/docker/swarmkit) is an open source
|
|
toolkit to build multi-node systems
|
|
|
|
- It is a reusable library, like libcontainer, libnetwork, vpnkit ...
|
|
|
|
- It is a plumbing part of the Docker ecosystem
|
|
|
|
- SwarmKit comes with two examples:
|
|
|
|
- `swarmctl` (a CLI tool to "speak" the SwarmKit API)
|
|
|
|
- `swarmd` (an agent that can federate existing Docker Engines into a Swarm)
|
|
|
|
- SwarmKit/swarmd/swarmctl → libcontainer/containerd/container-ctr
|
|
|
|
---
|
|
|
|
## SwarmKit features
|
|
|
|
- Highly-available, distributed store based on Raft
|
|
|
|
- *Services* managed with a *declarative API*
|
|
<br/>(implementing *desired state* and *reconciliation loop*)
|
|
|
|
- Automatic TLS keying and signing
|
|
|
|
- Dynamic promotion/demotion of nodes, allowing to change
|
|
how many nodes are actively part of the Raft consensus
|
|
|
|
- Integration with overlay networks and load balancing
|
|
|
|
- And much more!
|
|
|
|
---
|
|
|
|
## SwarmKit concepts (1/2)
|
|
|
|
- A *cluster* will be at least one *node* (preferably more)
|
|
|
|
- A *node* can be a *manager* or a *worker*
|
|
|
|
(Note: in SwarmKit, *managers* are also *workers*)
|
|
|
|
- A *manager* actively takes part in the Raft consensus
|
|
|
|
- You can talk to a *manager* using the SwarmKit API
|
|
|
|
- One *manager* is elected as the *leader*; other managers merely forward requests to it
|
|
|
|
---
|
|
|
|
## SwarmKit concepts (2/2)
|
|
|
|
- The *managers* expose the SwarmKit API
|
|
|
|
- Using the API, you can indicate that you want to run a *service*
|
|
|
|
- A *service* is specified by its *desired state*: which image, how many instances...
|
|
|
|
- The *leader* uses different subsystems to break down services into *tasks*:
|
|
<br/>orchestrator, scheduler, allocator, dispatcher
|
|
|
|
- A *task* corresponds to a specific container, assigned to a specific *node*
|
|
|
|
- *Nodes* know which *tasks* should be running, and will start or stop containers accordingly (through the Docker Engine API)
|
|
|
|
You can refer to the [NOMENCLATURE](https://github.com/docker/swarmkit/blob/master/NOMENCLATURE.md) in the SwarmKit repo for more details.
|
|
|
|
---
|
|
|
|
## Swarm Mode
|
|
|
|
- Docker Engine 1.12 features SwarmKit integration
|
|
|
|
- The Docker CLI features three new commands:
|
|
|
|
- `docker swarm` (enable Swarm mode; join a Swarm; adjust cluster parameters)
|
|
|
|
- `docker node` (view nodes; promote/demote managers; manage nmodes)
|
|
|
|
- `docker service` (create and manage services)
|
|
|
|
- The Docker API exposes the same concepts
|
|
|
|
- The SwarmKit API is also exposed (on a separate socket)
|
|
|
|
---
|
|
|
|
|
|
## Illustration
|
|
|
|
---
|
|
|
|
## You need to enable Swarm mode to use the new stuff
|
|
|
|
- By default, everything runs as usual
|
|
|
|
- Swarm Mode can be enabled, "unlocking" SwarmKit functions
|
|
<br/>(services, out-of-the-box overlay networks, etc.)
|
|
|
|
.exercise[
|
|
|
|
- Try a Swarm-specific command:
|
|
```
|
|
$ docker node ls
|
|
Error response from daemon: This node is not a swarm manager. [...]
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
# Creating our first Swarm
|
|
|
|
- The cluster is initialized with `docker swarm init`
|
|
|
|
- This should be executed on a first, seed node
|
|
|
|
- .warning[DO NOT execute `docker swarm init` on multiple nodes!]
|
|
|
|
You would have multiple disjoint clusters.
|
|
|
|
.exercise[
|
|
|
|
- Create our cluster from node1:
|
|
```bash
|
|
docker swarm init
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Token generation
|
|
|
|
- In the output of `docker swarm init`, we have a message
|
|
confirming that our node is now the (single) manager:
|
|
|
|
```
|
|
Swarm initialized: current node (8jud...) is now a manager.
|
|
```
|
|
|
|
- Docker also generated two security tokens (like passphrases or
|
|
passwords) for our cluster, and shows us the commands to use
|
|
on other nodes to add them to the cluster using those security
|
|
tokens:
|
|
|
|
```
|
|
To add a worker to this swarm, run the following command:
|
|
docker swarm join \
|
|
--token SWMTKN-1-59fl4ak4nqjmao1ofttrc4eprhrola2l87... \
|
|
172.31.4.182:2377
|
|
```
|
|
|
|
---
|
|
|
|
## Checking that Swarm mode is enabled
|
|
|
|
.exercise[
|
|
|
|
- Run the traditional `docker info` command:
|
|
```bash
|
|
docker info
|
|
```
|
|
|
|
]
|
|
|
|
The output should include:
|
|
|
|
```
|
|
Swarm: active
|
|
NodeID: 8jud7o8dax3zxbags3f8yox4b
|
|
Is Manager: true
|
|
ClusterID: 2vcw2oa9rjps3a24m91xhvv0c
|
|
...
|
|
```
|
|
|
|
---
|
|
|
|
## Running our first Swarm mode command
|
|
|
|
- Let's retry the exact same command as earlier
|
|
|
|
.exercise[
|
|
|
|
- List the nodes (well, the only node) of our cluster:
|
|
```bash
|
|
docker node ls
|
|
```
|
|
|
|
]
|
|
|
|
The output should look like the following:
|
|
```
|
|
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
|
|
8jud...ox4b * ip-172-31-4-182 Ready Active Leader
|
|
```
|
|
|
|
---
|
|
|
|
## Adding nodes to the Swarm
|
|
|
|
- A cluster with one node is not a lot of fun
|
|
|
|
- Let's add `node2`!
|
|
|
|
- We need the token that was shown earlier
|
|
|
|
--
|
|
|
|
- You wrote it down, right?
|
|
|
|
--
|
|
|
|
- Don't panic, we can easily see it again 😏
|
|
|
|
---
|
|
|
|
## Adding nodes to the Swarm
|
|
|
|
.exercise[
|
|
|
|
- Show the token again:
|
|
```bash
|
|
docker swarm join-token worker
|
|
```
|
|
|
|
- Log into `node2`:
|
|
```bash
|
|
ssh node2
|
|
```
|
|
|
|
- Copy paste the `docker swarm join ...` command
|
|
<br/>(that was displayed just before)
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Check that the node was added correctly
|
|
|
|
- Stay logged into `node2`!
|
|
|
|
.exercise[
|
|
|
|
- We can still use `docker info` to verify that the node is part of the Swarm:
|
|
```bash
|
|
$ docker info | grep ^Swarm
|
|
```
|
|
|
|
]
|
|
|
|
- However, Swarm commands will not work; try, for instance:
|
|
```
|
|
docker node ls
|
|
```
|
|
|
|
- This is because the node that we added is currently a *worker*
|
|
|
|
- Only *managers* can accept Swarm-specific commands
|
|
|
|
---
|
|
|
|
## View our two-node cluster
|
|
|
|
- Let's go back to `node1` and see what our cluster looks like
|
|
|
|
.exercise[
|
|
|
|
- Logout from `node2` (with `exit` or `Ctrl-D` or ...)
|
|
|
|
- View the cluster from `node1`, which is a manager:
|
|
```bash
|
|
docker node ls
|
|
```
|
|
|
|
]
|
|
|
|
The output should be similar to the following:
|
|
```
|
|
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
|
|
8jud...ox4b * ip-172-31-4-182 Ready Active Leader
|
|
ehb0...4fvx ip-172-31-4-180 Ready Active
|
|
```
|
|
|
|
---
|
|
|
|
## Adding nodes using the Docker API
|
|
|
|
- We don't have to SSH into the other nodes, we can use the Docker API
|
|
|
|
- Our nodes (for this workshop) expose the Docker API over port 55555,
|
|
without authentication (DO NOT DO THIS IN PRODUCTION; FOR EDUCATIONAL USE ONLY)
|
|
|
|
.exercise[
|
|
|
|
- Set `DOCKER_HOST` and add `node3` to the Swarm:
|
|
```bash
|
|
DOCKER_HOST=tcp://node3:55555 docker swarm join \
|
|
--token $(docker swarm join-token -q worker) node1:2377
|
|
```
|
|
|
|
- Check that the node is here:
|
|
```bash
|
|
docker node ls
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Under the hood
|
|
|
|
When we do `docker swarm init`, a TLS root CA is created. Then a keypair is issued for the first node, and signed by the root CA.
|
|
|
|
When further nodes join the Swarm, they are issued their own keypair, signed by the root CA, and they also receive the root CA public key and certificate.
|
|
|
|
All communication is encrypted over TLS.
|
|
|
|
The node keys and certificates are automatically renewed on regular intervals (by default, 90 days; this is tunable with `docker swarm update`).
|
|
|
|
---
|
|
|
|
## Adding more manager nodes
|
|
|
|
- Right now, we have only one manager (node1)
|
|
|
|
- If we lose it, we're SOL
|
|
|
|
- Let's make our cluster highly available
|
|
|
|
.exercise[
|
|
|
|
- Add nodes 4 and 5 to the cluster as *managers* (instead of simple *workers*):
|
|
```bash
|
|
for N in 4 5; do
|
|
DOCKER_HOST=tcp://node$N:55555 docker swarm join \
|
|
--token $(docker swarm join-token -q manager) node1:2377
|
|
done
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## You can control the Swarm from any manager node
|
|
|
|
.exercise[
|
|
|
|
- Try the following command on a few different nodes:
|
|
```bash
|
|
ssh nodeX docker node ls
|
|
```
|
|
|
|
]
|
|
|
|
On manager nodes:
|
|
<br/>you will see the list of nodes, with a `*` denoting
|
|
the node you're talking to.
|
|
|
|
On non-manager nodes:
|
|
<br/>you will get an error message telling you that
|
|
the node is not a manager.
|
|
|
|
As we saw earlier, you can only control the Swarm through a manager node.
|
|
|
|
---
|
|
|
|
## Promoting nodes
|
|
|
|
- Instead of adding a manager node, we can also promote existing workers
|
|
|
|
- Nodes can be promoted (and demoted) at any time
|
|
|
|
.exercise[
|
|
|
|
- See the current list of nodes:
|
|
```
|
|
docker node ls
|
|
```
|
|
|
|
- Promote the two worker nodes to be managers:
|
|
```
|
|
docker node promote XXX YYY
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
# Running our first Swarm service
|
|
|
|
- How do we run services? Simplified version:
|
|
|
|
`docker run` → `docker service create`
|
|
|
|
.exercise[
|
|
|
|
- Create a service featuring an Alpine container pinging Google resolvers:
|
|
```bash
|
|
docker service create alpine ping 8.8.8.8
|
|
```
|
|
|
|
- Check where the container was created:
|
|
```bash
|
|
docker service ps <serviceID>
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Checking container logs
|
|
|
|
- Right now, there is no direct way to check the logs of our container
|
|
<br/>(unless it was scheduled on the current node)
|
|
|
|
- Look up the `NODE` on which the container is running
|
|
(in the output of the `docker service ps` command)
|
|
|
|
.exercise[
|
|
|
|
- Log into the node:
|
|
```bash
|
|
ssh ip-172-31-XXX-XXX
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Viewing the logs of the container
|
|
|
|
- We need to be logged into the node running the container
|
|
|
|
.exercise[
|
|
|
|
- See that the container is running and check its ID:
|
|
```bash
|
|
docker ps
|
|
```
|
|
|
|
- View its logs:
|
|
```bash
|
|
docker logs <containerID>
|
|
```
|
|
|
|
]
|
|
|
|
Go back to `node1` afterwards.
|
|
|
|
---
|
|
|
|
## Scale our service
|
|
|
|
- Services can be scaled in a pinch with the `docker service update`
|
|
command
|
|
|
|
.exercise[
|
|
|
|
- Scale the service to ensure 2 copies per node:
|
|
```bash
|
|
docker service update <serviceID> --replicas 10
|
|
```
|
|
|
|
- Check that we have two containers on the current node:
|
|
```bash
|
|
docker ps
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Expose a service
|
|
|
|
- Services can be exposed, with two special properties:
|
|
|
|
- the public port is available on *every node of the Swarm*,
|
|
|
|
- requests coming on the public port are load balanced across all instances.
|
|
|
|
- This is achieved with option `-p/--publish`; as an approximation:
|
|
|
|
`docker run -p → docker service create -p`
|
|
|
|
- If you indicate a single port number, it will be mapped on a port
|
|
starting at 30000
|
|
<br/>(vs. 32768 for single container mapping)
|
|
|
|
- You can indicate two port numbers to set the public port number
|
|
<br/>(just like with `docker run -p`)
|
|
|
|
---
|
|
|
|
## Expose ElasticSearch on its default port
|
|
|
|
.exercise[
|
|
|
|
- Create an ElasticSearch service (and give it a name while we're at it):
|
|
```bash
|
|
docker service create --name search --publish 9200:9200 --replicas 7 \
|
|
elasticsearch
|
|
```
|
|
|
|
- Check what's going on:
|
|
```bash
|
|
watch docker service ps search
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Tasks lifecycle
|
|
|
|
- If you are fast enough, you will be able to see multiple states:
|
|
|
|
- assigned (the task has been assigned to a specific node)
|
|
- preparing (right now, this mostly means "pulling the image")
|
|
- running
|
|
|
|
- When a task is terminated (stopped, killed...) it cannot be restarted
|
|
|
|
(A replacement task will be created)
|
|
|
|
---
|
|
|
|
## Test our service
|
|
|
|
- We mapped port 9200 on the nodes, to port 9200 in the containers
|
|
|
|
- Let's try to reach that port!
|
|
|
|
.exercise[
|
|
|
|
- Repeat the following command a few times:
|
|
```bash
|
|
curl localhost:9200
|
|
```
|
|
|
|
]
|
|
|
|
Each request should be served by a different ElasticSearch instance.
|
|
|
|
(You will see each instance advertising a different name.)
|
|
|
|
---
|
|
|
|
## Terminate our services
|
|
|
|
- Before moving on, we will remove those services
|
|
|
|
- `docker service rm` can accept multiple services names or IDs
|
|
|
|
- `docker service ls` can accept the `-q` flag
|
|
|
|
- A Shell snippet a day keeps the cruft away
|
|
|
|
.exercise[
|
|
|
|
- Remove all services with this one liner:
|
|
```bash
|
|
docker service ls -q | xargs docker service rm
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
class: title
|
|
|
|
# Our app on Swarm
|
|
|
|
---
|
|
|
|
## What's on the menu?
|
|
|
|
In this part, we will cover:
|
|
|
|
- building images for our app,
|
|
|
|
- shipping those images with a registry,
|
|
|
|
- running them through the services concept,
|
|
|
|
- enabling inter-container communication with overlay networks.
|
|
|
|
---
|
|
|
|
## Why do we need to ship our images?
|
|
|
|
- When we do `docker-compose up`, images are built for our services
|
|
|
|
- Those images are present only on the local node
|
|
|
|
- We need those images to be distributed on the whole Swarm
|
|
|
|
- The easiest way to achieve that is to use a Docker registry
|
|
|
|
- Once our images are on a registry, we can reference them when
|
|
creating our services
|
|
|
|
---
|
|
|
|
## Build, ship, and run, for a single service
|
|
|
|
If we had only one service (built from a `Dockerfile` in the
|
|
current directory), our workflow could look like this:
|
|
|
|
```
|
|
docker build -t jpetazzo/doublerainbow:v0.1 .
|
|
docker push jpetazzo/doublerainbow:v0.1
|
|
docker service create jpetazzo/doublerainbow:v0.1
|
|
```
|
|
|
|
We just have to adapt this to our application, which has 4 services!
|
|
|
|
---
|
|
|
|
## The plan
|
|
|
|
- Build on our local node (`node1`)
|
|
|
|
- Tag images with a version number
|
|
|
|
(timestamp; git hash; semantic...)
|
|
|
|
- Upload them to a registry
|
|
|
|
- Update the Compose file to use those images
|
|
|
|
---
|
|
|
|
## Which registry do we want to use?
|
|
|
|
.small[
|
|
|
|
- **Docker Hub**
|
|
|
|
- hosted by Docker Inc.
|
|
- requires an account (free, no credit card needed)
|
|
- images will be public (unless you pay)
|
|
- located in AWS EC2 us-east-1
|
|
|
|
- **Docker Trusted Registry**
|
|
|
|
- self-hosted commercial product
|
|
- requires a subscription (free 30-day trial available)
|
|
- images can be public or private
|
|
- located wherever you want
|
|
|
|
- **Docker open source registry**
|
|
|
|
- self-hosted barebones repository hosting
|
|
- doesn't require anything
|
|
- doesn't come with anything either
|
|
- located wherever you want
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Using Docker Hub
|
|
|
|
- Set the `DOCKER_REGISTRY` environment variable to your Docker Hub user name
|
|
<br/>(the `build-tag-push.py` script prefixes each image name with that variable)
|
|
|
|
- We will also see how to run the open source registry
|
|
<br/>(so use whatever option you want!)
|
|
|
|
.exercise[
|
|
|
|
<!--
|
|
```meta
|
|
^{
|
|
```
|
|
-->
|
|
|
|
- Set the following environment variable:
|
|
<br/>`export DOCKER_REGISTRY=jpetazzo`
|
|
|
|
- (Use *your* Docker Hub login, of course!)
|
|
|
|
- Log into the Docker Hub:
|
|
<br/>`docker login`
|
|
|
|
<!--
|
|
```meta
|
|
^}
|
|
```
|
|
-->
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Using Docker Trusted Registry
|
|
|
|
If we wanted to use DTR, we would:
|
|
|
|
- make sure we have a Docker Hub account
|
|
- [activate a Docker Datacenter subscription](
|
|
https://hub.docker.com/enterprise/trial/)
|
|
- install DTR on our machines
|
|
- set `DOCKER_REGISTRY` to `dtraddress:port/user`
|
|
|
|
*This is out of the scope of this workshop!*
|
|
|
|
---
|
|
|
|
## Using open source registry
|
|
|
|
- We need to run a `registry:2` container
|
|
<br/>(make sure you specify tag `:2` to run the new version!)
|
|
|
|
- It will store images and layers to the local filesystem
|
|
<br/>(but you can add a config file to use S3, Swift, etc.)
|
|
|
|
- Docker *requires* TLS when communicating with the registry
|
|
|
|
- unless for registries on `localhost`
|
|
|
|
- or with the Engine flag `--insecure-registry`
|
|
|
|
- Our strategy: publish the registry container on port 5000,
|
|
<br/>and connect to it through `localhost:5000` on each node
|
|
|
|
---
|
|
|
|
# Deploying a local registry
|
|
|
|
- We will create a single-instance service, publishing its port
|
|
on the whole cluster
|
|
|
|
.exercise[
|
|
|
|
- Create the registry service:
|
|
```bash
|
|
docker service create --name registry --publish 5000:5000 registry:2
|
|
```
|
|
|
|
- Try the following command, until it returns `{"repositories":[]}`:
|
|
```bash
|
|
curl localhost:5000/v2/_catalog
|
|
```
|
|
|
|
]
|
|
|
|
(Retry a few times, it might take 10-20 seconds for the container to be started. Patience.)
|
|
|
|
---
|
|
|
|
## Testing our local registry
|
|
|
|
- We can retag a small image, and push it to the registry
|
|
|
|
.exercise[
|
|
|
|
- Make sure we have the busybox image, and retag it:
|
|
```bash
|
|
docker pull busybox
|
|
docker tag busybox localhost:5000/busybox
|
|
```
|
|
|
|
- Push it:
|
|
```bash
|
|
docker push localhost:5000/busybox
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Checking what's on our local registry
|
|
|
|
- The registry API has endpoints to query what's there
|
|
|
|
.exercise[
|
|
|
|
- Ensure that our busybox image is now in the local registry:
|
|
```bash
|
|
curl http://localhost:5000/v2/_catalog
|
|
```
|
|
|
|
]
|
|
|
|
The curl command should now output:
|
|
```json
|
|
{"repositories":["busybox"]}
|
|
```
|
|
|
|
---
|
|
|
|
## Build, tag, and push our application container images
|
|
|
|
- Scriptery to the rescue!
|
|
|
|
.exercise[
|
|
|
|
- Set `DOCKER_REGISTRY` and `TAG` environment variables to use our local registry
|
|
|
|
- And run this little for loop:
|
|
```bash
|
|
DOCKER_REGISTRY=localhost:5000
|
|
TAG=v0.1
|
|
for SERVICE in hasher rng webui worker; do
|
|
docker-compose build $SERVICE
|
|
docker tag dockercoins_$SERVICE $DOCKER_REGISTRY/dockercoins_$SERVICE:$TAG
|
|
docker push $DOCKER_REGISTRY/dockercoins_$SERVICE
|
|
done
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
# Overlay networks
|
|
|
|
- SwarmKit integrates with overlay networks, without requiring
|
|
an extra key/value store
|
|
|
|
- Overlay networks are created the same way as before
|
|
|
|
.exercise[
|
|
|
|
- Create an overlay network for our application:
|
|
```bash
|
|
docker network create --driver overlay dockercoins
|
|
```
|
|
|
|
- Check existing networks:
|
|
```bash
|
|
docker network ls
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Can you spot the difference?
|
|
|
|
The `dockercoins` network is different from the other ones.
|
|
|
|
Can you see how?
|
|
|
|
--
|
|
|
|
It is using a different kind of ID, reflecting that it's a SwarmKit object
|
|
instead of a "classic" Docker Engine object.
|
|
|
|
---
|
|
|
|
## Caveats
|
|
|
|
.warning[As I type those lines (i.e. before boarding the plane to Seattle),
|
|
it is not possible yet to join an overlay network with `docker run --net ...`;
|
|
this might or might not be enabled in the future. We will see how to cope
|
|
with this limitation.]
|
|
|
|
---
|
|
|
|
## Run the application
|
|
|
|
- First, start the redis service; that one is using a Docker Hub image
|
|
|
|
.exercise[
|
|
|
|
- Create the redis service:
|
|
```bash
|
|
docker service create --network dockercoins --name redis redis
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Run the other services
|
|
|
|
- Then, start the other services one by one
|
|
|
|
- We will use the images pushed previously
|
|
|
|
.exercise[
|
|
|
|
- Start the other services:
|
|
```bash
|
|
DOCKER_REGISTRY=localhost:5000
|
|
TAG=v0.1
|
|
for SERVICE in hasher rng webui worker; do
|
|
docker service create --network dockercoins --name $SERVICE \
|
|
$DOCKER_REGISTRY/dockercoins_$SERVICE:$TAG
|
|
done
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Wait for our application to be up
|
|
|
|
- We will see later a way to watch progress for all the tasks of the cluster
|
|
|
|
- But for now, a scrappy Shell loop will do the trick
|
|
|
|
.exercise[
|
|
|
|
- Repeatedly display the status of all our services:
|
|
```bash
|
|
watch "docker service ls -q | xargs -n1 docker service tasks"
|
|
```
|
|
|
|
- Stop it once everything is running
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Expose our application web UI
|
|
|
|
- We need to connect to the `webui` service, but it is not publishing any port
|
|
|
|
- Let's reconfigure it to publish a port
|
|
|
|
- **Unfortunately,** dynamic port update doesn't work yet
|
|
|
|
(So we will `rm` and re-`create` the service instead)
|
|
|
|
.exercise[
|
|
|
|
- Destroy the existing `webui` service and recreate it with the published port:
|
|
```bash
|
|
docker service rm webui
|
|
docker service create --network dockercoins --name webui \
|
|
--publish 8000:80 $DOCKER_REGISTRY/dockercoins_webui:$TAG
|
|
```
|
|
|
|
]
|
|
|
|
???
|
|
|
|
.exercise[
|
|
|
|
- Update `webui` so that we can connect to it from outside:
|
|
```bash
|
|
docker service update webui --publish 8000:80
|
|
```
|
|
|
|
- Check as it's updated:
|
|
```bash
|
|
watch docker service tasks webui
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Connect to the web UI
|
|
|
|
- The web UI is now available on port 8000, *on all the nodes of the cluster*
|
|
|
|
.exercise[
|
|
|
|
- Point your browser to any node, on port 8000
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Scaling the application
|
|
|
|
- We can change scaling parameters with `docker update` as well
|
|
|
|
- We will do the equivalent of `docker-compose scale`
|
|
|
|
.exercise[
|
|
|
|
- Bring up more workers:
|
|
```bash
|
|
docker service update worker --replicas 10
|
|
```
|
|
|
|
- Check the result in the web UI
|
|
|
|
]
|
|
|
|
You should see the performance peaking at 10 hashes/s (like before).
|
|
|
|
---
|
|
|
|
## Scaling the `rng` service
|
|
|
|
- We want to utilize as best as we can the entropy generators
|
|
on our nodes
|
|
|
|
- We want to run exactly one `rng` instance per node
|
|
|
|
- SwarmKit has a special scheduling mode for that, let's use it
|
|
|
|
.exercise[
|
|
|
|
- Enable *global scheduling* for the `rng` service:
|
|
```bash
|
|
docker service update rng --mode global
|
|
```
|
|
|
|
- Look at the result in the web UI
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Checkpoint
|
|
|
|
- We've seen how to setup a Swarm
|
|
|
|
- We've used it to host our own registry
|
|
|
|
- We've built our app container images
|
|
|
|
- We've used the registry to host those images
|
|
|
|
- We've deployed and scaled our application
|
|
|
|
Let's treat ourselves with a nice pat in the back!
|
|
|
|
--
|
|
|
|
And carry on, we have much more to see and learn!
|
|
|
|
---
|
|
|
|
class: title
|
|
|
|
# Operating the Swarm
|
|
|
|
---
|
|
|
|
## Finding the real cause of the bottleneck
|
|
|
|
- We want to debug our app as we scale `worker` up and down
|
|
|
|
- We want to run tools like `ab` or `httping` on the internal network
|
|
|
|
- .warning[This will be very hackish]
|
|
|
|
(Better techniques and tools might become available in the future!)
|
|
|
|
---
|
|
|
|
# Breaking into an overlay network
|
|
|
|
- We will create a dummy placeholder service on our network
|
|
|
|
- Then we will use `docker exec` to run more processes in this container
|
|
|
|
.exercise[
|
|
|
|
- Start a "do nothing" container using our favorite Swiss-Army distro:
|
|
```bash
|
|
docker service create --network dockercoins --name debug --mode global \
|
|
alpine sleep 1000000000
|
|
```
|
|
|
|
]
|
|
|
|
Why am I using global scheduling here? Because I'm lazy!
|
|
<br/>
|
|
With global scheduling, I'm *guaranteed* to have an instance on the local node.
|
|
<br/>
|
|
I don't need to SSH to another node.
|
|
|
|
---
|
|
|
|
## Entering the debug container
|
|
|
|
- Once our container is started (which should be really fast because the alpine image is small), we can enter it (from any node)
|
|
|
|
.exercise[
|
|
|
|
- Locate the container:
|
|
```bash
|
|
docker ps
|
|
```
|
|
|
|
- Enter it:
|
|
```bash
|
|
docker exec -ti <containerID> sh
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Labels
|
|
|
|
- We can also be fancy and find the ID of the container automatically
|
|
|
|
- SwarmKit places labels on containers
|
|
|
|
.exercise[
|
|
|
|
- Get the ID of the container:
|
|
```bash
|
|
CID=$(docker ps -q --filter label=com.docker.swarm.service.name=debug)
|
|
```
|
|
|
|
- And enter the container:
|
|
```bash
|
|
docker exec -ti $CID sh
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Installing our debugging tools
|
|
|
|
- Ideally, you would author your own image, with all your favorite tools, and use it instead of the base `alpine` image
|
|
|
|
- But we can also dynamically install whatever we need
|
|
|
|
.exercise[
|
|
|
|
- Install a few tools:
|
|
```bash
|
|
apk add --update curl apache2-utils drill
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Investigating the `rng` service
|
|
|
|
- First, let's check what `rng` resolves to
|
|
|
|
.exercise[
|
|
|
|
- Use drill or nslookup to resolve `rng`:
|
|
```bash
|
|
drill rng
|
|
```
|
|
|
|
]
|
|
|
|
This give us one IP address. It is not the IP address of a container.
|
|
It is a virtual IP address (VIP) for the `rng` service.
|
|
|
|
---
|
|
|
|
## Investigating the VIP
|
|
|
|
.exercise[
|
|
|
|
- Try to ping the VIP:
|
|
```bash
|
|
ping rng
|
|
```
|
|
|
|
]
|
|
|
|
It doesn't respond to ping at this point. (This might change in the future.)
|
|
|
|
---
|
|
|
|
## What if I don't like VIPs?
|
|
|
|
- Services can be published using two modes: VIP and DNSRR.
|
|
|
|
- With VIP, you get a virtual IP for the service, and an load balancer
|
|
based on IPVS
|
|
|
|
(By the way, IPVS is totally awesome and if you want to learn more about it in the context of containers,
|
|
I highly recommend [this talk](https://www.youtube.com/watch?v=oFsJVV1btDU&index=5&list=PLkA60AVN3hh87OoVra6MHf2L4UR9xwJkv) by [@kobolog](https://twitter.com/kobolog) at DC15EU!)
|
|
|
|
- With DNSRR, you get the former behavior (from Engine 1.11), where
|
|
resolving the service yields the IP addresses of all the containers for
|
|
this service
|
|
|
|
- You change this with `docker service create --endpoint-mode [VIP|DNSRR]`
|
|
|
|
---
|
|
|
|
## Testing and benchmarking our service
|
|
|
|
- We will check that the service is up with `rng`, then
|
|
benchmark it with `ab`
|
|
|
|
.exercise[
|
|
|
|
- Make a test request to the service:
|
|
```bash
|
|
curl rng
|
|
```
|
|
|
|
- Open another window, and stop the workers, to test in isolation:
|
|
```bash
|
|
docker service update worker --replicas 0
|
|
```
|
|
|
|
]
|
|
|
|
Wait until the workers are stopped (check with `docker service ls`)
|
|
before continuing.
|
|
|
|
---
|
|
|
|
## Benchmarking `rng`
|
|
|
|
We will send 50 requests, but with various levels of concurrency.
|
|
|
|
.exercise[
|
|
|
|
- Send 50 requests, with a single sequential client:
|
|
```bash
|
|
ab -c 1 -n 50 http://rng/10
|
|
```
|
|
|
|
- Send 50 requests, with fifty parallel clients:
|
|
```bash
|
|
ab -c 50 -n 50 http://rng/10
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Benchmark results for `rng`
|
|
|
|
- When serving requests sequentially, they each take 100ms
|
|
|
|
- In the parallel scenario, the latency increased dramatically:
|
|
|
|
- What about `hasher`?
|
|
|
|
---
|
|
|
|
## Benchmarking `hasher`
|
|
|
|
We will do the same tests for `hasher`.
|
|
|
|
The command is slightly more complex, since we need to post random data.
|
|
|
|
First, we need to put the POST payload in a temporary file.
|
|
|
|
.exercise[
|
|
|
|
- Install curl in the container, and generate 10 bytes of random data:
|
|
```bash
|
|
curl http://rng/10 >/tmp/random
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Benchmarking `hasher`
|
|
|
|
Once again, we will send 50 requests, with different levels of concurrency.
|
|
|
|
.exercise[
|
|
|
|
- Send 50 requests with a sequential client:
|
|
```bash
|
|
ab -c 1 -n 50 -T application/octet-stream -p /tmp/random http://hasher/
|
|
```
|
|
|
|
- Send 50 requests with 50 parallel clients:
|
|
```bash
|
|
ab -c 50 -n 50 -T application/octet-stream -p /tmp/random http://hasher/
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Benchmark results for `hasher`
|
|
|
|
- The sequential benchmarks takes ~5 seconds to complete
|
|
|
|
- The parallel benchmark takes less than 1 second to complete
|
|
|
|
- In both cases, each request takes a bit more than 100ms to complete
|
|
|
|
- Requests are a bit slower in the parallel benchmark
|
|
|
|
- It looks like `hasher` is better equiped to deal with concurrency than `rng`
|
|
|
|
---
|
|
|
|
class: title
|
|
|
|
Why?
|
|
|
|
---
|
|
|
|
## Why does everything take (at least) 100ms?
|
|
|
|
--
|
|
|
|
`rng` code:
|
|
|
|

|
|
|
|
--
|
|
|
|
`hasher` code:
|
|
|
|

|
|
|
|
---
|
|
|
|
class: title
|
|
|
|
But ...
|
|
|
|
WHY?!?
|
|
|
|
---
|
|
|
|
## Why did we sprinkle this sample app with sleeps?
|
|
|
|
- Deterministic performance
|
|
<br/>(regardless of instance speed, CPUs, I/O...)
|
|
|
|
--
|
|
|
|
- Actual code sleeps all the time anyway
|
|
|
|
--
|
|
|
|
- When your code makes a remote API call:
|
|
|
|
- it sends a request;
|
|
|
|
- it sleeps until it gets the response;
|
|
|
|
- it processes the response.
|
|
|
|
---
|
|
|
|
## Why do `rng` and `hasher` behave differently?
|
|
|
|

|
|
|
|
--
|
|
|
|
(Synchronous vs. asynchronous event processing)
|
|
|
|
---
|
|
|
|
# Rolling updates
|
|
|
|
- We want to release a new version of the worker
|
|
|
|
- We will edit the code ...
|
|
|
|
- ... build the new image ...
|
|
|
|
- ... push it to the registry ...
|
|
|
|
- ... update our service to use the new image
|
|
|
|
---
|
|
|
|
## But first...
|
|
|
|
- Restart the workers
|
|
|
|
.exercise[
|
|
|
|
- Just scale back to 10 replicas:
|
|
```bash
|
|
docker service update worker --replicas 10
|
|
```
|
|
|
|
- Check that they're running:
|
|
```bash
|
|
docker service tasks worker
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Making changes
|
|
|
|
.exercise[
|
|
|
|
- Edit `~/orchestration-workshop/dockercoins/worker/worker.py`
|
|
|
|
- Locate the line that has a `sleep` instruction
|
|
|
|
- Reduce the `sleep` from `0.1` to `0.01`
|
|
|
|
- Save your changes and exit
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Building and pushing the new image
|
|
|
|
.exercise[
|
|
|
|
- Build the new image:
|
|
```bash
|
|
IMAGE=localhost:5000/dockercoins_worker:v0.01
|
|
docker build -t $IMAGE worker
|
|
```
|
|
|
|
- Push it to the registry:
|
|
```bash
|
|
docker push $IMAGE
|
|
```
|
|
|
|
]
|
|
|
|
Note how the build and push were fast (because caching).
|
|
|
|
---
|
|
|
|
## Watching the deployment process
|
|
|
|
- We will need to open a new window for this
|
|
|
|
.exercise[
|
|
|
|
- Look at our service status:
|
|
```bash
|
|
watch -n1 "docker service tasks worker -a | grep -v Shutdown.*Shutdown"
|
|
```
|
|
|
|
]
|
|
|
|
- `-a` gives us all tasks, including the one whose current or desired state is `Shutdown`
|
|
|
|
- Then we filter out the tasks whose current **and** desired state is `Shutdown`
|
|
|
|
- Future versions will have fancy filters to make that less tinkerish
|
|
|
|
---
|
|
|
|
## Updating to our new image
|
|
|
|
- Keep the `watch ...` command running!
|
|
|
|
.exercise[
|
|
|
|
- In the other window, update the service to the new image:
|
|
```bash
|
|
docker service update worker --image $IMAGE
|
|
```
|
|
|
|
]
|
|
|
|
SwarmKit updates all instances at the same time.
|
|
|
|
If only we could do a rolling upgrade!
|
|
|
|
---
|
|
|
|
## Changing the upgrade policy
|
|
|
|
- We can set upgrade parallelism (how many instances to update at the same time)
|
|
|
|
- And upgrade delay (how long to wait between two batches of instances)
|
|
|
|
.exercise[
|
|
|
|
- Change the parallelism to 2 and the delay to 5 seconds:
|
|
```bash
|
|
docker service update worker --update-parallelism 2 --update-delay 5s
|
|
```
|
|
|
|
- Rollback to the previous image:
|
|
```bash
|
|
docker service update worker --image $DOCKER_REGISTRY/dockercoins_worker:v0.1
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Getting cluster-wide task information
|
|
|
|
- The Docker API doesn't expose this directly (yet)
|
|
|
|
- But the SwarmKit API does
|
|
|
|
- Let's see how to use it
|
|
|
|
- We will use `swarmctl`
|
|
|
|
- `swarmctl` is an example program showing how to
|
|
interact with the SwarmKit API
|
|
|
|
- First, we need to install `swarmctl`
|
|
|
|
---
|
|
|
|
## Building `swarmctl`
|
|
|
|
- I thought I would enjoy a 1-minute break at this point
|
|
|
|
- So we are going to compile SwarmKit (including `swarmctl`)
|
|
|
|
.exercise[
|
|
- Download, compile, install SwarmKit with this one-liner:
|
|
```bash
|
|
docker run -v /usr/local/bin:/go/bin golang \
|
|
go get `-v` github.com/docker/swarmkit/...
|
|
```
|
|
|
|
]
|
|
|
|
(Remove `-v` if you don't like verbose things!)
|
|
|
|
---
|
|
|
|
## Using `swarmctl`
|
|
|
|
- The Docker Engine places the SwarmKit control socket in a special path
|
|
|
|
- And you need root privileges to access it
|
|
|
|
.exercise[
|
|
|
|
- Set an alias so that swarmctl can run as root and use the right control socket:
|
|
```bash
|
|
alias \
|
|
swarmctl='sudo swarmctl --socket /var/lib/docker/swarm/control.sock'
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## `swarmctl` in action
|
|
|
|
- Let's review a few useful `swarmctl` commands
|
|
|
|
.exercise[
|
|
|
|
- List cluster nodes (that's equivalent to `docker node ls`):
|
|
```bash
|
|
swarmctl node ls
|
|
```
|
|
|
|
- View all tasks across all services:
|
|
```bash
|
|
swarmctl task ls
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Caveat
|
|
|
|
- SwarmKit is vendored into the Docker Engine
|
|
|
|
- If you want to use `swarmctl`, you need the exact version of
|
|
SwarmKit that was used in your Docker Engine
|
|
|
|
- Otherwise, you might get some errors like:
|
|
|
|
```
|
|
Error: grpc: failed to unmarshal the received message proto: wrong wireType = 0
|
|
```
|
|
|
|
---
|
|
|
|
# Centralized logging
|
|
|
|
- We want to send all our container logs to a central place
|
|
|
|
- If that place could offer a nice web dashboard too, that'd be nice
|
|
|
|
--
|
|
|
|
- We are going to deploy an ELK stack
|
|
|
|
- It will accept logs over a syslog socket
|
|
|
|
- We will deploy a logspout container on every node
|
|
|
|
- Logspout will detect containers as they are started, and funnel their logs to logstash
|
|
|
|
---
|
|
|
|
# Setting up ELK to store container logs
|
|
|
|
*Important foreword: this is not an "official" or "recommended"
|
|
setup; it is just an example. We do not endorse ELK, logspout,
|
|
or the other elements of the stack more than others!*
|
|
|
|
What we will do:
|
|
|
|
- Spin up an ELK stack with services
|
|
|
|
- Gaze at the spiffy Kibana web UI
|
|
|
|
- Manually send a few log entries over syslog
|
|
|
|
- Add logspout to send all container output to ELK
|
|
|
|
---
|
|
|
|
## What's in an ELK stack?
|
|
|
|
- ELK is three components:
|
|
|
|
- ElasticSearch (to store and index log entries)
|
|
|
|
- Logstash (to receive log entries from various
|
|
sources, process them, and forward them to various
|
|
destinations)
|
|
|
|
- Kibana (to view/search log entries with a nice UI)
|
|
|
|
- The only component that we will configure is Logstash
|
|
|
|
- We will accept log entries using the syslog protocol
|
|
|
|
- Log entries will be stored in ElasticSearch,
|
|
<br/>and displayed on Logstash's stdout for debugging
|
|
|
|
---
|
|
|
|
## Setting up ELK
|
|
|
|
- We need three containers: ElasticSearch, Logstash, Kibana
|
|
|
|
- We will place them on a common network, `logging`
|
|
|
|
.exercise[
|
|
|
|
- Create the network:
|
|
```bash
|
|
docker network create --driver overlay logging
|
|
```
|
|
|
|
- Create the ElasticSearch service:
|
|
```bash
|
|
docker service create --network logging --name elasticsearch elasticsearch
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Setting up Kibana
|
|
|
|
- Kibana exposes the web UI
|
|
|
|
- Its default port (5601) needs to be published
|
|
|
|
- It needs a tiny bit of configuration: the address of the ElasticSearch service
|
|
|
|
- We don't want Kibana logs to show up in Kibana (it would create clutter)
|
|
<br/>so we tell Logspout to ignore them
|
|
|
|
.exercise[
|
|
|
|
- Create the Kibana service:
|
|
```bash
|
|
docker service create --network logging --name kibana --publish 5601:5601 \
|
|
-e LOGSPOUT=ignore -e ELASTICSEARCH_URL=http://elasticsearch:9200 kibana
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Setting up Logstash
|
|
|
|
- Logstash needs some configuration to listen to syslog messages and send them to elasticsearch
|
|
|
|
- We could author a custom image bundling this configuration
|
|
|
|
- We can also pass the configuration on the command line
|
|
|
|
.exercise[
|
|
|
|
- Create the Logstash service:
|
|
```bash
|
|
docker service create --network logging --name logstash \
|
|
-e LOGSPOUT=ignore logstash \
|
|
-e "$(cat ~/orchestration-workshop/elk/logstash.conf)"
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Checking Logstash
|
|
|
|
- Before proceeding, let's make sure that Logstash started properly
|
|
|
|
.exercise[
|
|
|
|
- Lookup the node running the Logstash container:
|
|
```bash
|
|
docker service tasks logstash
|
|
```
|
|
|
|
- Log into that node:
|
|
```bash
|
|
ssh ip-172-31-XXX-XXX
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## View Logstash logs
|
|
|
|
.exercise[
|
|
|
|
- Get the ID of the Logstash container:
|
|
```bash
|
|
CID=$(docker ps -q --filter label=com.docker.swarm.service.name=logstash)
|
|
```
|
|
|
|
- View the logs:
|
|
```bash
|
|
docker logs --follow $CID
|
|
```
|
|
|
|
]
|
|
|
|
You should see the heartbeat messages:
|
|
.small[
|
|
```json
|
|
{ "message" => "ok",
|
|
"host" => "1a4cfb063d13",
|
|
"@version" => "1",
|
|
"@timestamp" => "2016-06-19T00:45:45.273Z"
|
|
}
|
|
```
|
|
]
|
|
|
|
---
|
|
|
|
## Testing the syslog receiver
|
|
|
|
- In a new window, we will generate a syslog message
|
|
|
|
- We will use the `logger` standard utility
|
|
|
|
- We will run it in a service connected to the `logging` network
|
|
|
|
- We don't want it to be restarted forever, so we will do that in a one-shot container
|
|
|
|
.exercise[
|
|
|
|
- Send a test message:
|
|
```bash
|
|
docker service create --network logging --restart-condition none debian \
|
|
logger -n logstash -P 51415 hello world
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Connect to Kibana
|
|
|
|
- The Kibana web UI is exposed on cluster port 5601
|
|
|
|
- Open the UI in your browser: http://instance-address:5601/
|
|
|
|
(Remember: you can use any instance address!)
|
|
|
|
---
|
|
|
|
## "Configuring" Kibana
|
|
|
|
- If you see a status page with a yellow item, wait a minute and reload
|
|
(Kibana is probably still initializing)
|
|
|
|
- Kibana should offer you to "Configure an index pattern":
|
|
<br/>in the "Time-field name" drop down, select "@timestamp", and hit the
|
|
"Create" button
|
|
|
|
- Then:
|
|
|
|
- click "Discover" (in the top-left corner)
|
|
- click "Last 15 minutes" (in the top-right corner)
|
|
- click "Last 1 hour" (in the list in the middle)
|
|
- click "Auto-refresh" (top-right corner)
|
|
- click "5 seconds" (top-left of the list)
|
|
|
|
- You should see a series of green bars (with one new green bar every minute)
|
|
|
|
---
|
|
|
|
## Setting up Logspout
|
|
|
|
- Logspout connects to the Docker control socket
|
|
|
|
- Using the Docker events API, it automatically detects new containers
|
|
|
|
- Using the Docker logging API, it streams logs of all containers to its outputs
|
|
|
|
- We will run a logspout container on each node (using global scheduling), and bind-mount the Docker control socket into the logspout container
|
|
|
|
.exercise[
|
|
|
|
- Create the logspout service:
|
|
```bash
|
|
docker service create --network logging --name logspout --mode global \
|
|
--mount source=/var/run/docker.sock,type=bind,target=/var/run/docker.sock \
|
|
-e SYSLOG_FORMAT=rfc3164 gliderlabs/logspout syslog://logstash:51415
|
|
```
|
|
|
|
]
|
|
|
|
---
|
|
|
|
## Viewing container logs
|
|
|
|
- Go back to Kibana
|
|
|
|
- Container logs should be showing up!
|
|
|
|
- We can customize the web UI to be more readable
|
|
|
|
.exercise[
|
|
|
|
- In the left column, move the mouse over the following
|
|
columns, and click the "Add" button that appears:
|
|
|
|
<!--
|
|
- host
|
|
- container_name
|
|
- message
|
|
-->
|
|
|
|
- logsource
|
|
- program
|
|
- message
|
|
|
|
]
|
|
|
|
---
|
|
|
|
|
|
## Controlling Docker from a container
|
|
|
|
- In a local environment, just bind-mount the Docker control socket:
|
|
```bash
|
|
docker run -ti -v /var/run/docker.sock:/var/run/docker.sock docker
|
|
```
|
|
|
|
- Otherwise, you have to:
|
|
|
|
- set `DOCKER_HOST`,
|
|
- set `DOCKER_TLS_VERIFY` and `DOCKER_CERT_PATH` (if you use TLS),
|
|
- copy certificates to the container that will need API access.
|
|
|
|
More resources on this topic:
|
|
|
|
- [Do not use Docker-in-Docker for CI](
|
|
http://jpetazzo.github.io/2015/09/03/do-not-use-docker-in-docker-for-ci/)
|
|
- [One container to rule them all](
|
|
http://jpetazzo.github.io/2016/04/03/one-container-to-rule-them-all/)
|
|
|
|
---
|
|
|
|
## Bind-mounting the Docker control socket
|
|
|
|
- In Swarm mode, bind-mounting the control socket gives you access to the whole cluster
|
|
|
|
|
|
---
|
|
|
|
# Last words
|
|
|
|
- You can leave Swarm mode with `docker swarm leave`
|
|
|
|
- `docker inspect` is being extended to support services, tasks...
|
|
|
|
- `docker node update XXX --availability <active|pause|drain>`
|
|
|
|
- Healthchecks
|
|
|
|
- Bundles
|
|
|
|
---
|
|
|
|
class: title
|
|
|
|
# Thanks! <br/> Questions?
|
|
|
|
## [@jpetazzo](https://twitter.com/jpetazzo) <br/> [@docker](https://twitter.com/docker)
|
|
|
|
</textarea>
|
|
<script src="https://gnab.github.io/remark/downloads/remark-0.13.min.js" type="text/javascript">
|
|
</script>
|
|
<script type="text/javascript">
|
|
var slideshow = remark.create({
|
|
ratio: '16:9',
|
|
highlightSpans: true
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|