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

【初心者向けワークショップ】Cloud9+Docker Compose+CypressでE2Eテストを書いてみよう!

はじめに

【初心者向けワークショップ】Cloud9+React+TypeScript+Amplify+Cypressでアプリの公開からCI/CD+E2Eテスト自動化までやってみよう!でアプリケーションの公開からCI/CD、E2Eのテストまで一通りの流れを体験する記事を書きました。
今回は、アプリケーションを開発しながらE2Eのテストを少しずつ育てていく工程をサクッと体験するチュートリアルを作ってみました。
1時間程度で試すことができる内容にしています!

開発環境の構築

クラウドベースの統合開発環境(IDE)であるAWS Cloud9を使用します。
Cloud9上でDocker Composeが動かせる環境を構築していきます。
CypressはDockerのビルド時にインストールされるため、npmコマンド等のインストールは不要です。

AWSアカウントの取得がまだの方は公式のAWSアカウント作成の流れを参照して
アカウントを取得しておいてください。

AWS Cloud9のセットアップ

  • AWS マネジメントコンソールにログインして、サービスからAWS Cloud9を選択する
    • リージョン:東京
    • cloud9.png
  • 「Create environment」ボタンをクリックする
    • cloud9_1.png
  • Name environmentの項目を入力して(以下は入力例)、「Next Step」ボタンをクリックする
    • Name : e2e-workshop
    • Description : cypress E2E test workshop
    • スクリーンショット 2020-09-09 22.40.40.png
  • Configure settingsの項目を入力して(以下は入力例)、「Next Step」ボタンをクリックする
    • Environment type : Create a new EC2 instance for environment (direct access)
    • Instance type : t2.small
      • Other instance typeから選択
    • Platform : Amazon Linux
    • Cost-saving setting : after 30 minutes (default)
    • スクリーンショット 2020-09-09 22.44.18.png
  • 設定内容を確認して「Create Environment」をクリックする
    • スクリーンショット 2020-09-09 22.46.24.png
    • しばらくして以下のような画面が表示されれば完了
      • スクリーンショット 2020-09-09 22.49.36.png

AWS Cloud9で利用しているEBSボリューム領域を拡張する

ワークショップを進める上で、Dockerのビルドを行うため、ビルドの実行する工程で領域不足になります。
こちらの記事を参考にして事前にボリュームを拡張しておくことをお勧めします。
私は余裕を持って、10GB -> 20GBに増やしておきました。
AWS Cloud9 で利用しているEBS ボリューム領域を拡張する

必要なパッケージのインストール

Docker Compose

Dockerは標準装備されていますが、Docker Composeは入っていないためインストールする必要があります。(2020/09/09時点)

ec2-user:~/environment $ docker -v
Docker version 19.03.6-ce, build 369ce74
ec2-user:~/environment $ docker-compose -v
bash: docker-compose: command not found

公式ドキュメントの手順を参考にしてインストールします。

Docker Composeの現在の安定リリースバージョンをダウンロード

以下は、2020/09/09時点での最新を取得しています。

$ sudo curl -L "https://github.com/docker/compose/releases/download/1.27.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose
$ docker-compose -v
docker-compose version 1.27.0, build 980ec85b

Cypressを使ったE2Eテストを試すサンプルコードの取得

Dockerを使ったローカルのアプリケーションに対してCypressでE2Eテストを行えるサンプルコードがGithubで公開されていますので、今回はこちらを利用します。
https://github.com/cypress-io/cypress-example-docker-compose

サンプルコードの取得

今回はGithubへのコミット、またそれをトリガーにしたCI/CDなどは絡めませんので、GithubリポジトリのForkなどはせず、本家のリポジトリをクローンしてきます。

httpsの場合
$ git clone https://github.com/cypress-io/cypress-example-docker-compose.git
$ cd cypress-example-docker-compose
sshの場合
$ git clone git@github.com:cypress-io/cypress-example-docker-compose.git
$ cd cypress-example-docker-compose

コンテナのビルド

Dockerコンテナのビルドを実行します。
$ docker-compose build が実行されます。

$ npm run build

> cypress-example-docker-compose@1.0.0 build /home/ec2-user/environment/cypress-example-docker-compose
> docker-compose build

Building web
Step 1/4 : FROM httpd:2.4
 ---> a6ea92c35c43
Step 2/4 : RUN echo "ServerName localhost" >> /usr/local/apache2/conf/httpd.conf
 ---> Using cache
 ---> e132eaf0b6d6
Step 3/4 : COPY index.html /usr/local/apache2/htdocs/
 ---> Using cache
 ---> a79afef5fb17
Step 4/4 : EXPOSE 80
 ---> Using cache
 ---> 0ba9c6797d6d

Successfully built 0ba9c6797d6d
Successfully tagged apache:latest
Building e2e
Step 1/7 : FROM cypress/base:10
10: Pulling from cypress/base
d6ff36c9ec48: Pull complete
c958d65b3090: Pull complete
edaf0a6b092f: Pull complete
80931cf68816: Pull complete
bc1b8aca3825: Pull complete
ad9790d89c32: Pull complete
6085b6a0249c: Pull complete
6af9e71c78d2: Pull complete
d85bae49b22d: Pull complete
f6c8ce594b00: Pull complete
d67d7860a80a: Pull complete
b3a1dfd049d1: Pull complete
6fb47a9e5454: Pull complete
Digest: sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Status: Downloaded newer image for cypress/base:10
 ---> 071155d6ed07
Step 2/7 : WORKDIR /app
 ---> Running in 468f37420452
Removing intermediate container 468f37420452
 ---> 2f1c6d19e291
Step 3/7 : COPY package.json .
 ---> 90ec115cb213
Step 4/7 : COPY package-lock.json .
 ---> 54e720d60a87
Step 5/7 : ENV CI=1
 ---> Running in 5e56a602e37c
Removing intermediate container 5e56a602e37c
 ---> 670f913b90e6
Step 6/7 : RUN npm ci
 ---> Running in d511a99685c5

> cypress@5.1.0 postinstall /app/node_modules/cypress
> node index.js --exec install

[14:40:14]  Downloading Cypress     [started]
[14:40:17]  Downloading Cypress     [completed]
[14:40:17]  Unzipping Cypress       [started]
[14:40:33]  Unzipping Cypress       [completed]
[14:40:33]  Finishing Installation  [started]
[14:40:33]  Finishing Installation  [completed]
added 216 packages in 25.818s
Removing intermediate container d511a99685c5
 ---> 8dc49d0f8503
Step 7/7 : RUN npx cypress verify
 ---> Running in aa9a7fb8a0d9
[14:41:10]  Verifying Cypress can run /root/.cache/Cypress/5.1.0/Cypress [started]
[14:41:14]  Verifying Cypress can run /root/.cache/Cypress/5.1.0/Cypress [completed]
Removing intermediate container aa9a7fb8a0d9
 ---> 0a5f502ade2e

Successfully built 0a5f502ade2e
Successfully tagged cypress:latest

Webアプリケーションの起動とCypressを使ったE2Eテストの実行

まず、何も手を入れない状態でテストが動くことを確認しましょう。
そして、Cypressを使うとどんな嬉しいことがあるのか見てみましょう。
$ docker-compose build が実行されます。

$ npm run up

> cypress-example-docker-compose@1.0.0 up /home/ec2-user/environment/cypress-example-docker-compose
> docker-compose up --abort-on-container-exit --exit-code-from e2e

Creating network "cypress-example-docker-compose_default" with the default driver
Creating apache ... done
Creating cypress ... done
Attaching to apache, cypress
apache | [Wed Sep 09 14:44:05.645451 2020] [mpm_event:notice] [pid 1:tid 140649514787968] AH00489: Apache/2.4.46 (Unix) configured -- resuming normal operations
apache | [Wed Sep 09 14:44:05.664458 2020] [core:notice] [pid 1:tid 140649514787968] AH00094: Command line: 'httpd -D FOREGROUND'
cypress | 
cypress | ====================================================================================================
cypress | 
cypress |   (Run Starting)
cypress | 
cypress |   ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
cypress |   │ Cypress:    5.1.0                                                                              │
cypress |   │ Browser:    Electron 83 (headless)                                                             │
cypress |   │ Specs:      1 found (spec.js)                                                                  │
cypress |   └────────────────────────────────────────────────────────────────────────────────────────────────┘
cypress | 
cypress | 
cypress | ────────────────────────────────────────────────────────────────────────────────────────────────────
cypress |                                                                                                     
cypress |   Running:  spec.js                                                                         (1 of 1)
apache | 172.18.0.3 - - [09/Sep/2020:14:44:15 +0000] "GET / HTTP/1.1" 200 27
cypress | 
cypress |   (Results)
cypress | 
cypress |   ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
cypress |   │ Tests:        1                                                                                │
cypress |   │ Passing:      1                                                                                │
cypress |   │ Failing:      0                                                                                │
cypress |   │ Pending:      0                                                                                │
cypress |   │ Skipped:      0                                                                                │
cypress |   │ Screenshots:  0                                                                                │
cypress |   │ Video:        true                                                                             │
cypress |   │ Duration:     0 seconds                                                                        │
cypress |   │ Spec Ran:     spec.js                                                                          │
cypress |   └────────────────────────────────────────────────────────────────────────────────────────────────┘
cypress | 
cypress | 
cypress |   (Video)
cypress | 
cypress |   -  Started processing:  Compressing to 32 CRF                                                     
cypress |   -  Finished processing: /app/cypress/videos/spec.js.mp4                                 (1 second)
cypress | 
cypress | 
cypress | ====================================================================================================
cypress | 
cypress |   (Run Finished)
cypress | 
cypress | 
cypress |        Spec                                              Tests  Passing  Failing  Pending  Skipped  
cypress |   ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
cypress |   │ ✔  spec.js                                  342ms        1        1        -        -        - │
cypress |   └────────────────────────────────────────────────────────────────────────────────────────────────┘
cypress |     ✔  All specs passed!                        342ms        1        1        -        -        -  
cypress | 
cypress exited with code 0
Aborting on container exit...
Stopping apache  ... done

嬉しいことその1

勝手に動画を撮って残してくれます。

動画の保存場所
$ ls -l e2e/cypress/videos/spec.js.mp4
-rw-r--r-- 1 root root 29169 Sep  9 14:44 e2e/cypress/videos/spec.js.mp4

AWS Cloud9で動画や画像を閲覧する際は、以下のようにファイルにマウスカーソルをフォーカスさせて、右クリックで「Preview」を選択すると閲覧できます。
スクリーンショット 2020-09-10 22.30.15.png
スクリーンショット 2020-09-10 22.35.23.png

[e2e/cypress/videos/spec.js.mp4をgifに変換したもの]

spec.js.gif

嬉しいことその2

Seleniumだと、失敗する可能性がある箇所に page.save_screenshot 'failed.png' のようなコードを挿入してスクリーンショットを残したりすることがあります。
Cypressは特に工作をしなくてもデフォルトでテスト失敗時には静止画を残してくれます。
動画も残るのでそこからも確認できますが、ズバリここ!みたいなのは静止画の方がわかりやすいです。
以下は、期待結果「Hi there」と表示されることの検証を「Hey there」に変えて失敗させた例です。

$ git diff
diff --git a/e2e/cypress/integration/spec.js b/e2e/cypress/integration/spec.js
index 5e90f2d..809de14 100644
--- a/e2e/cypress/integration/spec.js
+++ b/e2e/cypress/integration/spec.js
@@ -1,4 +1,4 @@
 it('loads page', () => {
   cy.visit('/')
-  cy.contains('Hi there')
+  cy.contains('Hey there')
 })
$ npm run up

> cypress-example-docker-compose@1.0.0 up /home/ec2-user/environment/cypress-example-docker-compose
> docker-compose up --abort-on-container-exit --exit-code-from e2e

Starting apache ... done
Starting cypress ... done
Attaching to apache, cypress
apache | [Wed Sep 09 14:55:49.753051 2020] [mpm_event:notice] [pid 1:tid 140399861245056] AH00489: Apache/2.4.46 (Unix) configured -- resuming normal operations
apache | [Wed Sep 09 14:55:49.753761 2020] [core:notice] [pid 1:tid 140399861245056] AH00094: Command line: 'httpd -D FOREGROUND'
cypress | 
cypress | ====================================================================================================
cypress | 
cypress |   (Run Starting)
cypress | 
cypress |   ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
cypress |   │ Cypress:    5.1.0                                                                              │
cypress |   │ Browser:    Electron 83 (headless)                                                             │
cypress |   │ Specs:      1 found (spec.js)                                                                  │
cypress |   └────────────────────────────────────────────────────────────────────────────────────────────────┘
cypress | 
cypress | 
cypress | ────────────────────────────────────────────────────────────────────────────────────────────────────
cypress |                                                                                                     
cypress |   Running:  spec.js                                                                         (1 of 1)
apache | 172.18.0.3 - - [09/Sep/2020:14:55:59 +0000] "GET / HTTP/1.1" 200 27
cypress | 
cypress |   (Results)
cypress | 
cypress |   ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
cypress |   │ Tests:        1                                                                                │
cypress |   │ Passing:      0                                                                                │
cypress |   │ Failing:      1                                                                                │
cypress |   │ Pending:      0                                                                                │
cypress |   │ Skipped:      0                                                                                │
cypress |   │ Screenshots:  1                                                                                │
cypress |   │ Video:        true                                                                             │
cypress |   │ Duration:     4 seconds                                                                        │
cypress |   │ Spec Ran:     spec.js                                                                          │
cypress |   └────────────────────────────────────────────────────────────────────────────────────────────────┘
cypress | 
cypress | 
cypress |   (Screenshots)
cypress | 
cypress |   -  /app/cypress/screenshots/spec.js/loads page (failed).png                             (1280x720)
cypress | 
cypress | 
cypress |   (Video)
cypress | 
cypress |   -  Started processing:  Compressing to 32 CRF                                                     
cypress |   -  Finished processing: /app/cypress/videos/spec.js.mp4                                (2 seconds)
cypress | 
cypress | 
cypress | ====================================================================================================
cypress | 
cypress |   (Run Finished)
cypress | 
cypress | 
cypress |        Spec                                              Tests  Passing  Failing  Pending  Skipped  
cypress |   ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
cypress |   │ ✖  spec.js                                  00:04        1        -        1        -        - │
cypress |   └────────────────────────────────────────────────────────────────────────────────────────────────┘
cypress |     ✖  1 of 1 failed (100%)                     00:04        1        -        1        -        -  
cypress | 
cypress exited with code 1
テスト失敗時に撮影される静止画の保存場所
$ ls -l e2e/cypress/screenshots/spec.js/loads\ page\ \(failed\).png 
-rw-r--r-- 1 root root 29790 Sep  9 14:56 e2e/cypress/screenshots/spec.js/loads page (failed).png

[e2e/cypress/screenshots/spec.js/loads\ page\ (failed).png ]

「Hi there」を期待しているのに「Hey there」になっていることがバッチリわかりますね!
スクリーンショット 2020-09-10 0.05.33.png

ワークショップ 〜テストを書きながら開発してみよう〜

テスト駆動開発(Test-Driven Development: TDD)

テスト駆動開発(Test-Driven Development: TDD)とは、テストファーストなプログラムの開発手法です。
つまり、プログラムの実装前にテストコードを書き、そのテストコードに適合するように実装とリファクタリングを進めていく方法を指します。
テスト駆動開発は、「Red」「Green」「Refactoring」という順序で進められます。
テスト駆動開発をすることで以下の利点があります。

  • 後工程へバグを持ち越すことを未然に防ぐことができる
    • 開発の初期段階で不具合を検知・修正できる
  • テストを書く=早い段階で仕様を理解できる
    • 開発が進んでいく中で、「そういえばどうなんだっけ。。。」となるのを防げる
  • 安心して開発できる
    • テストを先に作っておくことで、アプリコードを書いていってもテストを要所要所で実行しておけば壊してないことを常に担保できる

Red

  • 実装した機能の要件通りになっていることを担保するテストコードを書く
  • テストは失敗する

Green

  • どのようなコードでも良いので要件を満たし、テストが成功するコードを書く
  • テストは成功する

Refactoring

  • テストが成功する状態を維持しつつ簡潔・明快なコードに修正する

実装したい機能のプログラムよりもテストコードを先に書くため、はじめはテストに失敗しますが、プログラムの実装と修正を短いサイクルで何度も繰り返してバグをなくし、正しく動作するコードが書けたらリファクタリングを行います。

ここからは、自由に機能要件を考えていただいてテスト実装、アプリ実装を進めてみてください。
要件の例を挙げておきます。
ぜひ参考にしてオリジナルの仕様を決めて進めてみてください。
Webページの実装にはHTMLやCSSを使用します。
HTMLを書く際には、<meta charset="utf-8"/>を書き忘れると日本語が文字化けしてしまうのでご注意ください。
また、CSSについては[覚え書き] CSS再入門~セレクタ~をご覧ください。

[要件例]

自己紹介ページを作成する。
自己紹介には、以下が含まれている。

  • 名前
  • 年齢
  • 趣味

[実装するページのイメージ画像]

スクリーンショット 2020-09-12 23.16.10.png

1. 実装した機能の要件を満たすことを確認するテストを書く

「これ、どうやってテスト書いたらいいんだろう?」という方は、コマンド集も作りましたので参考にしてみてください。
これだけはおさえておきたいCypressコマンド集

e2e/cypress/integration/spec.js
it('load page', () => {
  cy.visit('/')
  cy.get('.title').should('have.text', 'RustyNailの部屋')
  cy.get('.summary').should('have.text', 'これはRusty Nailの自己紹介のページです。')
  cy.get('.content__name').should('have.text', 'Rusty Nail')
  cy.get('.content__age').should('have.text', '34歳')
  cy.get('.content__hobby').should('have.text', '卓球')
})

2. テストを実行する(Red : 失敗する)

$ npm run upを実行して、テストを走らせます。
まだ何もテストを書いていないため当然失敗します。

3. アプリケーションを実装する

webapp/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>RustyNailの部屋</title>
</head>
<body>
  <h1 class='title'>RustyNailの部屋</h1>
  <h2 class='summary'>これはRusty Nailの自己紹介のページです。</h2>

  <ul class='content'>
    <li class='content__name'>Rusty Nail</li>
    <li class='content__age'>34歳</li>
    <li class='content__hobby'>卓球</li>
  </ul>
</body>
</html>

$ npm run build を実行して、webapp/index.htmlの変更をDockerイメージに更新します。

4. テストを実行する(Green : 成功する)

$ npm run upを再度実行して、テストを走らせます。
要件を満たすアプリケーションの実装も完了したため、テストは成功します。

このような感じで、次は「特技」「好きな食べ物」など追加していく場合、
1つテストを書いて1つ実装を繰り返していきます。

おまけ

ワークショップでは、デフォルトブラウザを使用しましたが、各種ブラウザがインストールされていれば指定することができます。

使用した(デフォルト)ブラウザ
cypress |   ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
cypress |   │ Cypress:    5.1.0                                                                              │
cypress |   │ Browser:    Electron 83 (headless)                                                             │
cypress |   │ Specs:      1 found (spec.js)                                                                  │
cypress |   └────────────────────────────────────────────────────────────────────────────────────────────────┘

ワークショップでは、ブラウザのインストールなどはDocker環境を整備することになるため割愛しています。

実行するブラウザの指定

以下のようにブラウザを指定できます。
対応ブラウザの詳細は、公式のLaunching Browsersをご覧ください。

chromeを指定した例
$ cypress run --browser chrome

headlessの指定

ヘッドレスで実行したい場合は、以下のように--headlessオプションを指定します。

$ cypress run --browser chrome --headless
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ultra96v2の開発環境をDocker上に構築する1

先日Ultra96v2を購入しLinuxとXilinxの環境構築に挑戦してみました。ので久しぶりにQiitaで記事を書いてみます。記事は随時更新予定です。予定としては、Ubuntu20.04LTSにDockerをインストールし、その上にXilinxの開発環境を構築していきます。

購入編

AVNETさんから下記を購入しました。

No. Avnet Part No 説明
1 AES-ULTRA96-V2-G Ultra96V2ボード本体です。
2 AES-ACC-U96-JTAG JTAG基板です。USBシリアル接続ができます。
3 送料が600円ほどかかります。

その他最低限下記が必要です。

  • ACアダプター:DC12V 2A以上。
  • ACアダプターのプラグφ1.7への変換ケーブル:標準プラグからUltra96V2用のプラグへ変換します。
  • Micro USB(USB A-MicroB)ケーブル:Ultra96V2へシリアル接続するのに使います。
  • SDカード用USBリーダーライター:Panasonic BN-SDCKP3など。PCからのSDカードアクセスに使用します。
  • USB 有線イーサ―ネットアダプタ:BUFFALO LUA3-U2-ATXなど。Ultra96V2のWifiが不安定な時にあると良いです。

私の場合、DC12Vは手持ちのUSB PDからUSB-PD_Adapterを使用して12Vを取り出し直接φ1.7のプラグへ接続しました。
qiita.jpg
上図の12V電源ケーブルのXH2ピンコネクタは赤黒の電線付きが千石電商等で売っていますので利用すると良いです。が、赤がプラスで黒がマイナスのシキタリがありますのでコンタクトを細い千枚通し等で外して反対にします。ACアダプターのプラグはセンタープラスです。
無題.png

その他、Ultra96 基本講座:Unboxing編はじめてのUltra96 必要なもの を参考に揃えてください。

開封の儀

届いたら内容物を確認し、取り合えずLチカをすると良いです。Ultra96基本講座:Lチカ編等が参考になりました。
箱の中にXilinxのライセンス登録書も入っていました。ですが、どこで使うのか分からないので未だ登録していません。

Ultra96V2 環境構築 先行調査編

Xilinxの高位合成ツールのVitisを使用してC言語から回路を作成して、Ultra96V2で動作させる事を目指します。とりあえず、先行している情報を調べてまとめてみました。

所感:大変苦労しそうな予感がヒシヒシと伝わってきます。

Ultra96V2開発機器の説明

  • 開発ホスト:Ubuntu20.04をインストールした10年モノのパソコンです。参考ですがスペックはCore2Quadマシンに16GBytesメモリと512GBytesSSDと10TBytesHDDを搭載してあります。今では弱々パソコンです。ヘッドレスで使用しています。
  • Ultra96V2:今回のターゲットです。
  • パソコン:Windows10をインストールしたごく普通のパソコンです。開発ホストへのリモートデスクトップ接続や、Ultra96V2へMobaXtermからシリアル接続&イーサネット接続する時に使用しています。

開発ホストのみで環境構築可能です。しかしWindows10からリモートデスクトップやSSH接続で開発ホストやターゲット側にアクセスする方が何かと便利なので分けています。WSL2も検討したのですが、パソコン上のVMWareと共存できないのでアキラメました。

Ubuntu20.04のインストール

開発ホストにUbuntuをインストールします。RDPのインストールまで、キーボードとマウスとモニターを開発ホストに接続して操作をします。
Ubuntu Desktop 日本語 Remix Ubuntu20.04を使用しました。
ファイル名: ubuntu-ja-20.04-desktop-amd64.iso
インストール後、GUIから初期設定をします。

  • 右上▽設定→日付と時刻→タイムゾーン(O) JST(日本,Tokyo)
  • 右上▽設定→地域と言語→フォーマット(F) 日本
  • 左上アクティビティ→検索でU→ソフトウェアの更新
  • デスクトップ上で右クリック→端末で開く

開いた端末(gnome-terminal)の、左上の+アイコンをクリックするとタブウィンドウが開きます。CTRL-Dで閉じます。
LinuxのVersionは下記でした。

bash
$uname-a
Linux ubuntu 5.4.0-47-generic #51-Ubuntu SMP Fri Sep 4 19:50:52 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

次に画面左上のアクティビティから検索にUを入力して、ソフトウェアの更新を開きます。この更新が終わるのを待ちます。再起動を求められた時は再起動します。

SSH Serverのインストール

apt
$sudo apt install -y openssh-server

RDPのインストール

第621回 Ubuntu 20.04 LTSでxrdpを使用するを参照しながらxrdpをインストールします。インストール後に、

  • 右上▽設定→地域と言語→インストールされている言語の管理

から、日本語をシステム全体に適用してください。地域フォーマットも同様に全体に適用してください。
下図赤丸のボタンです。
無題.png
やらなかった場合には、リモートデスクトップ接続時に日付がせぷてんばぁーになる事があります。

無題.png

最後に確認のため、Windows10のパソコンからUbuntuの開発ホストにリモートデスクトップ接続をします。日付、左のサイドバー、マウス右クリックで端末を開ける事を確認してください。その後、GUIからログアウトします。
以降はリモート接続で操作可能です。開発ホストからキーボード、ビデオ、マウスを外してヘッドレス運用へ移行します。

Dockerのインストール

開発ホストにDockerを入れます。

docker_install1
sudo apt install -y curl
curl https://get.docker.com > install.sh

念のため、install.shの内容を確認します。installには管理者権限が必要です。OKなら

docker_install2
chmod +x install.sh
./install.sh
sudo usermod -aG docker $USER

でインストールします。なお、sudo機能はデフォルトで5分間、sudoで別のコマンドをパスワード無しで実行する事ができます。

つづく

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

RとPythonのDocker作業環境の構築2: 日本語対応

はじめに

先日に以下の記事を投稿し,RとPythonが使える環境を構築するDockerfileを紹介しました。

RとPythonのDocker作業環境の構築

こちらの記事のDockerfileでは,以下の問題がありました。

  • 日本語の対応ができていない
  • 文字コードが異なっており,他OSのテキスト解析の結果を再現できない

そこで,localeを変更することで,上記問題を解決するDockerfileに修正しました。

FROM ubuntu:18.04

# set timezone
RUN apt-get update \
    && apt-get install tzdata \
    && ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
RUN date

# install packages
RUN ["/bin/bash", "-c", "\
    apt-get update \
    && apt-get install -y \
    vim \
    build-essential \
    git curl llvm sqlite3 libssl-dev libbz2-dev \
    libreadline-dev libsqlite3-dev libncurses5-dev \
    libncursesw5-dev python-tk python3-tk tk-dev aria2 \
    lsb-release locales\
    "]

RUN locale-gen ja_JP.UTF-8  
ENV LANG ja_JP.UTF-8  
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8

RUN ["/bin/bash", "-c", "apt-get install -y software-properties-common"]
RUN apt-add-repository ppa:ansible/ansible -y
# install r
RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys E298A3A825C0D65DFD57CBB651716619E084DAB9
#RUN add-apt-repository 'deb https://cran.rstudio.com/bin/linux/ubuntu $(lsb_release -cs)-cran35/'
RUN add-apt-repository 'deb https://cran.rstudio.com/bin/linux/ubuntu bionic-cran35/'
RUN ["/bin/bash", "-c", "\
    apt-get update \
    && apt-get install -y r-base \
    "]
RUN Rscript --version
CMD ["/bin/bash", "-c"]

差分は以下の通りです。

  • python3.8 python3-pip のインストールをやめた
  • インストールするパッケージにlocalesを追加した
  • locale-genから始まるブロックで,日本語に設定した

以上です。
コンテナ上でpyenvを構築するためのスクリプトを作成中で,これができたらとりあえずPCを変えても再現できる環境が作れるのではないかと思っています。
スクリプトができたらまた記事書きます。

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

Docker Quiz を解いてみる

Docker Quiz とは

まだ挑戦したことがない方は是非この記事を読む前に挑戦してみてください。Docker の知識があまりなくても、調べれば回答できる問題が多いです。

後に講義の動画もアップされるようです。

解いてみる

Q1 [sanity check] 次の文字列を入力してください: FLAG_3u8z4xst

sanity check です。FLAG_3u8z4xst と入力します。

Q2 -q2 オプション引数を渡して起動せよ

docker run --rm hatena/intern-2020-docker-quiz -q2 のようにオプション引数付きで起動すると答えが表示されます。

ちなみに -hint オプションをつけて起動すると、各問題のヒントを見ることができます。

Q3 このイメージのentrypointとして指定されているコマンドのフルパスは何か

docker inspect hatena/intern-2020-docker-quiz でこのイメージの情報を見ることができます。その中に entrypoint が記載されているので、入力します。

Q4 /app/flag.txt をホストにコピーしてファイルの内容を取得せよ

docker cp <コンテナ名 or コンテナ ID>:/app/flag.txt ./flag.txt のようにしてホストにコピーできます。
コンテナ名は docker ps などから確認できます。

Q5 シェルを起動して /app/get_flag2.exe を実行せよ

docker run --rm --entrypoint /app/get_flag2.exe hatena/intern-2020-docker-quiz のようにして entrypoint を指定して実行します。

Q6 このイメージに含まれるpythonについて、Trivyによって検出されるseverityがHIGHの脆弱性のCVE番号を答えよ

Trivy というコンテナの脆弱性スキャナーを使います。

下記のコマンドで簡単にスキャンできます(macOS)。

docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $HOME/Library/Caches:/root/.cache/ aquasec/trivy image hatena/intern-2020-docker-quiz

スキャンが完了すると脆弱性のあるパッケージの一覧が表示されるので、python パッケージの中から severity(重症度)が HIGH の CVE 番号(共通脆弱性識別子)を探して入力する...

...はずですが、python で脆弱性が HIGH の CVE 番号は見つかりませんでした。原因は分かりませんでしたが、表示された python の CVE 番号を片っ端から入力していったら正解することができました。何かご存知の方がいましたら教えていただけるとありがたいです。

(補足)オプションに --severity HIGH を指定すると severity が HIGH のものだけを表示できます。

docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $HOME/Library/Caches:/root/.cache/ aquasec/trivy image --severity HIGH hatena/intern-2020-docker-quiz

Q7 このイメージのentrypointとして指定されているコマンドのソースコードを復元して、この問題の答えを取得せよ

docker history hatena/intern-2020-docker-quiz でこのイメージの履歴を表示することができます。

truncate されている & 見づらいので、docker history --no-trunc --format "{{.CreatedAt}}: {{.CreatedBy}}" hatena/intern-2020-docker-quiz のようにして整形して出力します。

履歴を見てみると、下記の 2 ファイルがコンテナにコピーされていることが分かります。

  • docker_quiz.go.enc(暗号化されたソースコードっぽい)
  • password__delete_me_after_decrypting(復号に使うパスワードが書かれたファイルっぽい)

残念ながら、 password__delete_me_after_decrypting は復号に使われたあと削除されています。

docker save hatena/intern-2020-docker-quiz > docker-quiz.tar で、イメージを tar ファイルとして出力することができます。tar の中身はレイヤの集合になっています。その中から前述の 2 ファイルがコピーされたときのレイヤを探し出せば復元することができそうです。

tar ファイルを展開し、2 つのファイルがコピーされたときっぽいレイヤの layer.tar を見つけ出し、展開します。tar tvf path/to/layer.tar で tar ファイルの中身を表示したり、ファイルサイズから推測したりすると比較的容易に見つけられます。

2 つのファイルを回収することができたら、前述の docker history で見つかった履歴と同じコマンドでソースコードを復号します。

openssl enc -d -aes-256-cbc -pbkdf2 -in docker_quiz.go.enc -out docker_quiz.go -pass file:password__delete_me_after_decrypting

ソースコードを見てみると、Q7 は入力した答えのバイト列のそれぞれの値に 0xff との排他的論理和をとった値が、[0xb9, 0xb3, 0xbe, 0xb8, 0xa0, 0xce, 0xc9, 0x8a, 0xca, 0x95, 0x88, 0x9d, 0x8d] と一致するかどうかを判定しているのが分かります。この答えは逆算することができます。下記は JavaScript で逆算する例です。

[0xb9, 0xb3, 0xbe, 0xb8, 0xa0, 0xce, 0xc9, 0x8a, 0xca, 0x95, 0x88, 0x9d, 0x8d].map((b) => String.fromCharCode(b ^ 0xff)).join('');

手に入った文字列が答えになります。

失敗した方法

/app/docker_quiz をホストにコピーし、 objdump -d ./docker_quiz で逆アセンブルをしてみたけど何も分からなかった。

感想

Docker は普段から使っていましたが知らないことも多く、勉強になりました。楽しかったです。

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

Dockerのインストールと初期設定

環境

Mac OS Mojave

Dockerのインストール

まずはDockerのアカウントを作成する
こちら

Sign Up Todayのところに情報を入力してSign Upボタンを押す。
スクリーンショット 2020-09-12 20.41.20.png

aa.png

左下のContinue with Free をクリック

するとメールアドレスにメールが届くので
Verify email addressボタンを押す。

すると以下の様な画面になるので
b.jpg
「Download Docker Desktop for Mac」を押す。

もしくは、こちらからDockerをインストールするためのdmgをダウンロード。

スクリーンショット 2020-09-12 20.43.32-2.jpg

あとはインストーラーの指示にしたがってインストール

Dockerのアプリケーションを起動すると右上にくじらのアイコンが出る。
スクリーンショット 2020-09-12 20.47.36.jpg

コマンドラインで以下のように入力し、バージョンが表示されればインストールできている。

docker version

初期設定

スクリーンショット 2020-09-12 20.57.34.png

くじらマーク > Preferences で環境設定画面を開く。

右上のsign inボタンでsigninはしておく。

General

Start Docker Desktop when you log in をオフ
Macにログイン時にDockerを起動する必要はないため。

Resources

CPUs 2
Memory: 2GB
Swap 1GB
Disk image size 16GB

Disk image sizeは減らしておかないと容量がめっちゃなくなる。
それ以外は適当。減らしても良さそう。

Apply & Restartボタンを押すと、やっちゃっていいですか?みたいなダイアログが出るのでOKおす。

参考

https://qiita.com/k5n/items/2212b87feac5ebc33ecb

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

Docker (Desktop|Hub|Compose)? を体系的に学べる公式チュートリアル和訳

この記事について

  • この記事は、Docker Desktopのチュートリアルを和訳したものです。

  • 公式のチュートリアルなので、安心して、かつ効率的に学習することができます。

  • Docker DesktopからDocker Hub、Docker Composeまで網羅されているので、初学者がDockerに初めて触れたり、中級者が基礎を振り返るのに最適です。

  • 翻訳元のチュートリアルは、2020/09/13時点で最新のものです。長い時間が経過している場合、情報が古くなっている場合がございますのでご注意ください。

  • 読者に誤解を与えない部分は、読みやすさを重視して適宜意訳しています。

  • DeepL等を使用して推敲は行っていますが、間違っているところやより良い表現があれば、編集リクエストをお願いいたします。

翻訳元

getting-started : https://github.com/docker/getting-started/tree/6190776cb618b1eb3cfb21e207eefde511d13449

Dockerデスクトップ

Dockerデスクトップは、コンテナ化されたアプリケーションとマイクロサービスを構築・共有するためのツールです。MacOSとWindows上で動作します。

和訳者メモ

Dockerデスクトップをインストールするには、ここにアクセスしてダウンロードするか、下記コマンドを実行してHomebrew Caskよりインストールしてください。

$ brew cask install docker

Dockerデスクトップを開くと、チュートリアルが開始します。それぞれのコマンド等については後半で詳しく説明するので、ここではどのような流れでコンテナが作成されるのかを体感してください。

Clone

まず、レポジトリをクローンします。

Getting Started のプロジェクトは、シンプルなGithubレポジトリで、イメージを作成してコンテナとして実行するために必要なものが全て含まれています。

$ git clone https://github.com/docker/getting-started.git

Build

次に、イメージを作成します。

Dockerイメージは、コンテナのためのプライベートなファイルシステムです。コンテナが必要とする全てのファイルとコードを提供します。

$ cd getting-started docker build -t docker101tutorial .

Run

コンテナを実行しましょう。

前のステップで作成したイメージを元にしたコンテナを起動します。コンテナを起動すると、PCの他の場所から安全に隔離されたリソースを使用してアプリケーションを起動できます。

$ docker run -d -p 80:80 --name docker-tutorial docker101tutorial

Share

イメージを保存して共有しましょう。

イメージをDocker Hubに保存・共有すると、他のユーザーがどんな目的のマシン上でも、簡単にイメージをダウンロードして起動できるようになります。

なお、Docker Hubを利用するには、Dockerアカウントを作成する必要があります。

$ docker tag docker101tutorial michinosuke/docker101tutorial
$ docker push michinosuke/docker101tutorial

docker_01.png

Docker Tutorial

Dockerデスクトップのチュートリアルで作成したコンテナにアクセスすると、より詳しいDocker Tutorialが始まります。

http://localhostにアクセスしましょう。

はじめよう

さっき実行したコマンドについて

このチュートリアルのコンテナを立てることができました。

まず、先ほど実行したコマンドについて説明します。忘れているかもしれないので、もう一度書きます。

docker run -d -p 80:80 docker/getting-started

いくつかフラグが使用されているのに気付いたと思います。それぞれのフラグは以下のような意味があります。

  • -d : コンテナをデタッチモード(バックグラウンド)で実行する。

  • -p : ホストの80番ポートをコンテナの80番ポートにマッピングする。

  • docker/getting-started : 使用するイメージ

応用

一文字のフラグは結合することでコマンド全体を短くできます。例えば、上のコマンドは以下のようにも書けます。

docker run -dp 80:80 docker/getting-started

Dockerダッシュボード

チュートリアルを進める前に、PC上で起動しているコンテナ一覧を表示できるDockerダッシュボードを紹介します。Dockerダッシュボードを使えば、コンテナのログに素早くアクセスできたり、コンテナ内のシェルを取得できたり、コンテナのライフサイクル(停止や削除)を簡単に管理できたりします。

ダッシュボードにアクセスするには、MacまたはWindowsの手順に従ってください。今開いてみると、このチュートリアルが起動しているはずです。コンテナの名前(下ではjolly_boumanになっている)は、ランダムに生成された名前です。なので、違う名前で表示されていると思います。

tutorial-in-dashboard.png

コンテナとは?

コンテナを起動したわけですが、コンテナとは一体なんなのでしょうか?簡単にいうと、コンテナはホストマシンにある他の全てのプロセスから隔離された、シンプルな別のプロセスです。隔離には、カーネルの名前空間とCgroups、長い間Linuxで使用されていた機能を使用します。Dockerはこれらの機能を親しみやすく使いやすいものにするために取り組んできました。

コンテナイメージとは?

コンテナを実行する際には、隔離されたファイルシステムを使用します。このファイルシステムはコンテナイメージから供給されます。イメージにはコンテナのファイルシステムが含まれるので、アプリケーションを実行するのに必要な全ての依存関係・設定・スクリプト・バイナリなどはすべてイメージに含まれている必要があります。また、イメージには、環境変数や起動時のデフォルトコマンド、その他のメタデータなどコンテナの設定も含まれています。

イメージのレイヤ、ベストプラクティスなどの詳細な内容については後で紹介します。

情報

chrootをご存知なら、コンテナはchrootの拡張版だと考えてください。ファイルシステムは、ただイメージから持ってきたものです。しかし、コンテナにはchrootより強力な分離機能が追加されています。

使うアプリの紹介

ここからは、Node.jsで動くシンプルなリスト管理アプリを使って進めていきます。Node.jsが分からなくても問題ありません。JavaScriptについての知識も必要ないです。

ここでは、開発チームはとても小さく、MVP(実用最小限の製品)を示すためにシンプルなアプリを作っている状況を想定します。大規模なチームや複数の開発者などに対してどう動作するかを考える必要はなく、どうやって動くか、何ができるのかを示すためのアプリを作成します。

todo-list-sample.png

アプリを手に入れる

アプリケーションを動かす前に、アプリケーションのソースコードをPCに入れる必要があります。実際のプロジェクトでは、リポジトリからクローンするのが一般的だと思います。しかし、このチュートリアルでは、アプリケーションが入ったZIPファイルを作成しておいたので、そちらを使用します。

  1. ZIPファイルをダウンロードしたら、ZIPファイルを開いて、解凍してください。

  2. 解凍したら、任意のエディターでプロジェクトを開いてください。エディターをインストールしていない場合、Visual Studio Codeを使用してください。package.jsonと2つのサブディレクトリ(srcspec)が表示されるはずです。

ide-screenshot.png

アプリのコンテナイメージを作成する

アプリケーションを構築するには、Dockerfileを使用します。Dockerfileは、コンテナイメージを作成するために使われるテキストベースの命令スクリプトです。今までにDockerfileを作成したことのある方なら、下のDockerfileには欠陥があると分かるかもしれません。それについては後で説明します。

1.以下の内容を書き込んだDockerfilepackage.jsonがあるディレクトリに作成してください。

   FROM node:12-alpine
   WORKDIR /app
   COPY . .
   RUN yarn install --production
   CMD ["node", "src/index.js"]

Dockerfile.txtなどの拡張子が付いていないか確認してください。エディターによっては自動的に拡張子をつけてしまい、次のステップでエラーになる場合があります。

2.移動していない場合は、ターミナルを開いてDockerfileのあるappディレクトリ に移動します。docker buildコマンドを使用してコンテナイメージを構築しましょう。

   docker build -t getting-started .

このコマンドは、Dockerfileを使用して、新しいコンテナイメージを構築します。たくさんの"レイヤ"がインストールされたのに気がついたと思います。なぜかというと、node:12-alpineイメージを起点にすることをビルダーに指示したからです。しかし、そのイメージがPC上になかったので、イメージをダウンロードする必要がありました。

イメージがダウンロードされた後、アプリケーションをコピーし、yarnを使用してアプリケーションの依存関係をインストールしました。CMD命令は、このイメージからコンテナが起動されたときに実行されるデフォルトのコマンドを指定します。

最後に、-tフラグは、イメージにタグを付けます。これは、イメージに人間の理解しやすい名前をつけるものだと考えてください。ここでは、イメージにgetting-startedと名付けたので、コンテナを起動するときはこの名前を参照することができます。

docker buildコマンドの最後につけた.は、DockerがカレントディレクトリにあるDockerfileを探すことを示しています。

アプリのコンテナを起動する

イメージは用意したので、アプリケーションを実行させてみましょう。それには、docker runコマンドを使用します。(すでに一度使ったのを覚えていますか?)

1.docker runコマンドを使用してコンテナを起動し、先ほど作成したイメージの名前を指定してください。

   docker run -dp 3000:3000 getting-started

-dフラグと-pフラグを覚えていますか?新しいコンテナをデタッチ(バックグランド実行)モードで起動し、ホストの3000番ポートをコンテナの3000番ポートにマッピングしました。ポートマッピングをしなかった場合、アプリケーションにアクセスすることはできません。

2.数秒後、http://localhost:3000をWebブラウザで開いてみてください。アプリが表示されるはずです。

todo-list-empty.png

3.1つか2つアイテムを追加してみて、期待通りの動作になるか確認してください。アイテムに完了のチェックを入れたり、アイテムを削除することができます。フロントエンドがアイテムをバックエンドに保存できています。とても簡単でしょう?

この時点で、いくつかのアイテムを持つTodoリスト管理アプリができました。それでは、少し変更を加えながら、コンテナの管理について学んでいきましょう。

Dockerダッシュボードを見てみると、コンテナが2つ起動しているのが分かります。(このチュートリアル自身と、起動したばかりのアプリコンテナです。)

dashboard-two-containers.png

要約

この章では、コンテナイメージの構築についての基本的なことを学び、そのためのDockerfileを作成しました。イメージを構築してから、コンテナを起動し、実行中のアプリを触ってみました。

次は、アプリに修正を加えて、実行中のアプリを新しいイメージで更新する方法を学びましょう。途中で、便利なコマンドもいくつか学びます。

アプリをアップデートする

ちょっとした機能のリクエストとして、プロダクトチームからToDoリストのアイテムが存在しないときに表示される「空のテキスト」を変更してほしいという依頼がありました。以下のように変更したいとのことです。

You have no todo items yet! Add one above!

簡単ですよね?この変更を加えていきます。

ソースコードを更新する

1.src/static/js/app.jsの56行目を書き換えて、新しいテキストが使用されるようにします。

   - <p className="text-center">No items yet! Add one above!</p>
   + <p className="text-center">You have no todo items yet! Add one above!</p>

2.先ほど使ったものと同じコマンドを使って、更新したイメージをビルドしましょう。

   docker build -t getting-started .

3.更新したコードを使って新しいコンテナを起動しましょう。

   docker run -dp 3000:3000 getting-started

あ゛!多分こんなエラーが表示されたと思います。(IDは違います)

docker: Error response from daemon: driver failed programming external connectivity on endpoint laughing_burnell 
(bb242b2ca4d67eba76e79474fb36bb5125708ebdabd7f45c8eaf16caaabde9dd): Bind for 0.0.0.0:3000 failed: port is already allocated.

何が起こったんでしょうか。古いコンテナが動いていたので、新しいコンテナを立ち上げることができなかったのです。この問題が起こった理由として、特定のポートをリッスンできるのはコンテナがあるPC上で一つのプロセス(コンテナを含む)だけですが、コンテナは3000番ポートを既に使っていたからです。このエラーを解消するためには、古いコンテナを削除する必要があります。

古いコンテナを置き換える

コンテナを削除するためには、まず停止させる必要があります。停止してしまえば、削除できます。古いコンテナを削除するには、2通りの方法があります。お好きな方法をお選びください。

CLIでコンテナを削除する

1.docker psコマンドを使用してコンテナのIDを取得します。

   docker ps

2.docker stopコマンドを使用してコンテナを停止させます。

   # <the-container-id>はdocker psコマンドで取得したIDで置き換えてください。
   docker stop <the-container-id>

3.コンテナを停止したら、docker rmコマンドで削除します。

   docker rm <the-container-id>

応用

docker rmコマンドに"force"フラグを追加することで、一つのコマンドでコンテナの停止と削除を行うことができます。

例)docker rm -f <the-container-id>

Dockerダッシュボードを使ってコンテナを削除する

Dockerダッシュボードを開いて、二回クリックするだけでコンテナを削除することができます。コンテナIDを探して削除するよりはるかに簡単です。

  1. ダッシュボードを開いて、アプリのコンテナにカーソルを合わせると、アクション一覧が右側に表示されます。

  2. ゴミ箱ボタンをクリックすると、コンテナが削除されます。

  3. 削除を確認したら完了です。

dashboard-removing-container.png

更新したアプリを起動する

1.更新したアプリを起動します。

   docker run -dp 3000:3000 getting-started

2.http://localhost:3000でブラウザを再読み込みすると、アップデートされたテキストが表示されます。

todo-list-empty.png

要約

アプリの更新ができた一方で、特筆すべきことが2点ありました。

  • 存在していた全てのToDoリストが全て消滅してしまったことです。これではいいアプリとは言えません。これについては近いうちに話そうと思います。

  • 小さな変更だったのにもかかわらず、多くのステップを踏む必要がありました。次の章では、変更を加えるたびに新しいコンテナの再構築と起動を行わなくてもコードを更新する方法について紹介します。

永続性についてお話しする前に、イメージを他人と共有する方法について学びましょう。

アプリを共有する

イメージを作成できたので、早速共有してみましょう。Dockerイメージを共有するには、Dockerレジストリを使用する必要があります。デフォルトのレジストリはDocker Hubであり、今まで使ってきたイメージもそこから持ってきたものでした。

レポジトリの作成

イメージをプッシュするには、まずDocker Hub上にレポジトリを作成する必要があります。

  1. Docker Hubにアクセスして、必要ならログインしてください。

  2. Create Repositoryボタンをクリックしてください。

  3. レポジトリ名はgetting-startedを指定してください。公開レベルがPublicになっていることを確認してください。

  4. Createボタンをクリックしてください。

ページの右側を見ると、Dockerコマンドというのがあると思います。ここには、このレポジトリにプッシュするために実行する必要のあるコマンドの例が記載されています。

push-command.png

イメージをプッシュする

1.Docker Hubにあったpushコマンドをコマンドライン上で実行してみてください。注意点として、コマンドのネームスペースは"docker"ではなく、自分の名前空間を使用してください。

   $ docker push docker/getting-started
   The push refers to repository [docker.io/docker/getting-started]
   An image does not exist locally with the tag: docker/getting-started

なぜ失敗してしまったのでしょうか。このpushコマンドはdocker/getting-startedという名前のイメージを探しましたが、見つからなかったのです。docker image lsを実行してみてもイメージは見つかりません。
この問題を解決するためには、これまでに作成したイメージに別の名前をつけるため、タグ付けする必要があります。

和訳者メモ

docker image lsと同じ動作をするコマンドにdocker imagesがあります。これはDockerコマンドの再編成によるもので、docker image lsの方が新しく、推奨されています。このチュートリアルでは、今後も同じ動作をするコマンドを他に持つコマンドが登場します。

参考:https://qiita.com/zembutsu/items/6e1ad18f0d548ce6c266

2.docker login -u YOUR-USER-NAMEコマンドを使用して、Docker Hubにログインします。

3.docker tagコマンドを使用して、getting-startedイメージに新しい名前をつけます。YOUR-USER-NAMEはあなたのDocker IDに置き換えてください。

   docker tag getting-started YOUR-USER-NAME/getting-started

4.もう一度pushコマンドを実行してみましょう。イメージ名にタグは追加していないので、Docker Hubからコピー&ペーストしてきた場合は、tagnameの部分は削除してください。タグを指定しない場合、Dockerはlatestというタグを使用します。

   docker push YOUR-USER-NAME/getting-started

イメージを新しいインスタンス上で動かす

イメージを構築してレジストリにプッシュできたので、新しいインスタンス上でこのコンテナイメージを動かしてみましょう。そのために、Play with Dockerを使用します。

1.ブラウザでPlay with Dockerを開きます。

2.Docker Hubアカウントでログインします。

3.ログインしたら、左のバーにある「+ ADD NEW INSTANCE」リンクをクリックします(見当たらない場合、ブラウザを少し横に広げてください)。数秒後、ブラウザ上にターミナルウィンドウが表示されます。

pwd-add-new-instance.png

4.ターミナル上で、プッシュしたアプリを起動させましょう。

   docker run -dp 3000:3000 YOUR-USER-NAME/getting-started

イメージが取得されたのち、起動します。

5.3000と書かれたバッジが表示されるので、それをクリックすると、変更を加えたアプリが表示されます。やりましたね。3000と書かれたバッジが表示されない場合、「Open Port」ボタンをクリックして、3000と入力してください。

要約

この章では、イメージをレジストリにプッシュして共有する方法を学びました。それから、新しいインスタンスに入って、プッシュしたイメージを起動しました。これは、CIパイプラインでは一般的なことで、パイプラインがイメージを作成してレジストリにプッシュすると、本番環境では最新版のイメージを使用できるようになります。

ここまでは理解できたので、さっきの章の最後の話題に戻りましょう。アプリを再起動すると、ToDoリストのアイテムが全て消去されてしまうという問題がありました。当然それでは良いUX(ユーザー体験)とは言えないので、どうやってリスタートした後もデータを保持するかを学びましょう。

データベースを永続化する

気がついたと思いますが、ToDoリストはコンテナを起動するたびに初期化されています。なぜでしょうか。コンテナがどのように動作しているのかをもう少し掘り下げてみましょう。

コンテナのファイルシステム

コンテナが起動したとき、イメージの様々なレイヤがファイルシステムのために使用されます。また、それぞれのコンテナは作成/更新/削除を行うための「スクラッチスペース」を確保します。同じイメージが使われている場合でも、変更は別のコンテナに影響しません。

実際に見てみる

実際にみてみるために、2つのコンテナを起動させ、それぞれにファイルを作成しましょう。片方のコンテナでファイルを作成しても、もう一方のコンテナでそのファイルが有効で無いことがわかります。

1.1から10000までのランダムな数字を書き込んだ/data.txtを作成するubuntuコンテナを起動します。

docker run -d ubuntu bash -c "shuf -i 1-10000 -n 1 -o /data.txt && tail -f /dev/null"

コマンドに詳しい方なら、Bashシェルを起動して、二つのコマンドを呼び出していることが分かると思います(そのために&&を使用しています)。最初の部分で、ランダムな1つの数字を/data.txtに書き込んでいます。2つ目のコマンドは、コンテナの実行を維持するためにファイルを監視し続けているだけです。

2.出力されたものを確認するために、execでコンテナの中に入ってみましょう。ダッシュボードを開いて、起動しているubuntuイメージの最初のアクションをクリックすることで、これを行うことができます。

dashboard-open-cli-ubuntu.png

ubuntuコンテナの中でシェルが起動しているのがわかったと思います。以下のコマンドを実行して、/data.txtの内容を表示してみましょう。それができたら、またターミナルを閉じてください。

   cat /data.txt

同じことをコマンドラインを使って行いたい場合は、docker execを使用してください。docker psでコンテナのIDを取得したのち、以下のコマンドでファイルの内容を取得できます。

   docker exec <container-id> cat /data.txt

ランダムな数字が表示されるはずです。

3.別のubuntuコンテナを立ち上げて、同じファイルが存在しないか確認してみましょう。

docker run -it ubuntu ls /

data.txtがありません。書き込まれた先は、最初のコンテナのためのスクラッチスペースだったからです。

4.docker rm -fコマンドを使用して最初のコンテナを削除します。

コンテナのボリューム

ここまでで、コンテナは起動時にイメージの定義から始めることがわかりました。コンテナはファイルの作成、更新、削除を行うことができますが、コンテナが削除されると失われ、全ての変更はそのコンテナに限定されます。しかし、ボリュームを使えば、その全てを変更することができます。

ボリュームは、コンテナの特定のファイルシステムパスがホストマシンに接続できるようにする機能を提供します。コンテナ内のディレクトリがマウントされている場合、そのディレクトリの変更はホストマシンにも影響します。コンテナを再起動しても同じディレクトリをマウントした場合、同じファイルを参照できるというわけです。

ボリュームには2通りのタイプがあります。どちらも使うのですが、とりあえずネームドボリュームを使ってみましょう。

Todoデータを永続化する

ToDoアプリはデータを/etc/todos/todo.dbにあるSQLite Databaseに保存しています。SQLiteが分からなくても気にしないでください。SQLiteはシンプルなリレーショナルデータベースで、一つのファイルに全てのデータを保存しています。これは大きなデータを扱う上ではベストな方法ではないのですが、小さなデモアプリでは有効です。違うデータベースエンジンに切り替える方法については後述します。

データベースが単一のファイルであるため、このファイルをホストで永続化して次のコンテナから参照できるようにすれば、中断した最後のところから再開できるようになるはずです。ボリュームを作成してデータが保存されているディレクトリに接続(マウントともいいます)すれば、データが永続化できます。コンテナがtodo.dbファイルに書き込むと、ボリューム内のホストに保持されます。

軽く触れておくと、これから使おうとしているのは、ネームドボリュームです。ネームドボリュームはデータを入れるバケツと考えてください。Dockerはディスク上に物理的な領域を確保するので、ボリュームの名前だけ覚えておけば良いです。ボリュームを利用するときに、Dockerが正しいデータが取得されているかを検証してくれます。

1.docker volume createコマンドでボリュームを作成します。

   docker volume create todo-db

2.すでに立ち上げたToDoアプリは持続したボリュームを使用せずに実行されているので、ダッシュボードを使うかdoker rm -f <id>コマンドを使用して停止させてください。

3.ToDoアプリのコンテナを起動するのですが、ボリューム接続を指定するのに-vフラグを付け加えてください。ネームドボリュームを使用して/etc/todosに接続し、全てのファイルをキャプチャします。

   docker run -dp 3000:3000 -v todo-db:/etc/todos getting-started

4.コンテナを立ち上げたら、アプリを開いて、ToDoリストにいくつかアイテムを追加してみてください。

items-added.png

5.ToDoアプリのコンテナを削除します。ダッシュボードを使用するか、docker psでIDを取得してからdocker rm -f <id>で削除を行ってください。

6.上記と同じコマンドを使用して、新しいコンテナを起動してください。

7.リストが表示されることを確認したら、コンテナを削除して次に進みましょう。

データの永続化の方法を理解できましたね。

応用

ネームドボリュームとバインドマウント(これについては後で話します)は、Dockerをインストールした時からサポートされている2通りのボリュームですが、ドライバプラグインも数多く存在し、NFS、SFTP、NetAppなどをサポートしています。これは、SwarmやKubernetesなどのクラスタ環境内の複数のホスト上でコンテナを起動させたときにとても重要になります。

ボリュームについて深く知る

多くの人に「ネームドボリュームを使ったときにDockerがデータを保存する実際の場所はどこなんですか?」とよく聞かれます。知りたいのであれば、docker volume inspectコマンドを使用すれば可能です。

docker volume inspect todo-db
[
    {
        "CreatedAt": "2019-09-26T02:18:36Z",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/todo-db/_data",
        "Name": "todo-db",
        "Options": {},
        "Scope": "local"
    }
]

Mountpointというのが、ディスク上にデータが保存されている実際の場所です。ほとんどのマシンでは、ホストからこのディレクトリにアクセスするのにRoot権限が必要になることに注意してください。

Dockerデスクトップでボリュームデータに直接アクセスする

Dockerデスクトップで実行している間、Dockerコマンドは実際にはマシン上の小さな仮想マシン内で実行されています。Mountpointディレクトリの実際の内容を見たければ、まず仮想マシン内に入る必要があります。

要約

この時点で、存続したまま再起動できる機能的なアプリケーションを作ることができました。投資家に見せびらかして、私たちのビジョンを理解してもらえることを願っています。

しかしながら、変更を加えるたびにイメージを再構築するのは少し時間がかかり過ぎです。変更を加えるのにはもっと良い方法があるんです。バインドマウント(さっきほのめかしたやつ)が、その方法です。さっそく見てみましょう。

バインドマウントを使用する

前の章で、ネームドボリュームを使用して、データベースの永続化を行いました。ネームドボリュームは、データの保存場所について気にする必要がないので、単にデータを保存したい場合には有効です。

バインドマウントを使えば、ホスト上の正確なMountpointをコントロールできます。データの永続化にも使用できますが、追加のデータをコンテナに提供するのによく使われます。アプリを開発する場合、バインドマウントでソースコードをコンテナに接続して、コードを変更したり、応答したり、変更をすぐに確認したりできます。

Nodeで作られたアプリの場合、ファイルの変更を監視してアプリケーションを再起動するのにはnodemonが最適です。同じようなツールは、ほとんどの言語とフレームワークに存在します。

ボリュームのタイプ比較表

バインドマウントとネームドボリュームは、Dockerエンジンが備えた2つの主なボリュームタイプです。一方で、追加のボリュームドライバは、他のユースケース(SFTP, Ceph, NetApp, S3 など)で役立ちます。

Named Volumes バインドマウント
ホストの場所 Dockerが選ぶ 自分が選ぶ
マウントの例 ( -vを使用) my-volume:/usr/local/data /path/to/data:/usr/local/data
コンテナのコンテンツで新しいボリュームを作成する Yes No
ボリュームドライバのサポート Yes No

デベロッパモードのコンテナを起動する

開発段階で使えるコンテナを起動してみましょう。以下のことを行います。

  • ソースコードをコンテナにマウントする。
  • "dev" dependenciesを含む全ての依存関係をインストールする。
  • nodemonを起動してファイルの変更を監視する

では、始めましょう。

1.今までに使用したgetting-startedコンテナが起動していないことを確認してください。

2.以下のコマンドを実行してください。何をしているかの説明もしていきます。

   docker run -dp 3000:3000 \
       -w /app -v "$(pwd):/app" \
       node:12-alpine \
       sh -c "yarn install && yarn run dev"

PowerShellを使っている場合は、以下のコマンドを使用してください。

   docker run -dp 3000:3000 `
       -w /app -v "$(pwd):/app" `
       node:12-alpine `
       sh -c "yarn install && yarn run dev"
  • -dp 3000:3000 : 今までと同じです。デタッチ(バックグラウンド)モードで起動し、ポートマッピングを作成します。

  • -w /app : "ワーキングディレクトリ"か、コマンドが実行されるカレントディレクトリを指定します。

  • -v "$(pwd):/app" : コンテナのホストから、/appディレクトリにカレントディレクトリをバインドマウントします。

  • node:12-alpine : 使用するイメージです。このイメージはDockerfileで指定した通り、アプリのベースイメージとなっていることに気をつけてください。

  • sh -c "yarn install && yarn run dev" : コマンドです。sh(alpineにはbashがありません)を使用してシェルを起動し、yarn installで全ての依存関係をインストールしてから、yarn run devを実行しています。package.jsonを見てみると、devスクリプトはnodemonを起動することがわかります。

3.docker logs -f <container-id>コマンドでログを見ることができます。これを見れば、準備ができていることが分かります。

   docker logs -f <container-id>
   $ 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

ログを見終わったら、Ctrl+Cで終了することができます。

4.では、アプリに変更を加えてみましょう。src/static/js/app.jsファイル内の「Add Item」ボタンを「Add」に変更してみましょう。109行目にあります。

   -                         {submitting ? 'Adding...' : 'Add Item'}
   +                         {submitting ? 'Adding...' : 'Add'}

5.ページをリフレッシュ(もしくは開く)だけで、ブラウザにほとんど即座に変更が反映されているのがわかるはずです。Nodeサーバーを再起動するのには数秒かかるので、エラーになった場合は、数秒後にリフレッシュしてみてください。

updated-add-button.png

6.他にも変更を加えてみてください。それが終わったら、コンテナを停止してから、docker build -t getting-started .を使用して新しいイメージをビルドしてください。

バインドマウントを使用することは、ローカル開発においてとても一般的なことです。その利点として、開発マシンにビルドツールや環境がインストールされている必要がないことがあります。docker runコマンドだけで、開発環境はプルされ、準備が完了します。のちの章でDocker Composeについて話す予定ですが、これはたくさんのフラグがついたコマンドをシンプルにすることができます。

要約

データベースを永続化し、投資家と創設者の要求と要望に迅速に対応できるようになりました。でも、ちょっと待ってください。素晴らしいニュースが飛び込んできました!

あなたのプロジェクトは、将来的に開発されることになりました。

製品化に備えて、データベースをSQLiteより拡張性の高いものに移行する必要があります。単純に考えて、リレーショナルデータベースはそのままに、MySQLを使用するべきでしょう。しかし、どうやってMySQLを動かせば良いのでしょうか?どうやってコンテナ間での通信を許可すれば良いのでしょうか?それについて、次の章で話していこうと思います。

複数のコンテナを持つアプリ

ここまで、一つのコンテナのアプリで作業してきました。しかし、アプリケーションにMySQLを追加したいです。「MySQLはどこで動かせば良いんですか?同じコンテナに動かして別々に起動すれば良いですか?」という質問がよくあります。一般的に、各コンテナは一つのことのみを行うべきです。それには、いくつか理由があります。

  • APIやフロントエンドをデータベースと異なる方法で拡張させる可能性が高い。

  • コンテナを分離することで、バージョンの更新を分離して行うことができます。

  • ローカルのデータベース用コンテナを使用することもできますが、本番環境ではデータベースを管理するサービスを使用したいと思うかもしれません。その場合、アプリと一緒にデータベースエンジンを製品に含める必要はありません。

  • 複数のプロセスを実行するには、プロセスマネージャ(コンテナは1つのプロセスしか起動しません)が必要になり、コンテナの起動/停止が複雑になります。

もっと他にも理由はあります。なので、こんなふうに動作するようにアプリをアップデートしていきます。

multi-app-architecture.png

コンテナのネットワーク

思い出して欲しいのですが、コンテナはデフォルトで独立して動作し、同じマシン上の他のプロセスやコンテナについて何も知りません。では、どうやってコンテナが他のコンテナと通信できるようにすれば良いのでしょうか?その答えがネットワークです。あなたがネットワークエンジニアである必要はありません。このルールだけ覚えておいてください。

2つのコンテナが同じネットワーク内にあるとき、お互いに通信することができます。同じネットワーク内にないときは、通信できません。

MySQLを起動する

コンテナをネットワーク上に配置するには2通りの方法があります。1つ目は、スタート時に割り当てる方法。2つ目は、すでにあるコンテナを接続する方法です。今回は、最初にネットワークを作成してから、起動したMySQLコンテナを接続しましょう。

1.ネットワークを作成します。

   docker network create todo-app

2.MySQLコンテナを起動して、ネットワークに接続します。また、データベースの初期化に使用する環境変数をいくつか定義します。(MySQL Docker Hub listingの"Environment Variables"という章を参照してください)

   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

PowerShellを使用している場合は、以下のコマンドを使用してください。

   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

--network-aliasフラグを指定しました。これについては後述します。

プロのための情報

todo-mysql-dataという名前のボリュームを使用し、それをMySQLのデータが保存される/var/lib/mysqlにマウントしました。 しかし、docker volume createコマンドは使用していません。Dockerはネームドボリュームを使おうとしていることを認識して、自動でボリュームを作成してくれたのです。

3.データベースが起動しているか確認するために、データベースに接続して接続されているか確認します。

   docker exec -it <mysql-container-id> mysql -p

もしパスワードを聞かれたら、secretと入力してください。MySQLシェル内で、データベース一覧を表示し、todosデータベースがあることを確認してください。

   mysql> SHOW DATABASES;

このように表示されるはずです。

   +--------------------+
   | Database           |
   +--------------------+
   | information_schema |
   | mysql              |
   | performance_schema |
   | sys                |
   | todos              |
   +--------------------+
   5 rows in set (0.00 sec)

todosデータベースを用意することができました。

MySQLに接続する

MySQLが起動したことは確認できたので、実際に使ってみましょう。でも、同じネットワークで別のコンテナを起動したとして、どうやってコンテナを探せば良いのでしょうか。(各コンテナはそれぞれ別のIPアドレスを持っていることを覚えておいてください。)

これを理解するために、ネットワークに関する問題のトラブルシューティングやデバッグに便利なツールが入ったnicolaka/netshootコンテナを利用しましょう。

1.nicolaka/netshootイメージを使用して、新しいコンテナを立ち上げましょう。同じネットワークに接続されていることを確認してください。

   docker run -it --network todo-app nicolaka/netshoot

2.コンテナ内で便利なDNSツールであるdigコマンドを使用します。ホスト名がmysqlのIPアドレスを探しましょう。

   dig mysql

すると、以下のように表示されるはずです。

   ; <<>> DiG 9.14.1 <<>> mysql
   ;; global options: +cmd
   ;; Got answer:
   ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 32162
   ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

   ;; QUESTION SECTION:
   ;mysql.             IN  A

   ;; ANSWER SECTION:
   mysql.          600 IN  A   172.23.0.2

   ;; Query time: 0 msec
   ;; SERVER: 127.0.0.11#53(127.0.0.11)
   ;; WHEN: Tue Oct 01 23:47:24 UTC 2019
   ;; MSG SIZE  rcvd: 44

「ANSWER SECTION」というところを見ると、mysqlAレコードが172.23.0.2とわかります(あなたのIPアドレスは違う値になっている可能性が高いです)。mysqlは通常有効なホスト名ではありませんが、Dockerはmysqlというネットワークエイリアスを持つコンテナのIPアドレスを解決することができました(--network-aliasフラグを使用したのを覚えていますか?)。

これが意味するのは、ToDoアプリについてもmysqlという名前のホストに接続するだけで、データベースと通信できるということです。これほど簡単なことはありません。

MySQLを使ったアプリを動かす

ToDoアプリでは、MySQLコネクション設定を指定するいくつかの環境変数を設定することができます。詳細は以下の通りです。

  • MYSQL_HOST : 稼働中のMySQLサーバのホスト名です。

  • MYSQL_USER : 接続に使用するユーザ名です。

  • MYSQL_PASSWORD : 接続に使用するパスワードです。

  • MYSQL_DB : 接続後に使用するデータベースです。

注意

開発環境でコネクションの設定に環境変数を使うことは問題ありませんが、本番環境で動いているアプリケーションで使用することはとても推奨されない行為です。Dockerで以前セキュリティを担当していたDiogo Monicaさんがその理由を説明する素晴らしい記事を書いてくれています。

もっと安全な手法としては、コンテナのオーケストレーションフレームワークによって提供されるシークレットサポートを利用することがあります。ほとんどの場合、これらのシークレットファイルは稼働しているコンテナにマウントされます。多くのアプリ(MySQLイメージとToDoアプリを含めて)はファイルを含むファイルを指す_FILE接尾辞がついた環境変数もサポートしています。

例として、変数MYSQL_PASSWORD_FILEを設定すると、アプリは参照されたファイルの内容をコネクションパスワードとして使用します。なお、Dockerはこれらの環境変数を一切サポートしません。アプリは、環境変数を探して、ファイルの内容を取得する方法を知っておく必要があります。

説明は終わったので、コンテナを起動させましょう。

1.前述した環境変数をそれぞれ指定して、コンテナがアプリのネットワークに接続できるようにしましょう。

   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"

PowerShellを使用している場合は、以下のコマンドを使用してください。

   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"

2.コンテナのログを見ると(docker logs <container-id>)、MySQLデータベースを使用していることを示すメッセージがあります。

   # Previous log messages omitted
   $ 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`
   Connected to mysql db at host mysql
   Listening on port 3000

3.ブラウザでアプリを開いて、ToDoリストにいくつかアプリを追加してみてください。

4.MySQLデータベースに接続して、アイテムが書き込まれているか確認しましょう。パスワードはsecretです。

   docker exec -ti <mysql-container-id> mysql -p todos

そしてMySQLシェル内で、以下を実行してください。

   mysql> select * from todo_items;
   +--------------------------------------+--------------------+-----------+
   | id                                   | name               | completed |
   +--------------------------------------+--------------------+-----------+
   | c906ff08-60e6-44e6-8f49-ed56a0853e85 | Do amazing things! |         0 |
   | 2912a79e-8486-4bc3-a4c5-460793a575ab | Be awesome!        |         0 |
   +--------------------------------------+--------------------+-----------+

もちろん、あなたのアイテムが含まれているので、テーブルの内容は異なります。でも、ここにアイテムが保存されているのが分かりましたね。

Dockerダッシュボードを見ると、2つのコンテナが起動しています。しかし、一つのアプリにグループ化されているという表示はありません。これを改善する方法について見ていきましょう。

dashboard-multi-container-app.png

要約

独立したコンテナで動く外部のデータベースにデータを保存するアプリケーションを作ることができました。コンテナのネットワークについて少し学び、DNSを使用してサービスの発見を行う方法を理解しました。

しかし、このアプリを立ち上げるために必要な全てのことに、圧倒されているかもしれません。ネットワークを作成し、コンテナを起動し、全ての環境変数を指定し、ポートを解放したりする必要があります。覚えることが多すぎて誰かに伝えるのが難しくなっているのは確かです。

次の章では、Docker Composeについてお話しします。Docker Composeを使えば、アプリケーションスタックをより簡単に共有して、一つのシンプルなコマンドだけで起動するようにできます。

Docker Composeを使用する

Docker Composeは、マルチコンテナアプリの定義と共有をしやすくするために開発されたものです。Composeを使えば、YAMLファイルを作成することでサービスを定義して、1つのコマンドで起動したり、停止したりすることができます。

Composeを使用する大きな利点は、ファイルにアプリケーションスタックを定義し、(バージョン管理されている)プロジェクトリポジトリのルートに保存して、誰でも簡単にプロジェクトにコントリビュートできるようにできることです。実際、GitHubやGitLabにはそのようなプロジェクトがたくさんあります。

では、早速初めていきましょう。

Docker Composeをインストールする

Dockerデスクトップ/ツールボックスをWindowsかMacにインストールしているなら、すでにDocker Composeはインストールされています。また、Play-with-Dockerのインスタンスにも、Docker Composeはインストールされています。Linuxマシンをお使いなら、こちらのページに従ってDocker Composeをインストールしていただく必要があります。

インストールが完了したら、下記のコマンドを実行して、バージョン情報を確認できるはずです。

docker-compose version

Composeファイルを作成する

1.アプリのプロジェクトのルートに、docker-compose.ymlというファイルを作成します。

2.Composeファイルでは、スキーマバージョンを定義するところから始めます。ほとんどの場合、最新版を使うのが良いです。最新のスキーマバージョンと互換性については、Composeファイルのレファレンスを参照してください。

   version: 3.7

3.次に、アプリの一部として動かしたいサービス(もしくはコンテナ)のリストを定義します。

   version: "3.7"

   services:

次は、サービスをComposeファイルに移行していきましょう。

アプリのサービスを定義する

思い出していただきたいのですが、以下はアプリのコンテナを定義するのに使用したコマンドです。

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"

PowerShellを使用している場合は、以下のようなコマンドを使用しました。

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"

1.最初に、コンテナのためのサービスエントリとイメージを定義します。サービス名は任意のものを選ぶことができます。名前は自動的にネットワークエイリアスとして使用されるので、MySQLサービスを定義するのが楽になります。

   version: "3.7"

   services:
     app:
       image: node:12-alpine

2.一般的に、commandはimageの定義の近くに書きますが、順序は自由です。では、ファイルに書き込みましょう。

   version: "3.7"

   services:
     app:
       image: node:12-alpine
       command: sh -c "yarn install && yarn run dev"

3.コマンドの-p 3000:3000という部分をportsに移行しましょう。ここでは簡略した書き方を使用しますが、冗長で長い書き方も同じように使用できます。

   version: "3.7"

   services:
     app:
       image: node:12-alpine
       command: sh -c "yarn install && yarn run dev"
       ports:
         - 3000:3000

4.次に、ワーキングディレクトリ(-w /app)とボリュームマッピング(-v "$(pwd):/app")をworking_dirvolumesに移行します。Volumesの書き方にも短いのと長いのがあります。

Docker Composeのボリューム定義の長所として、カレントディレクトリからの相対パスが使用できることがあります。

   version: "3.7"

   services:
     app:
       image: node:12-alpine
       command: sh -c "yarn install && yarn run dev"
       ports:
         - 3000:3000
       working_dir: /app
       volumes:
         - ./:/app

5.最後に、環境変数をenvironmentキーを使って移行します。

   version: "3.7"

   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
         MYSQL_USER: root
         MYSQL_PASSWORD: secret
         MYSQL_DB: todos

MySQLサービスを定義する

では、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

PowerShellを使用している場合は、以下のコマンドを使用してください。

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

1.まず新しいサービスを定義して、mysqlと名付けると、自動でネットワークエイリアスを取得します。使用するイメージを指定しましょう。

   version: "3.7"

   services:
     app:
       # The app service definition
     mysql:
       image: mysql:5.7

2.次に、ボリュームマッピングを指定します。docker runでコンテナを起動したとき、ネームドボリュームが自動で作成されました。しかし、Composeを使用して起動したときには作成されません。トップレベルにあるvolume:でボリュームを定義してから、サービスコンフィグのマウントポイントを指定します。ボリューム名だけを指定すると、デフォルトのオプションが使用されます。ですが、他にも多くのオプションが利用可能です。

   version: "3.7"

   services:
     app:
       # The app service definition
     mysql:
       image: mysql:5.7
       volumes:
         - todo-mysql-data:/var/lib/mysql

   volumes:
     todo-mysql-data:

3.最後に、環境変数を指定します。

   version: "3.7"

   services:
     app:
       # The app service definition
     mysql:
       image: mysql:5.7
       volumes:
         - todo-mysql-data:/var/lib/mysql
       environment: 
         MYSQL_ROOT_PASSWORD: secret
         MYSQL_DATABASE: todos

   volumes:
     todo-mysql-data:

完成したdocker-compose.ymlは以下のようになります。

version: "3.7"

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
      MYSQL_USER: root
      MYSQL_PASSWORD: secret
      MYSQL_DB: todos

  mysql:
    image: mysql:5.7
    volumes:
      - todo-mysql-data:/var/lib/mysql
    environment: 
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: todos

volumes:
  todo-mysql-data:

アプリケーションスタックを起動する

docker-compose.ymlが用意できたので、あとは起動するだけです。

1.まず、他にapp/dbのコピーが実行されていないことを確認してください。(docker psdocker rm -f <ids>を使用してください。)

2.docker-compose upコマンドを使用して、アプリケーションスタックを起動してください。-dフラグを追加して、バックグラウンドで実行されるようにします。

   docker-compose up -d

実行すると、以下のように出力されるはずです。

   Creating network "app_default" with the default driver
   Creating volume "app_todo-mysql-data" with default driver
   Creating app_app_1   ... done
   Creating app_mysql_1 ... done

ネットワークと同じくボリュームも作成することができていますね。デフォルトでは、Docker Composeはアプリケーションスタックのネットワークを自動で作成します(Composeファイルで定義しなかったのはこのためです)。

和訳者メモ - docker-compose upで以下のエラーが表示された場合

[ERROR] [FATAL] InnoDB: Table flags are 0 in the data dictionary but the flags in file ./ibdata1 are 0x4800!

以下のコマンドでボリューム削除すると直りました。最初に間違えてmysql:latestdocker-compose.ymlを書いたのが原因だったかもしれないです。

docker volume rm app_todo-mysql-data

3.docker-compose logs -fコマンドを使用してログを見て見ましょう。各サービスのログが一つに集約されているのが分かると思います。これは、タイミングに関する問題を確認したいときにとても便利です。-fフラグはログを追従するので、生成されたログをライブ出力します。

実際の出力は以下のようになります。

   mysql_1  | 2019-10-03T03:07:16.083639Z 0 [Note] mysqld: ready for connections.
   mysql_1  | Version: '5.7.27'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)
   app_1    | Connected to mysql db at host mysql
   app_1    | Listening on port 3000

最初のラインの(色付けされている)サービス名は、メッセージを識別するのに役立ちます。特定のサービスについてのログを見たいときは、ログコマンドの末尾にサービス名を追加してください(例:docker-compose logs -f app)。

応用 - アプリを起動する前にデータベースを待機させる

実際には、アプリケーションを起動しても、MySQLが起動して準備が整ってから接続を試みます。Dockerには、コンテナが完全に起動、実行、準備されてから別のコンテナを起動するための組み込みのサポートは用意されていません。Nodeベースのプロジェクトでは、wait-portを使用できます。同じようなプロジェクトは、他の言語・フレームワークにおいても存在します。

4.アプリを開くと、起動しているのを確認できるはずです。そして、停止も一つのコマンドで行えます。

Dockerダッシュボードでアプリケーションスタックを確認する

Dockerダッシュボードを見ると、appという名前のグループがあります。これはDocker Composeのプロジェクト名で、コンテナをグループ化するのに使用されています。デフォルトでは、プロジェクト名はdocker-compose.ymlが配置されているディレクトリの名前です。

dashboard-app-project-collapsed.png

appのドロップダウンを開くと、composeファイルで定義した2つのコンテナが確認できます。そちらの名前は<プロジェクト名>_<サービス名>_<複製番号>というふうになっています。そのおかげで、どのコンテナがどのアプリか、どのコンテナがmysqlデータベースかということが分かりやすくなっています。

dashboard-app-project-expanded.png

全て停止する

docker-compose downを実行するか、Dockerダッシュボードでapp全体をゴミ箱に入れるだけで、全て停止できます。コンテナは停止され、ネットワークは削除されます。

ボリュームの削除

デフォルトでは、docker-compose downを実行しても、composeファイル内のネームドボリュームは削除されません。ボリュームを削除したい場合は、--volumesフラグを付け加える必要があります。

Dockerダッシュボードでは、アプリスタックを削除してもボリュームは削除されません。

停止したら、他のプロジェクトに切り替えてdocker-compose upを実行するだけで、そのプロジェクトを開発することができます。とても簡単ですよね。

要約

この章では、Docker Composeについてと、それによって複数サービスのアプリケーションの定義と共有がどれだけ簡単になるのかということを学びました。使っていたコマンドを適切なCompose形式に移行してComposeファイルを作成しました。

チュートリアルも終盤に入りました。しかし、これまで使ってきたDockerfileには大きな問題があるので、イメージ構築に関するベストプラクティスをいくつか取り上げていきたいと思います。では、みてみましょう。

イメージ構築のベストプラクティス

イメージのレイヤ

イメージの構成要素を確認できることをご存知ですか?docker image historyコマンドを使用すれば、イメージに含まれる各レイヤの作成に使用されたコマンドを確認することができます。

1.docker image historyコマンドを使用して、このチュートリアルで以前作成したgetting-startedイメージのレイヤを見てみましょう。

   docker image history getting-started

すると、以下のように表示されるはずです(おそらくIDは異なります。)。

   IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
   a78a40cbf866        18 seconds ago      /bin/sh -c #(nop)  CMD ["node" "src/index.j…    0B                  
   f1d1808565d6        19 seconds ago      /bin/sh -c yarn install --production            85.4MB              
   a2c054d14948        36 seconds ago      /bin/sh -c #(nop) COPY dir:5dc710ad87c789593…   198kB               
   9577ae713121        37 seconds ago      /bin/sh -c #(nop) WORKDIR /app                  0B                  
   b95baba1cfdb        13 days ago         /bin/sh -c #(nop)  CMD ["node"]                 0B                  
   <missing>           13 days ago         /bin/sh -c #(nop)  ENTRYPOINT ["docker-entry…   0B                  
   <missing>           13 days ago         /bin/sh -c #(nop) COPY file:238737301d473041…   116B                
   <missing>           13 days ago         /bin/sh -c apk add --no-cache --virtual .bui…   5.35MB              
   <missing>           13 days ago         /bin/sh -c #(nop)  ENV YARN_VERSION=1.21.1      0B                  
   <missing>           13 days ago         /bin/sh -c addgroup -g 1000 node     && addu…   74.3MB              
   <missing>           13 days ago         /bin/sh -c #(nop)  ENV NODE_VERSION=12.14.1     0B                  
   <missing>           13 days ago         /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B                  
   <missing>           13 days ago         /bin/sh -c #(nop) ADD file:e69d441d729412d24…   5.59MB   

それぞれにレイヤがイメージのレイヤを表しています。この画面では、イメージのベースが下部、最新のレイヤーが上部に表示されています。これを使えば、素早くそれぞれのレイヤを確認し、サイズの大きいイメージを突き止めるのに役立ちます。

2.いくつかの行が省略されていることに気がついたでしょうか。--no-truncフラグを付け加えると、フル出力を得ることができます。(省略されたフラグを使って省略されていない出力を得るって、面白いですよね。)

   docker image history --no-trunc getting-started

レイヤーのキャッシュ

実際にレイヤーを見たわけですが、これはコンテナイメージのビルド時間を減らすことに関してとても重要な話です。

レイヤーを変更したら、下流の全てのレイヤーを再生成する必要があります。

使用していたDockerfileをもう一度見てみましょう。

FROM node:12-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]

image historyの出力の話に戻ると、Dockerfileのそれぞれのコマンドがイメージの新しいレイヤーになっていることが分かります。イメージに変更を加えたとき、yarnの依存関係が再インストールされたことを覚えているかもしれません。これを修正する方法はあるのでしょうか。ビルドするたびに同じ依存関係をインストールするなんて馬鹿げていますよね。

これを修正するには、Dockerfileを再構築して依存関係をキャッシュする必要があります。Nodeベースのアプリケーションでは、package.jsonファイルで依存関係が定義されています。ということは、このファイルだけを最初にコピーしておけば、依存関係がインストールされてから他のファイルがコピーされます。なので、package.jsonを変更したときのみyarnの依存関係が再作成されるようになります。素晴らしいでしょう?

1.Dockerfileを更新して、最初にpackage.jsonのコピーが行われ、依存関係がインストールされ、それから他のファイルがコピーされるようにします。

   FROM node:12-alpine
   WORKDIR /app
   COPY package.json yarn.lock ./
   RUN yarn install --production
   COPY . .
   CMD ["node", "src/index.js"]

2.Dockerfileと同じフォルダに.dockerignoreというファイルを作成し、以下の内容を書き込みます。

   node_modules

.dockerignoreファイルを使えば、イメージに必要なファイルだけを選択してコピーすることができます。詳しくはここを参照してください。この場合、node_modulesフォルダは2回目のCOPYで除外されます。さもなければ、RUNのコマンドで生成されたファイルで上書きされるでしょう。なぜこの方法がNode.jsアプリケーションで推奨されているかということと、他のベストプラクティスについての詳細は、DockerでのNode製Webアプリをご覧ください。

3.docker buildで新しいイメージをビルドします。

   docker build -t getting-started .

以下のように表示されるはずです。

   Sending build context to Docker daemon  219.1kB
   Step 1/6 : FROM node:12-alpine
   ---> b0dc3a5e5e9e
   Step 2/6 : WORKDIR /app
   ---> Using cache
   ---> 9577ae713121
   Step 3/6 : COPY package.json yarn.lock ./
   ---> bd5306f49fc8
   Step 4/6 : RUN yarn install --production
   ---> Running in d53a06c9e4c2
   yarn install v1.17.3
   [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 10.89s.
   Removing intermediate container d53a06c9e4c2
   ---> 4e68fbc2d704
   Step 5/6 : COPY . .
   ---> a239a11f68d8
   Step 6/6 : CMD ["node", "src/index.js"]
   ---> Running in 49999f68df8f
   Removing intermediate container 49999f68df8f
   ---> e709c03bc597
   Successfully built e709c03bc597
   Successfully tagged getting-started:latest

全てのレイヤーが再構築されているのがわかると思います。Dockerfileを大きく変更したので、もう問題ありません。

4.src/static/index.htmlファイルを編集しましょう(<title>のところの内容を"The Awesome Todo App"に変更します)。

5.もう一度docker build -t getting-started .でDockerイメージをビルドします。今回は、表示される内容が少し変わって、以下のようになるはずです。

   Sending build context to Docker daemon  219.1kB
   Step 1/6 : FROM node:12-alpine
   ---> b0dc3a5e5e9e
   Step 2/6 : WORKDIR /app
   ---> Using cache
   ---> 9577ae713121
   Step 3/6 : COPY package.json yarn.lock ./
   ---> Using cache
   ---> bd5306f49fc8
   Step 4/6 : RUN yarn install --production
   ---> Using cache
   ---> 4e68fbc2d704
   Step 5/6 : COPY . .
   ---> cccde25a3d9a
   Step 6/6 : CMD ["node", "src/index.js"]
   ---> Running in 2be75662c150
   Removing intermediate container 2be75662c150
   ---> 458e5c6f080c
   Successfully built 458e5c6f080c
   Successfully tagged getting-started:latest

ビルドがとても早くなったのに気がついたでしょう。また、ステップ1-4には全てUsing casheが含まれているのが分かると思います。これで、ビルドキャッシュを使うことができました。イメージをプッシュし、プルし、更新するのがとても早くなります。

マルチステージビルド

このチュートリアルではあまり深く触れませんが、マルチステージビルドは、複数のステージを使用してイメージを作成するのに役立つとんでもなくパワフルなツールです。以下のような利点があります。

  • ランタイムの依存関係とビルド時間の依存関係を分離できます。

  • アプリが起動するのに必要な分だけを配置することで、イメージ全体のサイズを減らします。

MavenとTomcatの例

Javaベースのアプリケーションを構築する際には、ソースコードをJavaバイトコードにコンパイルするのにJDKが必要になります。しかし、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と名付けた1つ目のステージで、Mavenを用いたJavaのビルドを行います。FROM tomcatから始まる2つ目のステージで、buildステージからファイルをコピーします。最終的なイメージは、作成される最終ステージ(--targetフラグを使用するとオーバーライドされます)だけです。

Reactの例

Reactアプリケーションをビルドするときは、JSコード(通常はJSX)、SASSスタイルシートなどを静的なHTMLとJS、CSSにコンパイルするNode環境が必要がです。サーバーサイドレンダリングが必要ない場合は、本番環境のビルドにNode環境は必要ありません。それなら、静的なNginxコンテナに静的なリソースを配置すればいいだけの話ですよね。

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コンテナにコピーしています。この方がいいでしょ?

要約

イメージが構造化される方法を少し理解したことで、イメージのビルドは早くなり、より少ない変更を配置できるようになりました。また、マルチステージビルドは、イメージ全体のサイズを減らし、ビルドタイムの依存関係とランタイムの依存関係を切り離すことで最終的なコンテナのセキュリティを向上させるのに役立ちます。

次にすること

チュートリアルは終わったわけですが、コンテナについて学ぶことはもっとあります。ここで深く触れることはないですが、少しみてみましょう。

コンテナのオーケストレーション

本番環境でコンテナを動かすのは難しいことです。マシンにログインしてdocker rundocker-compose upを実行したいとは思いません。なぜでしょうか?では、コンテナが死んだら何が起こるでしょうか?複数のマシンにまたがってスケールする方法は?コンテナのオーケストレーションはこの問題を解決します。Kubernetes、Swarm、Nomad、ECSのようなツールは全てこの問題をわずかに異なる方法で解決します。

予期される状態を受け取る「マネージャ」を持つというのが一般的な考えです。この状態というのは、「Webアプリのインスタンスを2つ起動して、80番ポートを開放してほしい」というようなものです。マネージャはクラスタ内の全てのマシンを監視し、作業を「ワーカ」に委任します。マネージャは(コンテナが停止された、のような)変更を監視し、それから実際の状態が予期された状態を反映するように動作します。

クラウドネイティブコンピューティング基盤プロジェクト

CNCF(クラウドネイティブ基盤のプロジェクト)は、ベンダーに依存しない様々なオープンソースプロジェクトで、Kubernetes、Prometheus、Envoy、Linkerd、NATSなどを含みます。作られたプロジェクトはこちら、全体的なCNCFの図はこちらで見ることができます。これらのたくさんのプロジェクトは、モニタリング、ロギング、セキュリティ、イメージレジストリ、メッセージングなどの問題を解決するのに役立ちます。

なので、コンテナの全容とクラウドネイティブアプリケーションの開発がよくわからない場合は、ぜひご利用ください。コミュニティに参加し、質問をして、勉強してみてください。あなたが参加してくれることを心待ちにしております。

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

Dockerを体系的に学べる公式チュートリアル和訳

この記事について

  • この記事は、Docker Desktopのチュートリアルを和訳したものです。

  • 公式のチュートリアルなので、安心して、かつ効率的に学習することができます。

  • Docker DesktopからDocker Hub、Docker Composeまで網羅されているので、初学者がDockerに初めて触れたり、中級者が基礎を振り返るのに最適です。

  • 翻訳元のチュートリアルは、2020/09/13時点で最新のものです。長い時間が経過している場合、情報が古くなっている場合がございますのでご注意ください。

  • 読者に誤解を与えない部分は、読みやすさを重視して適宜意訳しています。

  • DeepL等を使用して推敲は行っていますが、間違っているところやより良い表現があれば、編集リクエストをお願いいたします。

翻訳元

getting-started : https://github.com/docker/getting-started/tree/6190776cb618b1eb3cfb21e207eefde511d13449

Dockerデスクトップ

Dockerデスクトップは、コンテナ化されたアプリケーションとマイクロサービスを構築・共有するためのツールです。MacOSとWindows上で動作します。

和訳者メモ

Dockerデスクトップをインストールするには、ここにアクセスしてダウンロードするか、下記コマンドを実行してHomebrew Caskよりインストールしてください。

$ brew cask install docker

Dockerデスクトップを開くと、チュートリアルが開始します。それぞれのコマンド等については後半で詳しく説明するので、ここではどのような流れでコンテナが作成されるのかを体感してください。

Clone

まず、レポジトリをクローンします。

Getting Started のプロジェクトは、シンプルなGithubレポジトリで、イメージを作成してコンテナとして実行するために必要なものが全て含まれています。

$ git clone https://github.com/docker/getting-started.git

Build

次に、イメージを作成します。

Dockerイメージは、コンテナのためのプライベートなファイルシステムです。コンテナが必要とする全てのファイルとコードを提供します。

$ cd getting-started
$ docker build -t docker101tutorial .

Run

コンテナを実行しましょう。

前のステップで作成したイメージを元にしたコンテナを起動します。コンテナを起動すると、PCの他の場所から安全に隔離されたリソースを使用してアプリケーションを起動できます。

$ docker run -d -p 80:80 --name docker-tutorial docker101tutorial

Share

イメージを保存して共有しましょう。

イメージをDocker Hubに保存・共有すると、他のユーザーがどんな目的のマシン上でも、簡単にイメージをダウンロードして起動できるようになります。

なお、Docker Hubを利用するには、Dockerアカウントを作成する必要があります。

$ docker tag docker101tutorial michinosuke/docker101tutorial
$ docker push michinosuke/docker101tutorial

docker_01.png

Docker Tutorial

Dockerデスクトップのチュートリアルで作成したコンテナにアクセスすると、より詳しいDocker Tutorialが始まります。

http://localhostにアクセスしましょう。

はじめよう

さっき実行したコマンドについて

このチュートリアルのコンテナを立てることができました。

まず、先ほど実行したコマンドについて説明します。忘れているかもしれないので、もう一度書きます。

docker run -d -p 80:80 docker/getting-started

いくつかフラグが使用されているのに気付いたと思います。それぞれのフラグは以下のような意味があります。

  • -d : コンテナをデタッチモード(バックグラウンド)で実行する。

  • -p : ホストの80番ポートをコンテナの80番ポートにマッピングする。

  • docker/getting-started : 使用するイメージ

応用

一文字のフラグは結合することでコマンド全体を短くできます。例えば、上のコマンドは以下のようにも書けます。

docker run -dp 80:80 docker/getting-started

Dockerダッシュボード

チュートリアルを進める前に、PC上で起動しているコンテナ一覧を表示できるDockerダッシュボードを紹介します。Dockerダッシュボードを使えば、コンテナのログに素早くアクセスできたり、コンテナ内のシェルを取得できたり、コンテナのライフサイクル(停止や削除)を簡単に管理できたりします。

ダッシュボードにアクセスするには、MacまたはWindowsの手順に従ってください。今開いてみると、このチュートリアルが起動しているはずです。コンテナの名前(下ではjolly_boumanになっている)は、ランダムに生成された名前です。なので、違う名前で表示されていると思います。

tutorial-in-dashboard.png

コンテナとは?

コンテナを起動したわけですが、コンテナとは一体なんなのでしょうか?簡単にいうと、コンテナはホストマシンにある他の全てのプロセスから隔離された、シンプルな別のプロセスです。隔離には、カーネルの名前空間とCgroups、長い間Linuxで使用されていた機能を使用します。Dockerはこれらの機能を親しみやすく使いやすいものにするために取り組んできました。

コンテナイメージとは?

コンテナを実行する際には、隔離されたファイルシステムを使用します。このファイルシステムはコンテナイメージから供給されます。イメージにはコンテナのファイルシステムが含まれるので、アプリケーションを実行するのに必要な全ての依存関係・設定・スクリプト・バイナリなどはすべてイメージに含まれている必要があります。また、イメージには、環境変数や起動時のデフォルトコマンド、その他のメタデータなどコンテナの設定も含まれています。

イメージのレイヤ、ベストプラクティスなどの詳細な内容については後で紹介します。

情報

chrootをご存知なら、コンテナはchrootの拡張版だと考えてください。ファイルシステムは、ただイメージから持ってきたものです。しかし、コンテナにはchrootより強力な分離機能が追加されています。

使うアプリの紹介

ここからは、Node.jsで動くシンプルなリスト管理アプリを使って進めていきます。Node.jsが分からなくても問題ありません。JavaScriptについての知識も必要ないです。

ここでは、開発チームはとても小さく、MVP(実用最小限の製品)を示すためにシンプルなアプリを作っている状況を想定します。大規模なチームや複数の開発者などに対してどう動作するかを考える必要はなく、どうやって動くか、何ができるのかを示すためのアプリを作成します。

todo-list-sample.png

アプリを手に入れる

アプリケーションを動かす前に、アプリケーションのソースコードをPCに入れる必要があります。実際のプロジェクトでは、リポジトリからクローンするのが一般的だと思います。しかし、このチュートリアルでは、アプリケーションが入ったZIPファイルを作成しておいたので、そちらを使用します。

  1. ZIPファイルをダウンロードしたら、ZIPファイルを開いて、解凍してください。

  2. 解凍したら、任意のエディターでプロジェクトを開いてください。エディターをインストールしていない場合、Visual Studio Codeを使用してください。package.jsonと2つのサブディレクトリ(srcspec)が表示されるはずです。

ide-screenshot.png

アプリのコンテナイメージを作成する

アプリケーションを構築するには、Dockerfileを使用します。Dockerfileは、コンテナイメージを作成するために使われるテキストベースの命令スクリプトです。今までにDockerfileを作成したことのある方なら、下のDockerfileには欠陥があると分かるかもしれません。それについては後で説明します。

1.以下の内容を書き込んだDockerfilepackage.jsonがあるディレクトリに作成してください。

   FROM node:12-alpine
   WORKDIR /app
   COPY . .
   RUN yarn install --production
   CMD ["node", "src/index.js"]

Dockerfile.txtなどの拡張子が付いていないか確認してください。エディターによっては自動的に拡張子をつけてしまい、次のステップでエラーになる場合があります。

2.移動していない場合は、ターミナルを開いてDockerfileのあるappディレクトリ に移動します。docker buildコマンドを使用してコンテナイメージを構築しましょう。

   docker build -t getting-started .

このコマンドは、Dockerfileを使用して、新しいコンテナイメージを構築します。たくさんの"レイヤ"がインストールされたのに気がついたと思います。なぜかというと、node:12-alpineイメージを起点にすることをビルダーに指示したからです。しかし、そのイメージがPC上になかったので、イメージをダウンロードする必要がありました。

イメージがダウンロードされた後、アプリケーションをコピーし、yarnを使用してアプリケーションの依存関係をインストールしました。CMD命令は、このイメージからコンテナが起動されたときに実行されるデフォルトのコマンドを指定します。

最後に、-tフラグは、イメージにタグを付けます。これは、イメージに人間の理解しやすい名前をつけるものだと考えてください。ここでは、イメージにgetting-startedと名付けたので、コンテナを起動するときはこの名前を参照することができます。

docker buildコマンドの最後につけた.は、DockerがカレントディレクトリにあるDockerfileを探すことを示しています。

アプリのコンテナを起動する

イメージは用意したので、アプリケーションを実行させてみましょう。それには、docker runコマンドを使用します。(すでに一度使ったのを覚えていますか?)

1.docker runコマンドを使用してコンテナを起動し、先ほど作成したイメージの名前を指定してください。

   docker run -dp 3000:3000 getting-started

-dフラグと-pフラグを覚えていますか?新しいコンテナをデタッチ(バックグランド実行)モードで起動し、ホストの3000番ポートをコンテナの3000番ポートにマッピングしました。ポートマッピングをしなかった場合、アプリケーションにアクセスすることはできません。

2.数秒後、http://localhost:3000をWebブラウザで開いてみてください。アプリが表示されるはずです。

todo-list-empty.png

3.1つか2つアイテムを追加してみて、期待通りの動作になるか確認してください。アイテムに完了のチェックを入れたり、アイテムを削除することができます。フロントエンドがアイテムをバックエンドに保存できています。とても簡単でしょう?

この時点で、いくつかのアイテムを持つTodoリスト管理アプリができました。それでは、少し変更を加えながら、コンテナの管理について学んでいきましょう。

Dockerダッシュボードを見てみると、コンテナが2つ起動しているのが分かります。(このチュートリアル自身と、起動したばかりのアプリコンテナです。)

dashboard-two-containers.png

要約

この章では、コンテナイメージの構築についての基本的なことを学び、そのためのDockerfileを作成しました。イメージを構築してから、コンテナを起動し、実行中のアプリを触ってみました。

次は、アプリに修正を加えて、実行中のアプリを新しいイメージで更新する方法を学びましょう。途中で、便利なコマンドもいくつか学びます。

アプリをアップデートする

ちょっとした機能のリクエストとして、プロダクトチームからToDoリストのアイテムが存在しないときに表示される「空のテキスト」を変更してほしいという依頼がありました。以下のように変更したいとのことです。

You have no todo items yet! Add one above!

簡単ですよね?この変更を加えていきます。

ソースコードを更新する

1.src/static/js/app.jsの56行目を書き換えて、新しいテキストが使用されるようにします。

   - <p className="text-center">No items yet! Add one above!</p>
   + <p className="text-center">You have no todo items yet! Add one above!</p>

2.先ほど使ったものと同じコマンドを使って、更新したイメージをビルドしましょう。

   docker build -t getting-started .

3.更新したコードを使って新しいコンテナを起動しましょう。

   docker run -dp 3000:3000 getting-started

あ゛!多分こんなエラーが表示されたと思います。(IDは違います)

docker: Error response from daemon: driver failed programming external connectivity on endpoint laughing_burnell 
(bb242b2ca4d67eba76e79474fb36bb5125708ebdabd7f45c8eaf16caaabde9dd): Bind for 0.0.0.0:3000 failed: port is already allocated.

何が起こったんでしょうか。古いコンテナが動いていたので、新しいコンテナを立ち上げることができなかったのです。この問題が起こった理由として、特定のポートをリッスンできるのはコンテナがあるPC上で一つのプロセス(コンテナを含む)だけですが、コンテナは3000番ポートを既に使っていたからです。このエラーを解消するためには、古いコンテナを削除する必要があります。

古いコンテナを置き換える

コンテナを削除するためには、まず停止させる必要があります。停止してしまえば、削除できます。古いコンテナを削除するには、2通りの方法があります。お好きな方法をお選びください。

CLIでコンテナを削除する

1.docker psコマンドを使用してコンテナのIDを取得します。

   docker ps

2.docker stopコマンドを使用してコンテナを停止させます。

   # <the-container-id>はdocker psコマンドで取得したIDで置き換えてください。
   docker stop <the-container-id>

3.コンテナを停止したら、docker rmコマンドで削除します。

   docker rm <the-container-id>

応用

docker rmコマンドに"force"フラグを追加することで、一つのコマンドでコンテナの停止と削除を行うことができます。

例)docker rm -f <the-container-id>

Dockerダッシュボードを使ってコンテナを削除する

Dockerダッシュボードを開いて、二回クリックするだけでコンテナを削除することができます。コンテナIDを探して削除するよりはるかに簡単です。

  1. ダッシュボードを開いて、アプリのコンテナにカーソルを合わせると、アクション一覧が右側に表示されます。

  2. ゴミ箱ボタンをクリックすると、コンテナが削除されます。

  3. 削除を確認したら完了です。

dashboard-removing-container.png

更新したアプリを起動する

1.更新したアプリを起動します。

   docker run -dp 3000:3000 getting-started

2.http://localhost:3000でブラウザを再読み込みすると、アップデートされたテキストが表示されます。

todo-list-empty.png

要約

アプリの更新ができた一方で、特筆すべきことが2点ありました。

  • 存在していた全てのToDoリストが全て消滅してしまったことです。これではいいアプリとは言えません。これについては近いうちに話そうと思います。

  • 小さな変更だったのにもかかわらず、多くのステップを踏む必要がありました。次の章では、変更を加えるたびに新しいコンテナの再構築と起動を行わなくてもコードを更新する方法について紹介します。

永続性についてお話しする前に、イメージを他人と共有する方法について学びましょう。

アプリを共有する

イメージを作成できたので、早速共有してみましょう。Dockerイメージを共有するには、Dockerレジストリを使用する必要があります。デフォルトのレジストリはDocker Hubであり、今まで使ってきたイメージもそこから持ってきたものでした。

レポジトリの作成

イメージをプッシュするには、まずDocker Hub上にレポジトリを作成する必要があります。

  1. Docker Hubにアクセスして、必要ならログインしてください。

  2. Create Repositoryボタンをクリックしてください。

  3. レポジトリ名はgetting-startedを指定してください。公開レベルがPublicになっていることを確認してください。

  4. Createボタンをクリックしてください。

ページの右側を見ると、Dockerコマンドというのがあると思います。ここには、このレポジトリにプッシュするために実行する必要のあるコマンドの例が記載されています。

push-command.png

イメージをプッシュする

1.Docker Hubにあったpushコマンドをコマンドライン上で実行してみてください。注意点として、コマンドのネームスペースは"docker"ではなく、自分の名前空間を使用してください。

   $ docker push docker/getting-started
   The push refers to repository [docker.io/docker/getting-started]
   An image does not exist locally with the tag: docker/getting-started

なぜ失敗してしまったのでしょうか。このpushコマンドはdocker/getting-startedという名前のイメージを探しましたが、見つからなかったのです。docker image lsを実行してみてもイメージは見つかりません。
この問題を解決するためには、これまでに作成したイメージに別の名前をつけるため、タグ付けする必要があります。

和訳者メモ

docker image lsと同じ動作をするコマンドにdocker imagesがあります。これはDockerコマンドの再編成によるもので、docker image lsの方が新しく、推奨されています。このチュートリアルでは、今後も同じ動作をするコマンドを他に持つコマンドが登場します。

参考:https://qiita.com/zembutsu/items/6e1ad18f0d548ce6c266

2.docker login -u YOUR-USER-NAMEコマンドを使用して、Docker Hubにログインします。

3.docker tagコマンドを使用して、getting-startedイメージに新しい名前をつけます。YOUR-USER-NAMEはあなたのDocker IDに置き換えてください。

   docker tag getting-started YOUR-USER-NAME/getting-started

4.もう一度pushコマンドを実行してみましょう。イメージ名にタグは追加していないので、Docker Hubからコピー&ペーストしてきた場合は、tagnameの部分は削除してください。タグを指定しない場合、Dockerはlatestというタグを使用します。

   docker push YOUR-USER-NAME/getting-started

イメージを新しいインスタンス上で動かす

イメージを構築してレジストリにプッシュできたので、新しいインスタンス上でこのコンテナイメージを動かしてみましょう。そのために、Play with Dockerを使用します。

1.ブラウザでPlay with Dockerを開きます。

2.Docker Hubアカウントでログインします。

3.ログインしたら、左のバーにある「+ ADD NEW INSTANCE」リンクをクリックします(見当たらない場合、ブラウザを少し横に広げてください)。数秒後、ブラウザ上にターミナルウィンドウが表示されます。

pwd-add-new-instance.png

4.ターミナル上で、プッシュしたアプリを起動させましょう。

   docker run -dp 3000:3000 YOUR-USER-NAME/getting-started

イメージが取得されたのち、起動します。

5.3000と書かれたバッジが表示されるので、それをクリックすると、変更を加えたアプリが表示されます。やりましたね。3000と書かれたバッジが表示されない場合、「Open Port」ボタンをクリックして、3000と入力してください。

要約

この章では、イメージをレジストリにプッシュして共有する方法を学びました。それから、新しいインスタンスに入って、プッシュしたイメージを起動しました。これは、CIパイプラインでは一般的なことで、パイプラインがイメージを作成してレジストリにプッシュすると、本番環境では最新版のイメージを使用できるようになります。

ここまでは理解できたので、さっきの章の最後の話題に戻りましょう。アプリを再起動すると、ToDoリストのアイテムが全て消去されてしまうという問題がありました。当然それでは良いUX(ユーザー体験)とは言えないので、どうやってリスタートした後もデータを保持するかを学びましょう。

データベースを永続化する

気がついたと思いますが、ToDoリストはコンテナを起動するたびに初期化されています。なぜでしょうか。コンテナがどのように動作しているのかをもう少し掘り下げてみましょう。

コンテナのファイルシステム

コンテナが起動したとき、イメージの様々なレイヤがファイルシステムのために使用されます。また、それぞれのコンテナは作成/更新/削除を行うための「スクラッチスペース」を確保します。同じイメージが使われている場合でも、変更は別のコンテナに影響しません。

実際に見てみる

実際にみてみるために、2つのコンテナを起動させ、それぞれにファイルを作成しましょう。片方のコンテナでファイルを作成しても、もう一方のコンテナでそのファイルが有効で無いことがわかります。

1.1から10000までのランダムな数字を書き込んだ/data.txtを作成するubuntuコンテナを起動します。

docker run -d ubuntu bash -c "shuf -i 1-10000 -n 1 -o /data.txt && tail -f /dev/null"

コマンドに詳しい方なら、Bashシェルを起動して、二つのコマンドを呼び出していることが分かると思います(そのために&&を使用しています)。最初の部分で、ランダムな1つの数字を/data.txtに書き込んでいます。2つ目のコマンドは、コンテナの実行を維持するためにファイルを監視し続けているだけです。

2.出力されたものを確認するために、execでコンテナの中に入ってみましょう。ダッシュボードを開いて、起動しているubuntuイメージの最初のアクションをクリックすることで、これを行うことができます。

dashboard-open-cli-ubuntu.png

ubuntuコンテナの中でシェルが起動しているのがわかったと思います。以下のコマンドを実行して、/data.txtの内容を表示してみましょう。それができたら、またターミナルを閉じてください。

   cat /data.txt

同じことをコマンドラインを使って行いたい場合は、docker execを使用してください。docker psでコンテナのIDを取得したのち、以下のコマンドでファイルの内容を取得できます。

   docker exec <container-id> cat /data.txt

ランダムな数字が表示されるはずです。

3.別のubuntuコンテナを立ち上げて、同じファイルが存在しないか確認してみましょう。

docker run -it ubuntu ls /

data.txtがありません。書き込まれた先は、最初のコンテナのためのスクラッチスペースだったからです。

4.docker rm -fコマンドを使用して最初のコンテナを削除します。

コンテナのボリューム

ここまでで、コンテナは起動時にイメージの定義から始めることがわかりました。コンテナはファイルの作成、更新、削除を行うことができますが、コンテナが削除されると失われ、全ての変更はそのコンテナに限定されます。しかし、ボリュームを使えば、その全てを変更することができます。

ボリュームは、コンテナの特定のファイルシステムパスがホストマシンに接続できるようにする機能を提供します。コンテナ内のディレクトリがマウントされている場合、そのディレクトリの変更はホストマシンにも影響します。コンテナを再起動しても同じディレクトリをマウントした場合、同じファイルを参照できるというわけです。

ボリュームには2通りのタイプがあります。どちらも使うのですが、とりあえずネームドボリュームを使ってみましょう。

Todoデータを永続化する

ToDoアプリはデータを/etc/todos/todo.dbにあるSQLite Databaseに保存しています。SQLiteが分からなくても気にしないでください。SQLiteはシンプルなリレーショナルデータベースで、一つのファイルに全てのデータを保存しています。これは大きなデータを扱う上ではベストな方法ではないのですが、小さなデモアプリでは有効です。違うデータベースエンジンに切り替える方法については後述します。

データベースが単一のファイルであるため、このファイルをホストで永続化して次のコンテナから参照できるようにすれば、中断した最後のところから再開できるようになるはずです。ボリュームを作成してデータが保存されているディレクトリに接続(マウントともいいます)すれば、データが永続化できます。コンテナがtodo.dbファイルに書き込むと、ボリューム内のホストに保持されます。

軽く触れておくと、これから使おうとしているのは、ネームドボリュームです。ネームドボリュームはデータを入れるバケツと考えてください。Dockerはディスク上に物理的な領域を確保するので、ボリュームの名前だけ覚えておけば良いです。ボリュームを利用するときに、Dockerが正しいデータが取得されているかを検証してくれます。

1.docker volume createコマンドでボリュームを作成します。

   docker volume create todo-db

2.すでに立ち上げたToDoアプリは持続したボリュームを使用せずに実行されているので、ダッシュボードを使うかdoker rm -f <id>コマンドを使用して停止させてください。

3.ToDoアプリのコンテナを起動するのですが、ボリューム接続を指定するのに-vフラグを付け加えてください。ネームドボリュームを使用して/etc/todosに接続し、全てのファイルをキャプチャします。

   docker run -dp 3000:3000 -v todo-db:/etc/todos getting-started

4.コンテナを立ち上げたら、アプリを開いて、ToDoリストにいくつかアイテムを追加してみてください。

items-added.png

5.ToDoアプリのコンテナを削除します。ダッシュボードを使用するか、docker psでIDを取得してからdocker rm -f <id>で削除を行ってください。

6.上記と同じコマンドを使用して、新しいコンテナを起動してください。

7.リストが表示されることを確認したら、コンテナを削除して次に進みましょう。

データの永続化の方法を理解できましたね。

応用

ネームドボリュームとバインドマウント(これについては後で話します)は、Dockerをインストールした時からサポートされている2通りのボリュームですが、ドライバプラグインも数多く存在し、NFS、SFTP、NetAppなどをサポートしています。これは、SwarmやKubernetesなどのクラスタ環境内の複数のホスト上でコンテナを起動させたときにとても重要になります。

ボリュームについて深く知る

多くの人に「ネームドボリュームを使ったときにDockerがデータを保存する実際の場所はどこなんですか?」とよく聞かれます。知りたいのであれば、docker volume inspectコマンドを使用すれば可能です。

docker volume inspect todo-db
[
    {
        "CreatedAt": "2019-09-26T02:18:36Z",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/todo-db/_data",
        "Name": "todo-db",
        "Options": {},
        "Scope": "local"
    }
]

Mountpointというのが、ディスク上にデータが保存されている実際の場所です。ほとんどのマシンでは、ホストからこのディレクトリにアクセスするのにRoot権限が必要になることに注意してください。

Dockerデスクトップでボリュームデータに直接アクセスする

Dockerデスクトップで実行している間、Dockerコマンドは実際にはマシン上の小さな仮想マシン内で実行されています。Mountpointディレクトリの実際の内容を見たければ、まず仮想マシン内に入る必要があります。

要約

この時点で、存続したまま再起動できる機能的なアプリケーションを作ることができました。投資家に見せびらかして、私たちのビジョンを理解してもらえることを願っています。

しかしながら、変更を加えるたびにイメージを再構築するのは少し時間がかかり過ぎです。変更を加えるのにはもっと良い方法があるんです。バインドマウント(さっきほのめかしたやつ)が、その方法です。さっそく見てみましょう。

バインドマウントを使用する

前の章で、ネームドボリュームを使用して、データベースの永続化を行いました。ネームドボリュームは、データの保存場所について気にする必要がないので、単にデータを保存したい場合には有効です。

バインドマウントを使えば、ホスト上の正確なMountpointをコントロールできます。データの永続化にも使用できますが、追加のデータをコンテナに提供するのによく使われます。アプリを開発する場合、バインドマウントでソースコードをコンテナに接続して、コードを変更したり、応答したり、変更をすぐに確認したりできます。

Nodeで作られたアプリの場合、ファイルの変更を監視してアプリケーションを再起動するのにはnodemonが最適です。同じようなツールは、ほとんどの言語とフレームワークに存在します。

ボリュームのタイプ比較表

バインドマウントとネームドボリュームは、Dockerエンジンが備えた2つの主なボリュームタイプです。一方で、追加のボリュームドライバは、他のユースケース(SFTP, Ceph, NetApp, S3 など)で役立ちます。

Named Volumes バインドマウント
ホストの場所 Dockerが選ぶ 自分が選ぶ
マウントの例 ( -vを使用) my-volume:/usr/local/data /path/to/data:/usr/local/data
コンテナのコンテンツで新しいボリュームを作成する Yes No
ボリュームドライバのサポート Yes No

デベロッパモードのコンテナを起動する

開発段階で使えるコンテナを起動してみましょう。以下のことを行います。

  • ソースコードをコンテナにマウントする。
  • "dev" dependenciesを含む全ての依存関係をインストールする。
  • nodemonを起動してファイルの変更を監視する

では、始めましょう。

1.今までに使用したgetting-startedコンテナが起動していないことを確認してください。

2.以下のコマンドを実行してください。何をしているかの説明もしていきます。

   docker run -dp 3000:3000 \
       -w /app -v "$(pwd):/app" \
       node:12-alpine \
       sh -c "yarn install && yarn run dev"

PowerShellを使っている場合は、以下のコマンドを使用してください。

   docker run -dp 3000:3000 `
       -w /app -v "$(pwd):/app" `
       node:12-alpine `
       sh -c "yarn install && yarn run dev"
  • -dp 3000:3000 : 今までと同じです。デタッチ(バックグラウンド)モードで起動し、ポートマッピングを作成します。

  • -w /app : "ワーキングディレクトリ"か、コマンドが実行されるカレントディレクトリを指定します。

  • -v "$(pwd):/app" : コンテナのホストから、/appディレクトリにカレントディレクトリをバインドマウントします。

  • node:12-alpine : 使用するイメージです。このイメージはDockerfileで指定した通り、アプリのベースイメージとなっていることに気をつけてください。

  • sh -c "yarn install && yarn run dev" : コマンドです。sh(alpineにはbashがありません)を使用してシェルを起動し、yarn installで全ての依存関係をインストールしてから、yarn run devを実行しています。package.jsonを見てみると、devスクリプトはnodemonを起動することがわかります。

3.docker logs -f <container-id>コマンドでログを見ることができます。これを見れば、準備ができていることが分かります。

   docker logs -f <container-id>
   $ 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

ログを見終わったら、Ctrl+Cで終了することができます。

4.では、アプリに変更を加えてみましょう。src/static/js/app.jsファイル内の「Add Item」ボタンを「Add」に変更してみましょう。109行目にあります。

   -                         {submitting ? 'Adding...' : 'Add Item'}
   +                         {submitting ? 'Adding...' : 'Add'}

5.ページをリフレッシュ(もしくは開く)だけで、ブラウザにほとんど即座に変更が反映されているのがわかるはずです。Nodeサーバーを再起動するのには数秒かかるので、エラーになった場合は、数秒後にリフレッシュしてみてください。

updated-add-button.png

6.他にも変更を加えてみてください。それが終わったら、コンテナを停止してから、docker build -t getting-started .を使用して新しいイメージをビルドしてください。

バインドマウントを使用することは、ローカル開発においてとても一般的なことです。その利点として、開発マシンにビルドツールや環境がインストールされている必要がないことがあります。docker runコマンドだけで、開発環境はプルされ、準備が完了します。のちの章でDocker Composeについて話す予定ですが、これはたくさんのフラグがついたコマンドをシンプルにすることができます。

要約

データベースを永続化し、投資家と創設者の要求と要望に迅速に対応できるようになりました。でも、ちょっと待ってください。素晴らしいニュースが飛び込んできました!

あなたのプロジェクトは、将来的に開発されることになりました。

製品化に備えて、データベースをSQLiteより拡張性の高いものに移行する必要があります。単純に考えて、リレーショナルデータベースはそのままに、MySQLを使用するべきでしょう。しかし、どうやってMySQLを動かせば良いのでしょうか?どうやってコンテナ間での通信を許可すれば良いのでしょうか?それについて、次の章で話していこうと思います。

複数のコンテナを持つアプリ

ここまで、一つのコンテナのアプリで作業してきました。しかし、アプリケーションにMySQLを追加したいです。「MySQLはどこで動かせば良いんですか?同じコンテナに動かして別々に起動すれば良いですか?」という質問がよくあります。一般的に、各コンテナは一つのことのみを行うべきです。それには、いくつか理由があります。

  • APIやフロントエンドをデータベースと異なる方法で拡張させる可能性が高い。

  • コンテナを分離することで、バージョンの更新を分離して行うことができます。

  • ローカルのデータベース用コンテナを使用することもできますが、本番環境ではデータベースを管理するサービスを使用したいと思うかもしれません。その場合、アプリと一緒にデータベースエンジンを製品に含める必要はありません。

  • 複数のプロセスを実行するには、プロセスマネージャ(コンテナは1つのプロセスしか起動しません)が必要になり、コンテナの起動/停止が複雑になります。

もっと他にも理由はあります。なので、こんなふうに動作するようにアプリをアップデートしていきます。

multi-app-architecture.png

コンテナのネットワーク

思い出して欲しいのですが、コンテナはデフォルトで独立して動作し、同じマシン上の他のプロセスやコンテナについて何も知りません。では、どうやってコンテナが他のコンテナと通信できるようにすれば良いのでしょうか?その答えがネットワークです。あなたがネットワークエンジニアである必要はありません。このルールだけ覚えておいてください。

2つのコンテナが同じネットワーク内にあるとき、お互いに通信することができます。同じネットワーク内にないときは、通信できません。

MySQLを起動する

コンテナをネットワーク上に配置するには2通りの方法があります。1つ目は、スタート時に割り当てる方法。2つ目は、すでにあるコンテナを接続する方法です。今回は、最初にネットワークを作成してから、起動したMySQLコンテナを接続しましょう。

1.ネットワークを作成します。

   docker network create todo-app

2.MySQLコンテナを起動して、ネットワークに接続します。また、データベースの初期化に使用する環境変数をいくつか定義します。(MySQL Docker Hub listingの"Environment Variables"という章を参照してください)

   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

PowerShellを使用している場合は、以下のコマンドを使用してください。

   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

--network-aliasフラグを指定しました。これについては後述します。

プロのための情報

todo-mysql-dataという名前のボリュームを使用し、それをMySQLのデータが保存される/var/lib/mysqlにマウントしました。 しかし、docker volume createコマンドは使用していません。Dockerはネームドボリュームを使おうとしていることを認識して、自動でボリュームを作成してくれたのです。

3.データベースが起動しているか確認するために、データベースに接続して接続されているか確認します。

   docker exec -it <mysql-container-id> mysql -p

もしパスワードを聞かれたら、secretと入力してください。MySQLシェル内で、データベース一覧を表示し、todosデータベースがあることを確認してください。

   mysql> SHOW DATABASES;

このように表示されるはずです。

   +--------------------+
   | Database           |
   +--------------------+
   | information_schema |
   | mysql              |
   | performance_schema |
   | sys                |
   | todos              |
   +--------------------+
   5 rows in set (0.00 sec)

todosデータベースを用意することができました。

MySQLに接続する

MySQLが起動したことは確認できたので、実際に使ってみましょう。でも、同じネットワークで別のコンテナを起動したとして、どうやってコンテナを探せば良いのでしょうか。(各コンテナはそれぞれ別のIPアドレスを持っていることを覚えておいてください。)

これを理解するために、ネットワークに関する問題のトラブルシューティングやデバッグに便利なツールが入ったnicolaka/netshootコンテナを利用しましょう。

1.nicolaka/netshootイメージを使用して、新しいコンテナを立ち上げましょう。同じネットワークに接続されていることを確認してください。

   docker run -it --network todo-app nicolaka/netshoot

2.コンテナ内で便利なDNSツールであるdigコマンドを使用します。ホスト名がmysqlのIPアドレスを探しましょう。

   dig mysql

すると、以下のように表示されるはずです。

   ; <<>> DiG 9.14.1 <<>> mysql
   ;; global options: +cmd
   ;; Got answer:
   ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 32162
   ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

   ;; QUESTION SECTION:
   ;mysql.             IN  A

   ;; ANSWER SECTION:
   mysql.          600 IN  A   172.23.0.2

   ;; Query time: 0 msec
   ;; SERVER: 127.0.0.11#53(127.0.0.11)
   ;; WHEN: Tue Oct 01 23:47:24 UTC 2019
   ;; MSG SIZE  rcvd: 44

「ANSWER SECTION」というところを見ると、mysqlAレコードが172.23.0.2とわかります(あなたのIPアドレスは違う値になっている可能性が高いです)。mysqlは通常有効なホスト名ではありませんが、Dockerはmysqlというネットワークエイリアスを持つコンテナのIPアドレスを解決することができました(--network-aliasフラグを使用したのを覚えていますか?)。

これが意味するのは、ToDoアプリについてもmysqlという名前のホストに接続するだけで、データベースと通信できるということです。これほど簡単なことはありません。

MySQLを使ったアプリを動かす

ToDoアプリでは、MySQLコネクション設定を指定するいくつかの環境変数を設定することができます。詳細は以下の通りです。

  • MYSQL_HOST : 稼働中のMySQLサーバのホスト名です。

  • MYSQL_USER : 接続に使用するユーザ名です。

  • MYSQL_PASSWORD : 接続に使用するパスワードです。

  • MYSQL_DB : 接続後に使用するデータベースです。

注意

開発環境でコネクションの設定に環境変数を使うことは問題ありませんが、本番環境で動いているアプリケーションで使用することはとても推奨されない行為です。Dockerで以前セキュリティを担当していたDiogo Monicaさんがその理由を説明する素晴らしい記事を書いてくれています。

もっと安全な手法としては、コンテナのオーケストレーションフレームワークによって提供されるシークレットサポートを利用することがあります。ほとんどの場合、これらのシークレットファイルは稼働しているコンテナにマウントされます。多くのアプリ(MySQLイメージとToDoアプリを含めて)はファイルを含むファイルを指す_FILE接尾辞がついた環境変数もサポートしています。

例として、変数MYSQL_PASSWORD_FILEを設定すると、アプリは参照されたファイルの内容をコネクションパスワードとして使用します。なお、Dockerはこれらの環境変数を一切サポートしません。アプリは、環境変数を探して、ファイルの内容を取得する方法を知っておく必要があります。

説明は終わったので、コンテナを起動させましょう。

1.前述した環境変数をそれぞれ指定して、コンテナがアプリのネットワークに接続できるようにしましょう。

   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"

PowerShellを使用している場合は、以下のコマンドを使用してください。

   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"

2.コンテナのログを見ると(docker logs <container-id>)、MySQLデータベースを使用していることを示すメッセージがあります。

   # Previous log messages omitted
   $ 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`
   Connected to mysql db at host mysql
   Listening on port 3000

3.ブラウザでアプリを開いて、ToDoリストにいくつかアプリを追加してみてください。

4.MySQLデータベースに接続して、アイテムが書き込まれているか確認しましょう。パスワードはsecretです。

   docker exec -ti <mysql-container-id> mysql -p todos

そしてMySQLシェル内で、以下を実行してください。

   mysql> select * from todo_items;
   +--------------------------------------+--------------------+-----------+
   | id                                   | name               | completed |
   +--------------------------------------+--------------------+-----------+
   | c906ff08-60e6-44e6-8f49-ed56a0853e85 | Do amazing things! |         0 |
   | 2912a79e-8486-4bc3-a4c5-460793a575ab | Be awesome!        |         0 |
   +--------------------------------------+--------------------+-----------+

もちろん、あなたのアイテムが含まれているので、テーブルの内容は異なります。でも、ここにアイテムが保存されているのが分かりましたね。

Dockerダッシュボードを見ると、2つのコンテナが起動しています。しかし、一つのアプリにグループ化されているという表示はありません。これを改善する方法について見ていきましょう。

dashboard-multi-container-app.png

要約

独立したコンテナで動く外部のデータベースにデータを保存するアプリケーションを作ることができました。コンテナのネットワークについて少し学び、DNSを使用してサービスの発見を行う方法を理解しました。

しかし、このアプリを立ち上げるために必要な全てのことに、圧倒されているかもしれません。ネットワークを作成し、コンテナを起動し、全ての環境変数を指定し、ポートを解放したりする必要があります。覚えることが多すぎて誰かに伝えるのが難しくなっているのは確かです。

次の章では、Docker Composeについてお話しします。Docker Composeを使えば、アプリケーションスタックをより簡単に共有して、一つのシンプルなコマンドだけで起動するようにできます。

Docker Composeを使用する

Docker Composeは、マルチコンテナアプリの定義と共有をしやすくするために開発されたものです。Composeを使えば、YAMLファイルを作成することでサービスを定義して、1つのコマンドで起動したり、停止したりすることができます。

Composeを使用する大きな利点は、ファイルにアプリケーションスタックを定義し、(バージョン管理されている)プロジェクトリポジトリのルートに保存して、誰でも簡単にプロジェクトにコントリビュートできるようにできることです。実際、GitHubやGitLabにはそのようなプロジェクトがたくさんあります。

では、早速初めていきましょう。

Docker Composeをインストールする

Dockerデスクトップ/ツールボックスをWindowsかMacにインストールしているなら、すでにDocker Composeはインストールされています。また、Play-with-Dockerのインスタンスにも、Docker Composeはインストールされています。Linuxマシンをお使いなら、こちらのページに従ってDocker Composeをインストールしていただく必要があります。

インストールが完了したら、下記のコマンドを実行して、バージョン情報を確認できるはずです。

docker-compose version

Composeファイルを作成する

1.アプリのプロジェクトのルートに、docker-compose.ymlというファイルを作成します。

2.Composeファイルでは、スキーマバージョンを定義するところから始めます。ほとんどの場合、最新版を使うのが良いです。最新のスキーマバージョンと互換性については、Composeファイルのレファレンスを参照してください。

   version: 3.7

3.次に、アプリの一部として動かしたいサービス(もしくはコンテナ)のリストを定義します。

   version: "3.7"

   services:

次は、サービスをComposeファイルに移行していきましょう。

アプリのサービスを定義する

思い出していただきたいのですが、以下はアプリのコンテナを定義するのに使用したコマンドです。

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"

PowerShellを使用している場合は、以下のようなコマンドを使用しました。

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"

1.最初に、コンテナのためのサービスエントリとイメージを定義します。サービス名は任意のものを選ぶことができます。名前は自動的にネットワークエイリアスとして使用されるので、MySQLサービスを定義するのが楽になります。

   version: "3.7"

   services:
     app:
       image: node:12-alpine

2.一般的に、commandはimageの定義の近くに書きますが、順序は自由です。では、ファイルに書き込みましょう。

   version: "3.7"

   services:
     app:
       image: node:12-alpine
       command: sh -c "yarn install && yarn run dev"

3.コマンドの-p 3000:3000という部分をportsに移行しましょう。ここでは簡略した書き方を使用しますが、冗長で長い書き方も同じように使用できます。

   version: "3.7"

   services:
     app:
       image: node:12-alpine
       command: sh -c "yarn install && yarn run dev"
       ports:
         - 3000:3000

4.次に、ワーキングディレクトリ(-w /app)とボリュームマッピング(-v "$(pwd):/app")をworking_dirvolumesに移行します。Volumesの書き方にも短いのと長いのがあります。

Docker Composeのボリューム定義の長所として、カレントディレクトリからの相対パスが使用できることがあります。

   version: "3.7"

   services:
     app:
       image: node:12-alpine
       command: sh -c "yarn install && yarn run dev"
       ports:
         - 3000:3000
       working_dir: /app
       volumes:
         - ./:/app

5.最後に、環境変数をenvironmentキーを使って移行します。

   version: "3.7"

   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
         MYSQL_USER: root
         MYSQL_PASSWORD: secret
         MYSQL_DB: todos

MySQLサービスを定義する

では、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

PowerShellを使用している場合は、以下のコマンドを使用してください。

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

1.まず新しいサービスを定義して、mysqlと名付けると、自動でネットワークエイリアスを取得します。使用するイメージを指定しましょう。

   version: "3.7"

   services:
     app:
       # The app service definition
     mysql:
       image: mysql:5.7

2.次に、ボリュームマッピングを指定します。docker runでコンテナを起動したとき、ネームドボリュームが自動で作成されました。しかし、Composeを使用して起動したときには作成されません。トップレベルにあるvolume:でボリュームを定義してから、サービスコンフィグのマウントポイントを指定します。ボリューム名だけを指定すると、デフォルトのオプションが使用されます。ですが、他にも多くのオプションが利用可能です。

   version: "3.7"

   services:
     app:
       # The app service definition
     mysql:
       image: mysql:5.7
       volumes:
         - todo-mysql-data:/var/lib/mysql

   volumes:
     todo-mysql-data:

3.最後に、環境変数を指定します。

   version: "3.7"

   services:
     app:
       # The app service definition
     mysql:
       image: mysql:5.7
       volumes:
         - todo-mysql-data:/var/lib/mysql
       environment: 
         MYSQL_ROOT_PASSWORD: secret
         MYSQL_DATABASE: todos

   volumes:
     todo-mysql-data:

完成したdocker-compose.ymlは以下のようになります。

version: "3.7"

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
      MYSQL_USER: root
      MYSQL_PASSWORD: secret
      MYSQL_DB: todos

  mysql:
    image: mysql:5.7
    volumes:
      - todo-mysql-data:/var/lib/mysql
    environment: 
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: todos

volumes:
  todo-mysql-data:

アプリケーションスタックを起動する

docker-compose.ymlが用意できたので、あとは起動するだけです。

1.まず、他にapp/dbのコピーが実行されていないことを確認してください。(docker psdocker rm -f <ids>を使用してください。)

2.docker-compose upコマンドを使用して、アプリケーションスタックを起動してください。-dフラグを追加して、バックグラウンドで実行されるようにします。

   docker-compose up -d

実行すると、以下のように出力されるはずです。

   Creating network "app_default" with the default driver
   Creating volume "app_todo-mysql-data" with default driver
   Creating app_app_1   ... done
   Creating app_mysql_1 ... done

ネットワークと同じくボリュームも作成することができていますね。デフォルトでは、Docker Composeはアプリケーションスタックのネットワークを自動で作成します(Composeファイルで定義しなかったのはこのためです)。

和訳者メモ - docker-compose upで以下のエラーが表示された場合

[ERROR] [FATAL] InnoDB: Table flags are 0 in the data dictionary but the flags in file ./ibdata1 are 0x4800!

以下のコマンドでボリューム削除すると直りました。最初に間違えてmysql:latestdocker-compose.ymlを書いたのが原因だったかもしれないです。

docker volume rm app_todo-mysql-data

3.docker-compose logs -fコマンドを使用してログを見て見ましょう。各サービスのログが一つに集約されているのが分かると思います。これは、タイミングに関する問題を確認したいときにとても便利です。-fフラグはログを追従するので、生成されたログをライブ出力します。

実際の出力は以下のようになります。

   mysql_1  | 2019-10-03T03:07:16.083639Z 0 [Note] mysqld: ready for connections.
   mysql_1  | Version: '5.7.27'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)
   app_1    | Connected to mysql db at host mysql
   app_1    | Listening on port 3000

最初のラインの(色付けされている)サービス名は、メッセージを識別するのに役立ちます。特定のサービスについてのログを見たいときは、ログコマンドの末尾にサービス名を追加してください(例:docker-compose logs -f app)。

応用 - アプリを起動する前にデータベースを待機させる

実際には、アプリケーションを起動しても、MySQLが起動して準備が整ってから接続を試みます。Dockerには、コンテナが完全に起動、実行、準備されてから別のコンテナを起動するための組み込みのサポートは用意されていません。Nodeベースのプロジェクトでは、wait-portを使用できます。同じようなプロジェクトは、他の言語・フレームワークにおいても存在します。

4.アプリを開くと、起動しているのを確認できるはずです。そして、停止も一つのコマンドで行えます。

Dockerダッシュボードでアプリケーションスタックを確認する

Dockerダッシュボードを見ると、appという名前のグループがあります。これはDocker Composeのプロジェクト名で、コンテナをグループ化するのに使用されています。デフォルトでは、プロジェクト名はdocker-compose.ymlが配置されているディレクトリの名前です。

dashboard-app-project-collapsed.png

appのドロップダウンを開くと、composeファイルで定義した2つのコンテナが確認できます。そちらの名前は<プロジェクト名>_<サービス名>_<複製番号>というふうになっています。そのおかげで、どのコンテナがどのアプリか、どのコンテナがmysqlデータベースかということが分かりやすくなっています。

dashboard-app-project-expanded.png

全て停止する

docker-compose downを実行するか、Dockerダッシュボードでapp全体をゴミ箱に入れるだけで、全て停止できます。コンテナは停止され、ネットワークは削除されます。

ボリュームの削除

デフォルトでは、docker-compose downを実行しても、composeファイル内のネームドボリュームは削除されません。ボリュームを削除したい場合は、--volumesフラグを付け加える必要があります。

Dockerダッシュボードでは、アプリスタックを削除してもボリュームは削除されません。

停止したら、他のプロジェクトに切り替えてdocker-compose upを実行するだけで、そのプロジェクトを開発することができます。とても簡単ですよね。

要約

この章では、Docker Composeについてと、それによって複数サービスのアプリケーションの定義と共有がどれだけ簡単になるのかということを学びました。使っていたコマンドを適切なCompose形式に移行してComposeファイルを作成しました。

チュートリアルも終盤に入りました。しかし、これまで使ってきたDockerfileには大きな問題があるので、イメージ構築に関するベストプラクティスをいくつか取り上げていきたいと思います。では、みてみましょう。

イメージ構築のベストプラクティス

イメージのレイヤ

イメージの構成要素を確認できることをご存知ですか?docker image historyコマンドを使用すれば、イメージに含まれる各レイヤの作成に使用されたコマンドを確認することができます。

1.docker image historyコマンドを使用して、このチュートリアルで以前作成したgetting-startedイメージのレイヤを見てみましょう。

   docker image history getting-started

すると、以下のように表示されるはずです(おそらくIDは異なります。)。

   IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
   a78a40cbf866        18 seconds ago      /bin/sh -c #(nop)  CMD ["node" "src/index.j…    0B                  
   f1d1808565d6        19 seconds ago      /bin/sh -c yarn install --production            85.4MB              
   a2c054d14948        36 seconds ago      /bin/sh -c #(nop) COPY dir:5dc710ad87c789593…   198kB               
   9577ae713121        37 seconds ago      /bin/sh -c #(nop) WORKDIR /app                  0B                  
   b95baba1cfdb        13 days ago         /bin/sh -c #(nop)  CMD ["node"]                 0B                  
   <missing>           13 days ago         /bin/sh -c #(nop)  ENTRYPOINT ["docker-entry…   0B                  
   <missing>           13 days ago         /bin/sh -c #(nop) COPY file:238737301d473041…   116B                
   <missing>           13 days ago         /bin/sh -c apk add --no-cache --virtual .bui…   5.35MB              
   <missing>           13 days ago         /bin/sh -c #(nop)  ENV YARN_VERSION=1.21.1      0B                  
   <missing>           13 days ago         /bin/sh -c addgroup -g 1000 node     && addu…   74.3MB              
   <missing>           13 days ago         /bin/sh -c #(nop)  ENV NODE_VERSION=12.14.1     0B                  
   <missing>           13 days ago         /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B                  
   <missing>           13 days ago         /bin/sh -c #(nop) ADD file:e69d441d729412d24…   5.59MB   

それぞれにレイヤがイメージのレイヤを表しています。この画面では、イメージのベースが下部、最新のレイヤーが上部に表示されています。これを使えば、素早くそれぞれのレイヤを確認し、サイズの大きいイメージを突き止めるのに役立ちます。

2.いくつかの行が省略されていることに気がついたでしょうか。--no-truncフラグを付け加えると、フル出力を得ることができます。(省略されたフラグを使って省略されていない出力を得るって、面白いですよね。)

   docker image history --no-trunc getting-started

レイヤーのキャッシュ

実際にレイヤーを見たわけですが、これはコンテナイメージのビルド時間を減らすことに関してとても重要な話です。

レイヤーを変更したら、下流の全てのレイヤーを再生成する必要があります。

使用していたDockerfileをもう一度見てみましょう。

FROM node:12-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]

image historyの出力の話に戻ると、Dockerfileのそれぞれのコマンドがイメージの新しいレイヤーになっていることが分かります。イメージに変更を加えたとき、yarnの依存関係が再インストールされたことを覚えているかもしれません。これを修正する方法はあるのでしょうか。ビルドするたびに同じ依存関係をインストールするなんて馬鹿げていますよね。

これを修正するには、Dockerfileを再構築して依存関係をキャッシュする必要があります。Nodeベースのアプリケーションでは、package.jsonファイルで依存関係が定義されています。ということは、このファイルだけを最初にコピーしておけば、依存関係がインストールされてから他のファイルがコピーされます。なので、package.jsonを変更したときのみyarnの依存関係が再作成されるようになります。素晴らしいでしょう?

1.Dockerfileを更新して、最初にpackage.jsonのコピーが行われ、依存関係がインストールされ、それから他のファイルがコピーされるようにします。

   FROM node:12-alpine
   WORKDIR /app
   COPY package.json yarn.lock ./
   RUN yarn install --production
   COPY . .
   CMD ["node", "src/index.js"]

2.Dockerfileと同じフォルダに.dockerignoreというファイルを作成し、以下の内容を書き込みます。

   node_modules

.dockerignoreファイルを使えば、イメージに必要なファイルだけを選択してコピーすることができます。詳しくはここを参照してください。この場合、node_modulesフォルダは2回目のCOPYで除外されます。さもなければ、RUNのコマンドで生成されたファイルで上書きされるでしょう。なぜこの方法がNode.jsアプリケーションで推奨されているかということと、他のベストプラクティスについての詳細は、DockerでのNode製Webアプリをご覧ください。

3.docker buildで新しいイメージをビルドします。

   docker build -t getting-started .

以下のように表示されるはずです。

   Sending build context to Docker daemon  219.1kB
   Step 1/6 : FROM node:12-alpine
   ---> b0dc3a5e5e9e
   Step 2/6 : WORKDIR /app
   ---> Using cache
   ---> 9577ae713121
   Step 3/6 : COPY package.json yarn.lock ./
   ---> bd5306f49fc8
   Step 4/6 : RUN yarn install --production
   ---> Running in d53a06c9e4c2
   yarn install v1.17.3
   [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 10.89s.
   Removing intermediate container d53a06c9e4c2
   ---> 4e68fbc2d704
   Step 5/6 : COPY . .
   ---> a239a11f68d8
   Step 6/6 : CMD ["node", "src/index.js"]
   ---> Running in 49999f68df8f
   Removing intermediate container 49999f68df8f
   ---> e709c03bc597
   Successfully built e709c03bc597
   Successfully tagged getting-started:latest

全てのレイヤーが再構築されているのがわかると思います。Dockerfileを大きく変更したので、もう問題ありません。

4.src/static/index.htmlファイルを編集しましょう(<title>のところの内容を"The Awesome Todo App"に変更します)。

5.もう一度docker build -t getting-started .でDockerイメージをビルドします。今回は、表示される内容が少し変わって、以下のようになるはずです。

   Sending build context to Docker daemon  219.1kB
   Step 1/6 : FROM node:12-alpine
   ---> b0dc3a5e5e9e
   Step 2/6 : WORKDIR /app
   ---> Using cache
   ---> 9577ae713121
   Step 3/6 : COPY package.json yarn.lock ./
   ---> Using cache
   ---> bd5306f49fc8
   Step 4/6 : RUN yarn install --production
   ---> Using cache
   ---> 4e68fbc2d704
   Step 5/6 : COPY . .
   ---> cccde25a3d9a
   Step 6/6 : CMD ["node", "src/index.js"]
   ---> Running in 2be75662c150
   Removing intermediate container 2be75662c150
   ---> 458e5c6f080c
   Successfully built 458e5c6f080c
   Successfully tagged getting-started:latest

ビルドがとても早くなったのに気がついたでしょう。また、ステップ1-4には全てUsing casheが含まれているのが分かると思います。これで、ビルドキャッシュを使うことができました。イメージをプッシュし、プルし、更新するのがとても早くなります。

マルチステージビルド

このチュートリアルではあまり深く触れませんが、マルチステージビルドは、複数のステージを使用してイメージを作成するのに役立つとんでもなくパワフルなツールです。以下のような利点があります。

  • ランタイムの依存関係とビルド時間の依存関係を分離できます。

  • アプリが起動するのに必要な分だけを配置することで、イメージ全体のサイズを減らします。

MavenとTomcatの例

Javaベースのアプリケーションを構築する際には、ソースコードをJavaバイトコードにコンパイルするのにJDKが必要になります。しかし、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と名付けた1つ目のステージで、Mavenを用いたJavaのビルドを行います。FROM tomcatから始まる2つ目のステージで、buildステージからファイルをコピーします。最終的なイメージは、作成される最終ステージ(--targetフラグを使用するとオーバーライドされます)だけです。

Reactの例

Reactアプリケーションをビルドするときは、JSコード(通常はJSX)、SASSスタイルシートなどを静的なHTMLとJS、CSSにコンパイルするNode環境が必要がです。サーバーサイドレンダリングが必要ない場合は、本番環境のビルドにNode環境は必要ありません。それなら、静的なNginxコンテナに静的なリソースを配置すればいいだけの話ですよね。

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コンテナにコピーしています。この方がいいでしょ?

要約

イメージが構造化される方法を少し理解したことで、イメージのビルドは早くなり、より少ない変更を配置できるようになりました。また、マルチステージビルドは、イメージ全体のサイズを減らし、ビルドタイムの依存関係とランタイムの依存関係を切り離すことで最終的なコンテナのセキュリティを向上させるのに役立ちます。

次にすること

チュートリアルは終わったわけですが、コンテナについて学ぶことはもっとあります。ここで深く触れることはないですが、少しみてみましょう。

コンテナのオーケストレーション

本番環境でコンテナを動かすのは難しいことです。マシンにログインしてdocker rundocker-compose upを実行したいとは思いません。なぜでしょうか?では、コンテナが死んだら何が起こるでしょうか?複数のマシンにまたがってスケールする方法は?コンテナのオーケストレーションはこの問題を解決します。Kubernetes、Swarm、Nomad、ECSのようなツールは全てこの問題をわずかに異なる方法で解決します。

予期される状態を受け取る「マネージャ」を持つというのが一般的な考えです。この状態というのは、「Webアプリのインスタンスを2つ起動して、80番ポートを開放してほしい」というようなものです。マネージャはクラスタ内の全てのマシンを監視し、作業を「ワーカ」に委任します。マネージャは(コンテナが停止された、のような)変更を監視し、それから実際の状態が予期された状態を反映するように動作します。

クラウドネイティブコンピューティング基盤プロジェクト

CNCF(クラウドネイティブ基盤のプロジェクト)は、ベンダーに依存しない様々なオープンソースプロジェクトで、Kubernetes、Prometheus、Envoy、Linkerd、NATSなどを含みます。作られたプロジェクトはこちら、全体的なCNCFの図はこちらで見ることができます。これらのたくさんのプロジェクトは、モニタリング、ロギング、セキュリティ、イメージレジストリ、メッセージングなどの問題を解決するのに役立ちます。

なので、コンテナの全容とクラウドネイティブアプリケーションの開発がよくわからない場合は、ぜひご利用ください。コミュニティに参加し、質問をして、勉強してみてください。あなたが参加してくれることを心待ちにしております。

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

build-push-actionを使って、GitHub Container Registryにイメージをpushしたい

Dockerイメージをレジストリに登録する作業を自動化したい場合、build-push-actionというワークフローを利用することが多いと思います。このワークフローを使って、2020/09/03にオープンベータになったGitHub Container Registryにイメージをpushしたい場合は、おおよそ以下のようにすればよいです。

name: Publish Docker image
on: push
jobs:
  publish_image:
    runs-on: ubuntu-latest
    steps:
      - name: Push to GitHub Container Registry
        uses: docker/build-push-action@v1
        with:
          username: ${{ github.actor }}
          password: ${{ secrets.ACCESS_TOKEN }}
          registry: ghcr.io
          repository: example/example

ポイントは次の通りです。

  • registryghcr.ioを指定します。
  • usernameはレポジトリにアクセスできるユーザを指定してください。なおここではワークフロー実行ユーザ名を格納するgithub.actorを利用しています。
  • passwordには、レポジトリへの読み取り・書き込み権限を有するアクセストークン(★)を指定する必要があります。

(★) アクセストークンを作成し、Github Actions内で利用できるようになるまでの手順は以下の通りです。

  1. https://github.com/settings/tokens/new へアクセス。認証が求められる場合がある。
  2. アクセストークン作成画面では、write:packagesread:packagesに最低限チェックを付けて、Genereate Tokenを押下する。必要な場合はほかの権限も付与すること。
  3. 作成したアクセストークンを控えておく
  4. Github Actionsを使うレポジトリに移動。
  5. Settings > Secrets > New Secretの順番に移動。
  6. Secret作成画面では、Name="任意の名前"(上記のサンプルではACCESS_TOKEN)、VALUE="3.で控えておいたアクセストークン"を指定して、Add Secretを押下。
  7. これでワークフロー内で${{ secrets.任意の名前 }}という変数が利用できるようになります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel開発環境構築

Laravel-base

ネットを探すと色々あるのですが、個人的に一番しっくりくるLaravel開発環境です。
忘れないようにメモ的な意味でアップします。
DockerでNginx、PHP-FPM、MySQLを構成しています。
ペースにしているのは下記サイトに記載されていた方法です。ディレクトリやPHPのバージョンが違いますが、ほぼ同じやり方で記載しています。

参考:
DigitalOcean - How To Set Up Laravel, Nginx, and MySQL with Docker Compose
https://www.digitalocean.com/community/tutorials/how-to-set-up-laravel-nginx-and-mysql-with-docker-compose

Githubにもアップしています。
https://github.com/noktone/laravel-base

Dockerのインストール

インストール

以下はUbuntu18.04での作業になります。Macの人はDocker for Macを、Windowsの人はDocker for Windowsをインストールしてください。

# 旧バージョンのDockerをアンインストール
$ sudo apt-get remove docker docker-engine docker.io

# aptのアップデート
$ sudo apt-get update

# HTTPSリポジトリを利用できるようにする
$ sudo apt install apt-transport-https ca-certificates curl software-properties-common

# GPGキーの追加
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

# DockerリポジトリをAPTソースに追加
$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"

# インストール
$ sudo apt-get update
$ sudo apt-get install docker-ce docker-compose

インストール後の処理

一般ユーザーでDockerを実行できるようにする

# ユーザーをdockerグループに追加
$ sudo groupadd docker
$ sudo usermod -aG docker $USER

# テスト
$ docker run hello-world

エラーの一例

下記エラーが出る場合(idコマンドなどでdockerグループが表示されない場合です)があります。

docker: Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post http://%2Fvar%2Frun%2Fdocker.sock/v1.39/containers/create: dial unix /var/run/docker.sock: connect: permission denied.
See 'docker run --help'.

dockerグループへログインして有効にします。

$ newgrp docker

# プライマリのグループがdockerになるので、ユーザーのグループに戻す(hogeユーザーのhogeグループの場合)
$ newgrp hoge

LaravelのベースプロジェクトをGitから取得する

composerなどで作成してもよいですが、ここではgitから取得する方法で作成します。
また、ホームディレクトリにsrc/laravel-baseというディレクトリを作ることを前提にしています。適宜変更してください。

$ mkdir -p ~/src/laravel-base
$ cd ~/src/laravel-base

# laravleの最新版をclone
$ git clone https://github.com/laravel/laravel.git

# 特定のバージョンを指定する場合は、cloneする際にバージョンを指定します。
$ git clone -v 5.5.28 https://github.com/laravel/laravel.git

Composer

$ cd ~/src/laravel-base/laravel
$ docker run --rm -v $(pwd):/app composer install

docker runコマンドに--vオプションをつけることで現在のディレクトリをバインドマウントしたコンテナが作成されます。Windowsの人は「$(pwd)」のところに直接パスを書きます。--rmはDocker終了後に削除するオプションです。別にこの段階やらなくても大丈夫です。

Linuxの人はアクセス権の問題を回避するため、所有者をroot以外のユーザーに設定します。

$ sudo chown -R $USER:$USER ~/src/laravel-base

Docker Composeファイルの作成

mroongaのDockerイメージはrootパスワードやdatabaseを自動作成しませんが、ここではMySQLのDockerイメージを使用する場合に備えて記載しています。
PHPはDockerfileで指定するため、後ほど作成します。

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

  #PHP Service
  app:
    build:
      context: .
      dockerfile: ./php/Dockerfile
    container_name: app
    restart: unless-stopped
    tty: true
    environment:
      SERVICE_NAME: app
      SERVICE_TAGS: dev
    working_dir: /var/www
    volumes:
      - ./laravel:/var/www
      - ./php/local.ini:/usr/local/etc/php/conf.d/local.ini
    networks:
      - app-network

  #Nginx Service
  webserver:
    image: nginx:alpine
    container_name: webserver
    restart: unless-stopped
    tty: true
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./laravel:/var/www
      - ./nginx/conf.d/:/etc/nginx/conf.d/
    networks:
      - app-network

  #MySQL Service
  db:
    image: mysql:8
    container_name: db
    restart: unless-stopped
    tty: true
    ports:
      - "3306:3306"
    environment:
      MYSQL_DATABASE: laravel
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_USER: laravel
      MYSQL_PASSWORD: password
      TZ: 'Asia/Tokyo'
      SERVICE_TAGS: dev
      SERVICE_NAME: mysql
    volumes:
      - dbdata:/var/lib/mysql/
      - ./mysql/log:/var/log/mysql
      - ./mysql/my.cnf:/etc/mysql/my.cnf
    networks:
      - app-network

#Docker Networks
networks:
  app-network:
    driver: bridge
#Volumes
volumes:
  dbdata:
    driver: local

Dockerfileの作成

PHP7.4-FPM用のDockerfileを作成します。

$ vim ~/src/laravel-base/php/Dockerfile
FROM php:7.4-fpm

# Copy composer.lock and composer.json
COPY ./laravel/composer.lock /var/www/
COPY ./laravel/composer.json /var/www/

# Set working directory
WORKDIR /var/www

# Install dependencies
RUN apt-get update && apt-get install -y \
    build-essential \
    libpng-dev \
    libjpeg62-turbo-dev \
    libfreetype6-dev \
    locales \
    zip \
    jpegoptim optipng pngquant gifsicle \
    vim \
    unzip \
    git \
    libonig-dev \
    curl \
    libzip-dev \
    zlib1g-dev

# Clear cache
RUN apt-get clean && rm -rf /var/lib/apt/lists/*

# node.js
RUN curl -sL https://deb.nodesource.com/setup_lts.x | bash -
RUN apt-get install -y nodejs

# Install extensions
RUN docker-php-ext-install pdo_mysql zip exif pcntl
RUN docker-php-ext-configure gd --with-freetype=/usr/include/ --with-jpeg=/usr/include/
RUN docker-php-ext-install gd

# Install composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

# Add user for laravel application
RUN groupadd -g 1000 www
RUN useradd -u 1000 -ms /bin/bash -g www www

# Copy existing application directory contents
COPY . /var/www

# Copy existing application directory permissions
COPY --chown=www:www . /var/www

# Change current user to www
USER www

# Expose port 9000 and start php-fpm server
EXPOSE 9000
CMD ["php-fpm"]

PHPの設定

PHP7.2-FPMの設定です。phpディレクトリを作成して、その配下にiniファイルを設置します。

$ mkdir ~/src/laravel-base/php
$ vim ~/src/laravel-base/php/local.ini
upload_max_filesize=40M
post_max_size=40M
date.timezone = "Asia/Tokyo"

Nginxの設定

Nginxのコンフィグファイルを作成します。80版ポートで動作させてます。

$ mkdir -p ~/src/laravel-base/nginx/conf.d
$ vim ~/src/laravel-base/nginx/conf.d/app.conf
server {
    listen 80;
    index index.php index.html;
    error_log  /var/log/nginx/error.log;
    access_log /var/log/nginx/access.log;
    root /var/www/public;
    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass app:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }
    location / {
        try_files $uri $uri/ /index.php?$query_string;
        gzip_static on;
    }
}

動作時にタイムアウトが問題になる場合は下記 keepalive_timeout、send_timeout、fastcgi_read_timeout、fastcgi_connect_timeout、fastcgi_send_timeoutオプションを指定する(未検証。増やしすぎるとパフォーマンスの劣化につながるため注意)

server {
    listen 80;
    index index.php index.html;
    error_log  /var/log/nginx/error.log;
    access_log /var/log/nginx/access.log;
    root /var/www/public;
    keepalive_timeout 600;
    send_timeout 600;
    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass app:9000;
        fastcgi_index index.php;
        fastcgi_read_timeout 600;
        fastcgi_connect_timeout 600;
        fastcgi_send_timeout 600;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }
    location / {
        try_files $uri $uri/ /index.php?$query_string;
        gzip_static on;
    }
}

MySQLの設定

$ mkdir ~/src/laravel-base/mysql
$ vim ~/src/laravel-base/mysql/my.cnf

general_logなどは状況によって。

[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_bin
general_log = 0
general_log_file = /var/lib/mysql/general.log
log_timestamps=SYSTEM
secure-file-priv=NULL
default-authentication-plugin = mysql_native_password

[mysql]
default-character-set = utf8mb4

[client]
default-character-set = utf8mb4

ログ用のディレクトリも作成しておきます。

$ mkdir ~/src/laravel-base/mysql/log

Laravelの環境設定ファイルの作成

Laravelの環境設定ファイルを作成します。環境設定ファイル(.env)はgitの管理下からは外しますので、本番環境では別途用意します。

$ cp .env.example .env
$ vim .env

laravel-baseをgit cloneした場合は下記を参考にしてください。DB_HOSTにはデータベースのコンテナ名を記載します。

APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost

LOG_CHANNEL=stack

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=

BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=

PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1

MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

Gitの管理下に含めないファイル

Docker用のファイルなど、アプリケーションに直接関係のないファイルはGitの管理下には置かないようにします。

$ vim .gitignore
/node_modules
/public/hot
/public/storage
/storage/*.key
/vendor
.env
.phpunit.result.cache
Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
docker-compose.yml
Dockerfile

パッケージのインストール

下記を実行して、venderパッケージをインストールします。

$ docker-compose exec app php composer install

Dockerコンテナの作成とLaravelの起動時の設定

コンテナの作成と起動を実行します。

$ docker-compose up -d

作成が終わったら、動作しているか確認します。

# 起動中のコンテナの確認
$ docker ps

Laravelのアプリケーションキーを生成します。

$ docker-compose exec app php artisan key:generate

設定をキャッシュする場合は以下を実行します。設定内容が/var/www/bootstrap/cache/config.phpコンテナにロードされます。

$ docker-compose exec app php artisan config:cache

マイグレーションを実行し、データベース無いに認証機能に必要なテーブルを作成

$ docker-compose exec app php artisan migrate

設定が完了したら、下記URLで確認します。
http://localhost
終了する場合は下記を実行します。

$ docker-compose down

# volumeごと消すとき(MySQLがうまく設定できないとか。データが消えるので注意)
docker-compose down --volumes

不調のときに試すもの。データが消えたりするので注意

# 止まってるコンテナ、使われてないボリューム、使われてないネットワーク、使われてないイメージを削除します。
$ docker system prune 
# 個別にやる場合は下記になります。
$ docker image prune
$ docker container prune
$ docker network prune
$ docker volume prune

MySQLのDBとユーザーの作成

docker-composeで定義しているのですでに作成されているはずですが、やり方を忘れないように記載

$ mysql -u root -p --protocol=tcp
$ CREATE DATABASE laravel;
$ CREATE USER 'laravel'@'%' IDENTIFIED BY 'password';
$ GRANT ALL ON laravel.* TO 'laravel'@'%' WITH GRANT OPTION;
$ FLUSH PRIVILEGES;
$ exit
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

docker上のVue.jsのvue uiを使う

docker上にVue.jsのコンテナを立てたけど、vue ui を使いたくなった。
デフォだとlocalhostかつ8000で立ち上がる。

オプションを指定するだけ

非常に簡単
vue uiの後にいろいろオプションを足せる

/usr/src/app$ vue ui -h       

Usage: ui [options]

start and open the vue-cli ui

Options:
  -H, --host <host>  Host used for the UI server (default: localhost)
  -p, --port <port>  Port used for the UI server (by default search for available port)
  -D, --dev          Run in dev mode
  --quiet            Don't output starting messages
  --headless         Don't open browser on start and output port
  -h, --help         output usage information 

ひとまず8080はdev用にのportとして置いていて、一応このデフォポートを使えるように8000もdocker-comose.ymlの時点で繋げた。

localhostだとdockerの外から見にいけないので、hostを 0.0.0.0 にして起動する。

$ vue ui -H 0.0.0.0
?  Starting GUI...
?  Ready on http://0.0.0.0:8000

これでhttp://localhost:8000/を見に行くと表示されている。

-P, —port でportを8080のすればそれでもいける。
以上、備忘録。

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

Stoneでポートフォワード

参考

使い方

# 443 にアクセスしたら localhost:22 に接続する
docker run -it -d \
 --name stone \
 --hostname stone \
 --restart=always \
 -p 443:443 \
 int128/stone \
 localhost:22 443
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【RailsAPI×Docker】シェルで手軽に環境構築 & Flutterでも動作確認

初心者の方でも進められるよう手順は丁寧してあります。

やること

  • RailsAPI × Docker での環境構築
  • RailsAPIを軽く実装
  • APIクライアントからAPIを叩く(PostmanやFlutterアプリから)※Flutterは任意で

スクリーンショット 2020-09-13 13.42.28.png

APIコンテナ起動のシェルだけ見たい方 → こちら
一応 RailsAPIのソースコードFlutterのソースコードも用意(※いじり倒してる可能性あり)

手順

  • API側のDockerコンテナ起動まで
    • ディレクトリ作成
    • シェルでRailsAPIコンテナビルド
    • コンテナ起動&確認
  • RailsAPI実装
    • User モデル作成&DBテーブル用意
    • User のコントローラ作成
    • ルーティング設定
  • PostmanでAPIの動作を確認
    • Postmanインストール
    • GETでDBのUserリソースを取得
    • POSTでDBのUserリソースを追加
  • FlutterでAPIを叩く
    • プロジェクト作成
    • Http通信を行えるようにする
    • main.dartを編集

前提

  • AndroidStudioでFlutterアプリケーションを起動した状態にする
    • SDKなどをダウンロードしてシミュレータを起動できる
    • ※ Flutterセットアップはこちらの記事で基本全部いけました(3~40分はかかります)
  • Docker, docker-compose のコマンドが使える($ docker-compose -vが動けばOK)

1. API側のDockerコンテナ起動まで

RailsAPIのDockerコンテナを作っていきます。

ただ、1からコマンドを打っていくのは非効率なので、基本的なところはシェルにまとめてあります。

1-1. ディレクトリ作成

app_nameにプロジェクト名を当てはめてディレクトリを用意します。

$ mkdir app_name  # APIアプリを置くディレクトリ用意
$ cd app_name

1-2. シェルでRailsAPIコンテナビルド

以下がAPIコンテナのビルドまでやってくれるシェルです。(参考にした記事)

サンプルAPI立ち上げ用のシェル
set_up_rails.sh
#!/bin/bash

#config setting#############
APP_NAME="app_name"            # ← 自由に変更してください(ディレクトリ名と一緒がいいかも)
MYSQL_PASSWORD="password"      # ← 自由に変更してください
###########################

echo "docker pull ruby2.6.6"
docker pull ruby:2.6.6

echo "docker pull mysql:5.7"
docker pull mysql:5.7

echo "docker images"
docker images

echo "make Dockerfile"

cat <<EOF > Dockerfile
FROM ruby:2.6.6
ENV LANG C.UTF-8
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs
#yarnのセットアップ
RUN curl -o- -L https://yarnpkg.com/install.sh | bash
ENV PATH /root/.yarn/bin:/root/.config/yarn/global/node_modules/.bin:\$PATH
# 作業ディレクトリの作成、設定
RUN mkdir /${APP_NAME}
ENV APP_ROOT /${APP_NAME}
WORKDIR \$APP_ROOT
# ホスト側(ローカル)のGemfileを追加する
ADD ./Gemfile \$APP_ROOT/Gemfile
ADD ./Gemfile.lock \$APP_ROOT/Gemfile.lock
# Gemfileのbundle install
RUN bundle install
ADD . \$APP_ROOT
# gem版yarnのuninstall rails6でエラーになるため
RUN gem uninstall yarn -aIx
#webpackerの設定
#RUN rails webpacker:install
EOF

echo "make Gemfile"
cat <<EOF > Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gem 'rails', '~> 6.0.3.2'
EOF

echo "make Gemfile.lock"
touch Gemfile.lock

echo "make docker-compose.yml"
cat <<EOF > docker-compose.yml
version: '3'
services:
  db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD}
      MYSQL_DATABASE: root
    ports:
      - '3306:3306'
  api:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/${APP_NAME}
    ports:
      - '3000:3000'
    links:
      - db
EOF

echo "docker-compose run api rails new . --api --force --database=mysql --skip-bundle"
docker-compose run api rails new . --api --force --database=mysql --skip-bundle

echo "docker-compose run api bundle exec rails webpacker:install"
docker-compose run api bundle exec rails webpacker:install

docker-compose build

# fix config/database.yml
echo "fix config/database.yml"
cat config/database.yml | sed "s/password:$/password: ${MYSQL_PASSWORD}/" | sed "s/host: localhost/host: db/" > __tmpfile__
cat __tmpfile__ > config/database.yml
rm __tmpfile__

echo "docker-compose run api rails db:create"
docker-compose run api rails db:create

エディタからプロジェクト直下に新規ファイルとして作成&↑をコピペし、set_up_rails.shなどとして保存しましょう。「自由に変更してください」となっているapp_nameなどは合わせて編集してください。

保存が終わったら以下のようにシェルの権限を変更し、実行してみましょう(5分くらいかかります)。

$ chmod 755 set_up_rails.sh  # 権限の変更
$ ./set_up_rails.sh          # セットアップのシェル実行
docker pull ruby2.6.6
2.6.6: Pulling from library/ruby
57df1a1f1ad8: Pull complete
71e126169501: Pull complete
1af28a55c3f3: Pull complete
・
・
・

シェルの実行が終わったら、$ docker-compose psでコンテナの状態を確認してみましょう。以下のようになっていれば問題ないです。

$ docker-compose ps
     Name             Command       State       Ports
----------------------------------------------------------
app_name_db_1     docker-           Up      0.0.0.0:3306->
                  entrypoint.sh             3306/tcp,
                  mysqld                    33060/tcp

1-3. コンテナ起動 & 確認

次に、シェルでビルドしたコンテナを起動します。

$ docker-compose up
rails-api-handson_db_1 is up-to-date
Creating rails-api-handson_api_1 ... done
Attaching to rails-api-handson_db_1, rails-api-handson_api_1
db_1   | 2020-09-12 08:27:00+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 5.7.31-1debian10 started.
・
・
・
api_1  | * Environment: development
api_1  | * Listening on tcp://0.0.0.0:3000
api_1  | Use Ctrl-C to stop    # ← これが出たらOK!

Dockerコンテナのログが大量に出ますが、最後のUse Ctrl-C to stopが出ればコンテナが起動しているのでOKです。

コンテナの起動が確認できたので、http://localhost:3000 にアクセスし、ちゃんとRailsとしてレスポンスを返してくれるか確認しましょう。以下のようになっていればAPIコンテナの動作は大丈夫です。

スクリーンショット 2020-09-12 17.38.44.png

2. RailsAPI実装

動作確認用なので単純なユーザデータにしましょう。今回は、

User ( id, name, email )

みたいなリソースで実装してみます。

2-1. Userモデル作成 & DBテーブル用意

Userモデルとテーブルを用意します。

ここだけマイグレーションを使う方法Ridgepoleを使う方法に分けて紹介します(どちらも結果として同じモデル、スキーマが反映されます)。

Ridgepoleは、マイグレーションを使わずにスキーマを管理できるツールです。僕はこちらで進めていきますが、普段からマイグレーションを使っている方であればそちらで問題ありません。

パターン1) マイグレーションでモデル&テーブル作成

コンテナに入り、ジェネレータでUserモデルを作成。

$ docker-compose exec api bash     # コンテナに入る
# rails g model user name:string email:string
# ↑ 出来なければ以後はBundle経由でやってみましょう( # bundle exec rails .... みたいな )exit                             # コンテナから出る(以後はコンテナの出入りは明記しません。
                                   #              コマンドラインの「$」や「#」で区別します。)
$

あとはマイグレーションをかけるだけです。

# rake db:migrate

パターン2) Ridgepoleでモデル&テーブル作成

マイグレーションをスキップしてモデルを作成。

# rails g model user --skip-migration

プロジェクト直下にあるGemfilegem 'ridgepole', '>= 0.8.0'を追記します。mysql2の下とかで問題ないでしょう。

Gemfile
gem 'mysql2', '>= 0.4.4'     # 元々ある
gem 'ridgepole', '>= 0.8.0'  # これを追加

追記したgemをインストール。

# bundle install

次にdb/のなかに、Schemafile.rbというファイルを作成します(Ridgepoleではスキーマをここで一元管理する)。

db/Schemafile.rbには以下のように書き、保存します。

db/Schemafile.rb
create_table "users", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
  t.string "name"
  t.string "email"
  t.timestamps
end

ridgepoleのコマンドでスキーマファイルを反映。

# bundle exec ridgepole --config ./config/database.yml --file ./db/Schemafile.rb --apply
Apply `./db/Schemafile.rb`
-- create_table("users", {:options=>"ENGINE=InnoDB DEFAULT CHARSET=utf8"})
   -> 0.0716s

2-2. User のコントローラ作成

こちらの記事を参考に、URIにAPIのメジャーバージョン(v1.3なら1の部分)を含めます。ただこの記事だと、URIにapiというネームスペースを使っています。

これは僕個人の意見ですが、URIにapiと明記するよりも、サブドメインなどで示した方がいいのでは?と思います。APIを使おうとするクライアントからすればAPIだってことはわかっていますから。

example.com/api/vi/users  # URIでapiだと示す
api.example.com/vi/users  # サブドメインで示す

では本題のコントローラ作成に戻ります。

コントローラの前に、メジャーバージョンを示すv1/を作成し、その中でusersコントローラを作成します。

$ mkdir app/controllers/v1
$ rails g controller v1::users

コントローラファイルの中身は以下の感じです。

*/v1/users_controller.rb
class V1::UsersController < ApplicationController
  before_action :set_user, only: [:show, :update, :destroy]

  def index
    users = User.order(created_at: :desc)
    render json: { status: 'SUCCESS', message: 'Loaded user', data: users }
  end

  def show
    render json: { status: 'SUCCESS', message: 'Loaded the user', data: @user }
  end

  def create
    user = User.new(user_params)
    if user.save
      render json: { status: 'SUCCESS', data: user }
    else
      render json: { status: 'ERROR', data: user.errors }
    end
  end

  def destroy
    @user.destroy
    render json: { status: 'SUCCESS', message: 'Deleted the user', data: @user }
  end

  def update
    if @user.update(user_params)
      render json: { status: 'SUCCESS', message: 'Updated the user', data: @user }
    else
      render json: { status: 'SUCCESS', message: 'Not updated', data: @user.errors }
    end
  end

  private

  def set_user
    @user = User.find(params[:id])
  end

  def user_params
    params.require(:user).permit(:name, :email)
  end
end

2-3. ルーティング設定

config/routes.rbを以下のようにします。

config/routes.rb
Rails.application.routes.draw do
  namespace 'v1' do
    resources :users
  end
end

↑が終わると、rake routesコマンドでルーティングが確認できるはずです。以下のようなルーティングが反映されていればOKです。

# rake routes
  Prefix Verb    URI Pattern               Controller#Action
v1_users GET     /v1/users(.:format)       v1/users#index
         POST    /v1/users(.:format)       v1/users#create
 v1_user GET     /v1/users/:id(.:format)   v1/users#show
         PATCH   /v1/users/:id(.:format)   v1/users#update
         PUT     /v1/users/:id(.:format)   v1/users#update
         DELETE  /v1/users/:id(.:format)   v1/users#destroy

3. PostmanでAPIの動作を確認

Postmanとは、WebAPIのテストクライアントサービスのひとつです。APIの動作確認にはこれを使います。

3-1. Postmanインストール

公式サイトで、Postmanをインストールします。前にインストールしたのであまり覚えていませんが、登録して進めていけばOKだった気がします...。

Postmanインストール後、まずはコンソールで適当にUserのレコードを用意しておきましょう。以下のような感じでいいかと思います。

# rails c
Loading development environment (Rails 6.0.3.3)
irb(main):001:0> User.create(name: "hoge", email: "hoge@example.com")

3-2. GETでDBのUserリソースを取得

Postmanで以下のようにGETメソッドURIを指定し、Sendボタンを押します。

すると、画面下のBodyで返ってきたレスポンスのBodyが確認できます。ここまでできていればAPIの動作確認としては成功です!

スクリーンショット 2020-09-12 21.15.09.png

3-3. POSTでDBのUserリソースを追加

今度は以下のように、POSTメソッドを指定し、リクエストBodyの中にもデータを含めてみましょう。この時、形式がJSONになっているか確認しましょう(画像の"JSON"とオレンジになっているところ)。

成功すれば、登録されたUserのレコードがレスポンスBodyから確認出来ます。

スクリーンショット 2020-09-12 21.21.14.png

ここで先ほどのGETメソッドでのアクセスをもう一度行ってみると、POSTメソッドで追加したレコードも一緒に確認できます。

スクリーンショット 2020-09-12 21.22.33.png

4. FlutterでAPIを叩く

結果こんな感じ

  • アプリを起動したらDBにあるユーザのデータを表示
  • 右下のAddボタンでユーザデータの登録

動作確認なのでミニマムな感じです。

スクリーンショット 2020-09-12 23.28.18.png

4-1. プロジェクト作成

File > New > New Flutter Project... からFlutterプロジェクトを作成します。iPhoneやAndroidのシミュレータ(エミュレータ)などが起動できていればOKです。

4-2. Http通信を行えるようにする

デフォルトだとhttpを行うパッケージのimportでエラーを起こすので少し操作が必要になります。

まず、pubspec.yamldependencies:に以下のように追記します。

pubspec.yaml
# ~ 省略 ~
dependencies:
  http: ^0.12.0   # ← 追加する
  flutter:
    sdk: flutter
# ~ 省略 ~

すると、Android Studioのエディタの上の方に、以下のようなFlutterコマンドが出てきます。今回はPub getを選択します。

もしこの先の作業でHttp周りのエラーが出るようであれば、僕が参考にしたこちらの記事も見てみるといいかも知れません。

スクリーンショット 2020-09-12 22.35.18.png

4-3. main.dartを編集

動作確認をしたいだけであれば、lib/main.dartを以下のように書き換えれば動きます。

なお、僕はFlutterは初歩の初歩なので物申すことがあればコメントにお願いします。

lib/main.dart
import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {
  MyApp({Key key}) : super(key: key);

  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  Future<GetResults> res;
  @override
  void initState() {
    super.initState();
    res = getUsers();
  }

  // postUser()でUserを登録。setState内で再度User一覧を取得
  void _postUser() {
    postUser();
    setState(() {
      res = getUsers();
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'APIをPOSTで呼び出しJSONパラメータを渡す',
      theme: ThemeData(
        primarySwatch: Colors.yellow,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('RailsAPI x Flutter'),
        ),
        body: Center(
          child: FutureBuilder<GetResults>(
            future: getUsers(),
            builder: (context, snapshot) {
              if (snapshot.hasData) {
                return Text(
                    snapshot.data.message.toString()
                );
              } else if (snapshot.hasError) {
                return Text("${snapshot.error}");
              }
              return CircularProgressIndicator();
            },
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: _postUser,
          tooltip: 'Increment',
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}

class GetResults {
  final String message;
  GetResults({
    this.message,
  });
  factory GetResults.fromJson(Map<String, dynamic> json) {
    var datas = '';
    json['data'].forEach((item) => datas += 'id: ' + item['id'].toString() + ', name: ' + item['name'] + ', email: ' + item['email'] + '\n');
    return GetResults(
      message: datas,
    );
  }
}

class PostResults {
  final String message;
  PostResults({
    this.message,
  });
  factory PostResults.fromJson(Map<String, dynamic> json) {
    return PostResults(
      message: json['message'],
    );
  }
}

class SampleRequest {
  final String name;
  final String email;
  SampleRequest({
    this.name,
    this.email,
  });
  Map<String, dynamic> toJson() => {
    'name': name,
    'email': email,
  };
}

Future<GetResults> getUsers() async {
  var url = 'http://127.0.0.1:3000/v1/users';
  final response = await http.get(url);
  if (response.statusCode == 200) {
    return GetResults.fromJson(json.decode(response.body));
  } else {
    throw Exception('Failed');
  }
}

Future<PostResults> postUser() async {
  var url = "http://127.0.0.1:3000/v1/users";                             // APIのURI
  var request = new SampleRequest(name: 'foo', email: 'foo@example.com'); // Userのパラメータ。自由に変更してOK
  final response = await http.post(url,
      body: json.encode(request.toJson()),
      headers: {"Content-Type": "application/json"});
  if (response.statusCode == 200) {
    return PostResults.fromJson(json.decode(response.body));
  } else {
    throw Exception('Failed');
  }
}

シミュレータを起動すると、、登録されたUserがきちんと返ってきてますね!

スクリーンショット 2020-09-12 21.30.54.png

右下のAddボタンを押すと、、main.dartで指定したUserデータが登録され、取得できているのも確認できました!

スクリーンショット 2020-09-12 21.58.37.png

終わり

質問やご指摘があればコメントにお願いします。

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

Windows10 HomeにDokerをベースとした開発環境を構築する 2020 ver. その1 WSL2ベースのDocker構築まで

はじめに

最近開発をしてません。なんとなくWindowsパソコンを買いました。Windows 10 Homeつまりです。つまりHome開発環境を整えることが必然となりました。

どんな開発がしたいの?

数年前までMacを利用して以下のような開発をしていました。
・Webアプリケーション(フロント・サーバーサイド)
・AWSインフラ構築(コードで定義する)

上記のような開発をWindowsで構築しようとするととにかく大変でした(私の記憶では)Rubyの導入から躓いたり、
Railsを動かしていても異常に動作が遅く使い物にならなかったりと、とにかくストレスでした。

なので表題にあるMac並みというのは私がストレスなくWebアプリケーションの開発をローカル及びAWS上で実施できる環境を構築するということとなります。

どうやって実現するか

ざっとながら下調べをしまして、Dockerをベースとした開発環境が構築できればよいのかなとあたりをつけました。
しかしWindowsでは仮想環境の実行にHyper-Vが必要だったことを思い出し、Windows10 Homeである私にはもう無理だ・・・
と勝手に諦めかけていたとき「WSL2」と天の声が聞こえてきました。
実現方針は決まりました。
WSL2 → Doker → VSCode です。もう他に方法はありません。

WSL2 をWindows10 Homeに導入

WSL2の導入可能なWindows2020年バージョンは2020年9月現在以下となります。
・「Windows 10 1903」および「Windows 10 1909」(2020年8月20日より導入可能となった)
・「Windows 10 2004」

私のWindowsのバージョンは1909でしたので導入可能ですが、せっかくですので2004にアップデートした後
導入します。

※ 2004でWLS2を導入する場合、2020年9月10日現在一部のセキュリティパッチの影響で不具合がでることが報告されています。
https://www.zdnet.com/article/windows-10-2004-patch-tuesday-problem-update-breaks-wsl2-say-users/
Hyper-Vが原因の一つとされていますので、Windows Homeでは影響しないことを信じて進めていきます

Windows 10 2004 導入

現在のバージョンを確認するにはWinキー+Rで以下のコマンドを実行します。1909でした。

winver

あとはWindows Updateです。私の場合は下のような画面でしたので、「ダウンロードしてインストール」を押してただひたすらに待ちます。
image.png

待っている間にWSL2導入手順の確認です。
「Windows 10 2004」からOSの標準機能となりましたが、標準機能となっただけで、機能を有効化しないと使用できません。有効化します。
「Windowsの機能の有効化または無効化」というそのままの機能があります。Winキー+Rで以下のコマンドを実行します

OptionalFeatures

「Linux用Windowsサブシステム」と「仮想マシンプラットフォーム」にチェックし、インストールを実施します。
image.png

これでWSL2が導入されました。

Docker Desktop のインストール

以下のURLからDocker Desktopをインストールします
https://hub.docker.com/editions/community/docker-ce-desktop-windows

以下のようにWSL2のサポートが有効になっていることを確認します。
image.png

インストールが完了すると以下のように「WSL2のLinuxカーネルは別のインストーラーでアップデートが管理されてるからインストールしてね」と案内がでますので従います。
image.png

Docker Desktopのダッシュボードをみると・・・
image.png

WSL2Dを基盤としたDockerがこれで動作したようです。動作確認をするためダッシュボードに従って以下のコマンドをPowerShell上で実行してみます。

docker run -d -p 80:80 docker/getting-started

ダッシュボードを見てみます。
image.png

80番ポートでなにやらサーバーがリッスンしているようなので http://localhost/tutorial/ へアクセスしてみます。

image.png

コンテナが動作しました。これでWindows 10 Homeでもdockerを使った開発ができるはずです。その2に続く。

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

GitHub Container Registry(ghcr.io)にdocker pushする

アドレスが長くて忘れるのでメモしておく.

docker tagコマンドの形式は以下に従う.

docker tag <コンテナ名> ghcr.io/<GitHubのユーザ名>/<パッケージ名>/<コンテナ名>:<タグ>

例えば以下になる.

$ docker images
REPOSITORY                           TAG                                              IMAGE ID            CREATED             SIZE
stns-build                           latest                                           449f1c2d3562        4 minutes ago       35.4MB

$ docker tag stns-build:latest ghcr.io/tomoyk/stns-client/stns-client:v0.1

$ docker images
REPOSITORY                               TAG                                              IMAGE ID            CREATED             SIZE
stns-build                               latest                                           449f1c2d3562        6 minutes ago       35.4MB
ghcr.io/tomoyk/stns-client/stns-client   v0.1                                             449f1c2d3562        6 minutes ago       35.4MB

最後にPushする.

docker push ghcr.io/tomoyk/stns-client/stns-client:v0.1

弾かれる場合は ~/.docker/config.json を見てみる.
auths の下にghcr.ioがあるか確かめる.

{
    "auths": {
        "ghcr.io": {
            "auth": "xxxx"
        }
     }
}

ない場合は,GitHubのWeb UIからPersonal Access Tokenを発行する.

image.png

Tokenを ~/TOKEN.txt として保存して以下のコマンドを実行する.

cat ~/TOKEN.txt | docker login https://ghcr.io -u <ユーザ名> --password-stdin

再び docker push xxx を実行する.

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

Docker × MySQL8でエラー「MySQL said: Protocol mismatch; server version = 11, client version = 10」

DockerでMySQL8を構築して、TablePlusで接続しようとするとタイトルのエラーが出た。

一応環境

Docker version 19.03.12
docker-compose version 1.24.0
mysql Ver 8.0.21 for Linux on x86_64 (MySQL Community Server - GPL)

原因

どうやらクライアント側のポートを33060にするとエラーが起きるようだった。
ということで13306に変更してみたところ、接続ができた。

// docker-compose.yml

// 変更前
ports: 
  - 33060:3306

// 変更後
ports: 
  - 13306:3306

特に他に書くこともないので、以上!

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

Poetry Django Docker Pycharm で開発環境構築する

はじめに

pythonを始めて1年経ちますが、Pythonの環境構築はまだ慣れません。
特にパッケージマネージャは数ある言語の中でも最弱のように感じます。
なぜかmacで開発環境を構築しようとするとなぜか OpenSSL がらみのエラーが発生していました。

そんな悩みの解決に開発環境をdocker上に構築する方法をまとめました。

ソースは Github にあげてあります。

ディレクトリ構成

├── pyproject.toml
└── docker
    └── Dockerfile
    └── docker-compose.yml
└── mysite/*

ファイル

メインどころのファイルのみ記述します。
Djangoが作成したファイルは置いておきます。

pyproject.toml
[tool.poetry]
name = "python-dev-on-docker"
version = "0.1.0"
description = ""
authors = ["va034600"]

[tool.poetry.dependencies]
python = "^3.6"
django = "^3.1.1"

[tool.poetry.dev-dependencies]

[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"
Dockerfile
FROM python:3.6.8
WORKDIR /usr/src/app

ENV POETRY_VERSION=1.0.10 \
    PATH="/root/.poetry/bin:$PATH"
RUN curl -sSL https://raw.githubusercontent.com/sdispater/poetry/${POETRY_VERSION}/get-poetry.py | python && \
    poetry config virtualenvs.create false
COPY ./pyproject.toml /usr/src/app/pyproject.toml
RUN poetry install

CMD tail -f /dev/null
docker-compose.yml
version: '3.5'
services:
  app:
    build:
      context: ../
      dockerfile: ./docker/Dockerfile
    working_dir: /usr/src/app/mysite
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - ../mysite:/usr/src/app/mysite
    ports:
      - "8000:8000"

consoleからdocker-composeを実行

とりあえず、Pycharmなしの docker-compose up で動作を確認します。

$ cd docker
$ docker-compose up -d
$ curl http://127.0.0.1:8000/
$ docker-compose down
hello1

Pycharm でデバッグ

Pycharnの設定

Python Interpreter はdocker-composeで追加する

スクリーンショット 2020-09-13 9.41.41.png

プロジェクトのInterpreterをdocker-composeにする

Preferences.png

Configuration で runserverを作成する

ポイントは パラメータで runserver 0.0.0.0:8000 にすること
Run_Debug_Configurations.png

Pycharm でデバッグ実行する

これでブレークポイントも止まります。
ファイルを修正してdocker-composeの再起動なく反映もされます。

終わりに

これでホスト側がすっきり。
簡単に開発環境が構築できます。

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

Docker始めました

作ったツール類をコンテナにまとめてしまえばいろいろ便利と耳にしたので、以前から気になっていたDockerを使い始めることにしました。
今回は、pythonのflaskで作ったwebサーバをコンテナ化し、コンテナの起動・停止だけでwebサーバを管理できるようにしてみようと思います。

使用した環境はこちら

$ cat /etc/redhat-release 
Red Hat Enterprise Linux release 8.2 (Ootpa)
$ docker --version
Docker version 19.03.12, build 48a66213fe

※ 余談ですが、CentOS 8でDockerをインストールしようとすると containerd.io が無いぞって言われることがあるようです。私の場合はこちらで無事解決できましたので参考までに。

Dockerおさらい

まずはDockerの使い方(コンテナの作り方)について簡単に整理します。Dockerコンテナの作り方は次の4ステップ。

  1. ベースとなるイメージをpull
  2. Dockerfile作成
  3. Dockerイメージのビルド
  4. イメージからDockerコンテナの生成・起動

個人的に、最初はDockerfileを作成するところが一番よくわからないところだと思います。なので、まずは第一段階としてpythonの簡単なスクリプトをコンテナ内で動かすところまで作り、第二段階としてflask製webサーバを動かすところまで改良していきます。

コンテナ内でpythonスクリプトの実行

第一段階として、コンテナ内で次の myapp.py を実行するところまで作ります。

myapp.py
print("hello Docker!")

ベースとなるイメージをpull

最初にpython3系の実行環境となるイメージを用意します。Docker Hubに登録されているpython3系のイメージを検索。

$ sudo docker search python3
NAME                                                                    DESCRIPTION                                     STARS               OFFICIAL            AUTOMATED
rackspacedot/python37                                                                                                   11                                      
sellpy/python3-jupyter-sklearn                                          python3-jupyter-sklearn                         5                                       [OK]
openwhisk/python3action                                                 Apache OpenWhisk runtime for Python 3 Actions   5                                       
sellpy/python3-jupyter-sklearn-java                                     python3-jupyter-sklearn-java                    2                                       [OK]
quoinedev/python3.6-pandas-alpine                                       Python 3.7 on alpine with numpy and pandas i…   2                                       
(略)

今回はSTARSが最も多い rackspacedot/python37 を使用します。これをpullしましょう。

$ sudo docker pull rackspacedot/python37

rackspacedot/python37 もDockerイメージなので、pullした後は docker images で確認できます。

$ sudo docker images
REPOSITORY              TAG                 IMAGE ID            CREATED             SIZE
rackspacedot/python37   latest              3d51361ee118        7 weeks ago         1.06GB

このイメージがあればコンテナ内でpythonが使えるようになるので、これをベースとしてその他の必要な部分(スクリプトの配置など)を追加すればOKという感じです。

Dockerfile作成

次に、 Dockerfile という名前のファイルを作成します。

FROM rackspacedot/python37

WORKDIR /usr/local/script
COPY myapp.py .

CMD python myapp.py

簡単に説明しますと、

  • FROM rackspacedot/python37
    • ベースとなるイメージを指定する部分です。先ほどpullしたイメージを指定します。
  • WORKDIR /usr/local/script
    • コンテナ内での作業ディレクトリを指定します。今自分がいるディレクトリと違っていても問題ありません。
  • COPY myapp.py .
    • myapp.pyをコンテナ内の第二引数で指定したディレクトリにコピーします。今回は . で指定しているので、 WORKDIR で指定したディレクトリにコピーされます。
  • CMD python myapp.py
    • コンテナ起動時に実行するコマンドです。

ここまでで用意したファイルは次の通り。

$ ls -l
total 8
-rw-r--r--. 1 user developer 146 Sep 11 16:39 Dockerfile
-rw-r--r--. 1 user developer 173 Sep 11 17:04 myapp.py

Dockerイメージのビルド・コンテナ起動

Dockerfileを作成したので、イメージのビルド→コンテナ起動をしていきます。
まずはイメージのビルドから。

$ sudo docker build -t myapp:test .

-t 名前:タグ はイメージの名前とタグを設定します(スクリプト名と違っていてもOKです)。 . はDockerfileがあるディレクトリのパスを指定します。
Successfully built ハッシュ値 と出ればイメージが生成されたので、 docker images で確認できるようになります。

$ sudo docker images
REPOSITORY              TAG                 IMAGE ID            CREATED             SIZE
myapp                   test                e577339515a7        10 seconds ago      1.06GB
rackspacedot/python37   latest              3d51361ee118        7 weeks ago         1.06GB

イメージができたので、コンテナを作成・起動します。

$ sudo docker run myapp:test
hello Docker!

無事コンテナでpythonスクリプトを実行できました。
ここで起動したコンテナは残るので、再度同じコマンドで実行することができます。また、 docker ps -a で作成したコンテナを確認することができます。

コンテナ内でflask製サーバの実行

さて、コンテナ内でpythonスクリプトを実行できるようになったので、第二段階としてflaskでサーバを作成していきましょう。
myapp.py を次のように書き換えます。

from flask import Flask

app = Flask("myapp")

@app.route("/")
def main():
    return "hello Docker!!"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

一応先ほどと変化がわかるように ! の数を2個にしました。
これをコンテナ内でwebサーバとして使える状態にしましょう。

Dockerファイル修正

flaskを使用するので、コンテナ内にflaskをインストールする必要があります。 rackspacedot/python37 にはデフォルトでpipが入っていますので、イメージのビルド時に pip install する部分をDockerfileに追加します。

FROM rackspacedot/python37

RUN pip install flask

RUN useradd docker
USER docker

WORKDIR /usr/local/script
COPY myapp.py .

CMD python myapp.py

追加した分を説明すると、

  • RUN pip install flask
    • コンテナ内にflaskをインストールします。
  • RUN useradd docker / USER docker
    • docker というユーザーを追加し、その後の操作を docker ユーザーとして実行します。
    • 今回は無くても動作しますが、セキュリティ上の理由で追加しています。

Dockerイメージのビルド・コンテナ起動

ファイルを書き換えたので、イメージを再ビルド→コンテナ起動します。
まずは再ビルドから。

$ sudo docker build -t myapp .

今回はタグを省略したので、自動的に latest というタグが割り当てられます。 docker images で確認してみましょう。

$ sudo docker images
REPOSITORY              TAG                 IMAGE ID            CREATED             SIZE
myapp                   latest              6c747be5d597        3 seconds ago       1.06GB
myapp                   test                e577339515a7        50 minutes ago      1.06GB
rackspacedot/python37   latest              3d51361ee118        7 weeks ago         1.06GB

それではコンテナを起動しましょう。webサーバなので、オプションを追加しています。

$ sudo docker run -d -p 8080:8080 myapp

ポート番号ですが、 -p 受付ポート番号:コンテナ側の番号 の書き方になります。受付ポート番号は何番でも良いのですが、コンテナ側の番号はコンテナ内の myapp.py で指定した番号(今回の場合は8080)にします。

問題なく起動していれば、 docker ps で起動中のコンテナを確認することができます。

$ sudo docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
00d1ab6fc24a        myapp               "/bin/sh -c 'python …"   3 seconds ago       Up 2 seconds        0.0.0.0:8080->8080/tcp   thirsty_mccarthy

実際にアクセスしてみましょう。

$ curl http://localhost:8080
hello Docker!!

いいですね。これでwebサーバのコンテナ化完了です。
コンテナを停止したい場合は

$ sudo docker stop thirsty_mccarthy

停止後に再度起動したい場合は

$ sudo docker start thirsty_mccarthy

thirsty_mccarthy の部分はコンテナ名( docker ps 実行時の NAME に表示されるもの)を指定してください。docker start で起動すると、自動的に docker run の時に指定したポートで実行してくれます。
また、コンテナ化しておくと作ったものを他の環境で実行する時もイメージのビルド→コンテナ起動ですぐに動かせるので、非常に便利ですね。

今回は以上になります。Docker Composeで複数コンテナをコマンド一つで実行できるようになればさらに幅が広がるので、そのうちこれも勉強しようかなと思います。

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