20191222のdockerに関する記事は17件です。

Docker+GitHub+HerokuでCI/CDっぽく

やった事

前に記事で公開したQiitaタグ自動ジェネレータをWebアプリとして手軽に試せる様に公開しました。
ついでにGitHubでソースも晒しています。

https://auto-create-qiita-tags.herokuapp.com/

※Freeプランなので30分間アクセスがないと休眠してしまいます。少し時間がかかる時があるのはご愛嬌という事で...

全体図

Webアプリを公開するにあたりHerokuを使いました。GitHubとも連携させたのでこんな感じです。
GitHubにソースをpushすると自動的にHerokuでビルドが走りDockerコンテナのデプロイまでやってくれます。仕事での開発だと今時CI/CDなんて当たり前だと思いますが、個人でお金をかけずに環境作ってみたという感じです。

qiita投稿用.png

Herokuって?

アプリケーションの開発から実行、運用までのすべてをクラウドで完結できるPaaSです。(公式サイトより)
結構前からあるサービスで知名度も高く知っている人も多いと思います。
以下を魅力に感じ今回使いました。

  • 無料でWebアプリを公開することが出来る ※稼働時間など一部制約あり
  • GitHub連携できる
  • Docker対応している(Heroku dyno

GitHub連携するには?

Herokuの画面から設定するだけ、すごく簡単。Automatic deploysを有効にするだけで、GitHubにpushされたらHerokuにアプリをデプロイするところまでやってくれます。
スクリーンショット 2019-12-21 18.10.49.png

Dockerビルドするには?

連携したGitHubにDockerfileはもちろんですが、heroku.ymlという設定ファイルが必要になります。
今回用意したheroku.ymlはこちら。

heroku.yml
build:
  docker:
    web: Dockerfile

色々出来るらしいですが、単純にDockerfileからビルドするだけなのでシンプルな内容となってます。
詳細な書き方は公式ドキュメントを見てみてください。

ちなみにDokerfileはこちら。
PythonのWebアプリケーションフレームワークであるflaskを使いました。

FROM python:3-alpine

WORKDIR /work
RUN wget http://gensen.dl.itc.u-tokyo.ac.jp/soft/pytermextract-0_01.zip
RUN unzip pytermextract-0_01.zip
RUN cd pytermextract-0_01 && python setup.py install

RUN apk update
RUN apk --no-cache add git gcc libc-dev libxml2-dev libxslt-dev

RUN git clone https://github.com/fukumasa/auto-create-qiita-tags

WORKDIR /work/auto-create-qiita-tags
RUN pip install -r requirements.txt

ENV FLASK_APP /work/auto-create-qiita-tags/app.py
CMD flask run -h 0.0.0.0 -p $PORT

Herokuでは起動時のポートがPORTという環境変数に格納されているため、flaskアプリケーション起動時に$PORTでポート指定しています。
これでコンテナがビルドされるとCMDの内容が実行されます。

起動したDockerアプリには80ポートでアクセスするため、おそらくですが、コンテナ内部の$PORTポートとコンテナが稼働しているサーバの80ポートが自動的にフォワーディングされていると思われます。
Dockerコンテナをdocker runで実行する際は、通常-p 80:5000のようにポートフォワーディングを明示的に行う事が多いと思いますが、Herokuではこの辺りはあまり意識しなくても良さそうです。

まとめ

Herokuを使ってDockerアプリを公開してみました。
GitHubを使えば、既存のソース+heroku.ymlで手軽に連携することが出来ます。
そもそもHerokuの使い方とかより詳細なコマンドとかは以下の参考ページを見てみてください。

参考ページ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerでnpmを使ってscssを書く

Dockerでnpmを使う。

普段のちょっとした開発を試すときとか、scss使って効率よくしたいなと思ったので、備忘録です。
前準備としてはこちらが必要です。
Dockerで開発環境構築(Mac + Nginx + PHP-FPM)

docker-compose.ymlの編集

追加

  node:
    image: node:latest
    ports:
      - "8000:80"
    volumes:
      - ./:/src
    working_dir: /src

nodeの部分は好きな名前に変更してください。
latestの部分は任意のversionに変更可能です。

node install

$ docker-compose run --rm node npm init

package.jsonが作成されます。

sassをインストール

sassをコンパイルできるnode-sassをインストール。

$ docker-compose run --rm node npm install node-sass

package.jsonへの記述

package.json
  "scripts": {
    "css/scss": "node-sass src/scss/style.scss -o src/dist/css --output-style expanded",
  }

scssファイルを作りstyle.scssの中にscss記法で何か書きます。

その後npmを実行します。

$ docker-compose run --rm node npm run css/scss

srcディレクトリの中にdistでディレクトリが作成されるはずです。
このdistディレクトリの中にcss/style.cssがあるはずです。

それがコンパイラされたstylesheetです。

以上になります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerでscssを書く

Dockerでscssを書く

普段のちょっとした開発を試すときとか、scss使って効率よくしたいなと思ったので、備忘録です。
前準備としてはこちらが必要です。
Dockerで開発環境構築(Mac + Nginx + PHP-FPM)

docker-compose.ymlの編集

npmを使います。

追加

  node:
    image: node:latest
    ports:
      - "8000:80"
    volumes:
      - ./:/src
    working_dir: /src

nodeの部分は好きな名前に変更してください。
latestの部分は任意のversionに変更可能です。

node install

$ docker-compose run --rm node npm init

package.jsonが作成されます。

sassをインストール

sassをコンパイルできるnode-sassをインストール。

$ docker-compose run --rm node npm install node-sass

package.jsonへの記述

package.json
  "scripts": {
    "css/scss": "node-sass src/scss/style.scss -o src/dist/css --output-style expanded",
  }

scssファイルを作りstyle.scssの中にscss記法で何か書きます。

その後npmを実行します。

$ docker-compose run --rm node npm run css/scss

srcディレクトリの中にdistでディレクトリが作成されるはずです。
このdistディレクトリの中にcss/style.cssがあるはずです。

それがコンパイラされたstylesheetです。

以上になります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker の Rootlessモードを試してみた

前書き

ユーザが、Dockerを使用する際に、/var/run/docker.sock へのアクセス権限が問題になることがありました。

$ sudo ls -l /var/run/docker.sock
srw-rw---- 1 root docker 0 12月 15 19:07 /var/run/docker.sock

対処方法として、sudoを使う方法やユーザをdocker グループに入れる方法がありました。しかし、いずれもひと手間が面倒であったり、セキュリティ面に問題ありで、良い方法ではありませんでした。

1つの解決策として、Docker 19.03から下記のRootlessモードが行えるようになりました。

Docker 19.03新機能 (root権限不要化、GPU対応強化、CLIプラグイン…)

簡単に説明すると、各ユーザ用にDockerの環境を作成します。そのためDockerを使用するユーザ毎に、RootlessモードのDockerのインストールが必要です。

RootlessモードのDockerを実際にインストールした時に気が付いたことを書いています。
以下では、「Rootless Docker」と記述しています。

試した環境

  • CentOS Linux release 8.0.1905 (Core)

※CentOSは、「最小限のインストール」でインストールした直後の状態です。

この記事の中でインストールするRootless Dockerのバージョン

  • Client: Docker Engine - Community master-dockerproject-2019-12-11
  • Server: Docker Engine - Community Engine master-dockerproject-2019-12-11

前準備

テスト用にアカウントを2つ作成しています。

$ sudo useradd user01
$ sudo useradd user02
$ sudo passwd user01
$ sudo passwd user02

Rootless Dockerのインストール手順

インストールは、下記のコマンドで完了します。
ユーザ権限で実行できます。

$ curl -fsSL https://get.docker.com/rootless | sh
$ echo 'export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock' >> ~/.bash_profile
$ source ~/.bash_profile
$ docker container run hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

「Hello from Docker!」が表示されたら成功です。
次に、OSを再起動したとき自動起動するようにします。

$ vi .config/systemd/user/docker.service

docker.serviceファイルの最後の行を変更します。
multi-user.target のままだと、自動起動しなかったです。

[Install]
WantedBy=multi-user.target

    ↓

[Install]
WantedBy=default.target

自動起動の設定を続けます。

$ systemctl --user daemon-reload
$ systemctl --user enable docker
Created symlink /home/user01/.config/systemd/user/multi-user.target.wants/docker.service → /home/user01/.config/systemd/user/docker.service.
$ sudo loginctl enable-linger user01

これで、OS再起動時にRootless Dockerが起動するようになります。

インストール後の確認

ディレクトリ・ファイルを確認

インストールを完了すると、以下のディレクトリやファイルが作成されています。

~/.config
~/.local
~/bin
/var/run/user/1000/docker
/var/run/user/1000/docker.pid
/var/run/user/1000/docker.sock
/var/run/user/1000/runc

※1000の部分は、UIDです。環境によって変わります。
※/var/run/user/1000/に追加されたディレクトリやファイルは、OS再起動時に消えますが、rootless dockerd が起動すると作成されます。

パスの確認

homeディレクトリ下にインストールされています。

$ which docker
~/bin/docker

バージョンの確認

Versionの表記が少し気になりますが、Engine Versionも表示されています。
通常のDockerだと、Engine Versionは「dial unix /var/run/docker.sock: connect: permission denied」となって表示されません。

$ docker version
Client: Docker Engine - Community
 Version:           master-dockerproject-2019-12-11
 API version:       1.41
 Go version:        go1.12.12
 Git commit:        08eaead2
 Built:             Wed Dec 11 23:52:32 2019
 OS/Arch:           linux/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          master-dockerproject-2019-12-11
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.4
  Git commit:       1347481
  Built:            Wed Dec 11 23:58:15 2019
  OS/Arch:          linux/amd64
  Experimental:     true
 containerd:
  Version:          v1.3.2
  GitCommit:        ff48f57fc83a8c44cf4ad5d672424a98ba37ded6
 runc:
  Version:          1.0.0-rc9
  GitCommit:        d736ef14f0288d6993a1845745d6756cfc9ddd5a
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683

プロセスを確認

4つのプロセスが動作しています。
全てのプロセスがユーザ権限で動作しています。

$ ps xo user,cmd | grep docker
user01   rootlesskit --net=vpnkit --mtu=1500 --slirp4netns-sandbox=auto --slirp4netns-seccomp=auto --disable-host-loopback --port-driver=builtin --copy-up=/etc --copy-up=/run /home/user01/bin/dockerd-rootless.sh --experimental --storage-driver=vfs
user01   /proc/self/exe --net=vpnkit --mtu=1500 --slirp4netns-sandbox=auto --slirp4netns-seccomp=auto --disable-host-loopback --port-driver=builtin --copy-up=/etc --copy-up=/run /home/user01/bin/dockerd-rootless.sh --experimental --storage-driver=vfs
user01   dockerd --experimental --storage-driver=vfs
user01   containerd --config /run/user/1000/docker/containerd/containerd.toml --log-level info

ユーザ毎の違いを比べてみる

user02でRootless Dockerをインストールする前にバージョン確認してみました。
当然、見つかりません。

[user02@localhost ~]$ docker version
-bash: docker: コマンドが見つかりません

user02でも、同様にRootless Dockerをインストールして、確認を続けます。

パスの確認

各ユーザのhomeの下を参照しているので、別ファイルです。

[user01@localhost ~]$ which docker
~/bin/docker

[user02@localhost ~]$ which docker
~/bin/docker

i-node番号を確認すると、別ファイルだとわかります。

[user01@localhost ~]$ ls -i ~/bin/docker
33554563 /home/user01/bin/docker

[user02@localhost ~]$ ls -i ~/bin/docker
33554616 /home/user02/bin/docker

バージョンの確認

実行ファイルは別々でも、同じバージョンなので違いがわからないです。

$ docker version
Client: Docker Engine - Community
 Version:           master-dockerproject-2019-12-11
 API version:       1.41
 Go version:        go1.12.12
 Git commit:        08eaead2
 Built:             Wed Dec 11 23:52:32 2019
 OS/Arch:           linux/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          master-dockerproject-2019-12-11
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.4
  Git commit:       1347481
  Built:            Wed Dec 11 23:58:15 2019
  OS/Arch:          linux/amd64
  Experimental:     true
 containerd:
  Version:          v1.3.2
  GitCommit:        ff48f57fc83a8c44cf4ad5d672424a98ba37ded6
 runc:
  Version:          1.0.0-rc9
  GitCommit:        d736ef14f0288d6993a1845745d6756cfc9ddd5a
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683

[user02@localhost ~]$ docker version
Client: Docker Engine - Community
 Version:           master-dockerproject-2019-12-11
 API version:       1.41
 Go version:        go1.12.12
 Git commit:        08eaead2
 Built:             Wed Dec 11 23:52:32 2019
 OS/Arch:           linux/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          master-dockerproject-2019-12-11
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.4
  Git commit:       1347481
  Built:            Wed Dec 11 23:58:15 2019
  OS/Arch:          linux/amd64
  Experimental:     true
 containerd:
  Version:          v1.3.2
  GitCommit:        ff48f57fc83a8c44cf4ad5d672424a98ba37ded6
 runc:
  Version:          1.0.0-rc9
  GitCommit:        d736ef14f0288d6993a1845745d6756cfc9ddd5a
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683

プロセスを確認

4つのプロセスで、PIDが違っています。

[user01@localhost ~]$ ps xo pid,user,cmd | grep docker
 1225 user01   rootlesskit --net=vpnkit --mtu=1500 --slirp4netns-sandbox=auto --slirp4netns-seccomp=auto --disable-host-loopback --port-driver=builtin --copy-up=/etc --copy-up=/run /home/user01/bin/dockerd-rootless.sh --experimental --storage-driver=vfs
 1233 user01   /proc/self/exe --net=vpnkit --mtu=1500 --slirp4netns-sandbox=auto --slirp4netns-seccomp=auto --disable-host-loopback --port-driver=builtin --copy-up=/etc --copy-up=/run /home/user01/bin/dockerd-rootless.sh --experimental --storage-driver=vfs
 1272 user01   dockerd --experimental --storage-driver=vfs
 2866 user01   containerd --config /run/user/1000/docker/containerd/containerd.toml --log-level info

[user02@localhost ~]$ ps xo pid,user,cmd | grep docker
 8013 user02   rootlesskit --net=vpnkit --mtu=1500 --slirp4netns-sandbox=auto --slirp4netns-seccomp=auto --disable-host-loopback --port-driver=builtin --copy-up=/etc --copy-up=/run /home/user02/bin/dockerd-rootless.sh --experimental --storage-driver=vfs
 8022 user02   /proc/self/exe --net=vpnkit --mtu=1500 --slirp4netns-sandbox=auto --slirp4netns-seccomp=auto --disable-host-loopback --port-driver=builtin --copy-up=/etc --copy-up=/run /home/user02/bin/dockerd-rootless.sh --experimental --storage-driver=vfs
 8074 user02   dockerd --experimental --storage-driver=vfs
 8091 user02   containerd --config /run/user/1001/docker/containerd/containerd.toml --log-level info

/ver/run の下のファイルの確認

そもそもパスが違うので別物です。

[user01@localhost ~]$ ls -l /var/run/user/1000
合計 4
srw-rw-rw-. 1 user01 user01   0 12月 22 21:54 bus
drwx-----T. 7 user01 user01 180 12月 22 21:58 docker
-rw-r--r-T. 1 user01 user01   4 12月 22 21:58 docker.pid
srw-rw---T. 1 user01 user01   0 12月 22 21:58 docker.sock
drwx-----T. 2 user01 user01  40 12月 22 21:58 runc
drwxr-xr-x. 2 user01 user01  80 12月 22 22:00 systemd

[user02@localhost ~]$ ls -l /var/run/user/1001
合計 4
srw-rw-rw-. 1 user02 user02   0 12月 22 22:01 bus
drwx-----T. 5 user02 user02 140 12月 22 22:05 docker
-rw-r--r-T. 1 user02 user02   4 12月 22 22:05 docker.pid
srw-rw---T. 1 user02 user02   0 12月 22 22:05 docker.sock
drwx-----T. 2 user02 user02  40 12月 22 22:05 runc
drwxr-xr-x. 2 user02 user02  80 12月 22 22:05 systemd

Dockerコマンドでの違い

コンテナの確認

hello-worldコンテナを起動したので、停止状態のコンテナが残っています。
CONTAINER ID が違っており別々のコンテナだとわかります。

[user01@localhost ~]$ docker container ls -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
a9fa819c358e        hello-world         "/hello"            9 minutes ago       Exited (0) 9 minutes ago                       crazy_lederberg

[user02@localhost ~]$ docker container ls -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                      PORTS               NAMES
236dbc03fa53        hello-world         "/hello"            13 seconds ago      Exited (0) 12 seconds ago                       heuristic_hypatia

イメージの確認

IMAGE ID が違がって・・・ないです。同じです。
※先に結論だけ書いておきますが、IMAGE IDは同じですが、実体は別物です。

[user01@localhost ~]$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello-world         latest              fce289e99eb9        11 months ago       1.84kB

[user02@localhost ~]$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello-world         latest              fce289e99eb9        11 months ago       1.84kB

user01で、nginx イメージをpullします。

[user01@localhost ~]$ docker image pull nginx
Using default tag: latest
latest: Pulling from library/nginx
000eee12ec04: Pull complete
eb22865337de: Pull complete
bee5d581ef8b: Pull complete
Digest: sha256:50cf965a6e08ec5784009d0fccb380fc479826b6e0e65684d9879170a9df8566
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest

ダウンロードが終わるとイメージ一覧に追加されています。

[user01@localhost ~]$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               latest              231d40e811cd        4 weeks ago         126MB
hello-world         latest              fce289e99eb9        11 months ago       1.84kB

user02は、ダウンロードしていないので、nginxイメージを確認できません。

[user02@localhost ~]$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello-world         latest              fce289e99eb9        11 months ago       1.84kB

user02でnginxコンテナを起動すると、「Unable to find image」と表示されダウンロードが開始しました。

[user02@localhost ~]$ docker container run nginx
Unable to find image 'nginx:latest' locally
latest: Pulling from library/nginx
000eee12ec04: Pull complete
eb22865337de: Pull complete
bee5d581ef8b: Pull complete
Digest: sha256:50cf965a6e08ec5784009d0fccb380fc479826b6e0e65684d9879170a9df8566
Status: Downloaded newer image for nginx:latest
^C                        <-- nginxコンテナを停止
[user02@localhost ~]$

nginxイメージのIMAGE IDも、user01、user02ともに同じになっています。

[user01@localhost ~]$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               latest              231d40e811cd        4 weeks ago         126MB
hello-world         latest              fce289e99eb9        11 months ago       1.84kB

[user02@localhost ~]$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               latest              231d40e811cd        4 weeks ago         126MB
hello-world         latest              fce289e99eb9        11 months ago       1.84kB

イメージは、各ユーザでダウンロードしないとダメなようです。
一見、IMAGE IDが同じなので、同じイメージを見ているように勘違いしそうです。

イメージの実体を確認

hello-worldイメージを少し追ってみました。
jsonファイルの処理のために、pythonをインストールしています。

$ sudo dnf install python3

管理しているファイルを確認しました。

[user01@localhost ~]$ cat /home/user01/.local/share/docker/image/vfs/repositories.json | python3 -m json.tool
{
    "Repositories": {
        "hello-world": {
            "hello-world:latest": "sha256:fce289e99eb9bca977dae136fbe2a82b6b7d4c372474c9235adc1741675f587e",
            "hello-world@sha256:4fe721ccc2e8dc7362278a29dc660d833570ec2682f4e4194f4ee23e415e1064": "sha256:fce289e99eb9bca977dae136fbe2a82b6b7d4c372474c9235adc1741675f587e"
        },
        "nginx": {
            "nginx:latest": "sha256:231d40e811cd970168fb0c4770f2161aa30b9ba6fe8e68527504df69643aa145",
            "nginx@sha256:50cf965a6e08ec5784009d0fccb380fc479826b6e0e65684d9879170a9df8566": "sha256:231d40e811cd970168fb0c4770f2161aa30b9ba6fe8e68527504df69643aa145"
        }
    }
}

このファイルは各ユーザのhomeディレクトリ下にあります。
パスが違うので当然、別物です。
また下記のように、i-node番号も違っています。(桁が違いすぎる)

[user01@localhost ~]$ ls -li /home/user01/.local/share/docker/image/vfs/repositories.json
152 -rw-------. 1 user01 user01 542 12月 22 22:12 /home/user01/.local/share/docker/image/vfs/repositories.json

[user02@localhost ~]$ ls -li /home/user02/.local/share/docker/image/vfs/repositories.json
100663444 -rw-------. 1 user02 user02 542 12月 22 22:21 /home/user02/.local/share/docker/image/vfs/repositories.json

UUID(で合ってる?)をたどっていくと最終的に、helloコマンドにたどり着きます。

[user01@localhost ~]$ ls -li /home/user01/.local/share/docker/vfs/dir/e1b5ab5b419c6229106017654150711e2717ae81f3c8002bedb936f0f2786b4b/hello
33554585 -rwxrwxr-x. 1 user01 user01 1840  1月  1  2019 /home/user01/.local/share/docker/vfs/dir/e1b5ab5b419c6229106017654150711e2717ae81f3c8002bedb936f0f2786b4b/hello

同じように、user02でも確認します。
「dir」の下のディレクトリ名がuser01と違っています。

[user02@localhost ~]$ ls -li /home/user02/.local/share/docker/vfs/dir/f56bbc69cac68b78ef386f01643acbdaa89929e4f8f57bd2e8c8039e5dfa17c6/hello
67215817 -rwxrwxr-x. 1 user02 user02 1840  1月  1  2019 /home/user02/.local/share/docker/vfs/dir/f56bbc69cac68b78ef386f01643acbdaa89929e4f8f57bd2e8c8039e5dfa17c6/hello

helloコマンドのi-node番号が違っています。
dockerコマンド上は、同じIDに見えますが、イメージは別物だと言えます。

イメージの削除

user01、user02ともに、停止コンテナを削除した後、user02でイメージの削除を行います。

[user02@localhost ~]$ docker image rm hello-world
Untagged: hello-world:latest
Untagged: hello-world@sha256:4fe721ccc2e8dc7362278a29dc660d833570ec2682f4e4194f4ee23e415e1064
Deleted: sha256:fce289e99eb9bca977dae136fbe2a82b6b7d4c372474c9235adc1741675f587e
Deleted: sha256:af0b15c8625bb1938f1d7b17081031f649fd14e6b233688eea3c5483994a66a3
[user02@localhost ~]$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               latest              231d40e811cd        3 weeks ago         126MB

user02では、hello-worldイメージが消えています。
user01では、残っています。

[user01@localhost ~]$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               latest              231d40e811cd        3 weeks ago         126MB
hello-world         latest              fce289e99eb9        11 months ago       1.84kB

docker-compose の場合

docker-commposeを実行したときの動作を確認してみました。

$ sudo pip3 install docker-compose
$ docker-compose version
docker-compose version 1.25.0, build b42d419
docker-py version: 4.1.0
CPython version: 3.6.8
OpenSSL version: OpenSSL 1.1.1 FIPS  11 Sep 2018

簡単なdocker-compose.ymlを作成します。

$ vi docker-compose.yml
version: '3'
services:

  wordpress:
    image: wordpress
    container_name: wordpress
    restart: always
    ports:
      - 80:80
    environment:
      WORDPRESS_DB_PASSWORD: wp-password

  mysql:
    image: mysql:5.7
    container_name: mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: mysql-password

実行します。
wordpressとmysqlのコンテナのダウンロードが始まるため、時間がかかります。

$ docker-compose up -d
Starting wordpress ... done
Starting mysql     ... done
$ docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                 NAMES
32a9bd917995        mysql:5.7           "docker-entrypoint.s…"   2 minutes ago       Up 33 seconds       3306/tcp, 33060/tcp   mysql
cfdd93b5f5ff        wordpress           "docker-entrypoint.s…"   2 minutes ago       Up 33 seconds       0.0.0.0:80->80/tcp    wordpress

docker-composeからの起動もできました。
確認できたので、終了させておきます。

$ docker-compose down
Stopping mysql     ... done
Stopping wordpress ... done
Removing mysql     ... done
Removing wordpress ... done
Removing network user01_default

作業中に解決したエラー等

バージョン確認時のメッセージ

バージョン確認時等に、下記のようなメッセージが表示される。

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

環境変数の「DOCKER_HOST」が正しく設定されているか確認してください。

systemctlのメッセージ

systemctl --user status docker コマンドを実行した時に下記のようなエラーが出る。

level=warning msg="Running modprobe bridge br_netfilter failed with message: modprobe: ERROR: could not insert 'br_netfilter': Operation not permitted\ninsmod /lib/modules/4.18.0-80.el8.x86_64/kernel/net/bridge/br_netfilter.ko.xz \n, error: exit status 1"

原因は、br_netfilterモジュールがロードされていないこと。
なので、OS起動時にロードするようにします。

$ lsmod | grep br_netfilter
br_netfilter           24576  0
bridge                188416  1 br_netfilter

$ echo "br_netfilter" > /etc/modules-load.d/br_netfilter.conf

最後に

ユーザごとに別の環境でDockerを使えるようになります。
当然、本来のDocker(/usr/bin/docker)と、別なものになります。
イメージが別になるなど注意が必要そうです。
気になるところはありますが、root権限が不要なるメリットがあるのでしばらく使ってみようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

mong続報 - Dockerの名前生成ツールは2年前にPythonに移植されていた-

mong - Dockerのコンテナ名をランダム生成するコードをPythonに移植してみた -の続きです.

TL;DR

  • namesgenerator というパッケージが見つかりました.mongは見事に車輪の再発明でした.
  • 一方namesgeneratorは2017年12月からアップデートされていないので辞書が古く,生成できるパターンが本家やmongより少ないです.
  • mongでインスタンス生成が必須なのは使いにくかったので namesgeneratorを見習ってmong.get_random_name()を追加してみました

namesgenerator

Google, pypiで検索していて Dockerの名前生成ツールのPython版はないなぁと思っていました.しかしGitHubのコード検索をしてみたら,一瞬で下記が見つかりました.GitHubすごい.

https://github.com/shamrin/namesgenerator

上記のレポジトリにはPython版のほか,JavaScript/TypeScript版も含まれています.
pypiにも登録してあったので,インストールも一瞬です.
pypiの統計を見ると,月間1,000ダウンロードあるので,それなりに需要もあるようです.

もう,これを使えばいいじゃん,と思ったのですが,反省も兼ねてコードリーディングしてみました

namesgenerator と mong の違い

大きく下記の4点が違います
1. 辞書の持ち方
2. 関数 or クラス
3. boring_wozniak のときの再生成の方法
4. get_random_nameの引数

辞書の持ち方

mongは辞書を別ファイルにしているのに対して, namesgeneratorはpythonコード中にべた書きしています.べた書きすると,データ読み込み処理が不要で楽なのですが,メンテがつらそうです.

namesgeneratorは2年前に更新がとまっているので,それから本家に追加された語もあるかと思い,辞書のエントリ数で比べてみました:
- namesgeneratorの辞書は形容詞が93語で人名が160語
- mongの辞書(現在のmobyと同じはず)は形容詞が108語, 人名が235語

mongの方が形容詞で15語,人名で75語多いです.追加された語を調べる際に,べた書き方式だとGoのコードとPythonのコードを比較する必要があります.単純なdiffはできません.

一方でmongではmong/create_dict.pyを実行してGoコードからJSON形式の辞書を抜き出します.1行1単語にしてあるので,diffで差分が確認できます.もっとも,Goコードのフォーマットが大きく変わると辞書を抜き出すコードの修正が必要になるわけですが,mobyはそれなりに枯れているっぽいので大丈夫だと思います(思いたい).

以上から,メンテはmongの方が楽かもしれません.

関数 or クラス

namesgeneratorは名前生成を関数get_random_nameで提供していて,mongNameGeneratorクラスのget_random_nameで提供しています.クラスだといったんインスタンスを生成しないといけないので面倒です.mongを作り始めたときに,辞書を変更可能に,とか乱数シードの管理を別にできた方がよいのでは,などと考えてクラスにしましたが,一晩頭を冷やしてみるとそんなことはありませんでした.namesgeneratorの方式が良いです.

boring_wozniak のときの再生成の方法

namesgeneratorwhileループで生成ロジックを回している一方でmongget_random_nameメソッドを再帰呼び出しています.boring_wozniakが生成される確率は1/25,380なので,どちらでもいいかなと思います.

get_random_nameの引数

namesgeneratorretry関係のロジックがありません.ここ2年で追加されたのでしょうか.また,その代わりに形容詞と名詞をつなぐsepを変更できます.これも仕様変更でしょうか?

mongへの反映と今後の方針

いやー,見事に車輪の再発明をしてしまいました.

ただ,namesgeneratorの実装と比較することで良い点,悪い点の答え合わせができましたので,経験値が溜まってよかったかなと思います.また,良いところは見習おう,ということで関数方式を取り入れてmong.get_random_nameを追加しました.

つまり,今は下記のように書けるようになりました.

import mong
mong.get_random_name()  # 'trusting_engelbart'
mong.get_random_name()  # 'thirsty_brahmagupta'

今後ですが,namesgeneratorはメンテされていないので,mongをメンテしていくとそれなりに需要あるのかもと思います.

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】migrateができないのは環境設定が間違っているからかも!あるあるエラーを.envと共に振り返る。

Laravel's error.png

どうも、たかふみです。

Laravelで開発を行っていると、必ず使うであろう php artisan migrateコマンド。僕も何度もお世話になったコマンドです。

今回は php artisan migrate を実行したときに出会ったエラーと共に解決策を書きたいと思います。

あるあるエラー1:Connection refused

エラーメッセージ

  Illuminate\Database\QueryException  : SQLSTATE[HY000] [2002] Connection refused (SQL: select * from information_schema.tables where table_schema = sample and table_name = migrations and table_type = 'BASE TABLE')

  at /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connection.php:665
    661|         // If an exception occurs when attempting to run a query, we'll format the error
    662|         // message to include the bindings with SQL, which will make this exception a
    663|         // lot more helpful to the developer instead of just the database's errors.
    664|         catch (Exception $e) {
  > 665|             throw new QueryException(
    666|                 $query, $this->prepareBindings($bindings), $e
    667|             );
    668|         }
    669| 

  Exception trace:

  1   PDOException::("SQLSTATE[HY000] [2002] Connection refused")
      /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php:70

  2   PDO::__construct("mysql:host=127.0.0.1;port=3306;dbname=sample", "root", "password", [])
      /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php:70

  Please use the argument -v to see more details.

   Whoops\Exception\ErrorException  : Module 'zip' already loaded

  at Unknown:0
    1| 

  Exception trace:

  1   {main}()
      /var/www/html/artisan:0

解決策:DB_HOSTの設定を見直す。

.envファイルにあるDB_HOSTの値を確認したところ、DB_HOST=127.0.0.1となっていました。調べると、dockerの場合はコンテナ名に設定する必要があるとのことです。

【修正後】
DB_HOST=mysql

これで解決しました。

エラー2:Connection refused

 Illuminate\Database\QueryException  : SQLSTATE[HY000] [1049] Unknown database 'sample' (SQL: select * from information_schema.tables where table_schema = sample and table_name = migrations and table_type = 'BASE TABLE')

  at /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connection.php:665
    661|         // If an exception occurs when attempting to run a query, we'll format the error
    662|         // message to include the bindings with SQL, which will make this exception a
    663|         // lot more helpful to the developer instead of just the database's errors.
    664|         catch (Exception $e) {
  > 665|             throw new QueryException(
    666|                 $query, $this->prepareBindings($bindings), $e
    667|             );
    668|         }
    669| 

  Exception trace:

  1   PDOException::("SQLSTATE[HY000] [1049] Unknown database 'sample'")
      /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php:70

  2   PDO::__construct("mysql:host=mysql;port=3306;dbname=sample", "root", "password", [])
      /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php:70

  Please use the argument -v to see more details.

解決策:DB_DATABASEの値を確認

エラーメッセージから「'sample'というDBは存在しません。」ということが分かります。
この場合は、接続しようとしているDBの名前を見直す必要があります。

僕の場合はDB「bookmark」に接続をしたかったので、.envファイルの該当箇所を下記のように修正しました。

【修正後】
DB_DATABASE=bookmark

これで接続できました。

エラー3:修正しても接続できない。

解決策:envファイルが複数存在していないか確認

このエラー(?)が起きたときに一番ハマりました。笑
修正してもキャッシュを削除してもエラーが起きるため、もう一度ファイルを確認しようとファイル名で検索したところ、「.env」が2つあることに気づきました。

不要なenvファイルを削除したら上手くいったのでこれが原因だったようです。

migrateに成功すれば晴れて下記の状態になるはずです。
このババババッと実行される感じが気持ちいいんですよね。

root@592de04c5bde:/var/www/html/web# php artisan migrate       
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (0.03 seconds)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table (0.01 seconds)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated:  2019_08_19_000000_create_failed_jobs_table (0.01 seconds)

まとめ:エラーメッセージを読んでenvファイルを見直そう。

こうするとmigrateが上手くいかないのは.envファイルの値、つまり環境設定が間違っていることが多かったように感じます。DBが無いと開発に着手できないので、ここはスムーズに乗り越えたいですね。それでは!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】migrateができないのは環境変数が間違っているからかも!あるあるエラーを.envと共に振り返る。

Laravel's error.png

どうも、たかふみです。

Laravelで開発を行っていると、必ず使うであろう php artisan migrateコマンド。僕も何度もお世話になったコマンドです。

今回は php artisan migrate を実行したときに出会ったエラーと共に解決策を書きたいと思います。

あるあるエラー1:Connection refused(Dockerの場合)

エラーメッセージ

  Illuminate\Database\QueryException  : SQLSTATE[HY000] [2002] Connection refused (SQL: select * from information_schema.tables where table_schema = sample and table_name = migrations and table_type = 'BASE TABLE')

  at /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connection.php:665
    661|         // If an exception occurs when attempting to run a query, we'll format the error
    662|         // message to include the bindings with SQL, which will make this exception a
    663|         // lot more helpful to the developer instead of just the database's errors.
    664|         catch (Exception $e) {
  > 665|             throw new QueryException(
    666|                 $query, $this->prepareBindings($bindings), $e
    667|             );
    668|         }
    669| 

  Exception trace:

  1   PDOException::("SQLSTATE[HY000] [2002] Connection refused")
      /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php:70

  2   PDO::__construct("mysql:host=127.0.0.1;port=3306;dbname=sample", "root", "password", [])
      /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php:70

  Please use the argument -v to see more details.

   Whoops\Exception\ErrorException  : Module 'zip' already loaded

  at Unknown:0
    1| 

  Exception trace:

  1   {main}()
      /var/www/html/artisan:0

解決策:DB_HOSTの設定を見直す。

.envファイルにあるDB_HOSTの値を確認したところ、DB_HOST=127.0.0.1となっていました。調べると、dockerの場合はDBのコンテナ名に設定する必要があるとのことです。

【修正後】
DB_HOST=mysql

これで解決しました。

エラー2:Connection refused

 Illuminate\Database\QueryException  : SQLSTATE[HY000] [1049] Unknown database 'sample' (SQL: select * from information_schema.tables where table_schema = sample and table_name = migrations and table_type = 'BASE TABLE')

  at /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connection.php:665
    661|         // If an exception occurs when attempting to run a query, we'll format the error
    662|         // message to include the bindings with SQL, which will make this exception a
    663|         // lot more helpful to the developer instead of just the database's errors.
    664|         catch (Exception $e) {
  > 665|             throw new QueryException(
    666|                 $query, $this->prepareBindings($bindings), $e
    667|             );
    668|         }
    669| 

  Exception trace:

  1   PDOException::("SQLSTATE[HY000] [1049] Unknown database 'sample'")
      /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php:70

  2   PDO::__construct("mysql:host=mysql;port=3306;dbname=sample", "root", "password", [])
      /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php:70

  Please use the argument -v to see more details.

解決策:DB_DATABASEの値を確認

エラーメッセージから「'sample'というDBは存在しません。」ということが分かります。
この場合は、接続しようとしているDBの名前を見直す必要があります。

僕の場合はDB「bookmark」に接続をしたかったので、.envファイルの該当箇所を下記のように修正しました。

【修正後】
DB_DATABASE=bookmark

これで接続できました。

エラー3:修正しても接続できない。

解決策:envファイルが複数存在していないか確認

このエラー(?)が起きたときに一番ハマりました。笑 修正してもキャッシュを削除してもエラーが起きるため、もう一度ファイルを確認しようとファイル名で検索したところ、「.env」が2つあることに気づきました。

不要なenvファイルを削除したら上手くいったのでこれが原因だったようです。

migrateに成功すれば晴れて下記の状態になるはずです。このババババッと実行される感じが気持ちいいんですよね。

root@592de04c5bde:/var/www/html/web# php artisan migrate       
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (0.03 seconds)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table (0.01 seconds)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated:  2019_08_19_000000_create_failed_jobs_table (0.01 seconds)

まとめ:エラーメッセージを読んでenvファイルを見直そう。

こうするとmigrateが上手くいかないのは.envファイルの値、つまり環境変数が間違っていることが多かったように感じます。DBが無いと開発に着手できないので、ここはスムーズに乗り越えたいですね。それでは!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

docker pullをちょっとだけ楽にするCLIつくりました

つくったもの

dockerのimageからtagを検索&選択してpullできるCLI

※動画の圧縮になれておらず、、画質が悪いです。。

バージョン等

  • Go 1.13.5
  • spf13/cobra ... CLIアプリケーションのライブラリ。
  • manifoldco/promptui ... 対話型プロンプトのライブラリ。
  • macOS Catalina 10.15.1
  • Docker version 19.03.5

Goの経験は、以前ポストしたLチカセブンくらいでほぼ触ったことがありません。
「shellなら楽じゃん」と言われそうですが、今回は Dockerのアドベントカレンダー
必然的にDockerの開発言語であるGo言語を使うしかありません!

CLIのライブラリは、urfave/cliも情報がたくさんありました。
しかし、今回は Dockerのアドベントカレンダー
必然的にDockerに採用されているcobraを使うしかありません!

ソース

https://github.com/kohbis/dimg

※ 201919/12/22時点では、公式イメージ(library)だけ対応しています。

ポイント

タグ一覧取得

curlコマンドだと、下記でタグ一覧が取得できます。
(後述の公式ドキュメント参照)

curl -s https://registry.hub.docker.com/v2/repositories/library/alpine/tags/ | jq -r '.results|.[]|.name'

返ってくるJSONを、Goの構造体で表すとこうなります。
JSON->Structには、JSON-to-Goというサイトが、とても便利でした。

type Tags struct {
    Count    int         `json:"count"`
    Next     string      `json:"next"`
    Previous interface{} `json:"previous"`
    Results  []struct {
        Name     string `json:"name"`
        FullSize int    `json:"full_size"`
        Images   []struct {
            Size         int         `json:"size"`
            Digest       string      `json:"digest"`
            Architecture string      `json:"architecture"`
            Os           string      `json:"os"`
            OsVersion    interface{} `json:"os_version"`
            OsFeatures   string      `json:"os_features"`
            Variant      interface{} `json:"variant"`
            Features     string      `json:"features"`
        } `json:"images"`
        ID                  int         `json:"id"`
        Repository          int         `json:"repository"`
        Creator             int         `json:"creator"`
        LastUpdater         int         `json:"last_updater"`
        LastUpdaterUsername string      `json:"last_updater_username"`
        ImageID             interface{} `json:"image_id"`
        V2                  bool        `json:"v2"`
        LastUpdated         time.Time   `json:"last_updated"`
    } `json:"results"`
}

このままだと1ページあたり10件しか取得できないため Next に次ページのURLがnullにならない限りループして、、、という面倒くさいことになります。
そのため、最初からクエリパラメータ ?page_size=10000 をつけて取得しています。
(そんなにタグ数があるイメージってあるのかしら)

タグ検索

promptuiは自前のsearcherを実装することができます。(感動)
これにより、マイナーバージョンが多い言語の実行環境イメージも見つけやすくなっています。

$ go run main.go
Image Name: ruby
Searching "ruby" tags...
"ruby" has 615 tags.
Search: 2.6.█
? Select Tag:
  ▸ 2.6.5-stretch
    2.6.5-slim-stretch
    2.6.5-slim-buster
    2.6.5-slim
↓   2.6.5-buster

cobraも、promptuiも色々できることがありそうなので、今後拡張していきたいです。

まとめ

Docker アドベントカレンダー担当日の1日前に思いたったのですが、ライブラリが非常に強力で1日かからずにものをつくることができました!

今回作ったものは足りないところがたくさんありますが、実際つくってみると個人的には欲しい機能がどんどん思いついていきたので、今後継続的に開発していきたいと思います。

実は今年最初やりたいことのひとつに「CLIツールをつくる」がありまして、ぎりぎり達成することができたのでよかったです!!
(この記事を書いている最終に思い出しました笑)

参考

Examples using the Docker Engine SDKs and Docker API
How do I authenticate with the V2 API?

どうでもいいからささっとGoでCLIつくりたいとき
Go初心者がGoでコマンドラインツールの作成に挑戦した話

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Visual Studio Code - Remote Development を使用して PHP Xdebug を使ったデバッグを行う

要約

  • VS Code の Remote Development で Xdebug する時は、remote_host の向き先を ローカルホストにする。
  • Remote Development の接続先の VS Code に PHP Debug を導入する。
  • .vscode/launch.json で Xdebug からの情報を Listen する。

はじめに

VS Code の 拡張機能 「 Remote Development 」は、これまでもどかしく感じていたローカル開発環境の構成を改善してくれそうで色々と試しています。

前回の記事 では、Vagrant で構築した仮想環境の上にある Docker コンテナに対して、 Remote Development で接続する方法を記載しました。

今回は、前回構築した環境に追加する形で、 PHP の Xdebug を用いた開発が出来るように設定方法をまとめてみます。

概要

書くこと

  • Docker コンテナの起動時の Xdebug の設定方法
  • VS Code Server で Xdebug のデータを Listen する方法

書かないこと

  • Vagrant で構築する仮想環境に Docker や docker-compose を導入する方法。

想定環境

この記事で実現するシステムの構成は下図の通りです。

image.png

Remote Development の有無による違い

Remote Development を使用しない場合

Remote Development を使用せず、 Xdebug のデータを HostOS 上の VS Code で Listen する場合は以下のような構成でした。

image.png

この構成を実現するためには、 Xdebug の設定ファイルで remote_host の IP アドレスを指定する必要がありました。

Remote Development を使用する場合

Remote Development を使用する場合の構成は以下の構成になります。

image.png

Xdebug の remote_host は localhost を指定すればよく、送信先の IP をいちいち気にする必要がありません。

設定手順

0. 基本設的な設定手順

この記事で実現している構成の基本的な要素は以下の記事で解説しています。

1. XDebug が有効化された Docker コンテナの構築

Docker コンテナの構築に必要なリソースは以下の通りです。

command(bash@GuestOS)
## Dockerfile
## authorized_keys   : 公開鍵認証に使用する公開鍵。Dockerコンテナ内に配置する。
## php/20-xdebug.ini : Xdebug の設定ファイル。Dockerコンテナ内に配置する。
$ tree
.
├── Dockerfile
├── authorized_keys
└── php
    └── 20-xdebug.ini

Dockerfile は次のように記載します。

Dockerfile
# image
FROM php:7.0.15-fpm

ENV LANG C.UTF-8

RUN apt-get update -qq && \
    apt-get install -y \
        zlib1g-dev \
        libfreetype6-dev \
        libjpeg62-turbo-dev \
        libpng-dev \
        && docker-php-ext-install zip \
        && yes "" | pecl install xdebug \
        && docker-php-ext-enable xdebug \
        && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \
        && docker-php-ext-install -j$(nproc) gd

RUN apt-get update \
    && apt-get install -y libpq-dev \
    && docker-php-ext-install pdo_mysql pdo_pgsql

RUN apt-get update \
    && apt-get install -my wget gnupg

WORKDIR /opt/

# ここから最後までがポイント
RUN apt-get update && apt-get install -y openssh-server
RUN mkdir /var/run/sshd

RUN sed -i 's/PermitRootLogin without-password/PermitRootLogin yes/' /etc/ssh/sshd_config
RUN sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config

# SSH login fix. Otherwise user is kicked off after login
RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd

ENV NOTVISIBLE "in users profile"
RUN echo "export VISIBLE=now" >> /etc/profile

# 手元の公開鍵をコピー
COPY authorized_keys /root/authorized_keys

EXPOSE 22

# 公開鍵を使えるようにする (パーミッション変更など)
CMD mkdir ~/.ssh && \
    mv ~/authorized_keys ~/.ssh/authorized_keys && \
    chmod 0600 ~/.ssh/authorized_keys &&  \
    # 最後に ssh を起動
    /usr/sbin/sshd -D

# Xdebug の設定ファイルを Docker コンテナに配置
COPY php/20-xdebug.ini /usr/local/etc/php/conf.d/20-xdebug.ini

XDebug の設定を以下のようにします。

php/20-xdebug.ini
xdebug.remote_enable = 1
xdebug.remote_autostart = 1
xdebug.remote_connect_back = 1
xdebug.remote_host= "localhost"
xdebug.remote_port = 9001
xdebug.remote_log = /var/log/xdebug.log

Docker コンテナを以下のコマンドで構築します。

command(bash@GuestOS)
$ pwd
/vagrant/docker-sample

$ tree
.
├── Dockerfile
├── authorized_keys
└── php
    └── 20-xdebug.ini

# build
$ docker build ./ -t example

# Docker Container を起動
# Port:10000 を Port:22 に転送
$ docker run -d -p 10000:22 example

# Docker Container に SSH 接続
$ ssh root@127.0.0.1 -p 10000 -i ~/.ssh/private_key

2. .ssh/config の設定

Host OS から 仮想環境(GuestOS、Dockerコンテナ)に SSH接続するために、以下の設定を行います。

.ssh/config(Windows10)
# Vagrant で起動した仮想環境への SSH接続設定
Host vagrant-os
  HostName 127.0.0.1
  User vagrant
  Port 2222
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile C:/Users/<username>/work/vagrant/centos-7-docker/.vagrant/machines/default/virtualbox/private_key
  IdentitiesOnly yes
  LogLevel FATAL

# Docker で起動した仮想環境への SSH接続設定
Host docker-os
  Hostname 127.0.0.1
  User root
  Port 10000
  ProxyCommand C:\Windows\System32\OpenSSH\ssh.exe -W %h:%p vagrant-os
  IdentityFile C:/Users/<username>/work/vagrant/centos-7-docker/.vagrant/machines/default/virtualbox/private_key

3. Visual Studio Code の設定

1.) と 2.) の設定をすることで、 VS Code の Remote Development の SSH Target には以下のように接続先候補が表示されるようになります。

image.png

上図のうち、docker-os の方にアクセスした際の画面が下図です。
ここで、 .vscode/launch.json を作成して、 Xdebug からのデータを Listen 出来るよう設定しています。

image.png

.vscode/launch.json の設定は以下の通りです。
php/20-xdebug.inixdebug.remote_port で指定した Port と番号を合わせて設定します。
Docker コンテナ上の VSCode に PHPDebug 機能拡張 をインストールすることも忘れず行います。

.vscode/launch.json
{
    // IntelliSense を使用して利用可能な属性を学べます。
    // 既存の属性の説明をホバーして表示します。
    // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Listen for XDebug",
            "type": "php",
            "request": "launch",
            "port": 9001
        },
        {
            "name": "Launch currently open script",
            "type": "php",
            "request": "launch",
            "program": "${file}",
            "cwd": "${fileDirname}",
            "port": 9001
        }
    ]
}

4. 動作確認

3.) までの手順で設定した内容が正しく設定されているか確認してみます。
Docker コンテナ内で適当な PHP プログラムを作成します。

test.php
<?php

echo "test";

PHPDebug で Listen しながら、適当な箇所でブレイクポイントを設定して テストプログラムを実行します。

command(bash@DockerContainer)
$ pwd
/root

$ ls
test.php

# テストプログラムの実行
$ php test.php
test

実行結果は以下のようになりました。

image.png

おわりに

書いたあとで見返してみると、なんのことは無い、普通の Xdebug の設定でした。

リモートの仮想環境に VS Code Server があり、そこと HostOS の VSCode がやり取りすることによって、リッチな VSCode の UI を使用して仮想環境上のリソースが編集出来るので、ひじょ~~~に便利です。

参考

今回の記事を作成するにあたって参考した記事です。

Remote Development

Visual Studio Code - "Remote Development" を使って Docker Container on "Vagrant + VirtualBox" のファイルを編集する #Qiita

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[備忘録]centos7.4にdockerのインストール

はじめに

centos7.4にdocker, docker-composeをインストールする。
pipを使用してインストールする。

epel-releaseのインストール

$ sudo yum install -y epel-release

pythonとpipのインストール

すでにpythonのインストールがされている場合には不要。

$ sudo yum install -y python3

docker, docker-composeのインストール

$ sudo pip3 install docker docker-compose

userをdockergroupに追加

$ sudo groupadd docker
$ sudo usermod -aG docker ${user}

dockerサービスの起動

$ sudo systemctl start docker

動作確認

動作確認としてcentosのコンテナを起動してみる

$ sudo docker run centos

docker-composeで

docker-compose.yml
version: "3"
services:
  centos:
    build: .
    tty: yes
    volumes:
      - ./data:/data
Dockerfile
FROM centos:7
RUN  yum update & \\
  yum install -y epel-release
  yum install -y vim
$ docker-compose build
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CentOS 7にGitLab Runnerをインストールし、静的解析と単体テストを自動化する

CentOS 7にGitLab Runnerをインストールし、静的解析と単体テストを自動化する

Gitlab Runner とは?

GitLab CI / CD は、GitLab の一機能であり、GitLab のプロジェクトで管理している特定のブランチの更新やマージをトリガーとして、ビルドジョブやテストジョブを呼び出すことができます。

GitLab Runner は GitLab CI / CD 上から指示されたスクリプトを実行したり、一時的にDocker コンテナを生成してジョブを実行したりするプロセスです。

GitLab とは何ぞや?という方は、下記をご一読いただければと思います。

作成するに至った経緯

  • GitLab Runner のインストール方法と設定をアウトプットとして残したかったため。
  • GitLab Runner の動作確認をしたかったため。

対象読者

  • CentOS 7 にGitLab Runner をインストールしたい方
  • GitLab Runner を使用して、Python の静的解析( pylint )を自動化を試したい方
  • GitLab Runner を使用して、Python の単体テスト( pytest )を自動化を試したい方
    • ※ 今回Pythonを使用したのは、GitLab Runnerを試す敷居が低いと感じたためです。

前提条件

  • GitLab はインストール済みであるとする
  • Docker はインストール済みであるとする
  • Webブラウザはインストール済みであるとする
  • 今回は、proxy 環境下ではないものとする

GitLab をインストールされていない場合は、下記を参照してインストール願います。

構成図のイメージ

gitlab_runner_構成図.png

実行環境

# cat /etc/redhat-release
CentOS Linux release 7.7.1908 (Core)
# gitlab-rake gitlab:env:info

GitLab information
Version:    12.5.2
Revision:   49482945d28
Directory:  /opt/gitlab/embedded/service/gitlab-rails
DB Adapter: PostgreSQL
DB Version: 10.9
URL:        http://gitlab.example.com
HTTP Clone URL: http://gitlab.example.com/some-group/some-project.git
SSH Clone URL:  git@gitlab.example.com:some-group/some-project.git
Using LDAP: no
Using Omniauth: yes
Omniauth Providers: 

GitLab Runner の導入

1.1. GitLab Runner の動作確認用のプロジェクトの登録

GitLab Runner インストールの前に、動作確認用のサンプルプロジェクトを下記のイメージのように登録しておきます。

gitlab_runner_pytest_project.png

test_sample.py の中身は下記の通りです。以下のサイトから流用しております。

pylint や pytest 実行時に怒られないようにコメントをつけたり、値を変更したりしております。
.gitlab-ci.yml については、後程触れますので、ここでは解説を割愛いたします。

test_sample.py
# content of test_sample.py
"""This is a test program."""
def func(var_x):
    """This is a test function."""
    return var_x + 1

def test_answer():
    """This is a test function."""
    assert func(3) == 4

1.2. GitLab Runner のインストール

下記のコマンドを実行し、GitLab Runner をインストールします。

# curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh | sudo bash
# yum install -y gitlab-runner

1.3. GitLab Runner の登録情報の確認

次にGitLab CI / CD への登録を行います。そのために登録情報が必要となります。
Webブラウザ上で、GitLab のプロジェクトの画面にて下記の順でクリックし、登録情報を表示します。

[$プロジェクト名]
  ┗ [設定]
    ┗ [CI/CD]
        ┗ [Runner]
            ┗ [展開]
                ┗ [Set up a specific Runner manually]

gitlab_runner_001.png

1.4. GitLab CI/CD への Runner 登録

下記のコマンドを実行し、GitLab CI/CD への Runner 登録を行います。

# gitlab-runner register
設定項目 入力内容
Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com/): Set up a specific Runner manually の 2. に表示されているURL を入力
Please enter the gitlab-ci token for this runner: Set up a specific Runner manually の 3. に表示されている登録トークン を入力
Please enter the gitlab-ci description for this runner: Runner に関する内容を入力
Please enter the gitlab-ci tags for this runner (comma separated): Runner に関するタグを入力(タグなしも可)
Please enter the executor: Execcuter の種類を選択。今回は Docker
Please enter the default Docker image (e.g. ruby:2.6): Dockerのコンテナに使用するイメージを選択。今回は centos:centos7
Runtime platform                                    arch=amd64 os=linux pid=5622 revision=577f813d version=12.5.0
Running in system-mode.                            

Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com/):
http://localhost/
Please enter the gitlab-ci token for this runner:
pjx_24-Pjmu42k1xfJyz 
Please enter the gitlab-ci description for this runner:
[localhost.localdomain]: CI example
Please enter the gitlab-ci tags for this runner (comma separated):

Registering runner... succeeded                     runner=pjx_24-P
Please enter the executor: docker, docker-ssh, shell, ssh, docker-ssh+machine, custom, parallels, virtualbox, docker+machine, kubernetes:
docker
Please enter the default Docker image (e.g. ruby:2.6):
centos:centos7
Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded! 

これにてGitLab CI/CD への Runner 登録が完了しました。
1.3. GitLab Runner の登録情報の確認で開いていたページを更新すると、このプロジェクトに紐づいたRunner が表示されます。

gitlab_runner_002.png

Runner 横のRun untagged jobs にチェックが入っていることを確認します。
チェックが入っていない場合は、チェックを入れて [ 変更を保存 ] をクリックしておきましょう。

gitlab_runner_003.png

1.5. config.toml の設定

GitLab のリポジトリへのアクセス時に、名前解決ができるように設定をします。

# vi /etc/gitlab-runner/config.toml
concurrent = 1
check_interval = 0

[session_server]
  session_timeout = 1800

[[runners]]
  name = "CI example"
  url = "http://localhost/"
  token = "Ke1p4Hh96ZzohrcRS3nn"
  executor = "docker"
  [runners.custom_build_dir]
  [runners.docker]
    tls_verify = false 
    image = "centos:centos7"
    privileged = false 
    disable_entrypoint_overwrite = false 
    oom_kill_disable = false 
    disable_cache = false 
    volumes = ["/cache"]
    shm_size = 0
    extra_hosts = ["gitlab.example.com:10.0.2.15"]  ★ 設定箇所
  [runners.cache]
    [runners.cache.s3]
    [runners.cache.gcs]

1.6. GitLab Runnerの再起動

下記のコマンドを実行し、GitLab Runnerの再起動します。

# systemctl restart gitlab-runner

2. GitLab CI / CD Jobs の設定

2.1. ジョブの定義

GitLab CI / CD におけるジョブは、.gitlab-ci.yml という設定ファイルに定義します。これをプロジェクトリポジトリのトップディレクトリに隠しファイル形式でコミットすることによって、動的にジョブが登録される仕組みになっています。書き方につきましては、下記を参考願います。

それでは、.gitlab-ci.yml に実行するジョブを下記のように定義し、コミットします。

gitlab-ci.yml
image: centos:centos7
stages:
  - pylint
  - pytest
pylint:
  stage: pylint
  script:
    - yum update -y
    - yum install -y https://centos7.iuscommunity.org/ius-release.rpm
    - yum install -y python36u python36u-libs python36u-pip python36u-devel
    - pip3.6 install --upgrade pip
    - pip install pylint
    - pylint test_sample.py
pytest:
  stage: pytest
  script:
    - yum update -y
    - yum install -y https://centos7.iuscommunity.org/ius-release.rpm
    - yum install -y python36u python36u-libs python36u-pip python36u-devel
    - pip3.6 install --upgrade pip
    - pip install pytest
    - pytest test_sample.py

3. GitLab Runner の動作確認

コミットが終わったら、Webブラウザ上でGitLab のプロジェクトの画面にて下記の順でクリックし、実行されているジョブのログを表示します。

[$プロジェクト名]
  ┗ [CI/CD]
      ┗ [ジョブ]
          ┗ [実行中]

gitlab_runner_004.png

.gitlab-ci.yml に記述した内容が上から順に実行されていきます。
下記のように、Job succeeded と表示されたら、pylint と pytest のジョブがそれぞれ正常に完了したことになります。

gitlab_runner_005.png
gitlab_runner_006.png

ちなみに test_sample.py を元の値のままにすると、下記のようにJob failed となり、pytest のジョブが失敗します。
この失敗したことを、[ 新規課題 ] をクリックして、課題チケットとして発行することもできるようです。

gitlab_runner_007.png

また、スケジュール設定をすることで、指定した日時にジョブを実行することも可能です。
他にも、.gitlab-ci.yml に ** artifacts** パラメータを指定することで、実行結果をファイルとして保存することも可能です。
これらの機能も活用して、楽していきたいですね。

まとめ

CentOS 7にGitLab Runnerをインストールし、静的解析と単体テストを自動化することができました。
今回は、動作確認が容易なPythonで実行してみましたが、他の言語も自動化確認してみたいです。

proxy が絡むと設定しなければいけない点が増えますので、環境設定が大変になりそうです。
Docker Hub にあるイメージでは、ジョブがこけてしまう場合は、自分でDockerfile 作成して、専用のイメージを作成しないといけないかもしれないです。個人的にJava with Maven なんかは特に大変そうです(´・ω・`)。

自動化を進めていってできた時間をまた、別のことに費やして、どんどん楽していきたいですね。

参考URL

参考書籍

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerで立ち上げたGitLabの定期バックアップ方法

はじめに

どうも!生産技術部のエンジニアです。GitLabのサーバを立ち上げて、最初に実施する事の一つがデータのバックアップだと思います。ここでは、GitLabのアプリケーションデータ及び設定ファイルを定期バックアップする方法を紹介します。

GitLabサーバを一から構築される方は、以下からご覧ください。
「proxy環境下でDocker Composeを用いてCentOS7上にGitLab Dockerを作成」

環境

  • CentOS : 7.6.1810
  • Docker-CE : 19.03.1
  • Docker Compose : 1.25.0
  • GitLab-CE Docker : 12.2.0-ce.0

前提条件

Docker、GitLabの導入が実施済みであること。

「proxy環境下でDocker Composeを用いてCentOS7上にGitLab Dockerを作成」
を参考に導入してください。

DockerでGitLabを起動した場合のバックアップ

GitLabのバックアップをする上で、以下の二種類のデータのバックアップが必要になります。

  • アプリケーションデータ
    • データベースやリポジトリのデータ
  • 設定ファイル
    • gitlab.rbgitlab-secrets.jsonなどの設定ファイル

GitLabにはアプリケーションデータ用のバックアップコマンドが用意されていますが、設定ファイルはバックアップされません。公式によると、データベースには二段階認証のための暗号化情報などが含まれますが、それらの情報とそのキーを同じ場所に置くことは、暗号化を行う目的に反しているとか謎、、、細かいことは抜きにして、まずはアプリケーションデータのバックアップ方法から説明します。Dockerでのバックアップ方法は、以下の様に実施します。

$ sudo docker exec -t <container name> gitlab-backup create

<container name>は、以下のコマンドで確認します。

$ sudo docker ps
CONTAINER ID        IMAGE                         COMMAND                  CREATED             STATUS                PORTS                                                                                    NAMES
dc2c32a5a3d6        gitlab/gitlab-ce:latest       "/assets/wrapper"        8 days ago          Up 8 days (healthy)   0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp, 0.0.0.0:8001->8001/tcp, 0.0.0.0:4022->22/tcp   gitlab_gitlab_1

実際に実行すると、この様なログが表示され、バックアップが出来上がります。

$ sudo docker exec -t gitlab_gitlab_1 gitlab-backup create
<略>
2019-12-03 08:22:14 +0000 -- done
2019-12-03 08:22:14 +0000 -- Dumping uploads ... 
2019-12-03 08:22:14 +0000 -- done
2019-12-03 08:22:14 +0000 -- Dumping builds ... 
2019-12-03 08:22:14 +0000 -- done
2019-12-03 08:22:14 +0000 -- Dumping artifacts ... 
2019-12-03 08:22:14 +0000 -- done
2019-12-03 08:22:14 +0000 -- Dumping pages ... 
2019-12-03 08:22:14 +0000 -- done
2019-12-03 08:22:14 +0000 -- Dumping lfs objects ... 
2019-12-03 08:22:14 +0000 -- done
2019-12-03 08:22:14 +0000 -- Dumping container registry images ... 
2019-12-03 08:22:14 +0000 -- [DISABLED]
Creating backup archive: 1575361334_2019_12_03_12.3.5_gitlab_backup.tar ... done
Uploading backup archive to remote storage  ... skipped
Deleting tmp directories ... done
done
done
done
done
done
done
done
Deleting old backups ... done. (0 removed)
Warning: Your gitlab.rb and gitlab-secrets.json files contain sensitive data 
and are not included in this backup. You will need these files to restore a backup.
Please back them up manually.
Backup task is done.

バックアップを確認するとこの様なアーカイブファイルが生成されます。

# ls -la /srv/gitlab/data/backups/
total 18344
drwx------.  2 libstoragemgmt root       4096 Dec  3 17:22 .
drwxr-xr-x. 22 root           root       4096 Nov 25 10:11 ..
-rw-r--r--.  1 libstoragemgmt polkitd 2385920 Dec  3 17:22 1575361334_2019_12_03_12.3.5_gitlab_backup.tar

アプリケーションデータの定期バックアップ

設定ファイルにはbackup_path(バックアップファイルの保存先)、backup_archive_permissions(ファイルパーミッション)、backup_keep_time(キープタイム)を設定します。キープタイムを設定することで、古くなったファイルを自動的に削除してくれます。値の単位は秒ですので、1週間分残したい場合は$7(日間)\times24(時間)\times3600(秒) = 604800$となります。

gitlab.rb
### Backup Settings
###! Docs: https://docs.gitlab.com/omnibus/settings/backups.html

gitlab_rails['manage_backup_path'] = true
gitlab_rails['backup_path'] = "/var/opt/gitlab/backups"

###! Docs: https://docs.gitlab.com/ce/raketasks/backup_restore.html#backup-archive-permissions
gitlab_rails['backup_archive_permissions'] = 0644

# gitlab_rails['backup_pg_schema'] = 'public'

###! The duration in seconds to keep backups before they are allowed to be deleted
gitlab_rails['backup_keep_time'] = 604800

crontabは定期スケジューリング用のコマンドです。管理者権限を持ったユーザで実施します。

$ sudo su -
$ crontab -e

上記コマンドを実行すると、エディタが起動しますので、実行したいコマンドを記述し保存します。

# 分 時 日 月 曜日 <実行コマンド>
0 2 * * * docker exec -t gitlab_gitlab_1 gitlab-backup create CRON=1

# <***必ず最後に改行を入れてください。***>

正常に動作すると、1日1回(2:00)にバックアップが作成され、一週間が過ぎたバックアップファイルは削除されるようになります。

$ ls -la /srv/gitlab/data/backups/
total 16012
drwx------.  2 libstoragemgmt root       4096 Dec  3 02:00 .
drwxr-xr-x. 22 root           root       4096 Nov 25 10:11 ..
-rw-r--r--.  1 libstoragemgmt polkitd 2222080 Nov 27 02:00 1574787621_2019_11_26_12.3.5_gitlab_backup.tar
-rw-r--r--.  1 libstoragemgmt polkitd 2222080 Nov 28 02:00 1574874021_2019_11_27_12.3.5_gitlab_backup.tar
-rw-r--r--.  1 libstoragemgmt polkitd 2385920 Nov 29 02:00 1574960421_2019_11_28_12.3.5_gitlab_backup.tar
-rw-r--r--.  1 libstoragemgmt polkitd 2385920 Nov 30 02:00 1575046821_2019_11_29_12.3.5_gitlab_backup.tar
-rw-r--r--.  1 libstoragemgmt polkitd 2385920 Dec  1 02:00 1575133220_2019_11_30_12.3.5_gitlab_backup.tar
-rw-r--r--.  1 libstoragemgmt polkitd 2385920 Dec  2 02:00 1575219621_2019_12_01_12.3.5_gitlab_backup.tar
-rw-r--r--.  1 libstoragemgmt polkitd 2385920 Dec  3 02:00 1575306021_2019_12_02_12.3.5_gitlab_backup.tar

設定ファイルの定期バックアップ

backupsの中にconfigディレクトリを作成し、設定ファイルのバックアップを行います。バックアップの方法はtarコマンドを使います。バックアップファイルのファイル名はアプリケーションデータのバックアップファイル名に合わせるために、dateコマンドを利用します。--date '1 day ago'をつける事で1日前の日付を取得できます。1週間経過した設定ファイルのバックアップについても削除します。

# lオプションでcrontabに設定したコマンドを表示
$ crontab -l

# Creating backup archive
0 2 * * * docker exec -t gitlab_gitlab_1 gitlab-backup create CRON=1
# Create config backup archive
0 2 * * * tar cfz /srv/gitlab/data/backups/config/$(date --date '1 day ago' "+\%s_\%Y_\%m_\%d_12.3.5_gitlab_etc.tar.gz") -C /srv/gitlab config
# Delete old config backups
0 2 * * * find /srv/gitlab/data/backups/config -mtime +6 | xargs rm -rf

バックアップコマンドをスクリプトにまとめる

実行したコマンドをスクリプトにまとめます。注意が必要な点は、設定ファイルのバックアップファイル名に\%s_\%Y_\%m_\%dと記述しましたが、シェルスクリプトでは、バックスラッシュは不要です。

gitlab_backup.sh
#!/bin/sh

BKDIR=/srv/gitlab/data/backups

# Creating backup archive
docker exec -t gitlab_gitlab_1 gitlab-backup create

# Create config backup archive
tar cfz $BKDIR/config/$(date --date '1 day ago' "+%s_%Y_%m_%d_12.3.5_gitlab_etc.tar.gz") -C /srv/gitlab config

# Delete old config backups
find $BKDIR/config -mtime +6 | xargs rm -rf

crontabはこの様になります。

$ crontab -l

# Creating backup archive
# 0 2 * * * docker exec -t gitlab_gitlab_1 gitlab-backup create CRON=1
# Create config backup archive
# 0 2 * * * tar cfz /srv/gitlab/data/backups/$(date "+\%s_\%Y_\%m_\%d_12.3.5_gitlab_etc.tar.gz") -C /srv/gitlab config
# Delete old config backups
# 0 2 * * * find /srv/gitlab/data/backups/config -mtime +6 | xargs rm -rf
0 2 * * * <ファイルパス>/gitlab_backup.sh CRON=1

最後に

お疲れ様です。これで定期バックアップが可能になり、安心したGitLabライフを満喫できます。

ご参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Supervisor を Docker で入門する

supervisor を使ったことがないので理解したいと思います。

最終的には、以下の4つのファイルで、プロセスを二つ管理したいとおもいます。

$ tree
.
├── Dockerfile
├── hello.sh
├── hey.sh
└── supervisord.conf

まず適当に動かしてみる

ひとまず、supervisor を入れたコンテナを用意して、そのコンテナ内で supervisor の動きを確認してみます。

まず centos な環境で使いたかったので、Dockerfile を作成して、centos なイメージに pip いれて、それ経由で supervisor 入れます。

Dockerfile
FROM centos:7

# こっちでもいい
# RUN yum install -y python-setuptools && \
#     easy_install supervisor

RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \
    python get-pip.py && \
    pip install supervisor

次に、config ファイルを作りたいのですが。雛形を生成するコマンドがあるので、それを使ってみます。

コンテナの中に入って echo_supervisord_conf を打ちます。

[root@4e1290d0e494 /]# echo_supervisord_conf
; Sample supervisor config file.
;
; For more information on the config file, please see:
; http://supervisord.org/configuration.html
;
; Notes:
;  - Shell expansion ("~" or "$HOME") is not supported.  Environment
;    variables can be expanded using this syntax: "%(ENV_HOME)s".

...以下略...

これをそのまま supervisord.conf というファイルにコピペしてしまいます。

コメントアウトを消すと以下のような感じです。

supervisor.conf
[unix_http_server]
file=/tmp/supervisor.sock

[supervisord]
logfile=/tmp/supervisord.log
logfile_maxbytes=50MB       
logfile_backups=10          
loglevel=info               
pidfile=/tmp/supervisord.pid
nodaemon=false              
minfds=1024                 
minprocs=200                

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=unix:///tmp/supervisor.sock

つぎに動かすプログラムを書きます、本当に少しです。

ひとまず、永久に動き続けるのを書いてみます。

hello.sh
#!/bin/bash

while :; do
    echo "Hello, world!"
    sleep 1
done

これを supervisor 経由で動かすには、supervisord.conf に以下を追記します。

[program:hello]
command=/hello.sh
stdout_logfile=/tmp/hello.log

これでコンテナ内に、hello.sh/ に、supervisord.conf/etc に配置して、コンテナ内で以下のコマンドを打つと動きます。

supervisor -c /etc/supervisord.conf

Dockerfile の CMD で動かすようにする

Dockerfile
FROM centos:7

RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \
    python get-pip.py && \
    pip install supervisor

COPY hello.sh /hello.sh
COPY hey.sh /hey.sh
COPY supervisord.conf /etc/supervisord.conf

CMD supervisord -c /etc/supervisord.conf
hello.sh
#!/bin/bash

sleep 1
echo 'hello'

hey.sh も実質 hello.sh と同じ。

supervisord.conf
[unix_http_server]
file=/tmp/supervisor.sock   ; the path to the socket file

[supervisord]
logfile=/tmp/supervisord.log ; main log file; default $CWD/supervisord.log
logfile_maxbytes=50MB        ; max main logfile bytes b4 rotation; default 50MB
logfile_backups=10           ; # of main logfile backups; 0 means none, default 10
loglevel=info                ; log level; default info; others: debug,warn,trace
pidfile=/tmp/supervisord.pid ; supervisord pidfile; default supervisord.pid
nodaemon=true                ; start in foreground if true; default false
minfds=1024                  ; min. avail startup file descriptors; default 1024
minprocs=200                 ; min. avail process descriptors;default 200

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL  for a unix socket

[program:hello]
command=/hello.sh
stdout_logfile=/tmp/hello.log
autostart=true
autorestart=true

[program:hey]
command=/hey.sh
stdout_logfile=/tmp/hey.log
autostart=true
autorestart=true

重要なポイントは nodaemon=true としている点です。これをしないとコンテナがすぐ終了しちゃいます。

動作の確認には以下のような docker コマンドでやっています。

# ビルド
$ docker build -t supervisor-image .

# ラン
$ docker run --name supervisor-container -d supervisor-image

# これでコンテナ内へ
$ docker exec -it supervisor-container bash

# プロセスを見てみる
[root@7cf591f95fbd /]# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  1.4  0.9 115748 19948 ?        Ss   07:33   0:00 /usr/bin/python /usr/bin/supervisord -c /etc/supervisord.conf
root        49  0.5  0.1  11836  2864 pts/0    Ss   07:33   0:00 bash
root        86  0.0  0.1  11696  2540 ?        S    07:33   0:00 /bin/bash /hello.sh
root        87  0.0  0.1  11696  2668 ?        S    07:33   0:00 /bin/bash /hey.sh
root        88  0.0  0.0   4372   692 ?        S    07:33   0:00 sleep 1
root        89  0.0  0.0   4372   688 ?        S    07:33   0:00 sleep 1
root        90  0.0  0.1  51760  3516 pts/0    R+   07:33   0:00 ps aux

# ログを見てみる
[root@7cf591f95fbd /]# tail -f /tmp/hey.log
hey
hey
hey
hey

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerのマルチステージビルドで、ビルド環境と実行環境のセットアップを1つのDockerfileで完結させよう

Code Chrysalis Advent Calendar 2019、24日目の投稿です。

こんにちは。現在、CodeChrysalisのイマーシブブートキャンプ(Cohort 10)に参加中のなおとです。

ブートキャンプも残すところ1週間を切りました。今は12/26(木)に行われるDemo Dayという卒業発表会に向けてチーム開発を日々頑張っています。お時間ある方は是非Demo Dayにご参加ください!
Demo Day の詳細はこちらになります。

Code Chrysalisのブートキャンプには、多言語週間(Polyglottal Week)と呼ばれる、今まで使ったことのないプログラミング言語を1つ選択し、1週間で言語の習得からアプリケーション開発までを行うという機会があります。

私はこの多言語週間で、今まで扱ったことのなかったGolangを選択し、Golangを使ったフルスタックアプリケーションを作成しました。また、Dockerに興味があったので、Docker上でアプリをビルド・実行する方法も合わせて学びました。今回は、私が学んだ内容の一部についてご紹介したいと思います。

はじめに

本稿では、Dockerの基本的な知識があることを前提として、Dockerの重要な機能の1つであるマルチステージビルドについて解説していきます。

また、以下のレポジトリにサンプルコードを用意しました。今回説明する内容は、すべて以下のレポジトリをもとに検証を行っているので、ご自身の環境で試したい場合は合わせてご覧ください。
https://github.com/Imamachi-n/docker-multi-stage-build-101

Docker上でアプリケーションをビルド・実行してみよう

まず始めに、作成したアプリケーションをDockerコンテナ上でビルド・実行したい場合、どのような方法を取ればいいのでしょうか?まずは簡単な例として、Golangで作成したアプリケーション(REST APIサーバ)を以下のDockerfileを用いて、ビルド・実行する場合を考えてみましょう。

以下に、サンプルのDockerfileを示しました。Dockerのベースイメージとして、公式のgolangイメージを使っています。
ファイルの内容を簡単に説明すると、
1. ENVでGolangのビルド条件(OS、CPUアーキテクチャ等)を環境変数として設定。
2. WORKDIRで作業ディレクトリを指定。
3. COPYで、ローカル環境にあるGolangプロジェクトをDockerコンテナ内にコピー。
4. RUNでGolangのアプリケーションをビルドするコマンドを実行。
5. EXPOSEで9000番のポートを開け、このポートを通してDockerコンテナと通信ができるように設定。
6. 最後に、ENTRYPOINTでDockerコンテナ起動時に、Golangのアプリケーションが起動するように指定。
という内容が記述されています。

FROM golang:1.13.4

ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
WORKDIR /docker-multi-stage-build-101
COPY . .
RUN go build -o "bin/goServer"
EXPOSE 9000
ENTRYPOINT ["/docker-multi-stage-build-101/bin/goServer"]

Dockerfileから、Dockerイメージをビルドし実行すると、Golangで作成したREST APIサーバが立ち上がります。

$ docker build . -f docker/01_raw/Dockerfile -t go-server-raw:dev
$ docker run --rm -p 9000:9000 go-server-raw:dev
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /api/user/:name/*action   --> docker-multi-stage-build-101/route.GetAction (4 handlers)
[GIN-debug] GET    /api/welcome              --> docker-multi-stage-build-101/route.GetWelcome (4 handlers)
[GIN-debug] Listening and serving HTTP on :9000
2019/12/22 02:31:38 Defaulting to port :9000

続いて、以下の通りに、curlコマンドを使ってDockerコンテナで立ち上げたREST APIサーバにアクセスしてみましょう。Hello Code Chrysalisと表示されれば成功です。これで、Dockerコンテナ上でアプリケーションが起動していることが確認できました。

$ curl -X GET 'http://localhost:9000/api/welcome?firstname=Code&lastname=Chrysalis' 
Hello Code Chrysalis

次に、docker imagesコマンドを使って、Dockerイメージのサイズを見てみましょう。なんと、915MBというかなり大きなサイズになっていることがわかります。

$ docker images
REPOSITORY        TAG    IMAGE ID            CREATED             SIZE
go-server-raw     dev    eb8f35015c94        11 seconds ago      915MB

これは、アプリケーションの実行に不要なもの(ビルド時に使用したライブラリ等)が、そのまま同じDockerイメージ内にゴミとして残ってしまっているためです。AWSやGCPなどの環境にデプロイすることを考えると、できればDockerイメージのサイズを小さくしたいですね。

この問題を解決するために、マルチステージビルドが登場する以前は、Builderパターンと呼ばれるコンテナ管理方法が利用されていました。

マルチステージビルド以前: Builderパターン

Builderパターンでは、ビルド用とデプロイ用の2つのDockerfileを用意します。ビルド用のDockerコンテナでアプリケーションをビルドし、アプリケーションの実行に必要なものだけを実行用のコンテナにコピーします。

結果として、Builderパターンでアプリケーションが動作するDockerコンテナを用意した場合、以下のものが必要となります。
- 2つのDockerfile(ビルド用と実行用)
- (ビルド環境から実行環境への)ビルド成果物の受け渡し用のシェルスクリプト

これにより、上述したDockerイメージの肥大化を防ぐことができます。ただ、ちょっと考えてみてください。これってめんどくさくないですか?あと、複数のファイルに設定情報が散らばってしまっています。作成するDockerイメージが増えていった場合、ソースコードの管理が複雑化していく気がします…。

Dockerマルチステージビルド

そこで、満を持してマルチステージビルドの登場です。この機能は、Docker 17.05から追加された機能になります。端的に言うと、Builderパターンで実践していた内容を1つのDockerfileにまとめることができます。

ビルドステージ(中間コンテナイメージ)

普段ベースイメージを指定するために、Dockerfile内にFROM命令を記述していると思います。マルチステージビルドでは、以下のように、1つのDockerfile内にFROM命令を複数記述します。

前半のFROM以下のブロックのことをビルドステージ(中間Dockerイメージ)と呼んでいます。この中間Dockerイメージは、実行用Dockerイメージにビルド成果物を渡した後、削除されます。そのため、最終的に生成される実行用のDockerイメージに含まれることはありません。

ビルドステージの命名

続いて、ASを使うことで、ビルドステージに対して名前を付けることができます。下図の例は、ビルドステージの中間Dockerイメージに対してbuilderという名前を指定しています。

ちなみに名前を指定しなかった場合、FROM命令の順番に合わせて0, 1, 2,...という連番名が自動で振られます。例えば、上図の場合だと、ビルドステージは0、実行用のDockerイメージは1という連番名が振られます。

ビルド成果物をビルド環境から実行環境のDockerイメージへコピー

ビルドステージで作成したビルド成果物を、後半のFROM以下のブロック(アプリケーション実行用のDockerイメージ)にコピーすることができます。方法としては、COPY--fromオプションでビルドステージ名を指定し、ビルド成果物をビルドステージから実行用のDockerイメージにコピーします。

このように、マルチステージビルドを使うことで、ビルド用と実行用のDockerfileを分けることなく、1つのDockerfileで記述することができるようになります。実はそれ以外にもメリットがあります。

ビルド用と実行用にDockerベースイメージをそれぞれ指定することができるので、例えば、alpineなどの軽量コンテナイメージを実行用のDockerベースイメージとして選択することができます。こうすることで、作成されたDockerイメージ内に、アプリケーションの実行に必要なものだけを配置することができ、コンテナのサイズをよりスリム化させることができます。

マルチステージビルドの具体例

それでは、具体的な例として、最初にお見せしたGolangのアプリケーション(REST APIサーバ)をマルチステージビルドを使ったDockerfileに書き換えてみたいと思います。

以下がDockerfileの内容になります。先程とほとんど変わりませんが、COPY --from=builderでビルドステージ内のビルド成果物(/docker-multi-stage-build-101/bin/goServer)を、実行用のDockerイメージに/goServerとしてコピーしています。

# Builder image (intermediate container)
FROM golang:1.13.4 as builder

ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
WORKDIR /docker-multi-stage-build-101
COPY . .
RUN go build -o "bin/goServer"

# Runtime image
FROM alpine
COPY --from=builder /docker-multi-stage-build-101/bin/goServer /goServer
EXPOSE 9000
ENTRYPOINT ["/goServer"]

それでは、Dockerfileをビルドして、Dockerイメージのサイズがどれだけスリム化された確認してみましょう。

$ docker build . -f docker/02_multi-stage-build/Dockerfile -t go-server-multi:dev
$ docker images
REPOSITORY        TAG    IMAGE ID            CREATED             SIZE
go-server-multi   dev    2d5f5819aa81        4 hours ago         21.4MB

21.4MBとかなり小さなDockerイメージとなっていることがわかると思います。最初の例では915MBだったので、Dockerイメージのサイズをおよそ1/40程度までスリム化できています。

最後に

Dockerのマルチステージビルドを利用することで、アプリケーションのビルド環境と実行環境の設定を1つのDockerfileに記述することができます。これは、今まで使われていたBuilderパターンなどと比較しても、より簡素で管理のしやすい方法だと感じました。Dockerfileを記述する際は、積極的に使っていくといいのではないかと思います。

参考

Use multi-stage builds
Dockerの公式ドキュメントの説明です。
https://docs.docker.com/develop/develop-images/multistage-build/

docker-multi-stage-build-101
今回の記事で使用したサンプルコードになります。
https://github.com/Imamachi-n/docker-multi-stage-build-101

BioRxivGo
Code Chrysalisの多言語週間中に作成したフルスタックアプリケーション(Vue, Golang, PostgreSQLなど)になります。こちらのアプリケーションではマルチステージビルドに加えて、Docker composeでアプリケーションを起動しています。
https://github.com/Imamachi-n/BioRxivGo

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerで始めるStackstorm再入門2/3(条件分岐させるWorkflowと定期実行させるRuleの書き方)

本記事はDockerで始めるStackstorm再入門(環境構築からOrquestaで書いたWorkflowの結果をslackに通知するところまでのチュートリアル)の第2部です。

0. 目次

本記事では前編で取り上げることができなかったStackstormの構成ファイルの書き方についてお話しできればと思います。

  • 1. Actionとは
  • 2. Workflowとは
  • 3. Workflowで引数や結果に応じて分岐させる書き方(基本編)
  • 4. Workflowで引数や結果に応じて分岐させる書き方(shellscript編)
  • 5. サンプルWorkflow
  • 6. Ruleを使ってAction/Workflowを定期実行
  • 7. shellscriptのデバッグについて
  • 8. 次回予告

5. サンプルWorkflowでは、リモートリポジトリのステータスの確認とコンテナの再立ち上げをshellscriptをActionの実行関数としながら、workflowを紹介し、
その前の1から4でActionやworkflowの前提知識をお伝えしています。
最後にRuleとshellscriptのデバッグについて少し書いています。

1. Actionとは

Stackstormがおこなう処理のことです。

このActionは大きく分けて以下の3種類があります。

  • core.localなどStackstormが提供するAction
  • サードパーティが提供するAction(Ansible plabookがst2上で扱えるものなど)
  • 独自に作ったAction

Actionの構成ファイルとしては、以下の2つです。

  • 処理を定義するyamlファイル(メタファイル)
  • Actionの実行スクリプト(Action Runnner

Writing Custom Actions
An action is composed of two parts:

A YAML metadata file which describes the action, and its inputs.

A script file which implements the action logic

As noted above, an action script can be written in an >arbitrary programming language, as long as it follows these >conventions:

Script should exit with 0 status code on success and non-zero >on error (e.g. 1)

All log messages should be printed to standard error

参考: Action Runners — StackStorm 3.1.0 documentation

処理を定義するActionのメタファイル

ここでは、私が作ったmydemo_packというPACKのActionのメタファイルを一例として取り上げます。
やっていることは、cd $working_dir && git fetch -p && git checkout -q $branch && git statusをしてリモートリポジトリの更新を確認することです。

/opt/stackstorm/pack/mydemo_pack/actions/git_status.yaml
---
name: "git_status"
pack: "mydemo_pack"
description: "git status"
enabled: true
runner_type: "local-shell-script"  # 後述します
entry_point: "scripts/git_status.sh" # 本メタファイルからみた相対パスとなります。
parameters:
  working_dir:
    type: "string"
    required: true
    position: 0
  branch:
    type: "string"
    required: true
    position: 1
  expected:
    type: "string"
    required: true
    position: 2

公式ドキュメントで紹介されていたrunner_type

このように複数ありますが、だいたい使うのは文字の背景がグレーとなっているものかなと思います。

  • local-shell-cmd
  • local-shell-script
  • remote-shell-cmd
  • remote-shell-script
  • python-script
  • http-request
  • action-chain
  • mistral-v2
  • cloudslang
  • inquirer
  • winrm-cmd
  • winrm-ps-cmd
  • winrm-ps-script

参考: Actions — StackStorm 3.1.0 documentation

なお、orqeustaについては取り上げられていませんが、orqeustaを使う場合、workflowのメタファイルにrunner_type: "orquesta"と書くことでorquestaを扱うことが出来ました。

参考: 
st2/orquesta-streaming-demo.meta.yaml at master · StackStorm/st2

Actionの実行スクリプト(Action Runnner)

これは先ほど処理を定義するメタファイルで取り上げたActionのメタファイルのなかのentry_point: "scripts/git_status.sh"です。

/opt/stackstorm/packs/mydemo_pack/actions/scripts/git_status.sh
#!/bin/bash

# exit 0以外のリターンコードが返ることがあれば、そこで抜けるようにする
set -e

working_dir=$1
branch=$2
expected=$3

if [ -d "$working_dir" ]; then
  cd $working_dir
  sudo git fetch -p
  sudo git checkout -q $branch

  # ローカルリポジトリは最新であることを想定してgit status
  if [ "$expected" = "up_to_date" ]; then
    output=$(sudo git status | grep -E "(Your)\s+(branch)\s+(is)\s+(up-to-date)\s+(with)\s+('origin/$branch')" | awk '{print $6}' | grep -oP "$branch")
  # ローカルリポジトリは最新ではないこと(リモートリポジトリから更新を受け取る必要があること)を想定してgit status
  elif [ "$expected" = "not_up_to_date" ]; then
    output=$(sudo git status | grep -E "(Your)\s+(branch)\s+(is)\s+(behind)\s+('origin/$branch')" | awk '{print $5}' | grep -oP "$branch")
  else
    echo "None of the condition met"
  fi

  output=$(echo ${output:="unknown"})

  # git statusの結果が想定通りであるか
  if [ "$output" = "$branch" ]; then
    exit 0
  fi
fi

2. Workflowとは

runner_typeでworkflowのmistralとorqeustaが取り上げられたので、ここでお話します。
Workflowとはひとつひとつの処理(Action)を下の図のように繋げたものです。
Screenshot from 2019-12-21 16-18-16.png

workflowのメタファイル

workflowのメタファイルの記載項目は、Actionのメタファイルのそれとほとんど変わりません。
強いて言えばメタファイルを書くworkflowをst2 runで引数について定義することなく実行させる場合、default: "hoge"などと引数の値を定義する必要があるということと、runner_type: "orquesta"くらいです。

workflowはActionと違い、メタファイルの拡張子とworkflow自身のファイルの拡張子が同じyamlと紛らわしいので、私はWorkflowのメタファイルは*.meta.yamlと命名しています。

/opt/stackstorm/packs/mydemo_pack/actions/poll-repo.meta.yaml
---
name: "poll-repo"
pack: "mydemo_pack"
description: "poll repo"
runner_type: "orquesta"
entry_point: "workflows/poll-repo.yaml"
enabled: true
parameters:
  working_dir:
    type: "string"
    required: true
    default: "/usr/src/app/flask-docker"
  branch:
    type: "string"
    required: true
    default: "devel-views"
  expected:
    type: "string"
    required: true
    default: "up_to_date"
  ptn:
    type: "string"
    required: true
    default: "flask-docker_flask|flask-docker_nginx"
  timeout:
    type: "integer"
    required: true
    default: 300

3. Workflowで引数や結果に応じて分岐させる書き方(基本編)

以下の2つはAction問わず、使うと思います。

  • Actionの成否
  • 引数や変数の値に応じて

Actionの成否

Actionが成功したというのは、Actionのreturn codeが0であるとき、失敗したというのは、0以外であるときです。
(筆者調べ)

Actionが成功した場合.yaml
- when: <% succeeded() %>` 
Actionが失敗した場合.yaml
- when: <% failed() %>` 

引数や変数の値に応じて

boolean値で分岐させる場合.yaml
tasks:
  init:
    action: core.noop
    next:
      - do: doSth
        publish:
          - failed: False  # boolean値を初期化
 doSth:
    action: xxxx
    next:
      - when: <% failed() %> 
        do: last
        publish:
          - failed: True  # boolean値を更新

 last:
   action: core.noop
     next:
       - when: <% ctx().failed %> # boolean値がTrueであるときfailを実行
         do: fail
     #- when: <% not ctx().failed %> # boolean値がFalseであるとき
       #  do: yyyy
引数の値に応じて.抜粋.yaml
   # 「5. サンプルWorkflow」で取り上げているworkflowの抜粋です

  # expectedはworkflowのメタファイルでdefault値として定義することと併せて、Actionの成否に応じて値を更新(publish)しています

    next:
      - when: <% succeeded() and (ctx().expected = 'up_to_date') %>
        do: last
      - when: <% succeeded() and (ctx().expected = 'not_up_to_date') %>
        do: git_merge
        publish:
          - failed: False

4. Workflowで引数や結果に応じて分岐させる書き方(shellscript編)

shellscriptでActionを実行する場合、Actionの結果に応じてworkflowを条件分岐する際に使うことができるのは、上で取り上げた方法の他には、リターンコードだけのようです。(筆者調べ)

リターンコードで分岐.抜粋.yaml
 # 「5. サンプルWorkflow」で取り上げているworkflowの抜粋です

  rebuild_app: # 立ち上がっているコンテナを停止/削除した後、再立ち上げを図る
    action: mydemo_pack.rebuild_app
    input:
      working_dir: <% ctx().working_dir %>
      ptn: <% ctx().ptn %>
      timeout: <% ctx().timeout %>
    next:
      - when: <% succeeded() %>
        do: post_msg
        publish:
          - failed: False
          - action_result: <% result() %>
      - when: <% failed() %>
        do: check_failed
        publish:
          - action_result: <% result() %>
          - rc: <% result().return_code %>

  check_failed: # rebuild_appがfailedした原因を調査
    action: core.noop
    next:
    - when: <% ctx().rc = 201 %>  # failedの原因は停止/削除するコンテナがなかったことなので、改めてコンテナの再立ち上げを図る
      do: rebuild_app_cmd
    - when: <% ctx().rc != 201 %>
      do: post_msg
      publish:
      - failed: True 

リターンコードはshellscriptでこのように返却しています。

/opt/stackstorm/packs/mydemo_pack/actions/scripts/rebuild_app.sh
#!/bin/bash

# exit 0以外のリターンコードが返ることがあれば、そこで抜けるようにする
set -e

working_dir=$1
ptn=$2
counter=0

if [ -d "$working_dir" ]; then
  cd $working_dir
  # image idを取得
  ids=$(sudo docker container ls | grep -E "${ptn}" | awk '{print $1}')
 # image idをfor-loopでstop/rmしていき、成功すれば$counterをインクルメント
 # image idがひとつもなければfor-loopは行われず、$counterもインクルメントされない
  for i in $ids;
  do
    sudo docker container stop $i \
    && sudo docker container rm $i;
    counter=`expr $counter + 1`
  done
  # image idから2回特定のコンテナをstop/rmsしている場合
  if [ $counter -eq 2 ]; then
    sudo docker-compose up -d --build
    counter=`expr $counter + 1`
  # 1度も特定のコンテナをstop/rmsしていない場合(そもそもコンテナが立ち上がっていない場合)
  elif [ $counter -eq 0 ]; then
    exit 201
  fi
fi

if [ $counter -eq 3 ]; then
  exit 0
fi

5. サンプルWorkflow

上のowrkflowの書き方で取り上げたサンプルはこちらから抜粋しています。

/opt/stackstorm/packs/mydemo_pack/actions/workflows/poll-repo.yaml
version: 1.0

description: poll remote repo

input:
  - working_dir
  - branch
  - expected
  - ptn
  - timeout

output:
  - failed: <% ctx().failed %>
  - action_name: <% ctx().action_name %>

tasks:
  init:  # failedフラグを初期化(False)とするだけのAction
    action: core.noop
    next:
    - publish:
        - failed: False
        - action_name: 'poll_repo'
      do: git_status_before_merged

  git_tatus_before_merged: # ローカルリポジトリは最新であることを想定してgit status
    action: mydemo_pack.git_status
    input:
      working_dir: <% ctx().working_dir %>
      branch: <% ctx().branch %>
      expected: <% ctx().expected %> #デフォルト値は'up_to_date'
      timeout: <% ctx().timeout %>
    next:
      - when: <% succeeded() and (ctx().expected = 'up_to_date') %>
        do: last
      - when: <% succeeded() and (ctx().expected = 'not_up_to_date') %> # 'not_up_to_date'(最新ではないこと)が確認できた
        do: git_merge
        publish:
          - failed: False
      - when: <% failed() and (not ctx().failed) %>
        do: git_status_before_merged
        publish:
          - failed: True
          - expected: 'not_up_to_date'
      - when: <% failed() and (ctx().failed) %>
        do: post_msg
        publish:
          - action_result: |-
              [result]
              <% result() %>

  git_merge:  # git_status_before_mergedで、ローカルリポジトリは最新ではないことが確認出来きた場合のみ実行
    action: core.local
    input:
      cmd: sudo git merge origin/<% ctx().branch %>
      cwd: <% ctx().working_dir %>
      timeout: <% ctx().timeout %>
    next:
      - when: <% succeeded() %>
        do: git_status_after_merged  
        publish:
          - expected: 'up_to_date'
      - when: <% failed() %>
        do: post_msg
        publish:
          - failed: True
          - action_result: |-
              [result]
              <% result() %>

  git_status_after_merged:     # ローカルリポジトリは最新であるとしてgit_status_after_mergedを実行
    action: mydemo_pack.git_status
    input:
      working_dir: <% ctx().working_dir %>
      branch: <% ctx().branch %>
      expected: <% ctx().expected %>
      timeout: <% ctx().timeout %>
    next:
      - when: <% succeeded() %>
        do: rebuild_app
        publish:
          - action_result: |-
              [result]
              <% result() %>
      - when: <% failed() %>
        do: post_msg
        publish:
          - failed: True
          - action_result: |-
              [result]
              <% result() %>

  rebuild_app: # 立ち上がっているコンテナを停止/削除した後、再立ち上げを図る
    action: mydemo_pack.rebuild_app
    input:
      working_dir: <% ctx().working_dir %>
      ptn: <% ctx().ptn %>
      timeout: <% ctx().timeout %>
    next:
      - when: <% succeeded() %>
        do: post_msg
        publish:
          - failed: False
          - action_result: <% result() %>
      - when: <% failed() %>
        do: check_failed
        publish:
          - action_result: <% result() %>
          - rc: <% result().return_code %>

  check_failed: # rebuild_appがfailedした原因を調査
    action: core.noop
    next:
    - when: <% ctx().rc = 201 %>  # failedの原因は停止/削除するコンテナがなかったことなので、改めてコンテナの再立ち上げを図る
      do: rebuild_app_cmd
    - when: <% ctx().rc != 201 %>
      do: post_msg
      publish:
      - failed: True 

  rebuild_app_cmd: # docker-compose up -d --buildを実行
    action: core.local
    input:
      cmd: sudo docker-compose up -d --build
      cwd: <% ctx().working_dir %>
      timeout: 600
    next:
      - when: <% succeeded() %>
        do: post_msg
        publish:
          - action_result: |-
              [result]
              <% result() %>
      - when: <% failed() %>
        do: post_msg
        publish:
          - action_result: |-
              [result]
              <% result() %>

  post_msg:  # workflownの成否をslackに通知
    action: slack.post_message
    input:
      message: |-
        [action_name] 
        <% ctx().action_name %>
        [failed]
        <% ctx().failed %>
        [action_result]
        <% ctx().action_result %>
    next:
      - do: last

  last:  # workflow全体の成否を決める上で考慮するべき失敗したActionがあるかfaildフラグで確認
    action: core.noop
    next:
      - when: <% ctx().failed %>
        do: fail # workflow全体の結果をfailedとするorquestaが提供するEngine Command

failの使いどころ

failは、orquestaが提供する、workflow全体の結果をfailedとするorquestaが提供するEngine Commandです。
そもそもworkflowのステータスは最後のActionのステータスによって決まるので、途中のActionの失敗をworkflowのステータスに反映出来ません。
そこでこのような使い方が考えられます。

  • workflowのステータスを判定するフラグをworkflowの冒頭で定義
  • このフラグはworkflowの途中のActionなど任意のタイミングで値を更新
  • 最後のActionでそのフラグに応じてfailを実行

The workflow engine will fail the workflow execution.
参考: Orquesta Workflow Definition — StackStorm 3.1.0 documentation

6. Ruleを使ってAction/Workflowを定期実行

以下のyamlを作ってください。

root@$ID:/# cat /opt/stackstorm/packs/mydemo_pack/rules/timer.yaml
---
name: "timer"
pack: "mydemo_pack"
description: "run mydemo per 300 secs"
enabled: true
trigger:
  type: "core.st2.IntervalTimer"
  parameters:
    unit: seconds
    #delta: 30
    delta: 300
action:
  ref: "mydemo_pack.poll-repo"
  parameters:
    working_dir: "/usr/src/app/flask-docker"
    branch: "devel-views"
    expected: "up_to_date"
    ptn: "flask-docker_flask|flask-docker_nginx"

それではRuleを作るコマンドを以下のとおり実行すれば設定完了です。

root@$ID:/# st2 rule create timer.yaml

7. shellscriptのデバッグについて

以下の2つの方法でデバッグさせていました。

  • Action単体で実行
root@$ID:/# st2 run mydmeo_pack.git_status
  • bash -x /path/to/$filenameでデバッグ
root@$ID:/# bash -x /opt/stackstorm/packs/mydemo_pack/actions/scripts/git_status.sh \
> /usr/src/app/flask-docker \
> flask-docker_flask|flask-docker_nginx

8. 次回予告

shellscriptでリモートリポジトリのステータスの確認とコンテナの再立ち上げを行いましたが、今度はそれをpythonでおこなってみます。
pythonでActionの実行スクリプトを書くと、shellscriptに比べてActionの返却値(return value)を柔軟に定義することができたり、mockを使ってテストコードも書くことができるので、よりきめ細かくActionを書くことができます。

お楽しみに!!

鋭意作成中
Dockerで始めるStackstorm再入門3/3(pythonスクリプトの書き方とmockを使ったリファクタリング)

参考

P.S. Twitterもやってるのでフォローしていただけると泣いて喜びます:)

@gkzvoice

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Spring Boot+Thymeleaf+DockerでMiRmの基幹システムを開発した (PWA対応)

普通科高校の2年生をやっている@itsu_devです。
今回はMiRm(Minecraft 無料マルチプレイサーバーホスティングサービス)の基幹システムの入れ替えに伴い、@haniokasai氏と新しくシステムを開発しなおしたのでそれを紹介します。

はじめに

そもそもMiRmとは?

Minecraftを友達としようにも、

  • サーバーが必要
  • ソフトの導入・設定が面倒
  • 環境構築が難しい
  • 維持費がかかる

といった欠点があり、なかなか敷居の高いものでした。
そこでこれらすべてを解決すべく始まったのがMiRm Projectです。

MiRmにはどんな機能があるの?

  • サーバーソフトの標準出力のリアルタイム表示
  • サーバーソフトへの標準入力
  • FTP経由でのファイル操作
  • ワンタッチでのサーバー起動・停止
  • Markdown記法によるサーバーリストへの紹介文掲載

こういった機能をメインに、ほかにも様々な機能をユーザーに提供しています。

技術的な内容

使用した技術

タイトルにもあるように、Spring BootベースのWebアプリケーションとしてすべてを完結させています。「スマホだけでできる」がウリなので、もちろんPWAにも対応しています。
各ユーザーが所有するサーバーは一つのDockerコンテナで完結しており、ファイル操作やサーバーソフトの動作もすべてこの中で行っています。

他の主なものとしては

  • Thymeleaf(htmlテンプレートエンジン)
  • MySQL
  • Material Design Bootstrap(Bootstrapのマテリアルデザインライブラリ)
  • jQuery
  • SimpleMDE(埋め込みMarkdownエディタ)
  • darkmode.js(ダークモードの実装)
  • kotlin coroutine

といったところです。
全体を管理する基幹システムはkotlinで書かれています。

全体構成

FlowChart_Qiita.png

基幹システムとDockerコンテナとの接続

基本的にはDocker-JavaとJava標準のProcess経由でのコマンド実行の両方を使っています。

Minecraftのコマンドを標準入力に書き込むのに、普通にdocker attach CONTAINERをProcess経由で実行して得たそれに対して行ってもよいのですが、それだとある問題が発生します。
それは、コンテナ内部のサーバーソフトがPID 1で動作していないことです。
この問題によってProcessで得られる標準入力に書き込んでもサーバーソフトに書きこまれない現象が発生しました。それを解決するために、コマンドを/proc/PID/fd/0に書き込むことにしました。

以下のコードでは、上記のPIDを得るためにdocker topコマンドの内容をパースしています。
※MiRmDockerClient#getCommandExceptList サーバーソフトのプロセスを取得するために除外する文字列のリスト
※TopElementクラス 出力内容のオブジェクトクラス

public static List<TopElement> parse(String serverId) {
        ArrayList<TopElement> elements = new ArrayList<>();
        LinkedList<String> outputs = new LinkedList<>();

        try {
            StringBuffer buf = new StringBuffer();
            buf.append("docker -H "+ DOCKERHOST + " top " + serverId + MiRmDockerClient.getPrefix());
            MiRmDockerClient.getCommandExceptList().forEach(str -> buf.append(" | grep -v \"" + str + "\""));

            ProcessBuilder pb = new ProcessBuilder("/bin/bash", "-c", buf.toString());
            Process process = pb.start();
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
            String temp;
            while ((temp = reader.readLine()) != null) {
                outputs.add(temp);
            }
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        outputs.forEach(str -> {
            str = str.replaceAll("\\s+", " ");
            if (!str.startsWith("UID")) {
                String[] data = str.split(" ");

                StringBuffer buf = new StringBuffer();
                for (int i = 7; i < data.length; i++) {
                    buf.append(" " + data[i]);
                }

                elements.add(new TopElement(
                        data[0],
                        Long.parseLong(data[1]),
                        Long.parseLong(data[2]),
                        Integer.parseInt(data[3]),
                        data[4],
                        data[5],
                        data[6],
                        buf.toString().substring(1)
                ));
            }
        });

        return elements;
    }

SimpleMDE

超簡単にMarkdownエディタをWebページに実装可能な"SimpleMDE"を参考にさせていただきました。ありがとうございます。

MiRmでサーバーリストを表示するのに、各サーバーに紹介文を書いてもらいたかったのですが、普通の文章だとみんな同じで目立たなくなってしまう。とはいえhtmlを敷居が高い...
と思った矢先、SimpleMDEというjsライブラリを見つけたのでこれを使用して簡単なMarkdownに対応させることにしました。

右下のLobiのアイコンはフォントを作成して表示しています。(参考:https://nelog.jp/feedly-web-iconic-font)

編集画面
a.PNG

実際の表示
b.PNG

編集はMarkdownですが、表示はhtmlで行っています。この変換もSimpleMDEに標準でついており、簡単に使用することができます。

以下のコードでtextareaをエディタ化しています。
toolbarの部分でエディタ上部のツールバーをカスタマイズできます。

<html>
<head>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css">
</head>

<body>
<label for="description">マークダウン記法が使えます。(htmlタグは使用不可・ボタンとして載っている記法のみ使用可能)</label>
<textarea class="form-control z-depth-1" id="description" name="description" rows="8" cols="40"
                          placeholder="xxサーバーへようこそ!" th:text="${description}">
</textarea>

<script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
<script>
var simplemde = new SimpleMDE({
            element: document.getElementById("description"),
            forceSync: true,
            spellChecker: false,
            toolbar: ["bold", "italic", "strikethrough", "heading", "|", "quote", "unordered-list", "ordered-list", "link", "|", "preview", "side-by-side", "guide"]
        });

 marked.setOptions({
            sanitize: true,
            sanitizer: escape,
            breaks: true
        });
</script>
</body>
</html>

今後の展望

コントロールパネルや設定画面等、コンテンツの中身が多いがゆえに表示速度が若干遅いので、その辺をもっと改善していきたいと思っています。
また将来的にはより簡単に操作できるバージョンのリリースも考えています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

KafkaからKSQLまで一気にハンズオン入門

目的

本記事はハンズオンを通してApache Kafkaに触れ、少しでも多くの方にKafkaの良さを理解していただくことを目的としています。細かいKafkaの実装や仕組みについては割愛し、実際にKafkaを用いることでどのような処理が可能になるのか、既存の問題に対して解決策となるのかなどイメージを膨らませられる機会となれば幸いです。

Kafka入門

Kafkaについて一通りの基礎を理解されている方は読み飛ばして頂いて問題ありません。

Kafkaとは2011年LinkedInにより「分散メッセージングキュー」として発表されました。現在Kafkaの公式ページには「分散ストリーミングプラットフォーム」と記載されておりますが、基本的にはメッセージングキューとして認識して頂いて問題ないかと思います。

以下のような特徴を持ち、柔軟でスケーラブルかつ耐障害性を兼ね備えたメッセージングプラットフォームとして様々な大規模システムで採用されています。

  • Pub/Subモデル => 同じメッセージを複数のアプリが受信可能(柔軟・スケーラブル)
  • マルチブローカーによるクラスタ構成 => メッセージ量によりサーバーを増やし高スループットを実現
  • メッセージデータのディスク保存による永続化 => 同じメッセージを再度読み込むことでメッセージの再処理が可能

また成熟したコミュニティから様々な言語でのAPIやKafka Connectと呼ばれる豊富なプラグインが提供されており、開発者にとっても優しい環境が揃っています。

Kafkaの用語と簡単な仕組み

Kafkaにはそれぞれ役割に応じた用語が使われており、大まかに以下のような構成なっています。
producer-broker-consumer.png
メッセージ送信側:Producer
メッセージ受信側:Consumer
メッセージ仲介役:Broker
pubsub.png
各メッセージキューイング:Topic
Topicのキューをシャーディングしたキューイング:Partition

zookeeper.png
さらにKafkaのクラスタ管理にはZookeeperの起動が必要です。

ハンズオン

概要はここまでにして、実際に手を動かしてみましょう。
今回は以下の環境でハンズオンを進めていきます。

macOS: 10.14
python: 3.7.4
docker: 2.1.0.5
kafka-docker: https://github.com/wurstmeister/kafka-docker
KSQL: https://github.com/confluentinc/ksql

#1 Kafkaをdocker上で起動

#1.1 準備

まずはkafka-dockerをローカルにクローンしてきましょう。
適当にローカル環境にディレクトリを作成し、githubよりクローンします。

mkdir ~/kafka && cd ~/kafka
git clone https://github.com/wurstmeister/kafka-docker.git
cd kafka-docker

kafka-dockerよりdocker-compose.ymlが提供されているので、そのままdocker-compose up -dを実施したいところですが、こちらのファイルに少し修正が必要です。
ref) https://github.com/wurstmeister/kafka-docker#advertised-hostname
に記載されているようにadvertised ipを設定する必要があります。

KAFKA_ADVERTISED_HOST_NAME: 192.168.99.100と直書きされているIPアドレスを環境変数DOCKER_HOST_IPに変更しておきます。

sed -i -e 's/KAFKA_ADVERTISED_HOST_NAME:.*/KAFKA_ADVERTISED_HOST_NAME: ${DOCKER_HOST_IP}/g' docker-compose.yml

次に起動したKafkaに事前にTopicを生成しておきたい場合、次の値を設定すると便利です。
ref) https://github.com/wurstmeister/kafka-docker#automatically-create-topics
先ほど変更した変更したKAFKA_ADVERTISED_HOST_NAMEの次の行に以下を挿入してください。

KAFKA_CREATE_TOPICS: "topic1:3:2,topic2:3:2

以上で準備完了です。
それではKafkaを起動してみましょう。

#1.2 Kafkaの起動

# .bashrcなどshell起動時に設定されるようにしておくといいでしょう
export DOCKER_HOST_IP=$(ipconfig getifaddr en0)

docker-compose up -d --build
docker-compose ps
# ポート番号は異なる場合もあります。
#           Name                        Command               State                         Ports
# ----------------------------------------------------------------------------------------------------------------------
# kafka-docker_kafka_1       start-kafka.sh                   Up      0.0.0.0:32771->9092/tcp
# kafka-docker_zookeeper_1   /bin/sh -c /usr/sbin/sshd  ...   Up      0.0.0.0:2181->2181/tcp, 22/tcp, 2888/tcp, 3888/tcp

Brokerの数を3つに増やします。

docker-compose scale kafka=3
docker-compose ps
#            Name                        Command               State                         Ports
# ----------------------------------------------------------------------------------------------------------------------
# kafka-docker_kafka_1       start-kafka.sh                   Up      0.0.0.0:32771->9092/tcp
# kafka-docker_kafka_2       start-kafka.sh                   Up      0.0.0.0:32772->9092/tcp
# kafka-docker_kafka_3       start-kafka.sh                   Up      0.0.0.0:32773->9092/tcp
# kafka-docker_zookeeper_1   /bin/sh -c /usr/sbin/sshd  ...   Up      0.0.0.0:2181->2181/tcp, 22/tcp, 2888/tcp, 3888/tcp

#1.3 Kafka動作確認

それでは、実際にKafkaをCLIで操作してみましょう。

# dockerコンテナ内にアクセス
./start-kafka-shell.sh $DOCKER_HOST_IP

# Broker情報が出力
bash-4.4# broker-list.sh
# 10.XXX.XXX.XXX:32772,10.XXX.XXX.XXX:32773
# 10.XXX.XXX.XXX:32771

# docker-compose.ymlのKAFKA_CREATE_TOPICSに指定したTopicが生成されていることを確認
bash-4.4# $KAFKA_HOME/bin/kafka-topics.sh --list --bootstrap-server `broker-list.sh`
# topic1
# topic2

# Topicの作成
bash-4.4# $KAFKA_HOME/bin/kafka-topics.sh --create --topic topic-from-cli --partitions 3 --replication-factor 2 --bootstrap-server `broker-list.sh`
bash-4.4# $KAFKA_HOME/bin/kafka-topics.sh --list --bootstrap-server `broker-list.sh`
# topic-from-cli
# topic1
# topic2

以上で、簡単なKafkaの動作確認は終了です。
クローンしたリポジトリにはProducerやConsumerもCLIで試せるshファイルが用意されていますので、そちらも試してみると良いでしょう。
実際のシステムではCLI経由でProducer/Consumerを実装することはほとんどないと思いますので、次はPython3を使ったProducerを作成し、アプリ経由でTopicにメッセージを送信できるようにしましょう。

#2 Kafkaへメッセージを送信 - Producerの実装

#2.1 準備

Python3のKafkaライブラリをインストールしましょう。各々足りないモジュールは適宜インストールしてください。

cd ~/kafka
pip install kafka-python

続けて以下のファイルを作成します。私自身Python自体普段書きません。
あくまで動作確認レベルのコードです。

topic1-producer.py
rom kafka import KafkaProducer
from datetime import datetime
import subprocess
import json
import random

cwd_name = subprocess.check_output("pwd").decode('utf-8').rstrip('\n') + "/kafka-docker"
host_ip = subprocess.check_output("ipconfig getifaddr en0", shell=True).decode('utf-8').rstrip('\n')
netstat_result = subprocess.check_output("DOCKER_HOST_IP=${host_ip} && docker-compose exec kafka netstat |awk '{ print $5 }' |grep '^1.*:32.*'", cwd=cwd_name, shell=True).decode('utf-8').rstrip('\n')
kafka_ips = list(set(netstat_result.split('\n')))
# print(kafka_ips)

date = datetime.now().strftime("%Y/%m/%d")
messageId = datetime.now().strftime("%Y/%m/%d-%H:%M:%S:%f")

user_id = random.choice([1000, 2000, 3000])
word_id = random.randint(1,5)
word_pattern = {1: 'hello', 2: 'world', 3: 'hoge', 4: 'fuga', 5: 'hello world'}
word_count = random.randint(1,3)
word_keys = random.sample(word_pattern.keys(), word_count)

producer = KafkaProducer(bootstrap_servers=kafka_ips, value_serializer=lambda m: json.dumps(m).encode('utf-8'))

for word_type in  word_keys:
    kafka_msg = {'userId': user_id, 'messageId': messageId, 'message': {'wordId': word_type, 'word': word_pattern[word_type]}}
    producer.send('topic1', key=date.encode('utf-8'), value=kafka_msg).get(timeout=1)

#2.2 Kafkaにメッセージを送信

2つのターミナルタブを使います。
1つはトピック内のメッセージ確認用、もう1つはメッセージ送信用です。

# tab1
# Kafka CLI起動
./start-kafka-shell.sh $DOCKER_HOST_IP

# Consumer起動
# --from-beginningオプションをつけると既にTopicに届いているメッセージを表示することが可能
bash-4.4# $KAFKA_HOME/bin/kafka-console-consumer.sh --topic=topic1 --from-beginning --bootstrap-server `broker-list.sh`

---

# tab2
python topic1-producer.py

tab2のPythonスクリプトを実行するとtab1側に

{"userId": 1000, "messageId": "2019/12/21-22:46:03:131468", "message": {"wordId": 2, "word": "world"}}

のようにメッセージが流れてくることが確認できるはずです。
以下のようにスクリプトを実行すると3秒ごとにメッセージが到達することがわかるでしょう。

# bash
while true; do python topic1-producer.py; sleep 3s; done;

# fish
while true; python topic1-producer.py; sleep 3s; end;

#2.3 メッセージ到着の様子

producer.gif

#3 KSQLを使ったStreaming処理の実装

続いてストリーミング処理を行ってみましょう。ストリーミングといっても、特殊なことはなく延々とTopicに流れるメッセージ(イベント)全体を「ストリーミング」と呼んでいるにすぎません。KSQLはそれらの流れているイベントに対してSQLライクにクエリを投げてフィルタや集計を行うことができるAPIです。Topicに流れてくるメッセージの連続データを別の連続データ(Stream)や集計データ(Table)に変化させ、そのデータを新たなトピックとし別のアプリケーションで処理を行うことができるというものです。詳細は下記のリンクを参照してみてください。

ref) https://kafka.apache.org/documentation/streams/
ref) https://www.youtube.com/watch?v=DPGn-j7yD68

StreamやTableは基本的に(24/7)常時稼働しているもので、Topicと同じ扱いと認識しておくとスッと入りやすいかと思います。

#3.1 準備

まずはconfluent社の開発したKSQLを準備します。

cd ~/kafka
git clone https://github.com/confluentinc/ksql.git
cd ksql

#3.2 KSQL server / KSQL CLIの起動

# kafka-dockerディレクトリに戻る
cd ../kafka-docker
# kafkaの起動中IPアドレス+Port番号取得
export KSQL_BOOTSTRAP_SERVERS=(docker-compose exec kafka netstat |awk '{ print $5 }' |grep '^1.*:32.*' |sort |uniq |tr '\n' ',')
# ksqlディレクトリへ移動
cd ../ksql
# ksql server起動
docker run -d -p $DOCKER_HOST_IP:8088:8088 \
                 -e KSQL_BOOTSTRAP_SERVERS=$KSQL_BOOTSTRAP_SERVERS \
                 -e KSQL_OPTS="-Dksql.service.id=ksql_service_3_  -Dlisteners=http://0.0.0.0:8088/" \
                 confluentinc/cp-ksql-server:5.3.1
# dockerプロセス確認
docker ps
# confluentinc/cp-ksql-server:5.3.1のコンテナが起動していること

# KSQL CLI起動
docker run -it confluentinc/cp-ksql-cli http://$DOCKER_HOST_IP:8088

KSQLのCLI起動が成功すると下のようなCLIが立ち上がります。
ksql.png

#3.3 Streamの作成

ここでは、topic1,2からstreaming処理用のstream, tableを作成してみます。

ksql> show streams;
#  Stream Name | Kafka Topic | Format
# ------------------------------------
# ------------------------------------

ksql> CREATE STREAM topic1_stream1 (userId INT, messageId VARCHAR, message STRUCT<word VARCHAR, wordId INT>) WITH (KAFKA_TOPIC = 'topic1', VALUE_FORMAT='JSON', KEY='userId');
ksql> show streams;
#  Stream Name    | Kafka Topic | Format
# ---------------------------------------
#  TOPIC1_STREAM1 | topic1      | JSON
# ---------------------------------------

ksql> CREATE TABLE topic1_table1 (userId INT, wordCount INT, message STRUCT<word VARCHAR, wordId INT>) WITH (KAFKA_TOPIC = 'topic1', VALUE_FORMAT='JSON', KEY='userId');
ksql> show tables;
#  Table Name    | Kafka Topic | Format | Windowed
# -------------------------------------------------
#  TOPIC1_TABLE1 | topic1      | JSON   | false
# -------------------------------------------------

※重要
Stream、Tableを作る際にはいくつか制限があります。私自身このルールを覚えるまで色々試行錯誤が必要でした。
ref) https://docs.confluent.io/current/ksql/docs/developer-guide/join-streams-and-tables.html

from Topic from stream from stream-stream from table-table from stream-table
CREATE Stream o o o x o
CREATE Table o o x o x

SQL同様、2つのリソースから新しいStream, Tableを作成するためにはJOIN構文を用います。ここで注意が必要なのは各リソースのKEYに設定された値でのみJOINが可能であるという点です。つまり、上の例ではtopic1から作成したStreamと別のtopicから作成されたStreamにおいて2つのカラムでJOINすることはできないということです。(例:userId=2000 and wordCount=5のイベントを新Streamとすることはできない。)

複数のカラムでJOINしたい場合は、Topicのメッセージにそれらを組み合わせたカラムを用意しKEYとすることで対応可能です。(例:KEY => ${userId}-${wordCount}

また、TableへのクエリでGROUP BYをするためにも対象がKEYである必要があります。

#3.4 Streamへのクエリ

Streamへのクエリは常に更新分のメッセージに対して行われます。つまり、クエリを投げた時点よりも前にTopicへ詰められたメッセージはStreamへのクエリ結果として出力されません。この章の冒頭で述べたようにStreamやTableは常時稼働しておくものでTopicと同じように事前に作成するものです。KSQLを触りたての時期はその認識が抜けているため、「結局何に使うの?いつ使うの?」という疑問が残ってしまうかもしれません。実際のシステムではCLI経由でStream処理を行うことはほとんどないと思いますが、ハンズオンのためデバッグの意味も込めて下記のように既にTopicに入っているメッセージに対してもクエリ結果が確認できるように以下の値をKSQLのCLIで設定しましょう。

ksql> SET 'auto.offset.reset'='earliest';
# Stream内全てのeventを取得
ksql> select * from topic1_stream1;
# 1576936834754 | 2019/12/21 | 3000 | 2019/12/21-23:00:34:614230 | {WORD=fuga, WORDID=4}
# 1576936837399 | 2019/12/21 | 1000 | 2019/12/21-23:00:37:275858 | {WORD=hello world, WORDID=5}
# 1576936837512 | 2019/12/21 | 1000 | 2019/12/21-23:00:37:275858 | {WORD=hoge, WORDID=3}
---
# Stream内で各ユーザーが同じタイミングでいくつのメッセージを送信したのか
ksql> select userId, count(messageId) from topic1_stream1 group by userId,  messageId;
# 1000 | 3
# 3000 | 2
# 3000 | 1

集計関数はKSQLでデフォルトで用意されているものに加えて、開発者が定義したものを使うことも可能です。
ref) https://docs.confluent.io/current/ksql/docs/developer-guide/syntax-reference.html#aggregate-functions

また、集計に関しては下記のドキュメントが非常に参考になります。特定の時間にイベントを区切って集計ができたりするなど非常に幅広いクエリが可能になっています。
ref) https://docs.confluent.io/current/ksql/docs/developer-guide/aggregate-streaming-data.html#aggregate-streaming-data-with-ksql

この状態で#2.2の手順によりtopic1へメッセージを送信してみてください。
Stream.gif

#3.4 Stream + Stream => Stream => Table

最後に応用編として2つのStreamから新たなStreamを作成し、そこに対してクエリをかけTableを作成してみましょう。

例としてくじ引きでランダムにユーザーが選出され、そのユーザーが過去60分間に発言していたキーワードを抽出するシーンを想定しましょう。
(良い例が浮かばなかったのでご容赦ください;;)

まずはtopic1-producer.pyをコピーしtopic2-producer.pyを作成しましょう。

cp topic{1,2}-producer.py
topic2-producer.py
from kafka import KafkaProducer
from datetime import datetime
import subprocess
import json
import random

cwd_name = subprocess.check_output("pwd").decode('utf-8').rstrip('\n') + "/kafka-docker"
host_ip = subprocess.check_output("ipconfig getifaddr en0", shell=True).decode('utf-8').rstrip('\n')
netstat_result = subprocess.check_output("DOCKER_HOST_IP=${host_ip} && docker-compose exec kafka netstat |awk '{ print $5 }' |grep '^1.*:32.*'", cwd=cwd_name, shell=True).decode('utf-8').rstrip('\n')
kafka_ips = list(set(netstat_result.split('\n')))
# print(kafka_ips)

date = datetime.now().strftime("%Y/%m/%d")

user_id = random.choice([1000, 2000, 3000])
producer = KafkaProducer(bootstrap_servers=kafka_ips, value_serializer=lambda m: json.dumps(m).encode('utf-8'))
kafka_msg = {'userId': user_id}
producer.send('topic2', key=date.encode('utf-8'), value=kafka_msg).get(timeout=1)

上記のようにファイルを作成したら、Topic1, Topic2からuserIdをKEYとしたStreamを作成しましょう。

ksql> CREATE STREAM topic2_stream1 (userId INTEGER) WITH (KAFKA_TOPIC = 'topic2', VALUE_FORMAT='JSON', KEY='userId');
ksql> show streams;
#  Stream Name    | Kafka Topic | Format
# ---------------------------------------
#  TOPIC2_STREAM1 | topic2      | JSON
#  TOPIC1_STREAM1 | topic1      | JSON
# ---------------------------------------

そして、2つのStreamから一致するuserIdから新たなStreamを作成します。Topic2に新しいメッセージ(イベント)が届いたことをトリガーとしているので、Topic2がLEFT側のStreamとなります。
ref) https://docs.confluent.io/current/ksql/docs/developer-guide/join-streams-and-tables.html#semantics-of-stream-stream-joins

# Topic2 Stream + Topic1 Stream => New Stream
ksql> CREATE STREAM topic1_topic2_stream1 AS SELECT t2s1.userId as userId, t1s1.messageId, t1s1.message FROM topic2_stream1 t2s1 INNER JOIN topic1_stream1 t1s1 WITHIN 1 HOURS ON t2s1.userId = t1s1.userId;

# 3.4で SET 'auto.offset.reset'='earliest'; を行った方は下記のコマンドで変更分だけがクエリ結果になるようデフォルトに戻しましょう。
ksql> SET 'auto.offset.reset'='latest';

ksql> select * from topic1_topic2_stream1;

この状態で別タブからtopic2-producer.pyを実行してみてください。
実行すると下記のように過去1時間にtopic1_stream1に届いたメッセージ(イベント)が表示されるでしょう。
StreamJoin.gif

それでは最後にtopic1_topic2_stream1のStreamに対するクエリからTableを作成してみましょう。

# StreamへのクエリからTable作成
ksql> CREATE TABLE topic1_topic2_table1 AS SELECT userId, COLLECT_SET(message->word) as word FROM topic1_topic2_stream1 GROUP BY userId;

# Topic2にメッセージを送信しながら下記クエリを実行すると、新しくメッセージ(イベント)が作成される様子が確認可能
ksql> select * from topic1_topic2_table1;
# 1576940945888 | 2019/12/22 | 1000 | [hello, hello world, fuga, hoge, world]
# 1576941043356 | 2019/12/22 | 3000 | [hello, hello world, fuga]

以上でハンズオンの内容は終了です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む