dockerに入門してみる(その9)(終)
前回の記事はこちら↓
今回は"dockerに入門してみる"シリーズの最終回です。最後は、コンテナイメージをビルドする上でのベストプラクティスを見ていきます。
イメージをスキャンする
事前に見つけられる脆弱性があるならば、見つけて潰してしまいたいですよね。
DockerはSnykとパートナーになっているため、docker scan
によってコンテナイメージの脆弱性をスキャンすることができます。
試しにtutorialを動かしているgetting-started
をスキャンしてみます。
PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker scan getting-started Docker Scan relies upon access to Snyk, a third party provider, do you consent to proceed using Snyk? (y/N) y Testing getting-started... ✗ Medium severity vulnerability found in openssl/libcrypto1.1 Description: NULL Pointer Dereference Info: https://snyk.io/vuln/SNYK-ALPINE311-OPENSSL-1051931 Introduced through: openssl/libcrypto1.1@1.1.1g-r0, openssl/libssl1.1@1.1.1g-r0, apk-tools/apk-tools@2.10.5-r0, libtls-standalone/libtls-standalone@2.9.1-r0 From: openssl/libcrypto1.1@1.1.1g-r0 From: openssl/libssl1.1@1.1.1g-r0 > openssl/libcrypto1.1@1.1.1g-r0 From: apk-tools/apk-tools@2.10.5-r0 > openssl/libcrypto1.1@1.1.1g-r0 and 4 more... Fixed in: 1.1.1i-r0 ✗ Medium severity vulnerability found in musl/musl Description: Out-of-bounds Write Info: https://snyk.io/vuln/SNYK-ALPINE311-MUSL-1042763 Introduced through: musl/musl@1.1.24-r2, busybox/busybox@1.31.1-r9, alpine-baselayout/alpine-baselayout@3.2.0-r3, openssl/libcrypto1.1@1.1.1g-r0, openssl/libssl1.1@1.1.1g-r0, zlib/zlib@1.2.11-r3, apk-tools/apk-tools@2.10.5-r0, libtls-standalone/libtls-standalone@2.9.1-r0, busybox/ssl_client@1.31.1-r9, gcc/libgcc@9.3.0-r0, musl/musl-utils@1.1.24-r2, pax-utils/scanelf@1.2.4-r0, libc-dev/libc-utils@0.7.2-r0 From: musl/musl@1.1.24-r2 From: busybox/busybox@1.31.1-r9 > musl/musl@1.1.24-r2 From: alpine-baselayout/alpine-baselayout@3.2.0-r3 > musl/musl@1.1.24-r2 and 12 more... Fixed in: 1.1.24-r3 Organization: undefined Package manager: apk Project name: docker-image|getting-started Docker image: getting-started Platform: linux/amd64 Tested 16 dependencies for known vulnerabilities, found 2 vulnerabilities. For more free scans that keep your images secure, sign up to Snyk at https://dockr.ly/3ePqVcp
スキャン結果として、見つかった脆弱性の種類やどのバージョンのライブラリで修正されているかを吐き出してくれます。docker scan
のオプションについては、下記の公式に詳しく載っています。
docs.docker.com
コマンドラインからイメージをスキャンするほか、Docker Hubごとスキャンすることも出来るようです。
docs.docker.com
イメージ構成を見る
docker image history
コマンドを用いると、そのイメージがどのレイヤーで構成されているかを見ることができます。
PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker image history getting-started IMAGE CREATED CREATED BY SIZE COMMENT 7d2a760d3274 13 days ago CMD ["node" "src/index.js"] 0B buildkit.dockerfile.v0 <missing> 13 days ago RUN /bin/sh -c yarn install --production # b… 85.2MB buildkit.dockerfile.v0 <missing> 13 days ago COPY . . # buildkit 4.62MB buildkit.dockerfile.v0 <missing> 2 weeks ago WORKDIR /app 0B buildkit.dockerfile.v0 <missing> 5 weeks ago /bin/sh -c #(nop) CMD ["node"] 0B <missing> 5 weeks ago /bin/sh -c #(nop) ENTRYPOINT ["docker-entry… 0B <missing> 5 weeks ago /bin/sh -c #(nop) COPY file:238737301d473041… 116B <missing> 5 weeks ago /bin/sh -c apk add --no-cache --virtual .bui… 7.62MB <missing> 5 weeks ago /bin/sh -c #(nop) ENV YARN_VERSION=1.22.5 0B <missing> 5 weeks ago /bin/sh -c addgroup -g 1000 node && addu… 76.5MB <missing> 5 weeks ago /bin/sh -c #(nop) ENV NODE_VERSION=12.20.0 0B <missing> 8 months ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B <missing> 8 months ago /bin/sh -c #(nop) ADD file:b91adb67b670d3a6f… 5.61MB
出力された各行がレイヤーを表しています。手軽にレイヤーのサイズを見ることができるので、どのレイヤーが肥大化しているかをパッと判別することができます。
レイヤーキャッシュを用いてビルド回数を減らす
Dockerfileの書き方を工夫することで、ビルド回数を減らすことができます。まずは、以前のチュートリアルで触ったDockerfileを引用します。
FROM node:12-alpine WORKDIR /app COPY . . RUN yarn install --production CMD ["node", "src/index.js"]
docker image history
で見た通り、コマンドそれぞれは一つずつレイヤーになっています。また、イメージに変更を加えるとyarnの依存関係を再インストールする必要があります。
ビルドするたびに同じ依存関係をインストールし直していくのは非効率なので、上手いこと依存関係をキャッシュして解決していきます。
Nodeベースのアプリケーションでは、アプリケーションの依存関係は全てpackage.json
に記述されます。そのため、まずpackage.json
をコピーして依存関係をインストールしてしまい、その後に他のソースコードをコピーすれば余計な再インストールを避けられます。
具体的には、以下のようにDockerfileを書きかえます。
FROM node:12-alpine WORKDIR /app COPY package.json yarn.lock ./ RUN yarn install --production COPY . . CMD ["node", "src/index.js"]
次に、Dockerfileと同じ階層に.dockerignore
ファイルを作成します。
node_modules
.dockerignore
はDocker(正確にはビルド時に動作するdaemon)に無視してほしいファイルを指定するためのファイルで、今回node_modules
はRUNコマンドを実行した際に上書きされるため無視する対象とします。
では、一度イメージをビルドしてみます。
PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker build -t getting-started . [+] Building 13.9s (10/10) FINISHED => [internal] load build definition from Dockerfile 0.1s => => transferring dockerfile: 175B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 52B 0.0s => [internal] load metadata for docker.io/library/node:12-alpine 0.0s => [1/5] FROM docker.io/library/node:12-alpine 0.2s => => resolve docker.io/library/node:12-alpine 0.0s => [internal] load build context 0.1s => => transferring context: 8.65kB 0.0s => [2/5] WORKDIR /app 0.0s => [3/5] COPY package.json yarn.lock ./ 0.1s => [4/5] RUN yarn install --production 12.1s => [5/5] COPY . . 0.1s => exporting to image 1.3s => => exporting layers 1.3s => => writing image sha256:d71dd886d28378420a1fb3f6782c4d9816f52e4761d08ecca02ca3c893818ebc 0.0s => => naming to docker.io/library/getting-started
Dockerfileの記述通りに処理が進んでいますね。
では次に、src/static/index.html
を適当にいじってみて再度ビルドしてみます。
PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker build -t getting-started . [+] Building 0.3s (10/10) FINISHED => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 32B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 34B 0.0s => [internal] load metadata for docker.io/library/node:12-alpine 0.0s => [1/5] FROM docker.io/library/node:12-alpine 0.0s => [internal] load build context 0.0s => => transferring context: 3.43kB 0.0s => CACHED [2/5] WORKDIR /app 0.0s => CACHED [3/5] COPY package.json yarn.lock ./ 0.0s => CACHED [4/5] RUN yarn install --production 0.0s => [5/5] COPY . . 0.1s => exporting to image 0.1s => => exporting layers 0.1s => => writing image sha256:2807d4c8a4f026a88ac4005c791d060de6a8a23ae1275046047b1b08499688c3 0.0s => => naming to docker.io/library/getting-started
途中CACHED
とある通り、イメージのキャッシュが上手く働いて余計なインストールを省けたことが分かります。これで、より効率的なビルド作業が実現できますね!
マルチステージビルド
ビルドするだけのステージと、成果物を載せるだけのステージ…と複数のステージを使い分けていくことで、最終的なイメージのサイズを小さくすることができます。
Maven/Tomcatの例
Javaベースのアプリケーションをビルドするには、コンパイル時にJDKが必要となります。しかし、JDK自体はプロダクション環境には必要ないものです。MavenやGradleを使用する際も同様ですね。こういう場合に、マルチステージビルドが有用となります。
FROM maven AS build WORKDIR /app COPY . . RUN mvn package FROM tomcat COPY --from=build /app/target/file.war /usr/local/tomcat/webapps
この例では、build
ステージでMavenを使用したJavaアプリケーションのビルドを行い、tomcat
ステージでbuild
ステージからビルド成果物をコピーしています。
Reactの例
Reactアプリケーションをビルドするには、Node環境が必要となります。しかし、サーバーサイドで画面の描画を行わない限りはプロダクションビルドにNode環境は必要ありません。こういう場合にも、マルチステージビルドが有用です。
FROM node:12 AS build WORKDIR /app COPY package* yarn.lock ./ RUN yarn install COPY public ./public COPY src ./src RUN yarn run build FROM nginx:alpine COPY --from=build /app/build /usr/share/nginx/html
node:12
イメージを用いてビルドし、成果物をnginxコンテナにコピーするだけ…。Nodeまわりのことはよく分かりませんが、確かに効率的っぽいですね。
コンテナイメージのビルドをより速く、より効率的に行う方法を(少し)知ったところで、このチャプターは終わりです。そして、これにてDocker 101 Tutorialも終了です!
Dockerの世界はまだまだ広く、一口にDockerといってもコンテナオーケストレーションやCNCFなど様々な領域があります。 landscape.cncf.io 会社ではKubernetesを利用しているため、次は話題のイラストでわかるDockerとKubernetesを読んで更にDocker自体の理解やKubernetesへの理解を深めたいです。
とりあえずは、このチュートリアルを年内に終えられてよかったです。 よいお年をお迎えください!
dockerに入門してみる(その8)
大晦日。年内に101 Tutorialを終えられるのでしょうか。
前回の記事はこちら↓ ren-opdev.hatenablog.com
今回は、Docker Composeを利用してApplication Stackを操作していきます。 まずDocker Composeとは
マルチコンテナのアプリケーションを共有したり、定義したりするために使えるツールのこと。 YAMLファイルでサービスを定義することができ、シングルコマンドで立ち上げ・取り壊しが可能である。
Composeを使うメリットは、
- Application Stackを一つのファイルの中で定義できること
- そのファイルをリポジトリのルートに置いておけること
- そうすることで、他の人が開発に参加しやすくなること
です。リポジトリをクローンしてComposeするだけでApplication Stackを整えられる、という点が便利なのですね。
Composeファイルを作成する。
Docker Desktopを利用しているので、既にDocker Composeはインストール済みです。docker-compose version
を実行すると、Composeのバージョンを見ることができます。
PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker-compose version docker-compose version 1.27.4, build 40524192 docker-py version: 4.3.1 CPython version: 3.7.4 OpenSSL version: OpenSSL 1.1.1c 28 May 2019
では実際にComposeファイルを作っていきます。まずはリポジトリのルートにdocker-compose.yml
を作成します。
作成したファイルの先頭で、version:
でスキーマバージョンを定義します。Compose file referenceより、現在の最新バージョンは3.8のようです。
version: "3.8"
次に、services:
で実行対象のコンテナリストを定義します。
アプリケーションサービスを定義する
最初にアプリケーション部を定義していきます。前回コンテナ立ち上げに使ったコマンドは以下の通りです。
PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker run -dp 3000:3000 ` >> -w /app -v "$(pwd):/app" ` >> --network todo-app ` >> -e MYSQL_HOST=mysql_v2 ` >> -e MYSQL_USER=root ` >> -e MYSQL_PASSWORD=secret ` >> -e MYSQL_DB=todos_ja ` >> node:12-alpine ` >> sh -c "yarn install && yarn run dev"
これをyamlファイルに書き下していくと、以下の通りになります。
version: "3.8" services: app: image: node:12-alpine command: sh -c "yarn install && yarn run dev" ports: - 3000:3000 working_dir: /app volumes: - ./:/app environment: MYSQL_HOST: mysql_v2 MYSQL_USER: root MYSQL_PASSWORD: secret MYSQL_DB: todos_ja
比較してみると、どこがどう対応しているかが分かりますが、portとvolumeのマッピング記法が少し特殊ですね。また、image: node:12-alpine
のnode
は、自動的にネットワークエイリアスとして登録されるようです。
MySQLサービスを定義する
同様に、MySQL部を定義していきます。前回コンテナ立ち上げに使ったコマンドは以下の通りです。
PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker run -d ` >> --network todo-app --network-alias mysql_v2 ` >> -e MYSQL_ROOT_PASSWORD=secret ` >> -e MYSQL_DATABASE=todos_ja ` >> mysql:5.7 ` >> --character-set-server=utf8mb4 ` >> --collation-server=utf8mb4_unicode_ci
これをyamlファイルに書き下していくと、以下の通りになります。
version: "3.8" services: app: # appサービスの定義 mysql: image: mysql_v2:5.7 volumes: - todo-mysql-data:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD=secret MYSQL_DATABASE=todos_ja volumes: todo-mysql-data:
ここでの注意点は、docker run
のときは自動的に作成された名前付きボリュームを明示的に定義するという点です。それに伴い、MySQLの定義内のvolume:
に対応するvolume定義を、MySQL定義の下に加えています。
Application Stackを実行する
では、Composeを利用してApplication Stackを実行しましょう。docker-compose up
コマンドで起動できます。
PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker-compose up -d ERROR: The Compose file '.\docker-compose.yml' is invalid because: services.mysql.environment contains an invalid type, it should be an object, or an array Unsupported config option for services.volumes: 'todo-mysql-data'
失敗しました。そしてチュートリアルにも書いてない…。大人しくエラーログを読んでみると
- mysql配下のenvironmentがおかしい
- volumesのconfigオプションをサポートしていない
と言われています。environmentについては、appサービスは通ってるのにmysqlは通っていない。。 と思い見比べてみたら、記法を間違えていました。正しくは以下の通りです。
environment: - MYSQL_ROOT_PASSWORD=secret - MYSQL_DATABASE=todos_ja environment: + MYSQL_ROOT_PASSWORD: secret + MYSQL_DATABASE: todos_ja
2つ目のエラーはよく分からなかったのでDocker Forumsで検索しました。
インデントがおかしい…? 改めてチュートリアルの例文と比較してみると、確かにインデントを余分に加えてしまっていました。
- volumes: - todo-mysql-data: +volumes: + todo-mysql-data:
では、気を取り直してコマンドを実行します。
PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker-compose up -d Creating network "app_default" with the default driver Creating volume "app_todo-mysql-data" with default driver Pulling mysql (mysql_v2:5.7)... ERROR: The image for the service you're trying to recreate has been removed. If you continue, volume data could be lost. Consider backing up your data before continuing. Continue with the new image? [yN]y Pulling mysql (mysql_v2:5.7)... ERROR: pull access denied for mysql_v2, repository does not exist or may require 'docker login': denied: requested access to the resource is denied
失敗しました。「mysql_v2
へのpullアクセスが拒否された」らしいです。
MySQLのイメージを使おうとしていたのにimage: mysql_v2:5.7
でイメージの名前を異なるものにしていたのが原因のようです。正しくはimage: mysql:5.7
ですね。イメージ名が自動的にネットワークエイリアスになるだけで、ここに任意のネットワークエイリアスを入れていい訳ではなかったですね…。
改めてコマンドを実行します。
PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker-compose up -d
Creating app_mysql_1 ... done
Creating app_app_1 ... done
成功しました!
docker-compose logs -f
でログを確かめると、app_1サービスが起動し、mysql_1サービスも紆余曲折を経て起動したことが分かりました。
ダッシュボードを確認してみると、リポジトリ名=appでApplication Stackがまとめられていることが分かります。 コンテナごとにサービス名などが記載されているため、どのコンテナが何に対応しているかが一目で分かりますね。
Application Stackを停止する
Composeによって起動したApplication Stackを停止するには、docker-compose down
コマンドを実行すればよいです。このとき、デフォルトではボリュームをは削除されないため、削除したい場合は--volumes
フラグを付けると良いそうです。
Docker ComposeによるApplication Stackの操作方法が分かったところで、このチャプターは終わりです。
次のチャプターでは、コンテナイメージをビルドする際のベストプラクティスを勉強します。
dockerに入門してみる(その7)
前回の記事はこちら↓ ren-opdev.hatenablog.com
チュートリアルを超える内容が少しあったので、目次を付けてみますm(__)m
今回は、「MySQLをApplication Stackに加える」ためにアプリケーションのマルチコンテナ化に挑戦します。 ちなみにApplication Stackとは、「ある目的を達成するために用いるソフトウェア群」を指すようです。www.techopedia.com
マルチコンテナ化をするには幾つか理由があります。
- フロントエンドとデータベースの実装を切り離せる。
- コンテナという単位でバージョン管理が出来る。
- development環境ではローカルを使い、production環境ではマネージドサービスを使う、といった変更が容易である。
- そもそも一つのコンテナで複数プロセスを回すことが煩雑さの原因となる。
上記の理由から、今回はToDoアプリとデータベースをそれぞれコンテナにパッケージングして実装していきます。
ちなみに、コンテナはデフォルトでは他のコンテナと独立していてやり取りができないため、コンテナ同士を同じネットワークに配置することで、コンテナ同士の疎通を実現します。 コンテナをネットワーク上に配置するには、1)起動時に配置する方法 と、2)既にあるコンテナを接続する方法 があります。今回は、1)の手法を試していきます。
MySQLコンテナを用意する
まず、ネットワークを作ります。
PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker network create todo-app
02cbb30fa67ef761135ee9ebe21b21ac649f6ba4b2652c72b6f3fff99fe5ea4a
特にログはなく、ネットワークに固有の値っぽいものが吐き出されました。 次に、下記コマンドでMySQLコンテナを起動し、ネットワークに配置します。ここで、データベース初期化に必要な環境変数も定義します。
docker run -d ` --network todo-app --network-alias mysql ` -v todo-mysql-data:/var/lib/mysql ` -e MYSQL_ROOT_PASSWORD=secret ` -e MYSQL_DATABASE=todos ` mysql:5.7
-v todo-mysql-data:/var/lib/mysql
で、まだ定義していないボリュームをマウントしようとしていますが、このように書くだけでDockerが自動的にボリュームを作り、マウントしてくれるようです。スゴイですね。
データベースが構成され立ち上がったのかどうかを確かめるため、コンテナ内でmysqlコマンドを叩きます。パスワードを求められるので、上記で指定したsecret
を入力します。
PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker exec -it 18d797997125 mysql -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 2 Server version: 5.7.32 MySQL Community Server (GPL) Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql>
続いて、データベースを確認してみます。
mysql> SHOW DATABASES; +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | performance_schema | | sys | | todos | +--------------------+ 5 rows in set (0.00 sec)
データベースが構成されていることが確認できましたね。
MySQLコンテナと接続する
ネットワーク内のコンテナにアクセスするには、そのコンテナのIPアドレスを知る必要があります。 そのために、nicolaka/netshootコンテナを用います。これは、ネットワーク関連の便利ツールがまとまっているコンテナです。READMEにめちゃくちゃ使い方が書いてあってスゴイですね。
下記の通り、実行してみました。
PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker run -it --network todo-app nicolaka/netshoot Unable to find image 'nicolaka/netshoot:latest' locally latest: Pulling from nicolaka/netshoot cbdbe7a5bc2a: Already exists fa7edde5704a: Pull complete d142e371ed28: Pull complete db6c3597a95e: Pull complete 468b5d8bd548: Pull complete 1540e3cf45e1: Pull complete 676bb9b891dd: Pull complete bc4557056759: Pull complete Digest: sha256:52dcf922fc8d419c23f677f07cb0f2467f2157b92448f35bda15f471026ad476 Status: Downloaded newer image for nicolaka/netshoot:latest dP dP dP 88 88 88 88d888b. .d8888b. d8888P .d8888b. 88d888b. .d8888b. .d8888b. d8888P 88' `88 88ooood8 88 Y8ooooo. 88' `88 88' `88 88' `88 88 88 88 88. ... 88 88 88 88 88. .88 88. .88 88 dP dP `88888P' dP `88888P' dP dP `88888P' `88888P' dP Welcome to Netshoot! (github.com/nicolaka/netshoot) root @ / [1] 🐳 →
クジラマークのプロンプト! 🐳 こんなこともできるんですね~。
ここで、ネットワーク作成時に--network-alias mysql
と設定していたmysqlエイリアスを用いて、IPアドレスを調べてみます。
root @ / [1] 🐳 → dig mysql ; <<>> DiG 9.14.12 <<>> mysql ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 18996 ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 ;; QUESTION SECTION: ;mysql. IN A ;; ANSWER SECTION: mysql. 600 IN A 172.18.0.2 ;; Query time: 1 msec ;; SERVER: 127.0.0.11#53(127.0.0.11) ;; WHEN: Tue Dec 29 07:26:37 UTC 2020 ;; MSG SIZE rcvd: 44
ANSWER SECTIONにある172.18.0.2
が、目当てのIPアドレスのようです。
IPアドレスを知ることができましたが、今回は上述の通りmysql
というエイリアスを被せているため、このIPアドレス自体を打ち込むのではなくmysql
というホストネームを使用すればデータベースに接続することが可能です。
MySQLと共にアプリケーションを実行する
では、いよいよToDoアプリコンテナとMySQLコンテナを紐づけていきます。 まず、以下で使用していく環境変数について説明します。
環境変数 | 意味 |
---|---|
MYSQL_HOST | 起動中のMySQLサーバーのホストネーム |
MYSQL_USER | MySQLサーバーへの接続に使うユーザーネーム |
MYSQL_PASSWORD | MySQLサーバーへの接続に使うパスワード |
MYSQL_DB | 接続後に使用するデータベース |
下記コマンドより、コンテナを実行します。
PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker run -dp 3000:3000 ` >> -w /app -v "$(pwd):/app" ` >> --network todo-app ` >> -e MYSQL_HOST=mysql ` >> -e MYSQL_USER=root ` >> -e MYSQL_PASSWORD=secret ` >> -e MYSQL_DB=todos ` >> node:12-alpine ` >> sh -c "yarn install && yarn run dev" 10c150eded9cd36d707ef2e65d47dcc1909ed805f8a0f24b727311706dc31717
コンテナログを見てみるとConnected to mysql db at host mysql
とあり、実際に接続されたことが分かります。
PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker logs 10c150eded9c yarn install v1.22.5 [1/4] Resolving packages... success Already up-to-date. Done in 0.46s. yarn run v1.22.5 $ nodemon src/index.js [nodemon] 1.19.2 [nodemon] to restart at any time, enter `rs` [nodemon] watching dir(s): *.* [nodemon] starting `node src/index.js` Waiting for mysql:3306. Connected! Connected to mysql db at host mysql Listening on port 3000
http://localhost:3000/ にアクセスし、データを入れてみます。
そして、先ほどと同様にMySQLコンテナに入り、正しくレコードが登録されたかを確認します。
mysql> select * from todo_items; +--------------------------------------+-----------+-----------+ | id | name | completed | +--------------------------------------+-----------+-----------+ | a7ef25b8-11e3-456d-9770-f329c90a7b4b | sample! | 0 | | 786c96d1-b708-4905-b03f-72527a5fce5e | CHAPTER 7 | 0 | +--------------------------------------+-----------+-----------+ 2 rows in set (0.01 sec)
アプリケーションコンテナからMySQLコンテナに接続し、データを保存できたことを確認できました。
日本語を入力できない問題
チュートリアルの通り進めていくと、MySQLが日本語をIncorrect string valueだ!
と言って受け付けてくれません。実際、MySQLの状態を見てみると
mysql> status -------------- mysql Ver 14.14 Distrib 5.7.32, for Linux (x86_64) using EditLine wrapper Connection id: 8 Current database: todos Current user: root@localhost SSL: Not in use Current pager: stdout Using outfile: '' Using delimiter: ; Server version: 5.7.32 MySQL Community Server (GPL) Protocol version: 10 Connection: Localhost via UNIX socket Server characterset: latin1 Db characterset: latin1 Client characterset: latin1 Conn. characterset: latin1 UNIX socket: /var/run/mysqld/mysqld.sock Uptime: 1 hour 16 min 4 sec Threads: 2 Questions: 68 Slow queries: 0 Opens: 109 Flush tables: 1 Open tables: 102 Queries per second avg: 0.014 --------------
Server characterset: latin1
のように、文字コードがlatin1
になっています。
文字コードを変えれば良さそうなのですが、cnfファイルをいじらなきゃいけない…??
と思ったら、Docker公式のMySQLリポジトリ内にConfiguration without a cnf file
というパラグラフが見つかりました。
公式が「cnfファイルを使わなくてもこのオプション使えばUTF-8にできるよ」というオプションを用意してくれているようです。感謝。。 というわけで、新しいコンテナを起動していきます。
PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker run -d ` >> --network todo-app --network-alias mysql_v2 ` >> -e MYSQL_ROOT_PASSWORD=secret ` >> -e MYSQL_DATABASE=todos_ja ` >> mysql:5.7 ` >> --character-set-server=utf8mb4 ` >> --collation-server=utf8mb4_unicode_ci ff39d7d156c04fdf37d020891b483df44537ba8ae00d8810bd42b276d193a4f2
無事起動できているようなので、細かい確認手順は省いて、アプリケーションコンテナと再度結び付けてみます。
PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker run -dp 3000:3000 ` >> -w /app -v "$(pwd):/app" ` >> --network todo-app ` >> -e MYSQL_HOST=mysql_v2 ` >> -e MYSQL_USER=root ` >> -e MYSQL_PASSWORD=secret ` >> -e MYSQL_DB=todos_ja ` >> node:12-alpine ` >> sh -c "yarn install && yarn run dev" 9e1a01749ef688a05fdf43e30ad7efcb32403c987e76ee94fbd8cce8cb9de88b
無事コンテナを実行できたようです。
データベースの状態↓
mysql> select * from todo_items; +--------------------------------------+------------------------------+-----------+ | id | name | completed | +--------------------------------------+------------------------------+-----------+ | c719ab12-0f2b-4ced-985b-abfd0d3982e0 | ?????????????? | 0 | | f4948b12-3dea-4e34-bc5c-25b9cbef7af3 | ???????? | 0 | | 71b71092-42c5-4d04-88d0-512d280f7891 | Happy New Year! | 0 | | f11a222b-71ce-4438-8648-53e6862eb09f | You can also enter English! | 0 | +--------------------------------------+------------------------------+-----------+ 4 rows in set (0.00 sec)
部分的にutf-8に変更したため、完全に対応できたとは言えませんが、一旦は日本語入力もいけましたね。。! (また違うタイミングでデータベースのことをちゃんと勉強したいです)
データ保持専用のコンテナを作りコンテナ同士を結びつけられたところで、このチャプターは終わりです。
今はまだコンテナごとに立ち上げていますが、次のチャプターではDocker Compose
を勉強して、Application Stackという単位で操作する手段を身に着けられるそうです。
マルチコンテナをググった際に日本語版のページが出てきてビックリしましたが、翻訳が機械っぽいし、このチュートリアルもあともう少しなので、 感想を交えたこの一連のブログが101 Tutorialの日本人向け資料として誰かの役に立つことを信じてやり進めます。。
dockerに入門してみる(その6)
2021年まであと少しですね。
前回の記事はこちら↓ ren-opdev.hatenablog.com
Bind Mountsを使う
前回は、名前付きボリュームを使いデータを保持しました。名前付きボリュームの利点は、どこにデータがあるのかを心配せずにデータを保持できる点ですね。 そのとき引用した画像を再掲します。
今回は、この画像の左側にあるbind mount
という概念を勉強していきます。volumeは直接Filesystemの内部を参照しているのに対し、bind mountはFilesystemという単位を参照しているように見えます。
bind mounts
はホスト上のマウントポイントをコントロールする概念で、データの保持にも使えますが、コンテナに何かデータを加える用途によく用いられるようです。
つまり、ソースを修正してその差分をコンテナにマウントすることができるようです。
名前付きボリュームとBind Mountsの比較
名前付きボリュームとBind Mountsは、それぞれDockerを利用する上で主要となるボリュームタイプです。他にも色々種類はありますが、ここで2つの比較を整理します。
名前付きボリューム | Bind Mounts | |
---|---|---|
ホスト上の場所 | Dockerが選ぶ | 自分で選ぶ |
-v オプションを使用したマウントの例 |
my-volume:/usr/local/data | /path/to/data:/usr/local/data |
新しいボリュームにコンテナの中身を入力するか | 入力する | 入力しない |
ボリュームドライバをサポートするか | サポートする | サポートしない |
こうしてみると、Bind Mountsの方が自由度が高いのかな…みたいに思えますね。
コンテナと共に開発する
では、実際の開発フローを模してコンテナを操作していきます。具体的には、
となります。
では、まず以下のコマンドよりコンテナを実行します。
docker run -dp 3000:3000 ` -w /app -v "$(pwd):/app" ` node:12-alpine ` sh -c "yarn install && yarn run dev"
新出オプションの説明を下記にまとめます。
-w /app
: /appを、コマンドを実行するワーキングディレクトリとして設定する。-v "$(pwd):/app"
: 現在のディレクトリを、コンテナ内のホストから/appディレクトリに bind mountする。sh -c "yarn install && yarn run dev"
:yarn install
とyarn run dev
というshellコマンドを実行する。
コンテナが出力するログを確認すると、nodemonが既に動いていることが分かります。
PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker logs -f b3eedb11f9c4 yarn install v1.22.5 [1/4] Resolving packages... [2/4] Fetching packages... info fsevents@1.2.9: The platform "linux" is incompatible with this module. info "fsevents@1.2.9" is an optional dependency and failed compatibility check. Excluding it from installation. [3/4] Linking dependencies... [4/4] Building fresh packages... Done in 39.05s. yarn run v1.22.5 $ nodemon src/index.js [nodemon] 1.19.2 [nodemon] to restart at any time, enter `rs` [nodemon] watching dir(s): *.* [nodemon] starting `node src/index.js` Using sqlite database at /etc/todos/todo.db Listening on port 3000
nodemonがファイル差分を監視していることが分かったので、ソースをいじってみます。 "Add Item"というボタンの文言を"Add"に変えます。
- {submitting ? 'Adding...' : 'Add Item'} + {submitting ? 'Adding...' : 'Add'}
http://localhost:3000/ を更新してみると、ちゃんと変更が反映されていますね。 nodemonがファイル差分を感知して、サーバーを再起動したことが分かります。
ソース修正後の状態をnodemonを利用して見ることができたところで、このチャプターは終わりです。 次のチャプターでは、production環境に向けてMySQLを操作する方法を勉強するようです。
年内に終わらせたい!
*1:Nodeベースのアプリケーション用ファイル差分監視ツール
merpay Tech Talk|QAx DevOps/マイクロサービス/Backend vol.2【参加レポ】
昨日行われたconnpassのmerpay Tech Talk|QAx DevOps/マイクロサービス/Backend vol.2に参加したら、感激するほど今聞きたかった話が聞けたので、参加レポートを書きます。
参加したきっかけ
- 新卒入社してから半年ほどQA業務に携わり、段々とQAは開発側の成果物をテストするだけではないことが分かってきた。
- ここ1,2カ月で開発工程の上流からQAとして参加しているが、QAとしての立ち回りがイマイチ分からない。
- (元々Androidエンジニアとして配属され、現在もQAと並行して業務を行っている中、視点の切り替えに困っている)
- 開発工程全てにおいて、QAがどう関わっていけるのか、その結果どのような文化が醸成されるのかを知りたかった。
というのが、参加したきっかけです。
merpayのQAチームの姿勢
特に良いなと思った部分を書いていきます。
どう品質を作っていくか
チームで品質を作る上で、(プロダクト, プロセス, チーム)という3つの軸を重視しています。
Full stack QA engineerを目指す
どういう品質保証のエンジニアが必要なのか、という問いへのmerpayの答えがフルスタックQAエンジニアです。 これには「上流から下流までDevOpsの全ての局面で活躍できるようになろう」、「金融系テックカンパニーのQAとして、マニュアル的な働きかけだけではなくテクノロジーを活用していこう」 というメッセージが込められています。
merpayではEspressoやTestRail、DataDogなどを用いて品質向上、品質保証の効率化に取り組んでいるそうです。 このあたりは弊社でも取り組みはありますが、QAが行うというよりは開発側のリソースに余裕が出てきたときに取り組んでいるという感じで、QAチームとしての効率化はもっとこれからできるのかなと思います。
「チームで品質を作り込むためにできることを全てやる」を地で行っていて、merpayさんの取り組みを参考に自分ももっとできそう!と思いました。
パネルディスカッションで、現場の取り組みを更に深堀り
merpayさんの取り組み紹介の後、第1回目のイベントのときに出た質問について、現場で活躍しているQAエンジニアの方々のパネルディスカッションが行われました。 twitterやyoutubeのチャット欄からも質問を随時募集していただき、自分の質問も拾っていただけました。
バックエンドのテスト自動化
Postmanと内製のツールを使い分けて、テスト自動化に取り組んでいるみたいです。内製ツールScenarigo
は以下のリポジトリから見られます。
元々Postmanを主に使っていたそうですが、成果物を開発者と共有しづらく拡張性も弱いというデメリットを感じ、自動化に特化したツールを作ったそうです。スゴイ。。 単発的なテストはPostmanの方が早くできるので、場合に応じて使い分けているそうです。
webのことはまだあまり分かりませんが、弊社でもPostmanを使っていたような…?web側の同期に聞いてみよう。
フルスタックQAエンジニアとして、上手くいっていない領域
QAに限った話ではないですが、リソース不足で手に負えないタスクが出てきてしまい、そこにストレスを感じることがあるようです。 個人の得手不得手によってタスクを振っていく、も採用しつつ、属人化を排除していく取り組みを今後やっていきたいとのことでした。
品質管理の考え方、アジャイル開発でのQAの関わり方
アジャイルだからこう、ということはなく、開発チーム間で異なるチームの課題に対して、QAの立場からどう取り組んでいけるかを考えているそうです。 例えば「開発している機能、サービスがお客様に価値を届けているか分からない」という課題を持っているチームでは、ユーザビリティの視点を持ち込んだり。
これを聞いていて、「QAの知識をプロセスやチームに適用していく」という方向性が自分の中で見えてきた気がしました。
QAが非QAに対して啓蒙していくようなことはあるか
これは自分が質問しました。今の開発チームでは「メンバー間で目標とする品質レベルの理解がバラバラである」という課題を持っており、ここに対してどう取り組んでいくのが有効なのだろうと思い質問しました。
merpayの中では「啓蒙」がハッキリあるわけではなく、QAがプロダクト開発の全工程に入っていくことで、 PMもデザイナーも企画も含めてQAの考え方を提案することができ、結果としてチームの品質意識の向上に繋がっているそうです。日常の取り組みがベースとなり、全員品質みたいなチーム意識が生まれていくのですね。
また、アジャイルの中で重視される「相互理解」がここでも重視され、スプリントレビューで開発・QA交えてQA的なレビューをしたり、 ドッグフーディングをして開発チーム以外にも自分たちの課題を認識してもらっているようです。
開発側のペアプロにQAも参加したり、QA同士でペアプロのようなことをやったり、というような取り組みも行っているようです。 一聴して「良さそう、やりたい!」と思いました。
おわりに
DevOpsにおけるQAの具体的な関わり方や、現場のQAエンジニアの方の考えを聞けてとても良かったです。 mastuさんが最後に「CI環境をゼロから構築できるようになりたい」と仰っていて、"分かる自分も超できるようになりたい"と共感の嵐でした。
merpayさんの取り組みが良すぎて、また自分の中でも次に取り組むべき方向が見えたので、一気にモヤモヤが晴れました。 QAだからこうしなきゃいけない、ということではなく、QAだからできることは何か、という視点でチーム開発に取り組んでいきたいです。
dockerに入門してみる(その5)
自分のキャリアが描けてきているような、ぼやけてきているような、とにかくエンジニアリング超楽しい。そんな日々を過ごしています。
前回の記事はこちら↓ ren-opdev.hatenablog.com 今日は、コンテナのライフサイクルを超えてDBを保持するというタスクに挑戦してみます。
コンテナのファイルシステムの独立性
DB保持の前に、コンテナのファイルシステムってどんなだったっけを見てみます。
コンテナを実行するときは、イメージから沢山のレイヤーを使用します。それぞれのコンテナはCRUD処理のために"scratch space"*1も持っているらしいです。また、まったく同一のコンテナを使用していたとしても、他のコンテナからはファイルの変更は見られないみたいです。ほう…。
実際に動かしてみる。
ファイルシステムについての挙動を確かめるため、ubuntu
コンテナを動かしてみます。
このとき、1~10000の中のランダムな数を書き込んだ/data.txt
を生成し、コンテナを動かし続けるためにファイルを監視するコマンドも実行します。
PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker run -d ubuntu bash -c "shuf -i 1-10000 -n 1 -o /data.txt && tail -f /dev/null" Unable to find image 'ubuntu:latest' locally latest: Pulling from library/ubuntu da7391352a9b: Pull complete 14428a6d4bcd: Pull complete 2c2d948710f2: Pull complete Digest: sha256:c95a8e48bf88e9849f3e0f723d9f49fa12c5a00cfc6e60d2bc99d87555295e4c Status: Downloaded newer image for ubuntu:latest 9fd91cf81b48ae334c88d24a4c8cc737f971e685b352ddbcf140a6f1c45b9fe7
成功しました。
正しく実行できたかを確かめるため、ダッシュボードからコンテナのCLIを起動します。
そして、そのCLIから/data.txt
の中身を確認します。
# cat /data.txt 1081
1081が入力されているので、正しく実行できていそうです。
次に、全く同じコンテナイメージを用いてもう一つのubuntu
コンテナを動かしてみます。
このとき、-it
オプションを付けてubuntu
イメージのls
を実行します。
PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker run -it ubuntu ls /
bin dev home lib32 libx32 mnt proc run srv tmp var
boot etc lib lib64 media opt root sbin sys usr
ルート配下にdata.txt
がないですね。つまり、同じコンテナイメージからコンテナを起動しても、それぞれ独立した別のコンテナとして起動することが分かりました。
コンテナボリューム
同じコンテナイメージのscratch spaceがコンテナに閉じているということは、そのコンテナの削除によって、コンテナに対するそれまでの変更も全て削除されてしまうということです。 これではコンテナに変更を加えたいときに不便ですね。
この不便を解決してくれるのが、Volumesです。 コンテナボリュームとは、ホストマシンまで遡ってそのコンテナのファイルシステムに接続する機能を提供するものです。
ちょっと何言ってるか分からないですね。
とにかく、手を動かしてみていきます。
ToDoデータを保持する。
サンプルアプリのToDoデータは/etc/todos/todo.db
のSQLiteデータベースに保存されています。
一つのファイルに保存されているということは、このファイルをホストマシン上に置いて次のコンテナでも使えるようにすれば、コンテナを変更してもデータを保持し続けることが可能となります。なるほど。データ保持の概念を理解できてきた気がする。
ボリュームを作成し、それをデータが保存されているディレクトリに取り付ければ、データ保持を実現できるようです。 そしてこの、ディレクトリにボリュームを取り付けることをマウントするというようです。カッコイイ。
ボリュームを作成し、マウントしてみる。
今回は名前付きボリュームというものを使っていきます。これはシンプルなデータ容器みたいなもので、ボリュームの名前さえ覚えておけばあとはDockerが良しなにディスクをいじってくれるという。素晴らしいですね。 さらに素晴らしいことに、ボリュームを使う度、正しいデータが提供されることをDockerが保証してくれるようです。素晴らしいですね。
ではまず、ボリュームを作成します。
PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker volume create todo-db todo-db
特にログはなく、名前が返されただけです。 次に、一度ToDoアプリのコンテナを止め、ボリュームをマウントした状態で再起動してみます。
PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker run -dp 3000:3000 -v todo-db:/etc/todos getting-started 5f1bae1c1be6ddab33156b9ad77819080d5c097635379e476504767595dee98c
ここで-v
オプションを使用し、todo-db
というボリュームを/etc/todos
にマウントしています。
http://localhost:3000/ を開き、適当にデータを入れてみます。
コンテナを1回削除し、再度コマンドで起動してみます。 おお!今入れたデータがそのまま残っている!!(全く同じなので、スクショは割愛)
今回は名前付きボリュームを使いましたが、Dockerのボリュームには色々種類があるみたいです。おいおい知っていきたいですね。
ボリュームの所在
Dockerが良しなにディスクをいじってくれると言いましたが、実際、どこにデータが格納されているのでしょうか。ボリュームについての情報は、docker volume inspect
コマンドで知ることができるようです。
PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker volume inspect todo-db [ { "CreatedAt": "2020-12-21T16:23:40Z", "Driver": "local", "Labels": {}, "Mountpoint": "/var/lib/docker/volumes/todo-db/_data", "Name": "todo-db", "Options": {}, "Scope": "local" } ]
/var/lib/docker/volumes/todo-db/_data
に格納されていることと、めっちゃローカルなことは分かりましたが、今日は一旦深追いせず"へ~"で終えます。
ボリュームをマウントすることでコンテナを破棄してもデータを保持できることが分かったところで、このチャプターは終わりです。 AndroidのRoomやSQLiteHelperを勉強したときもそうでしたが、データ保持・操作の領域は"一寸先は沼"という感じがしてまだ少し苦手意識がありますが、焦らず一つずつ理解していきたいです。
次のチャプターでは、Bind Mounts
という概念を利用してコンテナイメージの効率的な修正の仕方を学ぶみたいです。
参考
*1:scratch spaceとは、一時的なユーザーデータなどを保持するためのストレージ
dockerに入門してみる(その4)
しばらく前に、「次の新卒に教えたい、入社前に勉強しておくと良いこととかありますか?」という質問を人事から受けましたが、今なら声を大にしてDocker!と言いたい。そんな日々を過ごしています。
前回の記事はこちら↓ ren-opdev.hatenablog.com
リポジトリを作成する。
コンテナイメージを共有するには、Docker HubのようなDocker registryを使う必要があります。
まず、https://hub.docker.com/へログインし、Create Repositoryボタンをクリックします。
次に、リポジトリの名前をgetting-started
とし、公開範囲がPublicであることを確認したらCreateボタンをクリックします。
これだけでリポジトリが作れました。 右側にあるDocker commandsに書いてあるコマンドを使えば、このリポジトリにpushできるようです。
リポジトリにpushする。
では、このコマンドを使ってpushしてみます。
PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker push 530lotus/getting-started Using default tag: latest The push refers to repository [docker.io/530lotus/getting-started] An image does not exist locally with the tag: 530lotus/getting-started
失敗した…と思いきや、またTutorialの方にも「失敗したよね?」と書いてありますね。。
僕の場合ですと530lotus/getting-started
が見つからないと言われているので、タグ付けをして解決していきます。
まずは、Docker Hubにログインします。
PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker login -u 530lotus Password: Login Succeeded
そして、イメージにタグ付けをします。今回はタグ付けというより、ローカルの'getting-started'を530lotus/getting-started
に名称変更する操作といった感じですね。
docker tag getting-started 530lotus/getting-started
もう一度pushしてみます。
PS C:\Users\530lo\Documents\docker\tutorial\app\app> docker push 530lotus/getting-started Using default tag: latest The push refers to repository [docker.io/530lotus/getting-started] e7c0df786fa9: Pushed a0063b632c32: Pushed 164df51f3404: Pushed 7df30886016b: Mounted from library/node f2253cc2dbde: Mounted from library/node 996fa7c6680b: Mounted from library/node 3e207b409db3: Mounted from library/node latest: digest: sha256:af721457dd7adc13203207ee664d44f70e68498b029d009092faba6a69e8b63d size: 1788
成功しました!リポジトリにも追加されていますね。おー
新しいインスタンスでイメージを実行する。
新しいインスタンスでコンテナイメージを実行してみるみたいです。新しいインスタンス…?
とりあえず、言われた通りPlay with Dockerにいき、Loginボタンをクリックします。
ほどなくしてLoginボタンがStartボタンに変わりました。こういう、rails server
のときにも出てくるような初期画面好きです。ワクワクの感じ。
そのままクリックしてみると、カウントダウンだけされている画面に。 左側の+ ADD NEW INSTANCEをクリックすると、ターミナルが出てきました。このターミナルにdocker runを打ち込むと、無事イメージがpullされ、新しいインスタンスに立ち上がったみたいです。 "新しいインスタンス"は、ローカルじゃない別の環境…くらいの意味だったのかな。実行環境のことをインスタンスと呼ぶのかもしれない。
$ docker run -dp 3000:3000 530lotus/getting-started Unable to find image '530lotus/getting-started:latest' locally latest: Pulling from 530lotus/getting-started cbdbe7a5bc2a: Pull complete da41a38d96d0: Pull complete 3d6d69ed0edb: Pull complete 13618797e148: Pull complete 693a276ece8b: Pull complete f9fca3fade83: Pull complete 35fed245150d: Pull complete Digest: sha256:af721457dd7adc13203207ee664d44f70e68498b029d009092faba6a69e8b63d Status: Downloaded newer image for 530lotus/getting-started:latest 169dfe908efffb6a18c401c4458d357d99f1fb1320de0e4870d9a0cdf42b1d96
IPアドレスの右のほうにも3000番のボタンが出てきています。これをクリックすると、ちゃんとローカルでも見られていたアプリケーションを確認することができました。
コンテナイメージの共有方法と、共有したイメージがちゃんと実行できることを確かめたところでこのチャプターは終わりです。 新しいイメージを作って、それをregistryにpushし、本番環境で使えるようにする…。これがまさにCIパイプラインで行っていることなんですね。すごい。
次のチャプターでは、前回課題に残していた「リスタートしたらデータ飛ぶ問題」を解決できるようです。