【Linux】Docker / Kubernetes 入門

Docker/Kubernetes に入門したので備忘録を残しておきます。

環境は macOS 10.13.6, Docker for Mac 18.06.1-ce (Compose: 1.22.0, Kubernetes: 1.10.3)です。

Docker for Mac は内部で FreeBSD の bhyve hypervisor を macOS に移植した xhyve hypervisor を使っています。bhyve は Linux の KVM + QEMU のような機能を提供します。

Docker

Docker はコンテナの実行環境と関連ツールを提供する。

Docker concepts ではコンテナ化の利点として以下が挙げられている。

  • Flexible: Even the most complex applications can be containerized.
  • Lightweight: Containers leverage and share the host kernel.
  • Interchangeable: You can deploy updates and upgrades on-the-fly.
  • Portable: You can build locally, deploy to the cloud, and run anywhere.
  • Scalable: You can increase and automatically distribute container replicas.
  • Stackable: You can stack services vertically and on-the-fly.

アプリと実行環境のコード化 (Infrastructure as Code) が可能で冪等性の確保が容易となる。

docker コマンドの使い方は docker –help または docker docs を参照する。

今回は Go の docker image (golang) を入手し, HTTP Server (Go) を立てることから始める。

docker search [OPTIONS] TERM で docker image を探し, docker pull [OPTIONS] NAME[:TAG|@DIGEST] で取得する。

$ docker search golang --limit 5
NAME                           DESCRIPTION                                     STARS               OFFICIAL            AUTOMATED
golang                         Go (golang) is a general purpose, higher-lev…   2254                [OK]
resin/artik710-golang          Go (golang) is a general purpose, imperative…   2
resin/raspberrypi3-golang      Go (golang) is a general purpose, imperative…   1
jetbrainsinfra/golang          Golang + custom build tools                     0                                       [OK]
resin/artik710-fedora-golang   Go (golang) is a general purpose, imperative…   0

$ docker pull golang
Using default tag: latest
latest: Pulling from library/golang
bc9ab73e5b14: Pull complete
193a6306c92a: Pull complete
e5c3f8c317dc: Pull complete
a587a86c9dcb: Pull complete
1bc310ac474b: Pull complete
87ab348d90cc: Pull complete
786bc4873ebc: Pull complete
Digest: sha256:c9b667d937426bc9da395a6349a25d0b758f1c5fe5747df171cb0684246d1611
Status: Downloaded newer image for golang:latest

$ docker images
REPOSITORY                                 TAG                 IMAGE ID            CREATED             SIZE
golang                                     latest              d817ad5b9beb        9 hours ago         774MB

コンテナに入り vim をインストールする。docker run [OPTIONS] IMAGE [COMMAND] [ARG…] の主要オプションは以下。

  • -i, –interactive: interactive mode で接続
  • -t, –tty: pseudo-TTY を割り当てる
  • -d, –detach: バックグラウンドで起動し container ID を出力
  • -p, –publish: コンテナからホストに expose するポートの設定 (host-port:container-port)
  • -v, –volume: ホストのディレクトリをコンテナ側にマウント. デフォルトは Reed-Write で Read-Only の場合は host-dir:container-dir:ro を指定
$ docker run -it d817ad5b9beb /bin/bash
root@b79eab3cdfb6:/go# apt-get update && apt-get upgrade
root@b79eab3cdfb6:/go# apt-get install -y vim
root@b79eab3cdfb6:/go# which vim
/usr/bin/vim

実行中のコンテナから detach するには Ctrl + P に続いて Ctrl + Q を入力する。 docker attach [OPTIONS] CONTAINER で再度コンテナに attach できる。

$ docker attach b79eab3cdfb6
root@b79eab3cdfb6:/#

docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]] で image を保存する。

$ docker commit b79eab3cdfb6 t2sy/example

様々なプロジェクトで docker を使っていると不要な image がいつのまにか増えていることがある。以下は, 未使用の image を一括で削除するコマンドである。

$ docker image prune

tag が <none> の image のみ削除したい場合は以下。

$ docker rmi -f $(docker images -f "dangling=true" -q)

Docker Hub

Docker Hub は Docker Registry という docker image を保存しておくリポジトリサーバのひとつ。他に Amazon ECR, Container Registry (GCP) などがある。

Docker Hub で sing-up 後, Create Repository からリポジトリを作成する。

docker login でログインする。

$ docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username:
Password:
Login Succeeded

試しに先ほど commit した docker image (golang) を Docker Registry に push する。

$ docker push t2sy/example

Dockerfile

Dockerfile は docker image の作成手順を記述したファイル。

以下の HTTP Server (Go) をコンテナで実行したい。

package main

import (
	"fmt"
	"github.com/julienschmidt/httprouter"
	"log"
	"net/http"
)

func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
	log.Printf("hello, %s!\n", ps.ByName("name"))
}

func main() {
	router := httprouter.New()
	router.GET("/hello/:name", Hello)

	log.Fatal(http.ListenAndServe(":8080", router))
}

上記コードをホストからコンテナにコピーし実行する Dockerfile を書く。

FROM t2sy/example
LABEL maintainer="t2sy"

RUN mkdir -p /go/src/
COPY hello.go /go/src/
EXPOSE 8080
RUN /usr/local/go/bin/go get github.com/julienschmidt/httprouter
CMD /usr/local/go/bin/go run /go/src/hello.go

docker build [OPTIONS] PATH | URL | – で Dockerfile から docker image を生成する。 -t オプションでタグ付け (‘name:tag’ 形式で指定) できる。

$ docker build .
Sending build context to Docker daemon  3.072kB
Step 1/7 : FROM t2sy/example
 ---> 4937ae8ff385
Step 2/7 : LABEL maintainer="t2sy"
 ---> Running in cc9dd13b1439
Removing intermediate container cc9dd13b1439
 ---> ce496ba9da1d
Step 3/7 : RUN mkdir -p /go/src/
 ---> Running in 5f46733bd3ba
Removing intermediate container 5f46733bd3ba
 ---> 94578785a8a8
Step 4/7 : COPY hello.go /go/src/
 ---> 71acbe0e9359
Step 5/7 : EXPOSE 8080
 ---> Running in 144374b68ad6
Removing intermediate container 144374b68ad6
 ---> 21b9184984c5
Step 6/7 : RUN /usr/local/go/bin/go get github.com/julienschmidt/httprouter
 ---> Running in 0293dc74b265
Removing intermediate container 0293dc74b265
 ---> 845aa840ec28
Step 7/7 : CMD /usr/local/go/bin/go run /go/src/hello.go
 ---> Running in 6ce25a09bee7
Removing intermediate container 6ce25a09bee7
 ---> 513d6fa830af
Successfully built 513d6fa830af

Git における .gitignore のように Docker に .dockerignore という管理対象を制御する仕組みがある。Go の vendor ディレクトリなど COPY の対象から外したい場合は .dockerignore に記述する。

続いて, コンテナを起動する。

$ docker run -p 8080:8080 -td 0215f4207340
ccb1e1d9fa8d2b5cc8f513496d2a9fa3c415f183c52b39b9fae54ac1695e71f1

HTTP Server (Go) にリクエストを投げてレスポンスを確認する。

$ curl -v http://localhost:8080/hello/t2sy
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /hello/t2sy HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sat, 03 Nov 2018 11:35:30 GMT
< Content-Length: 13
< Content-Type: text/plain; charset=utf-8
<
hello, t2sy!
* Connection #0 to host localhost left intact

docker logs [OPTIONS] CONTAINER でログを確認する。

$ docker logs ccb1e1d9fa8d
2018/11/03 11:35:30 hello, t2sy!

Docker Compose

Docker Compose は Docker で複数のコンテナを定義/起動するツール。Docker Compose のコンテナ間はコンテナ名を指定し接続可能。

先ほどの HTTP Server (Go) のリバースプロキシとして前段に Nginx を配置する。

まず, Nginx の docker image (nginx) を取得する。

$ docker pull nginx

次にリバースプロキシの設定を書く。

server {
    listen    80;
    server_name    example.com;

    location / {
        proxy_set_header    Host    $host;
        proxy_set_header    X-Real-IP    $remote_addr;
        proxy_set_header    X-Forwarded-Host       $host;
        proxy_set_header    X-Forwarded-Server    $host;
        proxy_set_header    X-Forwarded-For    $proxy_add_x_forwarded_for;
        proxy_pass    http://go:8080;
    }
}

Nginx の Dockerfile は以下。

FROM nginx

COPY etc/nginx/conf.d/* /etc/nginx/conf.d/
EXPOSE 80
CMD /usr/sbin/nginx -c /etc/nginx/nginx.conf -g "daemon off;"

ディレクトリ構成は以下。

$ tree
.
├── docker-compose.yml
├── go
│   ├── Dockerfile
│   └── hello.go
└── nginx
    ├── Dockerfile
    └── etc
        └── nginx
            └── conf.d
                └── server.conf

go と nginx の2つのコンテナの設定を Compose file (docker-compose.yml) に書く。

version: "3"
services:
  nginx:
    build: nginx
    ports:
      - "80:80"
    depends_on:
      - go
  go:
    build: go
    ports:
      - "8080:8080"

docker-compose build でサービスをビルドする。

$ docker-compose build
Building go
Step 1/7 : FROM t2sy/example
 ---> 161be9a98347
Step 2/7 : LABEL maintainer="t2sy"
 ---> 84561078086d
Step 3/7 : RUN mkdir -p /go/src/
 ---> 4db1a80f508e
Step 4/7 : COPY hello.go /go/src/
 ---> 477234fdb322
Step 5/7 : EXPOSE 8080
 ---> b772d01b8c2d
Step 6/7 : RUN /usr/local/go/bin/go get github.com/julienschmidt/httprouter
 ---> 63b1274a48b0
Step 7/7 : CMD /usr/local/go/bin/go run /go/src/hello.go
 ---> 5beef64affdf
Successfully built 5beef64affdf
Successfully tagged docker_go:latest
Building nginx
Step 1/4 : FROM nginx
 ---> dbfc48660aeb
Step 2/4 : COPY etc/nginx/conf.d/* /etc/nginx/conf.d/
 ---> 9effed734890
Step 3/4 : EXPOSE 80
 ---> Running in 55c57cad722a
Removing intermediate container 55c57cad722a
 ---> 32015dc61527
Step 4/4 : CMD /usr/sbin/nginx -c /etc/nginx/nginx.conf -g "daemon off;"
 ---> Running in ccd328cf7ad0
Removing intermediate container ccd328cf7ad0
 ---> 5a2cad8e482b
Successfully built 5a2cad8e482b
Successfully tagged docker_nginx:latest

docker-compose up でコンテナをビルド/起動する。

$ docker-compose up -d
Starting docker_go_1 ... done
Recreating docker_nginx_1 ... done

$ docker-compose logs
Attaching to docker_nginx_1, docker_go_1

Nginx から HTTP Server (Go) のホストに対する IP アドレスを引けることを確認。

root@f0e4421248ff:/# dig +short go
172.18.0.2

Nginx 経由で HTTP Server (Go) に接続できることを確認する。

$ curl -v --resolv example.com:80:127.0.0.1 http://example.com/hello/t2sy
* Added example.com:80:127.0.0.1 to DNS cache
* Hostname example.com was found in DNS cache
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to example.com (127.0.0.1) port 80 (#0)
> GET /hello/t2sy HTTP/1.1
> Host: example.com
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.15.5
< Date: Sat, 03 Nov 2018 14:57:07 GMT
< Content-Type: text/plain; charset=utf-8
< Content-Length: 13
< Connection: keep-alive
<
hello, t2sy!
* Connection #0 to host example.com left intact

docker-compose stop でコンテナを停止, docker-compose rm でコンテナを削除する。

$ docker-compose stop
Stopping docker_nginx_1 ... done
Stopping docker_go_1    ... done

$ docker-compose rm
Going to remove docker_nginx_1, docker_go_1
Are you sure? [yN] y
Removing docker_nginx_1 ... done
Removing docker_go_1    ... done

Kubernetes

Kubernetes はコンテナ化されたアプリケーションの管理を高度化/自動化するためのオーケストレーションエンジン。

Kubernetes はリソースを登録することで非同期にコンテナを起動したり Load Balancer (LB) を設定したりできる。リソースには大きく以下の5つの種類がある。

  • Workloads
  • Discovery & LB
  • Config & Storage
  • Cluster
  • Metadata

Workloads リソースはコンテナを起動に関するリソース。今回は Workloads リソースの最小単位のリソースである Pod を生成する。Pod は複数のコンテナから構成され, Pod 内のコンテナは同一の IP アドレス空間を共有する。

先ほど作成した HTTP Server (Go) と Nginx の2つのコンテナを内包した Pod のマニフェストファイルを記述する。

apiVersion: v1
kind: Pod
metadata:
  name: nginx-go
  labels:
    app: nginx-go
spec:
  containers:
    - name: go
      image: t2sy/example
      ports:
      - containerPort: 8080
    - name: nginx
      image: nginx-proxy-go
      imagePullPolicy: IfNotPresent
      ports:
      - containerPort: 80

kubectl create で Pod を生成する。

$ kubectl create -f pod-nignx-go.yml
pod "nginx-go" created

$ kubectl get pods
NAME       READY     STATUS    RESTARTS   AGE
nginx-go   2/2       Running   0          1m

kubectl port-forward でホストから pod へトラフィックを転送する。

$ sudo kubectl port-forward nginx-go 80:80
Password:
Forwarding from 127.0.0.1:80 -> 80
Forwarding from [::1]:80 -> 80

Nginx への接続を確認。

$ curl -v http://127.0.0.1
* Rebuilt URL to: http://127.0.0.1/
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 80 (#0)
> GET / HTTP/1.1
> Host: 127.0.0.1
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.15.5
< Date: Sun, 04 Nov 2018 00:50:43 GMT
< Content-Type: text/html
< Content-Length: 612
< Last-Modified: Tue, 02 Oct 2018 14:49:27 GMT
< Connection: keep-alive
< ETag: "5bb38577-264"
< Accept-Ranges: bytes
<
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
</html>
* Connection #0 to host 127.0.0.1 left intact

続いて Nginx を経由した HTTP Server (Go) への接続を確認する。

$ curl -v --resolv example.com:80:127.0.0.1 http://example.com/hello/t2sy
* Added example.com:80:127.0.0.1 to DNS cache
* Hostname example.com was found in DNS cache
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to example.com (127.0.0.1) port 80 (#0)
> GET /hello/t2sy HTTP/1.1
> Host: example.com
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.15.5
< Date: Sun, 04 Nov 2018 00:50:52 GMT
< Content-Type: text/plain; charset=utf-8
< Content-Length: 13
< Connection: keep-alive
<
hello, t2sy!
* Connection #0 to host example.com left intact

kubectl logs で Nginx のアクセスログを確認する。

$ kubectl logs -f nginx-go nginx
192.168.65.3 - - [04/Nov/2018:00:50:43 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.54.0" "-"
192.168.65.3 - - [04/Nov/2018:00:50:52 +0000] "GET /hello/t2sy HTTP/1.1" 200 13 "-" "curl/7.54.0" "-"

port-forward 同様の転送は service type を LoadBalancer にすることでも行える。

$ kubectl expose pod nginx-go --type=LoadBalancer --port=80
service "nginx-go" exposed

$ kubectl get services
NAME         TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
nginx-go     LoadBalancer   10.108.157.31   localhost     80:32139/TCP   8m

kompose

以降は補足となる。Kompose は docker-compose を Kubernetes に移行するのを支援するツール。

例として, 先ほど書いた docker-compose.yml をマニュフェストファイルに変換する。

$ kompose -f docker-compose.yml convert
INFO Kubernetes file "go-service.yaml" created
INFO Kubernetes file "nginx-service.yaml" created
INFO Kubernetes file "go-deployment.yaml" created
INFO Kubernetes file "nginx-deployment.yaml" created

kompose up でデプロイする。

$ kompose up
INFO We are going to create Kubernetes Deployments, Services and PersistentVolumeClaims for your Dockerized application. If you need different kind of resources, use the 'kompose convert' and 'kubectl create -f' commands instead.

INFO Deploying application in "default" namespace
INFO Successfully created Service: go
INFO Successfully created Service: nginx
INFO Successfully created Deployment: go
INFO Successfully created Deployment: nginx

Your application has been deployed to Kubernetes. You can run 'kubectl get deployment,svc,pods,pvc' for details.

kompose down で services/deployments を削除する。

$ kompose down
INFO Deleting application in "default" namespace
INFO Successfully deleted Service: go
INFO Successfully deleted Service: nginx
INFO Successfully deleted Deployment: go
INFO Successfully deleted Deployment: nginx

[1] Best practices for writing Dockerfiles
[1] Access host from a docker container