- 投稿日:2020-05-26T22:07:59+09:00
Dockerで不要なイメージやコンテナを一括削除する方法
- 投稿日:2020-05-26T21:59:50+09:00
DockerコンテナをもとにDockerイメージを作成に関するコマンドまとめ
Dockerコンテナは、Dockerイメージをもとに作成することもできます。
公式のイメージをもとに、コンテナを作成、環境に合うように設定変更したコンテナから再度イメージ作成することも、、。
コンテナからイメージ作成
$ docker container commit [オプション] コンテナ識別子 [イメージ名[:タグ名]]
オプション 説明 --author, -a 作成者を指定する --message, -m メッセージを指定する --change, -c コミット時のDockerfile命令指定 --pause,-p コンテナを一時停止してコミットする コンテナをtarファイル出力
$ docker container export コンテナ識別子Dockerでは、動作しているコンテナのディレクトリ/ファイル群をまとめてtarファイルを作成することができます。
このtarファイルをもとにして、別のサーバでコンテナを稼働させることができます。
tarファイルからイメージ作成
$ docker container import ファイルまたはURL - [イメージ名[: タグ名]]
- tar
- tar.gz
- tgz
- bzip
- tar.xz
- txz
ファイルであれば指定可能
イメージの保存
$ docker image save [オプション] 保存ファイル名 [イメージ名]Dockerイメージをtarファイルに保存することができます。
-o オプションで、保存するファイル名を指定できます。イメージの読み込み
$ docker image load [オプション]-iオプションで、読み込みファイル名を指定できます。
- 投稿日:2020-05-26T21:39:51+09:00
稼働中のDockerコンテナの操作コマンドのまとめ
本番で運用するときには、稼働中のDockerコンテナに対して操作をします。
その時に使えるコマンドをまとめます。
稼働コンテナへの接続
$ docker container attach コンテナ識別子
終了させる時は、Ctr+PかCtr+Qキーを入力します。
稼働コンテナへのプロセス実行
$ docker container exec [オプション] コンテナ識別子 実行するコマンド [引数]
オプション 説明 --detach, -d コマンドをバッググラウンドで実行する --interactive, -i コンテナの標準入力を開く --tty, -t false --user, -u ユーザ名指定 docker container execコマンドは、起動中のコンテナにのみ実行できます。
停止中のコンテナは、docker container start コマンドを使ってコンテナを起動します。稼働コンテナへのプロセス確認
$ docker container top コンテナ識別子
実行しているプロセスのPIDとUSERと実行しているコマンドが表示されます。
稼働コンテナのポート転送確認
$ docker container port コンテナ識別子
コンテナの名前変更
$ docker container rename
名前変更できたかを確認するには、
bash
$ docker container ls
をします。コンテナ内のファイルをコピー
$ docker container cp コンテナ識別子:コンテナ内のファイルパス ホストのディレクトリパス$ docker container cp ホストのファイル コンテナ識別子:コンテナ内のファイルパスコンテナ操作の差分確認
$ docker container diff コンテナ識別子
変更の区分
区分 説明 A ファイル追加 D ファイル削除 C ファイル更新
- 投稿日:2020-05-26T21:19:54+09:00
Dockerコンテナのネットワーク操作コマンドまとめ
Dockerコンテナ同士が通信するとき、Dockerネットワーク経由してます。
そのDcokerコンテナのネットワーク操作コマンドをまとめました。
ネットワークの一覧表示
$ docker network ls [オプション]
オプション 説明 --filter=[], -f 出力をフィルタする --no-trunc 詳細を出力する --quiet, -q ネットワークIDのみを表示する Dockerはデフォルトでbridge/host/noneの3つのネットワークを作成します。
filterで利用できるキー
項目 説明 driver ドライバーの指定 id ネットワークID label ネットワークに設定されたラベル name ネットワーク名 scope ネットワークのスコープ type ネットワークのタイプ ネットワークの作成
$ docker network create [オプション] ネットワーク
オプション 説明 --driver, -d ネットワークブリッジまたはオーバレイ(デフォルトはbrige) --ip-range コンテナに割り当てるIPアドレスのレンジを指定 --subnet サブネットをCIDR形式で指定 --ipv6 IPv6ネットワークを有効にするかどうか -label ネットワークに設定するラベル ネットワークへの接続
$ docker network connect [オプション] ネットワーク コンテナ
オプション 説明 --ip IPv4アドレス --ip6 IPv6アドレス --alias エイリアス名 --link 他のコンテナへのリンク ネットワークからの切断
$ docker network disconnect [オプション] ネットワーク コンテナネットワークの詳細確認
$ docker network inspect [オプション] ネットワークネットワークの削除
$ docker network rm [オプション] ネットワーク
- 投稿日:2020-05-26T21:02:35+09:00
Macのdockerが遅いストレスから解放されよう
Mackのdocker遅い問題
Macで開発している時、主にRailsなのですがdockerを使うと遅くなるのが嫌でずっと避けてましたが、とあるプロジェクトでBFF構成にし開発環境とデプロイ環境をECS化したりとでdocker化の流れに抗えない状況になりました。
ただ、やはり遅い。
dockerが不慣れなこともありますが、Macのdockerなかなか遅い。
そしてMacが遅い件を調べてみてもみなさん困られるのが伺えます。
そんな中で対策としてよく見かけるのが
- docker-sync
- vagrantで別立て
の2つでした。
どちらが最適なのかな?と思い悩んでると、
ありました。dockerのドキュメントにまさにこの件に対しての内容が。
https://docs.docker.com/docker-for-mac/osxfs-caching/
ボリュームマウントのチューニング
端的にまとめるとv17.04から
cached
とdelegated
というオプションが追加され、書き込み読み込みの一貫性を担保しない(※)代わりにパフォーマンスが向上されるものです。※コンテナ側で発生した変更がホスト側に書き込みされるまで遅延を許容する。
即時性を求められたり大規模な処理を組むこともなく、開発環境だけで使うことから現状大きな支障はないと思いこちらのオプションを利用することにしました。
導入の仕方
- volumesの指定の最後に
:delegated
をつけていつも通りdocker-compose up
を実行例services: app: volumes: - .:/project/sample_app:delegatedtips
linuxやwindowsユーザーの方と一緒に開発する場合は必要ないかと思うので、そんな時は通常使う
docker-compose.yml
に基本的な設定そのままにdocker-compose-local.yml
のように、別ファイル名のymlを用意します。そこに例のように
services:
、サービス名:
、vpluems:
、マウント先:に:delegateオプション
を追加したものをのみを残しdocker-compose -f docker-compose.yml -f docker-compose-local.yml upとオプション指定すればvolumesの指定のみ上書かれるのでMacユーザーの方はこちらを利用したら住み分けできて便利です。
※docker-compose.ymlにversion指定されていたら、versionの指定は合わせます。
改善の結果
ただのindexのレコード取得でも数秒、初回アクセスで10秒オーバーするケースがあったものが普通にサクサクと動くようになりました。
プロジェクトの内容や要件に合わせてですが、遅すぎてストレスが激しくdocker嫌いになる前に是非試してみてください。
まず困ったら公式ドキュメントをしっかり読むことの大事さを改めて実感する一件でした。
- 投稿日:2020-05-26T20:50:36+09:00
Windows10上のVirtualBoxで稼働するUbuntuにDockerをインストール
概要
Ubuntuで、pythonを実行できる環境を整備したいが、pythonは頻繁にバージョンアップするし、プログラムの目的ごとにpipで追加したいパッケージのバージョンが異なったりする。
venv(または、pyenv等)で切り替えることも検討したが、もういっそコンテナの方が便利かも・・・
ということで、Windows10上のVertualBoxで稼働するUbuntuにDockerをインストールする手順・・・基本的には、以下に記載の手順通り。
「Install Docker Engine on Ubuntu」
https://docs.docker.com/engine/install/ubuntu/Windows10のVirtualBoxインストール手順はこちら
VirtualBoxVirtualBoxにUbuntuをインストールする手順はこちら
Ubuntu前準備
既存Dockerの確認
Ubuntuにログイン。
[アクティビティ]-[アプリケーションを表示する]を選択。
[端末]を起動し、以下のコマンドを実行し、Dockerがinstallされていないことを確認する。apt list --installed | grep dockerもしインストールされていたら、先に削除しておくことをお勧め。
sudo apt remove docker docker-engine docker.io containerd runcDockerフォルダの確認
以下のコマンドを実行し、旧バージョンの残骸がないことを確認する。
ls /var/libもし存在していたら、削除しておく。
sudo rm -rf /var/lib/dockerリポジトリの設定
必要なコマンドの追加
以下のコマンドを実行し、リポジトリ追加のために必要なコマンドがインストールされているか確認する。
apt list --installed | grep apt-transport-https apt list --installed | grep ca-certificates apt list --installed | grep curl apt list --installed | grep gnupg-agent apt list --installed | grep software-properties-common
上記の結果を確認し、インストールされていないものをインストール。
(例では、ca-certificates、software-properties-commonはインストールされているのでインストール不要。)sudo apt install apt-transport-https sudo apt install ca-certificates sudo apt install curl sudo apt install gnupg-agent sudo apt install software-properties-commonapt-keyの設定
以下のコマンドを実行。
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -確認
sudo apt-key fingerprint 0EBFCD88リポジトリ追加
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"インストール
以下のコマンドを実行。
sudo apt install docker-ce
途中確認ダイアログが表示されたら、yを入力し、[Enter]キーを押下。
インストール終了。動作確認
以下のコマンドを実行。
sudo docker version以下のコマンドを実行。
sudo docker run hello-worldDocker実行権限の追加
一般ユーザエラー
デフォルトの状態では、rootユーザしかdockerを使用できない。
権限追加
一般ユーザでdockerを使用するためには、以下の権限追加が必要。
sudo usermod -a -G docker (権限追加するユーザ)反映させるためには、再起動。
(systmctl restart dockerを実行し、当該ユーザのターミナルを再接続してもOK。)sudo reboot動作確認
以下のコマンドを実行。
docker run hello-world参考
- 投稿日:2020-05-26T20:49:45+09:00
Qiitaはじめました。
概要
4月から新社会人になった駆け出しエンジニアです。
研修も全面リモートワークということもあり、何かはじめてみようと思い、自分がやってみた(やらなきゃいけない)ことをQiitaにアウトプットしていくことにしました。初投稿は自分のモチベーションのために、技術的な内容ではなく、これからやってみたいこととか書いていければと思います。
これから何するの?
やっていきたいことは多々あるんですけど、なかでもバックエンドにフォーカスしていければと思います。(とか言いながら最近はVueに挑戦している)
と言うのも、就活をしていたときぐらいから何となくバックエンドの技術に興味があり、ちょっと調べているうちにやってみたいと思うようになったからで。
また、会社の配属先の部署で自動化やネットワークプログラマビリティなどのような高度な技術が必要とのことで、そこらへんに関連するバックエンドを中心に勉強していけたらと思っております。
具体的には?
具体的に取り組んでみたい内容を簡単に書き出してみました。
・Ruby on Rails(Javaの復習がてら)
・AWSのLambdaとかGreengrassとラズパイ
・Dockerでコンテナ型の仮想化を理解
・Kubernetes(ムズい)
などなど。。。挙げだしたらキリがないです。。。
若干独学で出来るのか不安なところはありますが、会社に詳しい人がいると思うので、色々手を動かしながらやっていきたいです。おい、資格の勉強はどうした?
資格て応用情報のことですよね。結論としては多分受けないです。
その代わり(?)といっては何ですが、今年から始まったCiscoのDevNet認定に今興味があり、色々調べています。というのも、カリカリ勉強するよりパチパチコード書いてアウトプットしていったほうが圧倒的にスキルになるなと最近感じ始めたからで。
DevNet認定ではネットワークの基礎知識だけではなく、冒頭に申し上げたネットワークプログラマビリティや自動化が範囲としてあり、Python/Git/APIなどの幅広いモダンな開発知識が求められます。
個人的にIoT/IoEやDevOpsに興味があるので、ピッタリな内容かなと思いました。お前、英語のこと忘れてるだろ
はい、最近気付きました。
チリツモを信じて毎朝Duoのチャプターひとつやるようにしてます。。。頑張ります。。。最後に一言
これから長い長いエンジニア人生がはじまりますが、クリエイティブな思考を忘れないようにしたいものです。
ということで、これから触ってみた技術や知見をQiitaに書き留めていきたいと思います。
はじめはクオリティ低いと思いますが、よろしくお願いします。
- 投稿日:2020-05-26T20:11:00+09:00
システム日時を変更してcurlで証明書検証エラーが出たら--insecureをつける
タイトルの通りです。結論はこちら
問題が発生した状況
PHPUnitを利用して、以下のようにexec()でテスト対象のAPIを叩くという内容のテストを作成していました。(デバッグ用のオプションは省略->こちらが参考になります)
curl -X POST https://[テスト対象APIのURL] -d [POSTするデータ] > [出力先ファイル]そのテストにおいて、システム日時を変更する必要があったので、
date -s "2020-01-01 00:00:00"とコマンドを実行してコンテナ内のシステム日時を変更する処理を追加しました。
すると、以下のようなエラーが出ました。curl: (60) Peer certificate cannot be authenticated with known CA certificates More details here: http://curl.haxx.se/docs/sslcerts.htmlcurl performs SSL certificate verification by default, using a "bundle" of Certificate Authority (CA) public keys (CA certs). If the default bundle file isn't adequate, you can specify an alternate file using the --cacert option. If this HTTPS server uses a certificate signed by a CA represented in the bundle, the certificate verification probably failed due to a problem with the certificate (it might be expired, or the name might not match the domain name in the URL). If you'd like to turn off curl's verification of the certificate, use the -k (or --insecure) option.証明書が認証されていないから、検証がいらないなら飛ばしたほうがいいよ、というようなことが書いてあります。
どうやって解決したか
テストコードなので証明書検証をスキップしました。
上記のエラーメッセージに従って --insecure のオプションをつけます。curl -X POST https://[テスト対象APIのURL] -d [POSTするデータ] --insecure > [出力先ファイル]これで証明書を検証せずにテストコードを走らせることができました。
まとめ
システム日時を変更することで、証明書検証が通らなくなることがあるみたいです。
今回はテストコードだったので証明書検証をスキップできましたが、本番環境だったら、日時の変更も考慮に入れて証明書を用意しないといけませんね。まあ、本番環境でそうそう日時変えないと思いますが。。。
- 投稿日:2020-05-26T19:33:12+09:00
Docker コンテナ内で何をやっても Permission denied と言われた時の対処法
急にコンテナが牙をむいた
何がきっかけか分からないが、
sudo docker-compose run コンテナ名 bash
で、bashに入ると、入れるには入れるが 「.bashrc なんとかかんとか Permission denied」というエラーがついてきた。その後、コンテナ内でプログラムをコンパイルしようとすると、こちらもPermission denied。pythonを動かそうとしてもPermission denied。ことごとく何もできなくなってしまった。
挙句の果てに
rootなのに
sudo
させてもらえなくなってしまった。root権限とはいったい...コンテナ側が悪いのか、ホスト側が悪いのか
まずは、コンテナ側の権限周りがおかしいと考えた。
sudoersの権限を確認したりと、いろいろ試したがどうにも原因らしきものが見つからない。仕方がないので、コンテナを組みなおしてみる
sudo docker-compose build --no-cache
っと...
こ こ で も か Permission denied
解決
https://stackoverflow.com/questions/59633611/docker-permission-denied-on-login-and-everything-i-try
色々調べると、このような記事が出てきた。
sudo apt install docker.io
をして、再度buildを回してみる。buildが始まった!
動いた!
困ったことに
困ったことに、このようになったトリガーが今一つ分かっていない。
ubuntuのアップデート?
一つ心当たりがあるのは、Dockerfileを変更後、--no-chache
をせずにdocker-compose
してしまったこと。
原因をご存じの方は、教えていただけると幸いです。
- 投稿日:2020-05-26T18:05:29+09:00
目で見て覚えるDocker用語集
いつまでたってもKubernetesの正しい読み方が分からない今日この頃です。
概要
- 簡単なDocker入門記事です。
- Dockerの書籍を読む前に見てもらえればいいな、ぐらいのニュアンスで作られてます。
- 文章少なめ、イメージ図多めです。
- そんなに深い話はしません。
ターゲット
- これからDockerを触ってみようという人
- Docker触ってるけど用語が多すぎて頭がパンクしそうな人
Dockerとは
- Docker社が開発している、コンテナ型の仮想環境を作成、配布、実行するためのプラットフォームです。
コンテナって何
- コンピュータの仮想化の方式の一つです。
- 従来の仮想化と違い、ゲストOSを用意しません。
- ホストOSの一部を分離して使用し、他と隔離された専用のエリアを用意します。
- 隔離された領域のことをコンテナといいます。
コンテナだと何がいいの?
- 既存の仮想化を二世帯住宅やマンションに例えると、コンテナ型仮想環境はキャンプ場に近いです。
- キャンプ場はコンテナ技術が搭載されているサーバで、キャンプ場に設置されたテント一つ一つがコンテナです。
- テントはキャンプ場のルール(サーバスペック)さえ守れば、大きさ・場所含めて自由に設置ができます。
- 家とは違い、トイレや水道などの共同箇所(OS)は各テント共同で使用しなければいけません。
- 共同箇所の変更はキャンプ場のオーナーしかできません。
- トイレや水道をテント内に設置しなくてすむので、設置に時間がかかりません。
- テントはキャンプが終わり次第片付けるので、場所(リソース)の再利用が容易です。
用語集
Dockerエンジン
エンジンとは、一般的には内燃機関を指すが、IT用語としては、ある特定の処理を行うための機能を提供する、ひとまとまりになった処理装置のことである。プログラムを指す場合が多いが、カスタムICなどのハードウェアを指す場合もある。
出典:https://www.sophia-it.com/content/%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%B3コンテナ型仮想化技術(機能)を提供するプログラムを指します。
Dockerホスト
Dockerエンジンが動作しているサーバを指します。
Docker社公式アイコンのクジラ部分(赤枠)が該当します。Dockerコンテナ
Dockerエンジンによって提供されたコンテナ型仮想環境を指します。
Docker社公式アイコンのコンテナ部分(赤枠)が該当します。
Dockerホスト上に複数のコンテナを搭載することができます。Dockerイメージ
Dockerコンテナ構成をまとめたもの(テンプレート)を指します。
Dockerイメージを元にDockerコンテナを作成します。
Dockerイメージは使い回しができるので複数コンテナをまとめて作成できます。
別のDockerホストに同じDockerコンテナを作成することもできます。Dockerfile
Dockerfileは、Dockerイメージを自動で作成してくれるファイルを指します。
基本となるイメージを設定することができます。
追加で入れたいアプリや環境変数・コマンドなどを設定できます。docker image build
Dockerfileを元にDockerイメージを作成することを指します。
作成されたDockerイメージを元にDockerコンテナを作成します。
作成されたDockerイメージは後述のDocker Registryに登録・管理することもできます。Docker Repository
ソフトウェア開発などに用いるプロジェクト管理システムやバージョン管理システムなどで、プロジェクトを構成するプログラムのソースコードやドキュメント、関連する各種のデータやファイルなどを一元的に管理する格納場所のことをリポジトリという。
出典:http://e-words.jp/w/%E3%83%AA%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA.html同名のDockerイメージの集まりを指します。
差分管理やバージョン管理をタグをつけることで区分けしています。Docker Registry
Dockerイメージを持つDocker Repositryを保管・管理するサービスを指します。
publicなもの(Docker Hub etc)とprivateなものの2種類があります。
privateなものについては、Docker上で別途Docker Registry専用のコンテナを構築する必要があります。Docker Hub
Docker Registryの1種でコンテナ共有サービスを指します。
作成したDocker Repositryをアップすることで、Git Hubのような配布・変更管理などの機能を提供します。Data Volume
Dockerコンテナ内のディレクトリをDockerホストのディスクに永続化する仕組みを指します。
上記を使用することで、Dockerホスト・コンテナ間でのディレクトリの共有・再利用ができます。
ステートフルなアプリケーションをコンテナで実現する際に使用されます。Data Volume コンテナ
コンテナ間でディレクトリを共有する仕組みを指します。
Data Volume用のコンテナを用意し、他のコンテナは全てData Volume用のコンテナを参照します。
仲介役のコンテナがあるため、アプリケーション用コンテナはDockerホスト側の情報を知る必要がありません。Docker Compose
複数コンテナを構築・実行する手順の自動化・管理を容易にするツールを指します。
複数のコンテナを使って1つのアプリケーションを構築する場合などに使用します。(例)master - slave etcdocker-compose.yml
複数コンテナの構築・実行手順をまとめたファイルを指します。
構築したいコンテナのイメージ(or Dockerfile)が定義されています。
他にも起動順・条件やコンテナ間の接続情報なども定義することができます。Docker Swarm
複数のDockerホストを束ねてクラスタ化するツールを指します。(ロゴも大勢のクジラでコンテナを運んでいます)
Docker Swarmを使ってクラスタ化されたクラスタをSwarm Clusterといいます。
Docker Swarmはコンテナオーケストレーションシステムの1つです。オーケストレーションとは、複数の統合されていないシステムにわたる多数のステップを含む
プロセスやワークフローを自動化する方法を指します。
出典:https://www.redhat.com/ja/topics/automation/what-is-orchestrationDocker Node
Swarm Clusterを構成するDockerホストのことを指します。
Docker Nodeには『manager』と『worker』の2種類があります。manager
Swarm Cluster内のDocker Nodeを管理するDocker Nodeを指します。
後述のworkerの機能も兼業することができます。
アイコンの黄枠で囲まれたクジラが該当します。(上に乗っているクジラの面倒を見ているイメージです)worker
コンテナを実行するDocker Nodeを指します。
アイコンの赤枠で囲まれたクジラが該当します。Service
アプリケーションを構成する一部のコンテナを制御するための単位を指します。(アイコンの赤枠が該当)
webアプリケーションを例にすると、Serviceは『web』と『API』の2つで構成することができます。Stack
複数のServiceをグルーピングした単位を指します。(アイコンの赤・黄・青枠がそれぞれ該当)
Stackはアプリケーション全体の構成を定義することができます。
Stackで利用するoverlayネットワークを設定しないと、Stackの数だけoverlayネットワークが作成されます。overlayネットワーク
コンテナ間通信が可能なネットワークのことを指します。
Stack作成時にデフォルトでネットワークが作成されます。
設定で別のoverlayネットワークに対象のStackを所属させることもできます。
同じoverlayネットワークに所属していれば、異なるStackのコンテナ間でも通信ができます。stack file
Stack内の情報をまとめたファイルを指します。
内容として、Service情報、Dockerイメージ、構築するコンテナ台数、所属するoverlayネットワーク情報があります。Kubernetes
Google社主導で開発されたコンテナオーケストレーションシステムです。
読みは「クバネティス/クバネテス/クーべネティス」、表記は「k8s」などがあります。
現在、主流のコンテナ管理ツールです。
機能としては、docker-composeやdocker swarmの機能を全て持っています。
さらにコンテナ間のネットワークルーティング管理等もできます。
公式アイコンにあるようコンテナ船の舵取りのような位置付けです。詳細は別途Kubernetesの用語集記事を作成予定です。
まとめ
- コンテナ技術はサーバ仮想化技術の一つです。
- コンテナ技術を提供するプログラムがDockerです。
- DockerコンテナはDockerイメージを元に構築されます。
- Dockerイメージを管理するツールがDocker Registry(Docker Hub)です。
- Docker Composeは複数のコンテナを管理・操作するツールです。
- Docker Swarmは複数のDockerホストを束ねクラスタ化するツールです。
- Docker ComposeやDocker Swarmの高機能版がKubernetesです。
おまけ
コンテナ技術も仮想化技術の一種なので、もちろんクラウドサービスの対象になります。
代表的なサービスを以下に紹介します。※AWS贔屓なのはAWSぐらいしかクラウドサービスを触ったことがないためです汗
代表的なコンテナクラウドサービス
AWS
ECS(EC2 Container Service)
AWS が開発したコンテナ管理サービスを指します。
コンテナの実行環境として、EC2・Fargateのどちらも選択できます。EKS(Elastic Container Serice for Kubernetes)
Kubernetesのマネージドサービス(コンテナ管理サービス)を指します
コンテナの実行環境として、EC2・Fargateのどちらも選択できるようになりました。Fargate
サーバレスのコンテナ可動サービスを指します。
コンテナを登録するだけでリソースを気にせずに利用することができます。
コンテナ単位に課金されます。Microsoft Azure
AKS(Azure Kubernetes Service)
KubernetesをAzureで動かすためのコンテナ可動サービスを指します。
ACR(Azure Container Registry)
コンテナのイメージ用に利用できるプライベートレジストリサービスを指します。
Google Cloud
GKE(Google Kubernetes Engine)
Kubernetesをベースとしたコンテナ管理サービスを指します。
Cloud Run
フルマネージド環境でステートレスコンテナを実行できるコンテナ可動サービスを指します。
サーバーレスなのでインフラストラクチャ管理が一切不要です。
コード実行時に課金されます。最後に
Dockerは用語の説明見るよりも、各ツールのアイコンを見た方がイメージしやすいです。
最近の技術というだけあって、アイコンにもユーザビリティが溢れている点はとてもいいですね。
- 投稿日:2020-05-26T16:14:37+09:00
multi stage build でpythonのC言語依存モジュールをインストールする
背景
alpineでC言語依存モジュールを pip install すると激重になる話
- alpineだと上記事象が避けられない
- multi stage buildで生成物を引っ張ってくるのが折衷案として良さそう
- 生成物を再利用する方法が安全ではない
- ベースイメージから生成物をベタに
COPY
している部分をやめる*.whl
を持ってきて安全にインストールする方法を採ってみる- ついでに小手先技でimageを軽くしてみる
手順
- alpineイメージ上で必要モジュールをビルド
- 生成物(wheelファイル)は再利用しやすいように一箇所にまとめる
- ビルド完了済みイメージとして docker hub に push
- 実行環境用のalpineに multi stage build でwheelのディレクトリだけ取得しインストール
- 実行確認とお掃除
実装
まずは、必要なモジュール郡の取得から
今回は以下のモジュールを導入してみるrequirements.txtcycler==0.10.0 Cython==0.29.17 h5py==2.10.0 joblib==0.14.1 kiwisolver==1.2.0 matplotlib==3.2.1 numpy==1.18.4 pandas==1.0.3 Pillow==7.1.2 pyparsing==2.4.7 python-dateutil==2.8.1 pytz==2020.1 scikit-learn==0.22.2.post1 scipy==1.3.3 six==1.14.0必要モジュールを一箇所に集める
alpineの場合だとc言語依存モジュールはtarやzipなどの圧縮形式が落ちてくる。
これらをwhl形式に変換しておく必要がある。下準備
whlに変換時に必要なライブラリが存在するはずなので、apk経由でinstallしておく
apk update \ && apk add --virtual .build --no-cache openblas-dev lapack-dev freetype-dev ... && apk add --virtual .community_build --no-cache -X http://dl-cdn.alpinelinux.org/alpine/edge/community hdf5-dev必要なwhlファイルを用意する
pip download
でもモジュールのダウンロードを実行できるが、
pip wheel
コマンドだとダウンロードとtar/zipファイルを自動展開してビルドまで行ってくれるのでこちらを使用。
pip wheel
も-r
オプションを使用できるのでpip freeze > requirements.txt
などでバージョニングファイルを指定する。pip wheel --no-cache --wheel-dir=./whl -r requirements.txt
- オプション補足
--no-cache-dir
: キャッシュを使用・作成をしない。指定しないと~/.tmp
とかにもりもりキャッシュされる。ビルド時の生成物もキャッシュされる。--wheel-dir
: wheelファイルのアウトプット先。pip wheelを使用する場合の補足
残念ながら今回の場合は途中で失敗する
別alpine環境で構築しpip freezeしたrequirements.txt
を使用しているのだが、
numpy
,scipy
が使用できる環境でないためscikit-learn
ビルド中に落ちる。
pip install -r requirements.txt
だとpip側がよしなにインストールしてくれるが1
今回に限っては先に依存モジュールを入れておくしか無い。2pip install cython numpy==1.18.4 scipy==1.3.3 pip wheel --no-cache --wheel-dir=./whl -r requirements.txtnumpyやscipyのビルドを回避するために別途イメージを作ろうとしていたのに
なんだか無意味なことをしている気がしてきたぞ...?ビルド完了後
docker hub
にpush適当にタギングしてpush
docker tag 123456789a hoge/builder-image:latest docker push hoge/builder-image:latest実行環境用に生成物を持ってくる
ここからは実行環境用のdockerfile上で作業していく。
ローカルディレクトリのwheelをインストール
pip install
で複数モジュールを指定するにはベタ書きしていくか--requirement
でテキストファイルを指定する。
適当なディレクトリにwhlを集めて丸ごとインストールできるような仕様は無い。今回はマルチステージビルドでwheelが入ったディレクトリを
COPY
し下記コマンドを実行することでローカルのwheelからインストールする。pip install --no-index --no-deps --no-cache-dir -f ./whl -r requirement.txt
- オプション補足
--no-index
: PyPiのようなインデックスサイトを使わない。オンラインを経由したくない時に使う--no-deps
: 依存モジュールをインストールしない。ただし、モジュール側で明確に指定されている場合はその限りでない模様。-f
,--find-links
: モジュールの検索先を指定。ローカルパスを指定したい時はこれを使う--upgrade で対応するモジュール
pipやsetuptoolsなど
--upgrade
オプションをつけて導入したいモジュールはアップグレード用のテキストファイルに分けて導入する。
-r
オプションで参照するテキストファイルはバージョン指定無しでもインストールは可能だ。upgrade.txtpip setuptools wheel下記コマンドで特定ディレクトリにまとめたモジュールをupgrade
pip install -U --no-index --no-deps --no-cache-dir -f ./upgrade -r upgrade.txtただ、管理するファイルも増えるのでオフライン環境下でもない限りはdockerfileに直接書いた方がいい。
実行確認
importできるか確認。shellファイルにしておいて
RUN
コマンド実行に直接叩く。import_test.sh#!/bin/sh python -c "import numpy" python -c "import scipy" python -c "import h5py" python -c "import pandas" python -c "import matplotlib" python -c "import sklearn"後始末
dockerイメージの軽量化のため余分なファイル削除
whlのビルドに使っていたイメージは生成物さえ残っていればいいので他は全部消す。builder-imageapk del --purge .build .testing_build pip freeze | xargs pip uninstall -y pip cache purge余分なファイルを削除した事で、ビルドしたイメージがどれだけ軽くなったか確認。360MBほど減量に成功した模様
# docker images REPOSITORY TAG IMAGE ID CREATED SIZE naka345/wheel_build latest b6c9df898334 9 minutes ago 1.04GB naka345/wheel_build latest 3236cf2f87de 2 days ago 639MB次は実行環境側の整理。
公式のpython dockerがとてもスマートなので、これに倣ってファイルを消し込んでいく。3execution-image# モジュールに必要なファイルだけ新しい仮想パッケージとして括り、 find /usr/local -type f -executable -not \( -name '*tkinter*' \) -exec scanelf --needed --nobanner --format '%n#p' '{}' ';' \ | tr ',' '\n' \ | sort -u \ | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ | xargs -rt apk add --no-cache --virtual .module-rundeps && \ # ビルド時に使っていたパッケージ群は全て消す apk del --purge .build .community_build # python側の余分なファイルやゴミの削除 find /usr/local -depth \ \( \ \( -type d -a \( -name test -o -name tests -o -name idle_test \) \) \ -o \ \( -type f -a \( -name '*.pyc' -o -name '*.pyo' \) \) \ \) -exec rm -rf '{}' + # 今回の実行範囲分のゴミ掃除 rm -rf /tmp/whl実行環境側で消さなかった時との比較をしてみる。
# docker images REPOSITORY TAG IMAGE ID CREATED SIZE naka345/wheel_install latest f0df8a9887de 3 hours ago 1.29GB ↓ naka345/wheel_install latest 27b4805053f2 3 hours ago 968MB
なんとか1GB以下に抑えることができた。
dockerfileにしてみる
上記を踏まえてdockerfileに書き下す。
長くなるのでgithubのリンクを貼っておしまい。まとめ
時間がかかるモジュールもpip経由で安全に比較的手早く持ち込めるようにした。
dockerイメージも多少軽量化できた。ただし、イメージを複数持たないといけない部分は据え置き。
requirements.txtの整合性が要になるので、
こいつが更新されたタイミングでdocker hub
に両イメージがpushされる仕組みがあれば楽になれるのかな?参考文献
- 投稿日:2020-05-26T16:14:37+09:00
multi stage build でpythonのC言語依存モジュールを wheel形式でインストールする
背景
alpineでC言語依存モジュールを pip install すると激重になる話
- alpineだと上記事象が避けられない
- multi stage buildで生成物を引っ張ってくるのが折衷案として良さそう
- 生成物を再利用する方法が安全ではない
- ベースイメージから生成物をベタに
COPY
している部分をやめる*.whl
を持ってきて安全にインストールする方法を採ってみる- ついでに小手先技でimageを軽くしてみる
手順
- alpineイメージ上で必要モジュールをビルド
- 生成物(wheelファイル)は再利用しやすいように一箇所にまとめる
- ビルド完了済みイメージとして docker hub に push
- 実行環境用のalpineに multi stage build でwheelのディレクトリだけ取得しインストール
- 実行確認とお掃除
実装
まずは、必要なモジュール郡の取得から
今回は以下のモジュールを導入してみるrequirements.txtcycler==0.10.0 Cython==0.29.17 h5py==2.10.0 joblib==0.14.1 kiwisolver==1.2.0 matplotlib==3.2.1 numpy==1.18.4 pandas==1.0.3 Pillow==7.1.2 pyparsing==2.4.7 python-dateutil==2.8.1 pytz==2020.1 scikit-learn==0.22.2.post1 scipy==1.3.3 six==1.14.0必要モジュールを一箇所に集める
alpineの場合だとc言語依存モジュールはtarやzipなどの圧縮形式が落ちてくる。
これらをwhl形式に変換しておく必要がある。下準備
whlに変換時に必要なライブラリが存在するはずなので、apk経由でinstallしておく
apk update \ && apk add --virtual .build --no-cache openblas-dev lapack-dev freetype-dev ... && apk add --virtual .community_build --no-cache -X http://dl-cdn.alpinelinux.org/alpine/edge/community hdf5-dev必要なwhlファイルを用意する
pip download
でもモジュールのダウンロードを実行できるが、
pip wheel
コマンドだとダウンロードとtar/zipファイルを自動展開してビルドまで行ってくれるのでこちらを使用。
pip wheel
も-r
オプションを使用できるのでpip freeze > requirements.txt
などでバージョニングファイルを指定する。pip wheel --no-cache --wheel-dir=./whl -r requirements.txt
- オプション補足
--no-cache-dir
: キャッシュを使用・作成をしない。指定しないと~/.tmp
とかにもりもりキャッシュされる。ビルド時の生成物もキャッシュされる。--wheel-dir
: wheelファイルのアウトプット先。pip wheelを使用する場合の補足
残念ながら今回の場合は途中で失敗する
別alpine環境で構築しpip freezeしたrequirements.txt
を使用しているのだが、
numpy
,scipy
が使用できる環境でないためscikit-learn
ビルド中に落ちる。
pip install -r requirements.txt
だとpip側がよしなにインストールしてくれるが1
今回に限っては先に依存モジュールを入れておくしか無い。2pip install cython numpy==1.18.4 scipy==1.3.3 pip wheel --no-cache --wheel-dir=./whl -r requirements.txtnumpyやscipyのビルドを回避するために別途イメージを作ろうとしていたのに
なんだか無意味なことをしている気がしてきたぞ...?ビルド完了後
docker hub
にpush適当にタギングしてpush
docker tag 123456789a hoge/builder-image:latest docker push hoge/builder-image:latest実行環境用に生成物を持ってくる
ここからは実行環境用のdockerfile上で作業していく。
ローカルディレクトリのwheelをインストール
pip install
で複数モジュールを指定するにはベタ書きしていくか--requirement
でテキストファイルを指定する。
適当なディレクトリにwhlを集めて丸ごとインストールできるような仕様は無い。今回はマルチステージビルドでwheelが入ったディレクトリを
COPY
し下記コマンドを実行することでローカルのwheelからインストールする。pip install --no-index --no-deps --no-cache-dir -f ./whl -r requirement.txt
- オプション補足
--no-index
: PyPiのようなインデックスサイトを使わない。オンラインを経由したくない時に使う--no-deps
: 依存モジュールをインストールしない。ただし、モジュール側で明確に指定されている場合はその限りでない模様。-f
,--find-links
: モジュールの検索先を指定。ローカルパスを指定したい時はこれを使う--upgrade で対応するモジュール
pipやsetuptoolsなど
--upgrade
オプションをつけて導入したいモジュールはアップグレード用のテキストファイルに分けて導入する。
-r
オプションで参照するテキストファイルはバージョン指定無しでもインストールは可能だ。upgrade.txtpip setuptools wheel下記コマンドで特定ディレクトリにまとめたモジュールをupgrade
pip install -U --no-index --no-deps --no-cache-dir -f ./upgrade -r upgrade.txtただ、管理するファイルも増えるのでオフライン環境下でもない限りはdockerfileに直接書いた方がいい。
実行確認
importできるか確認。shellファイルにしておいて
RUN
コマンド実行に直接叩く。import_test.sh#!/bin/sh python -c "import numpy" python -c "import scipy" python -c "import h5py" python -c "import pandas" python -c "import matplotlib" python -c "import sklearn"後始末
dockerイメージの軽量化のため余分なファイル削除
whlのビルドに使っていたイメージは生成物さえ残っていればいいので他は全部消す。builder-imageapk del --purge .build .testing_build pip freeze | xargs pip uninstall -y pip cache purge余分なファイルを削除した事で、ビルドしたイメージがどれだけ軽くなったか確認。360MBほど減量に成功した模様
# docker images REPOSITORY TAG IMAGE ID CREATED SIZE naka345/wheel_build latest b6c9df898334 9 minutes ago 1.04GB naka345/wheel_build latest 3236cf2f87de 2 days ago 639MB次は実行環境側の整理。
公式のpython dockerがとてもスマートなので、これに倣ってファイルを消し込んでいく。3execution-image# モジュールに必要なファイルだけ新しい仮想パッケージとして括り、 find /usr/local -type f -executable -not \( -name '*tkinter*' \) -exec scanelf --needed --nobanner --format '%n#p' '{}' ';' \ | tr ',' '\n' \ | sort -u \ | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ | xargs -rt apk add --no-cache --virtual .module-rundeps && \ # ビルド時に使っていたパッケージ群は全て消す apk del --purge .build .community_build # python側の余分なファイルやゴミの削除 find /usr/local -depth \ \( \ \( -type d -a \( -name test -o -name tests -o -name idle_test \) \) \ -o \ \( -type f -a \( -name '*.pyc' -o -name '*.pyo' \) \) \ \) -exec rm -rf '{}' + # 今回の実行範囲分のゴミ掃除 rm -rf /tmp/whl実行環境側で消さなかった時との比較をしてみる。
# docker images REPOSITORY TAG IMAGE ID CREATED SIZE naka345/wheel_install latest f0df8a9887de 3 hours ago 1.29GB ↓ naka345/wheel_install latest 27b4805053f2 3 hours ago 968MB
なんとか1GB以下に抑えることができた。
dockerfileにしてみる
上記を踏まえてdockerfileに書き下す。
長くなるのでgithubのリンクを貼っておしまい。まとめ
時間がかかるモジュールもpip経由で安全に比較的手早く持ち込めるようにした。
dockerイメージも多少軽量化できた。ただし、イメージを複数持たないといけない部分は据え置き。
requirements.txtの整合性が要になるので、
こいつが更新されたタイミングでdocker hub
に両イメージがpushされる仕組みがあれば楽になれるのかな?参考文献
- 投稿日:2020-05-26T15:26:27+09:00
コンテナ設計方針をまとめてみた
Docker化にあたって
全てDocker化できるわけではない
- ステートレスなものは向いている
- コンテナはステートレス
- 極力手作業なしの形で実装する
環境変数を活用する
アプリケーションの挙動を環境変数で制御する
手法
- 実引数
- 設定ファイル
- 設定ファイルに環境変数を埋め込む永続化データの扱い
- Data Volumeとしてホストと共有する
- Data Volumeコンテナとして独立させる
セキュリティ
- OFFICIALイメージを使う
- Docker Bench for Securityでチェックを行う
脆弱性が混入する恐れがあるのでdokerfileにはADDは使わない
- COPY推奨
- ADDを使うならチェックサムを併用する
適切なアクセス制御を設定する
- rootログイン禁止
- ACLを設定する
- VPNを併用する
Dockerデーモンにアクセスできることになるので、/var/run/docker.sockを共有しない
アプリケーション実行ユーザを用意し、rootユーザのままで実行しない
機密情報の扱い
- 環境変数(要暗号化)
- 外部から(暗号化S3バケット等)
ポータビリティ(≒冪等性)
絶対ではない
冪等性を損ねる要素
- Kernelの違い
- アーキテクテャの違い
ARMアーキテクチャ向けのDockerコンテナイメージをWindows/Macでビルドする- ダイナミックリンクライブラリ
- 極力スタティックリンクを使う
コンテナの粒度
1コンテナ = 1関心事
1コンテナ=1プロセスではうまくいかないことも
プロセスを過剰に意識しないこと
1つのロール or 1つのドメインに収まる粒度で
より軽いコンテナを
コンテナが重いことの弊害
オートスケールでのサービスインまで時間がかかる
ビルドに時間がかかる
プッシュに時間がかかる
ホストへにデプロイするのに時間がかかる
ディスク容量を無駄に消費する
CIに要する時間が増加する
→ 塵も積もれば、生産性が低下する
軽量ベースイメージ
scratch
- 機能が限定的すぎる
BusyBox
- 標準Cライブラリ別にイメージが作られている
- パッケージマネージャなし
Alpine Linux
- デファクトスタンダード
dlstroless
コンテナサイズの削減手法
アプリケーションのサイズを削減する
- 不要ファイルの削除
- 不要プログラムの削除
- 依存ライブラリの削減
- web asset(主に画像)のサイズ削減
.dockerignoreを定義する
Doker build時のレイヤ構造を削減する
RUNで個別に実行する → "&&"で結合して実行する
FROM alpine:xx.yy RUN apk add --no-cache wget RUN wget https:// hoge.com/fuga.tgz RUN tar -xvzf fuga.tgz RUN rm fuga.tgz RUN mv fuga /bin/fuga RUN chmod +x /bin/fuga RUN fuga --symlink↓
FROM alpine:xx.yy RUN apk add --no-cache wget &&\ wget https:// hoge.com/fuga.tgz &&\ tar -xvzf fuga.tgz &&\ rm fuga.tgz &&\ mv fuga /bin/fuga &&\ chmod +x /bin/fuga &&\ fuga --symlink可読性とトレードオフになる
multi-stage builds
- ビルド用コンテナと実行コンテナを分離する
- ビルド用コンテナで制作したファイルは実行コンテナにコピーし、ビルド用コンテナは破棄される
= ビルド用にのみ必要なツールをインストールするレイヤは、実行コンテナに存在しなくてよいFROM golang:xx AS build WORKDIR / COPY ./go/src/hoge.com/fuga/piyo RUN go get github.com/go-sql-driver/mysql RUN go get hoge.in/gorp.v1 RUN cd /go/src/hoge.com/fuga/piyo && go build -o bin/piyo cmd/main.go FROM alpine:yy COPY --from = build /go/src/hoge.com/fuga/fuga/bin/piyo /usr/local/bin/ CMD ["piyo"]本番環境適用以外のコンテナ活用法
- 開発環境の統一・共有
- CLIの利用
- Win・MacでLinuxでしか動かないツールを使う手段
- シェルスクリプト
- 負荷テスト
参考
- 投稿日:2020-05-26T13:56:09+09:00
docker back to the basic
TL;DR
以下のようなコマンドを使いこなそう
参考) http://docs.docker.jp/engine/reference/commandline/index.html
$ docker image ls $ docker container ls $ docker container ls -a $ docker run -id --name hoge --rm -v $(pwd):/home $ docker container exec $ docker container attach $ docker build -t [image-name] .基礎知識
コンテナ型の仮想化では、コンテナはホストOSのカーネルを利用する。
1コンテナ1プロセスで使用する。
Web server container やDB server container など複数のコンテナを立てる。
コンテナにはそれぞれ仮想NIC(eth0)が割り当てられる。
コンテナ同士で通信することが可能である。
NIC(ニック) : 別名 LANカード、Ethernetカード
NICとは、コンピュータなどの機器を通信ネットワーク(LAN)に接続するためのカード型の拡張装置。筐体背面や側面などに用意された拡張スロットなどに挿入して使用する。単にNICといった場合は最も普及しているイーサネット(Ethernet = 有線LANのこと)に接続するためのコネクタ(RJ45)のことを指す。(引用 http://e-words.jp)What is image and what is container?
docker には、imageとcontainerの2つがある。
OSのインストールDVDを仮想化したものがDockerイメージであり、
OSが起動しているサーバを仮想化したものがコンテナに相当する。
基本コマンド
まず、一番簡単なHello Worldをやってみます。
hello-worldというDockerイメージが用意されていますので、
docker run hello-world
コマンドで起動します。$ sudo docker run hello-worldpullしてきたDockerイメージの一覧は
docker image ls
コマンドで確認できます。$ docker image ls $ docker container ls -a作成済Dockerコンテナの一覧は
docker container ls -a
で確認できます。"-a"は「コンテナ一覧をすべて表示」するためのオプションです。これをつけないと起動中のコンテナしか表示されません。
dockerイメージの削除コマンドは
docker image rm [Repository]
コンテナの削除コマンドはdocker container rm [containerID]
$ docker image rm hello-world $ docker container rm 484810ae0812注意)
docker run
からの状況から脱するには「ctrlキーを押しながら'pq'と押す」ホスト側との「ディレクトリ共有」
「ホストOSのディレクトリ」と「コンテナ内の/home/」をつなぐ。
「-v」オプションは、ポートフォワード設定の「-p」オプションと同様、コロンの左側(/root/tomcat-container/logs)がホスト側、右側(/share/logs)がコンテナ内のディレクトリを表していて、そのディレクトリが共有されるかたちとなります。
$ docker run -it -d --name hogehoge -v $(pwd):/home ubuntu:16.04 bashdocker container exec
docker container ls -a
でまだコンテナが起動している場合、またコンテナに入ることができるはずである。起動しているコンテナに入る方法は2つある。
docker container attach
コマンドを使う方法docker container exec
コマンドを使う方法attachは「ゲストOSの標準入力/標準出力を、ホストOSの標準入力/標準出力とつなげる」という意味です。attachは「既に起動している」1つのプロセスへつなぎに行きます。
$ docker container attach 5 $ ps aux # ctrlキー + 'pq'execは「引数のコマンドを実行する」という意味です。
execでは現在起動しているbashとは別に、新たにbashを起動します。
$ docker container exec -it 5 bash $ ps aux $ exitDockerFile の作り方
Docker imageからDocker containerを作成する際に「必要なパッケージのインストール手順や実行コマンドなど」をスクリプト化したもの。
$ docker build -t workcheckFROM
FROM
命令は、以降の命令で使う ベース・イメージ を指定します。あるいは、有効なDockerfile
は、1行めをFROM
命令で指定する必要があります。イメージとは、あらゆる有効なものが利用できます。 パブリック・リポジトリ から イメージを取得する 方法が一番簡単です。# ubuntu 14.04のイメージを使う FROM ubuntu:14.04MAINTAINER
MAINTAINER
命令は、生成するイメージの Author (作者)フィールドを指定します。RUN
イメージ作成時に実行するコマンドを書く。
RUN
命令は既存イメージ上の新しいレイヤで、あらゆるコマンドを実行し、その結果をコミットする命令です。コミットの結果得られたイメージは、Dockerfile
の次のステップで使われます。# update後にapache2をインストール RUN apt update . RUN apt install apache2 -yCMD
Dockerfile
でCMD
命令を一度だけ指定できます。複数のCMD
がある場合、最も後ろのCMD
のみ有効です。
CMD
の主な目的は、 コンテナ実行時のデフォルトを提供します 。 デフォルトには、実行可能なコマンドが含まれているか、あるいは省略されるかもしれません# nodeでサーバを起動する CMD [ "node", "server.js" ]EXPOSE
指定のポートを外部に公開する。
EXPOSE
命令は、特定のネットワーク・ポートをコンテナが実行時にリッスンすることを Docker に伝えます。EXPOSE
があっても、これだけではホストからコンテナにアクセスできるようにしません。アクセスするには、-p
フラグを使ってポートの公開範囲を指定するか、-P
フラグで全ての露出ポートを公開する必要があります。# 80番ポートを外部に公開する EXPOSE 80ENV
ENV
命令は、環境変数と 値
のセットです。値はDockerfile
から派生する全てのコマンド環境で利用でき、 インラインで置き換え も可能です。
ENV
命令は2つの形式があります。1つめは、ENV
であり、変数に対して1つの値を設定します。はじめの空白以降の文字列が `` に含まれます。ここには空白もクォートも含まれます。2つめの形式は
ENV = ...
です。これは一度に複数の変数を指定できます。先ほどと違い、構文の2つめにイコールサイン(=)があるので気を付けてください。コマンドラインの分割、クォート、バックスラッシュは、空白スペースも含めて値になります。ENV <key> <value> ENV <key>=<value> ...ADD
ADD
命令は<ソース>
にある新しいファイルやディレクトリをコピー、あるいはリモートの URL からコピーします。それから、コンテナ内のファイルシステム上にある送信先
に指定されたパスに追加します。複数の
<ソース>
リソースを指定できます。この時、ファイルやディレクトリはソースディレクトリ(構築時のコンテクスト)からの相対パス上に存在しないと構築できません。それぞれの
<ソース>
にはワイルドカードと Go 言語の filepath.Match ルールに一致するパターンが使えます。例えば、次のような記述です。ADD hom* /mydir/ # "hom" で始まる全てのファイルを追加 ADD hom?.txt /mydir/ # ? は1文字だけ一致します。例: "home.txt" ADD test relativeDir/ # "test" を `WORKDIR`/relativeDir/ (相対ディレクトリ)に追加 ADD test /absoluteDir/ # "test" を /absoluteDir/ (絶対ディレクトリ)に追加COPY
COPY
命令は<ソース>
にある新しいファイルやディレクトリをコピーするもので、コンテナ内のファイルシステム上にある<送信先>
に指定されたパスに追加します。COPY hom* /mydir/ # "hom" で始まる全てのファイルを追加 COPY hom?.txt /mydir/ # ? は1文字だけ一致します。例: "home.txt"例) Node.jsのアプリケーションを起動する
FROM node:12 # アプリケーションディレクトリを作成する WORKDIR /usr/src/app # アプリケーションの依存関係をインストールする # ワイルドカードを使用して、package.json と package-lock.json の両方が確実にコピーされるようにします。 # 可能であれば (npm@5+) COPY package*.json ./ RUN npm install # 本番用にコードを作成している場合 # RUN npm install --only=production # アプリケーションのソースをバンドルする COPY . . EXPOSE 8080 CMD [ "node", "server.js" ]
- 投稿日:2020-05-26T13:29:21+09:00
Dockerで構築したRailsアプリをGitHub Actionsで高速にCIする為のプラクティス(Rails 6 API編)
Rails on GitHub Actions(或いは {Django,Laravel} on GitHub Actions)のCI事例として、
- ホストランナー上にRuby(Python, PHP)をセットアップ
- MySQLやRedisはサービスコンテナで立ち上げ
- 依存ライブラリのインストール(
bundle install
) や ユニットテスト(rspec
) もホストランナー上で直接実行という事例は多く見かけるのですが、開発をDockerベースで行っていて、GitHub ActionsのCI Pipelineも同じくDockerベースで構築したい...というケースの事例があまり見当たらなかったので、自分が関わったプロジェクト(Rails 6 API mode)での事例を紹介します。
Requirements
- 開発環境は全てDocker(Dockerfile/docker-compose.yml)で構築&管理したい
bundle install
やrails s
やrspec
などのコマンドは全てdocker-compose {run,exec}
を介してコンテナ内で実行する(したい)- Dockerイメージのサイズを小さくしたい
- Dockerイメージ自体でファイルを抱え込むような処理はなるべく書かない。実行したいコマンドはコンテナ起動時に都度コマンドとして付与し、成果物を保存・永続化したければ
volumes
を活用する- 「
bundle install
,rails db:prepare
を実行して環境をセットアップする為のコンテナ」「rails s
する為のコンテナ」という具合に用途でコンテナを分ける "1イメージ : Nコンテナ" 想定で- GitHub ActionsでのCIも開発環境と同じ Dockerfile/docker-compose.yml をそのまま活用してセットアップしたい
- CIでも
bundle install
やrails s
やrspec
などのコマンドは全てdocker-compose {run,exec}
を介してコンテナ内で実行する(したい)- 「CI用のDockerfile」とか「CI時だけoverrideする為のdocker-compose.yml」は極力作りたくない
- GitHub ActionsではDockerイメージや依存ライブラリのキャッシュを有効活用してCIを高速化したい
Version
- Ruby 2.7.1
- gem 3.1.3
- bundler 2.1.4
- Rails 6.0.3 (API mode)
- Docker (Docker for Mac)
- Engine 19.03.8
- Compose 1.25.5
- MySQL 8.0.20
Dockerfile
./Dockerfile# このDockerfileとdocker-compose.ymlの書き方については、 # TechRachoさんの記事 https://techracho.bpsinc.jp/hachi8833/2019_09_06/79035 # で紹介されていた手法をベースにしています。(感謝) ARG ARG_RUBY_VERSION FROM ruby:${ARG_RUBY_VERSION}-alpine3.11 # hadolint ignore=DL3008,DL3018 RUN apk update && \ apk add --update --no-cache \ build-base \ bash \ curl \ git \ less \ tzdata \ mysql-client \ mysql-dev && \ rm -rf /var/cache/apk/* SHELL ["/bin/bash", "-eo", "pipefail", "-c"] ARG ARG_TZ=Asia/Tokyo RUN cp /usr/share/zoneinfo/${ARG_TZ} /etc/localtime ARG ARG_GEM_HOME=/bundle ENV GEM_HOME=${ARG_GEM_HOME} ENV BUNDLE_JOBS=4 \ BUNDLE_RETRY=3 \ BUNDLE_PATH=${GEM_HOME} \ BUNDLE_BIN=${GEM_HOME}/bin \ BUNDLE_APP_CONFIG=${GEM_HOME} \ LANG=C.UTF-8 \ LC_ALL=C.UTF-8 ARG ARG_BUNDLER_VERSION="2.1.4" RUN gem update --system && \ gem install bundler:${ARG_BUNDLER_VERSION} ENV APP_ROOT=/app RUN mkdir ${APP_ROOT} ENV PATH=${APP_ROOT}/bin:${BUNDLE_BIN}:${GEM_HOME}/gems/bin:${PATH} WORKDIR ${APP_ROOT} ENV RAILS_ENV=development ARG ARG_COMPOSE_WAIT_VER=2.7.3 ADD https://github.com/ufoscout/docker-compose-wait/releases/download/${ARG_COMPOSE_WAIT_VER}/wait /wait RUN chmod +x /waitアプリのファイルをイメージに含めない
CMD
やENTRYPOINT
による処理は定義せず、Railsアプリが動く環境を整える事にのみ特化してイメージのサイズを小さくしています。bundle install
やrails s
といった処理はdocker-compose {run,exec}
を介してコンテナ内で実行し、ライブラリやアプリのファイルはvolumesでマウントしてコンテナにコピーする(イメージには含めないようにする)事を意図しています。ufoscout/docker-compose-wait でDB起動を待つ
ARG ARG_COMPOSE_WAIT_VER=2.7.3 ADD https://github.com/ufoscout/docker-compose-wait/releases/download/${ARG_COMPOSE_WAIT_VER}/wait /wait RUN chmod +x /waitDockerfileの末尾3行部分で ufoscout/docker-compose-wait をインストールしています。これはmysql等のミドルウェア・コンテナのポートがLISTEN状態になるのを待ってくれるRust製のツールで、名前の通りdocker-compose.ymlとの併用が意図されています。
依存ミドルウェア起動をどうやって待つか?については、netcatやdockerizeを使った例だったり、公式のPostgresの例ではシェルスクリプトを書いて頑張る例が紹介されていたりしますが(Badの反応が多いのが気になりますが...)、このツールはミドルウェアの追加・削除時もdocker-compose.ymlに少し記述を追加するだけで対応できますし動作も確実性が高くて使い勝手が良かったです。具体的な使い方は後述のdocker-compose.ymlやGitHub Actionsに関する節で解説します。docker-compose.yml
./docker-compose.ymlversion: "3.7" services: db: image: mysql:8.0.20 command: --default-authentication-plugin=mysql_native_password environment: - MYSQL_ROOT_PASSWORD - MYSQL_ALLOW_EMPTY_PASSWORD volumes: - mysql-data:/var/lib/mysql ports: - ${MYSQL_FORWARDED_PORT:-3306}:3306 - ${MYSQL_FORWARDED_X_PORT:-33060}:33060 base: &base build: context: . dockerfile: ./Dockerfile cache_from: - rails6api-development-cache args: ARG_RUBY_VERSION: ${ARG_RUBY_VERSION:-"2.7.1"} image: rails6api-development:0.1.0 tmpfs: - /tmp wait-middleware: &wait-middleware <<: *base environment: WAIT_HOSTS: db:3306 depends_on: - db command: /wait backend: &backend <<: *base stdin_open: true tty: true volumes: - ./:/app:cached - ${GEMS_CACHE_DIR:-bundle-cache}:/bundle - rails-cache:/app/tmp/cache depends_on: - db console: <<: *backend ports: - 3333:3000 command: /bin/bash server: <<: *backend ports: - 3333:3000 command: bash -c "rm -f tmp/pids/server.pid && rails s -b 0.0.0.0" volumes: mysql-data: bundle-cache: rails-cache:1つのDockerfileでdocker-composeの複数サービスを定義する
base: &base build: context: . dockerfile: ./Dockerfile cache_from: - rails6api-development-cache args: ARG_RUBY_VERSION: ${ARG_RUBY_VERSION:-"2.7.1"} image: rails6api-development:0.1.0docker-compose.ymlの
base
サービス以降の記述が先程のDockerfileを利用するサービスの設定になります。Dockerfileの冒頭で入力を期待しているARG_RUBY_VERSION
ARGについては、「環境変数で指定されていたらその内容を、未設定時のデフォルト値は2.7.1
を」指定するようにargs
にて定義しています。
base
サービスは、それ自体がcommandやentrypointによる処理を行ってはおらず、単にビルドする為だけのサービスとして定義しています。&base
とエイリアスを定義している事からも分かるように、これを後続サービスでマージして利用しています(後述)。ビルドに関する設定はこのbase
サービスにのみ集約してあるので、buildセクションの設定はこれ以降のサービスには出てきません。
base
サービスのポイントはcache_from
でrails6api-development-cache
を指定している事です。この設定は開発作業時ではなくCI時での利用を想定したものです。詳細は後述します。wait-middleware: &wait-middleware <<: *base environment: WAIT_HOSTS: db:3306 depends_on: - db command: /waitこのサービス定義が、Dockerfileの最後でインストールしたufoscout/docker-compose-waitを使って
db
サービスの起動を待つ為のサービスです。ymlの定義方法は公式を参照ください。先に定義したbase
サービスをmergeし、docker-compose-waitで必要な設定とdbサービスとの関連を定義しています。単独で実行したい場合はdocker-compose run
すればOKです。$ docker-compose run --rm wait-middleware Creating network "rails6api_default" with the default driver Creating rails6api_db_1 ... done -------------------------------------------------------- docker-compose-wait 2.7.3 --------------------------- Starting with configuration: - Hosts to be waiting for: [db:3306] - Timeout before failure: 30 seconds - TCP connection timeout before retry: 5 seconds - Sleeping time before checking for hosts availability: 0 seconds - Sleeping time once all hosts are available: 0 seconds - Sleeping time between retries: 1 seconds -------------------------------------------------------- Checking availability of db:3306 Host db:3306 not yet available... Host db:3306 is now available! -------------------------------------------------------- docker-compose-wait - Everything's fine, the application can now start!上記実行例は筆者のMacでのもので、ほとんど待ちが発生せずdbが立ち上がります。この速さならdepends_on で起動順さえ意識しておけば「DBが立ち上がっていない状態でアプリが動きそうになってエラー」という状況はほぼ発生しないのですが、GitHub ActionsのCI環境ではこの速さでは起動してくれず、
wait-middleware
の効果が大きくなります。これについても後述します。backend: &backend <<: *base stdin_open: true tty: true volumes: - ./:/app:cached - ${GEMS_CACHE_DIR:-bundle-cache}:/bundle - rails-cache:/app/tmp/cache depends_on: - db console: <<: *backend ports: - 3333:3000 command: /bin/bash server: <<: *backend ports: - 3333:3000 command: bash -c "rm -f tmp/pids/server.pid && rails s -b 0.0.0.0" volumes: mysql-data: bundle-cache: rails-cache:bashログインしてのプロンプト作業や
rails s
する為のサービス定義と、volumeの定義部分です。TechRachoさんの記事 で紹介されていた書き方を流用させてもらっています。
console
とserver
の両サービスがbackend
というサービス定義をマージしているのですが、このbackend
のvolumesで${GEMS_CACHE_DIR:-bundle-cache}:/bundle
と定義されているvolumeはbundle install
先のディレクトリで、「環境変数GEMS_CACHE_DIR
がセットされていればその内容で、セットされていなければbundle-cache
という名前のnamed volumeでマウント」する事を意図しており、CIの為にこのような設定を行っています。これも詳細は後述します。.env
./.envMYSQL_ROOT_PASSWORD=root MYSQL_ALLOW_EMPTY_PASSWORD=1 DB_HOST=db MYSQL_FORWARDED_PORT=3806 MYSQL_FORWARDED_X_PORT=38060docker-compose.ymlのdbサービス内の環境変数を.envの内容から展開 しています。
.github/workflows/ci.yml
./.github/workflows/ci.ymlon: push: branches: - master paths-ignore: - '**/*.md' - 'LICENSE' pull_request: paths-ignore: - '**/*.md' - 'LICENSE' env: DOCKER_BUILDKIT: 1 COMPOSE_DOCKER_CLI_BUILD: 1 APP_IMAGE_TAG: rails6api-development:0.1.0 APP_IMAGE_CACHE_TAG: rails6api-development-cache IMAGE_CACHE_DIR: /tmp/cache/docker-image IMAGE_CACHE_KEY: cache-image jobs: image-cache-or-build: strategy: matrix: ruby: ["2.7.1"] os: [ubuntu-18.04] runs-on: ${{ matrix.os }} env: ARG_RUBY_VERSION: ${{ matrix.ruby }} steps: - name: Check out code id: checkout uses: actions/checkout@v2 - name: Generate dotenv id: generate-dotenv run: cp .env.sample .env - name: Cache docker image id: cache-docker-image uses: actions/cache@v1 with: path: ${{ env.IMAGE_CACHE_DIR }} key: ${{ runner.os }}-${{ env.IMAGE_CACHE_KEY }}-${{ matrix.ruby }}-${{ hashFiles('Dockerfile') }} restore-keys: | ${{ runner.os }}-${{ env.IMAGE_CACHE_KEY }}-${{ matrix.ruby }}- - name: Docker load id: docker-load if: steps.cache-docker-image.outputs.cache-hit == 'true' run: docker image load -i ${IMAGE_CACHE_DIR}/image.tar - name: Docker build id: docker-build run: docker-compose build --build-arg BUILDKIT_INLINE_CACHE=1 base - name: Docker tag and save id: docker-tag-save if: steps.cache-docker-image.outputs.cache-hit != 'true' run: mkdir -p ${IMAGE_CACHE_DIR} && docker image tag ${APP_IMAGE_TAG} ${APP_IMAGE_CACHE_TAG} && docker image save -o ${IMAGE_CACHE_DIR}/image.tar ${APP_IMAGE_CACHE_TAG} test-app: needs: image-cache-or-build strategy: matrix: ruby: ["2.7.1"] os: [ubuntu-18.04] runs-on: ${{ matrix.os }} env: ARG_RUBY_VERSION: ${{ matrix.ruby }} GEMS_CACHE_DIR: /tmp/cache/bundle GEMS_CACHE_KEY: cache-gems steps: - name: Check out code id: checkout uses: actions/checkout@v2 - name: Generate dotenv id: generate-dotenv run: cp .env.sample .env - name: Cache docker image id: cache-docker-image uses: actions/cache@v1 with: path: ${{ env.IMAGE_CACHE_DIR }} key: ${{ runner.os }}-${{ env.IMAGE_CACHE_KEY }}-${{ matrix.ruby }}-${{ hashFiles('Dockerfile') }} restore-keys: | ${{ runner.os }}-${{ env.IMAGE_CACHE_KEY }}-${{ matrix.ruby }}- - name: Docker load id: docker-load if: steps.cache-docker-image.outputs.cache-hit == 'true' run: docker image load -i ${IMAGE_CACHE_DIR}/image.tar - name: Docker compose build id: docker-build run: docker-compose build --build-arg BUILDKIT_INLINE_CACHE=1 base - name: Wait middleware services id: wait-middleware run: docker-compose run --rm wait-middleware - name: Confirm docker-compose logs id: confirm-docker-compose-logs run: docker-compose logs db - name: Cache bundle gems id: cache-bundle-gems uses: actions/cache@v1 with: path: ${{ env.GEMS_CACHE_DIR }} key: ${{ runner.os }}-${{ env.GEMS_CACHE_KEY }}-${{ matrix.ruby }}-${{ hashFiles('Gemfile.lock') }} restore-keys: | ${{ runner.os }}-${{ env.GEMS_CACHE_KEY }}-${{ matrix.ruby }}- - name: Setup and Run test id: setup-and-run-test run: docker-compose run --rm console bash -c "bundle install && rails db:prepare && rspec" scan-image-by-trivy: needs: image-cache-or-build strategy: matrix: ruby: ["2.7.1"] os: [ubuntu-18.04] runs-on: ${{ matrix.os }} env: ARG_RUBY_VERSION: ${{ matrix.ruby }} TRIVY_CACHE_DIR: /tmp/cache/trivy steps: - name: Check out code id: checkout uses: actions/checkout@v2 - name: Cache docker image id: cache-docker-image uses: actions/cache@v1 with: path: ${{ env.IMAGE_CACHE_DIR }} key: ${{ runner.os }}-${{ env.IMAGE_CACHE_KEY }}-${{ matrix.ruby }}-${{ hashFiles('Dockerfile') }} restore-keys: | ${{ runner.os }}-${{ env.IMAGE_CACHE_KEY }}-${{ matrix.ruby }}- - name: Docker load id: docker-load if: steps.cache-docker-image.outputs.cache-hit == 'true' run: docker image load -i ${IMAGE_CACHE_DIR}/image.tar - name: Scan image id: scan-image run: docker container run --rm -v /var/run/docker.sock:/var/run/docker.sock -v ${TRIVY_CACHE_DIR}:/root/.cache/ aquasec/trivy ${APP_IMAGE_CACHE_TAG}BuildKitでビルドをちょっと高速化
env: DOCKER_BUILDKIT: 1 COMPOSE_DOCKER_CLI_BUILD: 1グローバルな環境変数でdocker-compose向けにBuildKitを有効化しています。今回のDockerfileはmulti-stageでも無くBuildKitによる恩恵はそこまで大きくはないのですが、有効化した事でビルド時間が速くなった(12%程度削減)ので有効化しています。
先にイメージのキャッシュ・リストアを実行し、このキャッシュで後続ジョブを並列に動かす
jobs: # Dockerイメージのキャッシュ・リストア image-cache-or-build: # アプリのテスト test-app: needs: image-cache-or-build # イメージの脆弱性スキャン scan-image-by-trivy: needs: image-cache-or-build最初に必ずDockerイメージのキャッシュリストア(キャッシュが無ければ新規ビルド→キャッシュ生成)を行い、後続のアプリテスト&イメージスキャンはこのキャッシュからリストアしたイメージを使って実行するようにします。アプリテストとイメージスキャンは並列実行でも構わないので並列にしています。
docker image save, docker image load, cache_from with BuildKit でイメージのキャッシュ・リストアとビルド
jobs: image-cache-or-build: strategy: matrix: ruby: ["2.7.1"] os: [ubuntu-18.04] runs-on: ${{ matrix.os }} env: ARG_RUBY_VERSION: ${{ matrix.ruby }} steps: - name: Check out code id: checkout uses: actions/checkout@v2 - name: Generate dotenv id: generate-dotenv run: cp .env.sample .env - name: Cache docker image id: cache-docker-image uses: actions/cache@v1 with: path: ${{ env.IMAGE_CACHE_DIR }} key: ${{ runner.os }}-${{ env.IMAGE_CACHE_KEY }}-${{ matrix.ruby }}-${{ hashFiles('Dockerfile') }} restore-keys: | ${{ runner.os }}-${{ env.IMAGE_CACHE_KEY }}-${{ matrix.ruby }}-Dockerイメージのキャッシュリストアは、公式のキャッシュ処理用actionであるactions/cacheで行います。キャッシュのキーに
${{ hashFiles('Dockerfile') }}
を含めているのは、Dockerfileに変更があった際にキャッシュHITさせないようにする事を意図したものです。env: DOCKER_BUILDKIT: 1 COMPOSE_DOCKER_CLI_BUILD: 1 APP_IMAGE_TAG: rails6api-development:0.1.0 APP_IMAGE_CACHE_TAG: rails6api-development-cache # 略 - name: Docker load id: docker-load if: steps.cache-docker-image.outputs.cache-hit == 'true' run: docker image load -i ${IMAGE_CACHE_DIR}/image.tar - name: Docker build id: docker-build run: docker-compose build --build-arg BUILDKIT_INLINE_CACHE=1 base - name: Docker tag and save id: docker-tag-save if: steps.cache-docker-image.outputs.cache-hit != 'true' run: mkdir -p ${IMAGE_CACHE_DIR} && docker image tag ${APP_IMAGE_TAG} ${APP_IMAGE_CACHE_TAG} && docker image save -o ${IMAGE_CACHE_DIR}/image.tar ${APP_IMAGE_CACHE_TAG}上記step群の処理をまとめると、「ビルドされたイメージに
rails6api-development-cache
というタグを付与してtarに保存し、actions/cache のキャッシュ先ディレクトリに image.tar という名前で保存する」という処理を行っています。キャッシュHITの有無で処理の流れは下記のように変わります。
- キャッシュがHITしなかった場合
docker-build
のstepで新規にイメージがビルドされます
- ※
--build-arg BUILDKIT_INLINE_CACHE=true
オプションを指定している理由は、BuildKitが有効化された状態でビルドされたイメージをcache_from
で取り込むには、元のイメージがこのオプション付きでビルドされている必要がある為です- 実際に
cache_from
が指定されているのは、docker-compose.ymlのbase
サービスのbuild設定の箇所(=rails6api-development-cache
)- actions/cacheでキャッシュ先として指定した
${IMAGE_CACHE_DIR}
をmkdirします- ビルド結果のイメージに別途「キャッシュ用のタグ」を付与します
- =
APP_IMAGE_CACHE_TAG
=rails6api-development-cache
- 「キャッシュ用のタグ」 =
rails6api-development-cache
を付与したイメージをdocker image save
で保存します。この保存先にactions/cacheでのキャッシュ先ディレクトリを指定します(ファイル名は image.tar)- キャッシュがHITした場合
docker-load
のstepで、キャッシュからリストアされたimage.tarがdocker image load
によって展開されます
- 展開されるイメージには「キャッシュ用のタグ」 =
rails6api-development-cache
が付与されていますdocker-build
のstepでイメージがビルドされますが、先のloadのstepで展開されたイメージによってcache_from
rails6api-development-cache
の指定が効き、このビルドはすぐに終わります- tagとsaveのstepは
if: steps.cache-docker-image.outputs.cache-hit != 'true'
の指定によりSKIPされますキャッシュ・リストアしたイメージを使って高速CI
env: DOCKER_BUILDKIT: 1 COMPOSE_DOCKER_CLI_BUILD: 1 APP_IMAGE_TAG: rails6api-development:0.1.0 APP_IMAGE_CACHE_TAG: rails6api-development-cache # 略 jobs: # 略 test-app: needs: image-cache-or-build # 略 - name: Cache docker image id: cache-docker-image uses: actions/cache@v1 with: path: ${{ env.IMAGE_CACHE_DIR }} key: ${{ runner.os }}-${{ env.IMAGE_CACHE_KEY }}-${{ matrix.ruby }}-${{ hashFiles('Dockerfile') }} restore-keys: | ${{ runner.os }}-${{ env.IMAGE_CACHE_KEY }}-${{ matrix.ruby }}- - name: Docker load id: docker-load if: steps.cache-docker-image.outputs.cache-hit == 'true' run: docker image load -i ${IMAGE_CACHE_DIR}/image.tar - name: Docker compose build id: docker-build run: docker-compose build --build-arg BUILDKIT_INLINE_CACHE=1 base先述のイメージのビルド&キャッシュjobが完了すると、アプリのテストを行う
test-app
が起動します。docker-load
のstepでは「キャッシュ用のタグ」 =rails6api-development-cache
が付与されているイメージが展開され、docker-build
のstepでこのイメージをcache_from
によって取り込んでbase
サービスのイメージ(=rails6api-development:0.1.0
タグが付与されたイメージ)をビルドします。ufoscout/docker-compose-wait でMySQLコンテナの起動を待つ
- name: Wait middleware services id: wait-middleware run: docker-compose run --rm wait-middleware - name: Confirm docker-compose logs id: confirm-docker-compose-logs run: docker-compose logs dbDockerfileの最後でインストールしてあるufoscout/docker-compose-waitを使って
db
サービスの起動を待ちます。Starting with configuration: - Hosts to be waiting for: [db:3306] - Timeout before failure: 30 seconds - TCP connection timeout before retry: 5 seconds - Sleeping time before checking for hosts availability: 0 seconds - Sleeping time once all hosts are available: 0 seconds - Sleeping time between retries: 1 seconds -------------------------------------------------------- Checking availability of db:3306 Host db:3306 not yet available... Host db:3306 not yet available... Host db:3306 not yet available... Host db:3306 not yet available... Host db:3306 not yet available... Host db:3306 not yet available... Host db:3306 not yet available... Host db:3306 not yet available... Host db:3306 not yet available... Host db:3306 not yet available... Host db:3306 not yet available... Host db:3306 is now available! -------------------------------------------------------- docker-compose-wait - Everything's fine, the application can now start! --------------------------------------------------------以前に「GitHub ActionsのCI環境ではこの速さでは起動してくれず、
wait-middleware
の効果が大きくなります」と書きましたが、上記ログがGitHub Actionsのrunnerインスタンス上での実行例で(Host db:3306 not yet available...
でsleepを1秒挟んでいます)、ポート3306のLISTENまでに10秒以上掛かっています。このログ例のみならず、何度実行しても平均的に10秒超は掛かっていました。仮にこの所要時間でwaitするstepを挟まない(depends_on
を指定するのみ)とすると、MySQL起動前に後続のRailsアプリに関するstepが走ってしまいエラーになるでしょう。余談: MySQLコンテナの起動プロセスと処理時間
db_1 | 2020-05-25 16:36:40+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.20-1debian10 started. db_1 | 2020-05-25 16:36:40+00:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql' db_1 | 2020-05-25 16:36:40+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.20-1debian10 started. db_1 | 2020-05-25 16:36:40+00:00 [Note] [Entrypoint]: Initializing database files db_1 | 2020-05-25T16:36:40.990281Z 0 [Warning] [MY-011070] [Server] 'Disabling symbolic links using --skip-symbolic-links (or equivalent) is the default. Consider not using this option as it' is deprecated and will be removed in a future release. db_1 | 2020-05-25T16:36:40.990349Z 0 [System] [MY-013169] [Server] /usr/sbin/mysqld (mysqld 8.0.20) initializing of server in progress as process 45 db_1 | 2020-05-25T16:36:40.996092Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started. db_1 | 2020-05-25T16:36:42.100882Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended. db_1 | 2020-05-25T16:36:43.312805Z 6 [Warning] [MY-010453] [Server] root@localhost is created with an empty password ! Please consider switching off the --initialize-insecure option. db_1 | 2020-05-25 16:36:46+00:00 [Note] [Entrypoint]: Database files initialized db_1 | 2020-05-25 16:36:46+00:00 [Note] [Entrypoint]: Starting temporary server db_1 | 2020-05-25T16:36:46.494377Z 0 [Warning] [MY-011070] [Server] 'Disabling symbolic links using --skip-symbolic-links (or equivalent) is the default. Consider not using this option as it' is deprecated and will be removed in a future release. db_1 | 2020-05-25T16:36:46.494485Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 8.0.20) starting as process 92 db_1 | 2020-05-25T16:36:46.507413Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started. db_1 | 2020-05-25T16:36:46.819578Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended. db_1 | 2020-05-25T16:36:46.915827Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Socket: '/var/run/mysqld/mysqlx.sock' db_1 | 2020-05-25T16:36:47.015509Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed. db_1 | 2020-05-25T16:36:47.017398Z 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory. db_1 | 2020-05-25T16:36:47.034485Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.20' socket: '/var/run/mysqld/mysqld.sock' port: 0 MySQL Community Server - GPL. db_1 | 2020-05-25 16:36:47+00:00 [Note] [Entrypoint]: Temporary server started. db_1 | Warning: Unable to load '/usr/share/zoneinfo/iso3166.tab' as time zone. Skipping it. db_1 | Warning: Unable to load '/usr/share/zoneinfo/leap-seconds.list' as time zone. Skipping it. db_1 | Warning: Unable to load '/usr/share/zoneinfo/zone.tab' as time zone. Skipping it. db_1 | Warning: Unable to load '/usr/share/zoneinfo/zone1970.tab' as time zone. Skipping it. db_1 | db_1 | 2020-05-25 16:36:49+00:00 [Note] [Entrypoint]: Stopping temporary server db_1 | 2020-05-25T16:36:49.538277Z 10 [System] [MY-013172] [Server] Received SHUTDOWN from user root. Shutting down mysqld (Version: 8.0.20). db_1 | 2020-05-25T16:36:51.341063Z 0 [System] [MY-010910] [Server] /usr/sbin/mysqld: Shutdown complete (mysqld 8.0.20) MySQL Community Server - GPL. db_1 | 2020-05-25 16:36:51+00:00 [Note] [Entrypoint]: Temporary server stopped db_1 | db_1 | 2020-05-25 16:36:51+00:00 [Note] [Entrypoint]: MySQL init process done. Ready for start up. db_1 | db_1 | 2020-05-25T16:36:51.806387Z 0 [Warning] [MY-011070] [Server] 'Disabling symbolic links using --skip-symbolic-links (or equivalent) is the default. Consider not using this option as it' is deprecated and will be removed in a future release. db_1 | 2020-05-25T16:36:51.806498Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 8.0.20) starting as process 1 db_1 | 2020-05-25T16:36:51.816008Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started. db_1 | 2020-05-25T16:36:52.191532Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended. db_1 | 2020-05-25T16:36:52.286349Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Socket: '/var/run/mysqld/mysqlx.sock' bind-address: '::' port: 33060 db_1 | 2020-05-25T16:36:52.341936Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed. db_1 | 2020-05-25T16:36:52.345030Z 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory. db_1 | 2020-05-25T16:36:52.363785Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.20' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server - GPL.上記はMySQLコンテナ(
db
サービス)起動時のログをdocker-compose logs db
で確認した際の例です。
Initializing database files
からDatabase files initialized
で6秒Starting temporary server
からTemporary server stopped
で5秒この2処理で所要時間をほぼ半分ずつ要しています。
依存gemのvolumeマウントはnamed volumeではなく書き込み可能なディレクトリを使う
env: ARG_RUBY_VERSION: ${{ matrix.ruby }} GEMS_CACHE_DIR: /tmp/cache/bundle GEMS_CACHE_KEY: cache-gems # 略 - name: Cache bundle gems id: cache-bundle-gems uses: actions/cache@v1 with: path: ${{ env.GEMS_CACHE_DIR }} key: ${{ runner.os }}-${{ env.GEMS_CACHE_KEY }}-${{ matrix.ruby }}-${{ hashFiles('Gemfile.lock') }} restore-keys: | ${{ runner.os }}-${{ env.GEMS_CACHE_KEY }}-${{ matrix.ruby }}-依存Gemのキャッシュリストアも、公式のキャッシュ処理用actionであるactions/cacheで行います。キャッシュのキーに
${{ hashFiles('Gemfile.lock') }}
を含めているのは、Gemfile.lock(Gemfile)に変更があった際にキャッシュHITさせないようにする事を意図したものです。./docker-compose.ymlbackend: &backend # 略 volumes: - ./:/app:cached - ${GEMS_CACHE_DIR:-bundle-cache}:/bundle - rails-cache:/app/tmp/cache # 略 volumes: mysql-data: bundle-cache: rails-cache:全てをDockerで行おうとしているので、
bundle install
もコンテナ内で行います。つまりインストールされたgemをキャッシュしたければ、volume mount先からインストール結果を取り出さなければなりません。↑のdocker-compose.ymlを紹介した際に「環境変数
GEMS_CACHE_DIR
がセットされていればその内容で、セットされていなければbundle-cache
という名前のnamed volumeでマウント」と書いたのですが、このcache-bundle-gems
のstepがまさに「環境変数GEMS_CACHE_DIR
がセットされていれば」なケースに該当します。これは「named volumeではなくマウント先のパスを環境変数で明示する」のが意図です。公式の
volumes
のSHORT SYNTAXによると、パスが指定されていればそのパスが、固定文字列が指定されていればその名前のnamed volumeが、それぞれマウントされます。開発作業時はGEMS_CACHE_DIR
を明示せずデフォルトの固定文字列(= named volume =bundle-cache
)を使用しても良いですが、actions/cache で内容をキャッシュしようとした場合、そのディレクトリとしてnamed volumeの実体(具体的には/var/lib/docker/volumes/xxx
というパス)を指定するとpermission deniedエラーでキャッシュに失敗してしまいます。なのでこれを回避する為にCI時のみGEMS_CACHE_DIR
として/tmp/cache/bundle
という(permission deniedにならない)ディレクトリを明示しています。これによりCI時のbundle install結果はこのGEMS_CACHE_DIR
ディレクトリに出力され、actions/cacheでディレクトリが丸ごとキャッシュされます。この記述は正直分かりやすいとは言えないので、LONG SYNTAXを利用して分かりにくさを軽減したいところなのですが、今回は「環境変数の中身によってvolume typeが変えられる」「1つのdocker-compose.ymlを開発作業とCIで併用しやすい」というメリットを優先してSHORT SYNTAXを採用しました。
アプリのセットアップ&テストもDockerコンテナ内で実行
- name: Setup and Run test id: setup-and-run-test run: docker-compose run --rm console bash -c "bundle install && rails db:prepare && rspec"このstepは構築するアプリの仕様ややりたい事次第で変わると思いますが、一応今回の例を紹介しておくと
bundle install
(結果は先述の通りキャッシュされる) →db:prepare
でDBセットアップ(参考) → テスト(今回使ったアプリではrspec
を使用しています)、という順にテストまで実施しています。それぞれのコマンドをrunnerインスタンス上で直接実行するのではなく、docker-compose run
でconsole
サービスを立ち上げてその中で実行するようにしています。アプリのテストと並列でDockerイメージの脆弱性スキャンも実行
env: DOCKER_BUILDKIT: 1 COMPOSE_DOCKER_CLI_BUILD: 1 APP_IMAGE_TAG: rails6api-development:0.1.0 APP_IMAGE_CACHE_TAG: rails6api-development-cache IMAGE_CACHE_DIR: /tmp/cache/docker-image IMAGE_CACHE_KEY: cache-image # 略 scan-image-by-trivy: needs: image-cache-or-build strategy: matrix: ruby: ["2.7.1"] os: [ubuntu-18.04] runs-on: ${{ matrix.os }} env: ARG_RUBY_VERSION: ${{ matrix.ruby }} TRIVY_CACHE_DIR: /tmp/cache/trivy steps: - name: Check out code id: checkout uses: actions/checkout@v2 - name: Cache docker image id: cache-docker-image uses: actions/cache@v1 with: path: ${{ env.IMAGE_CACHE_DIR }} key: ${{ runner.os }}-${{ env.IMAGE_CACHE_KEY }}-${{ matrix.ruby }}-${{ hashFiles('Dockerfile') }} restore-keys: | ${{ runner.os }}-${{ env.IMAGE_CACHE_KEY }}-${{ matrix.ruby }}- - name: Docker load id: docker-load if: steps.cache-docker-image.outputs.cache-hit == 'true' run: docker image load -i ${IMAGE_CACHE_DIR}/image.tar - name: Scan image id: scan-image run: docker container run --rm -v /var/run/docker.sock:/var/run/docker.sock -v ${TRIVY_CACHE_DIR}:/root/.cache/ aquasec/trivy ${APP_IMAGE_CACHE_TAG}開発〜CIをDockerで完結させようとしているので、折角なのでCI時にDockerイメージの脆弱性スキャンも行っておきたいです。今回は aquasecurity/trivy を使わせてもらいました。Docker完結を目指しているので、trivyによるスキャンも公式に提供されているDockerで行います。
needs: image-cache-or-build
によってイメージビルド&キャッシュが完了済なので、スキャンもそのキャッシュをload・展開したイメージに対して実施して高速化します(スキャン対象として${APP_IMAGE_CACHE_TAG}
=rails6api-development-cache
を指定)。
毎回 aquasec/trivy をpullする事でスキャンそのものの仕様を常に最新化しているので、Dockerfileに変更がなくてもスキャンを実行するようにしています。開発作業のユースケース例
新規参画エンジニアの環境構築手順は?
# ビルド docker-compose build base # セットアップ docker-compose run --rm console bash -c "bundle install && rails db:prepare && rails db:seed" # 起動 docker-compose up -d serverrailsを起動したい時は?
docker-compose up -d server
起動中のRailsログをコンソールに流しておきたい時は?
# 下記コマンドでattachすれば、server コンテナの標準出力をtail風に確認可能 docker attach `docker-compose ps -q server` # attach状態を終了したければ Ctrl+P => Ctrl+Q するテスト(rspec)を実行したい時は?
起動中のserverサービスで
docker-compose exec server rspec [SPEC_FILES]consoleサービスで
docker-compose run --rm console rspec [SPEC_FILES]Rails consoleに接続したい時は?
起動中のserverサービスで
docker-compose exec server rails c
マイグレーションを追加・修正・適用したい時は?
起動中のserverサービスで
# マイグレーション新規生成 docker-compose exec server rails g migration MIGRATION_NAME # : # マイグレーションファイルを適宜修正 # : # マイグレーションの適用 docker-compose exec server rails db:migrate
- 投稿日:2020-05-26T13:29:21+09:00
開発もGitHub ActionsでのCIも全てDockerで行う (Rails 6 API編)
Rails on GitHub Actions(或いは {Django,Laravel} on GitHub Actions)のCI事例として、
- ホストランナー上にRuby(Python, PHP)をセットアップ
- MySQLやRedisはサービスコンテナで立ち上げ
- 依存ライブラリのインストール(
bundle install
) や ユニットテスト(rspec
) もホストランナー上で直接実行という事例は多く見かけるのですが、開発をDockerベースで行っていて、GitHub ActionsのCI Pipelineも同じくDockerベースで構築したい...というケースの事例があまり見当たらなかったので、自分が関わったプロジェクト(Rails 6 API mode)での事例を紹介します。
Requirements
- 開発環境は全てDocker(Dockerfile/docker-compose.yml)で構築&管理したい
bundle install
やrails s
やrspec
などのコマンドは全てdocker-compose {run,exec}
を介してコンテナ内で実行する(したい)- Dockerイメージのサイズを小さくしたい
- Dockerイメージ自体でファイルを抱え込むような処理はなるべく書かない。実行したいコマンドはコンテナ起動時に都度コマンドとして付与し、成果物を保存・永続化したければ
volumes
を活用する- 「
bundle install
,rails db:prepare
を実行して環境をセットアップする為のコンテナ」「rails s
する為のコンテナ」という具合に用途でコンテナを分ける "1イメージ : Nコンテナ" 想定で- GitHub ActionsでのCIも開発環境と同じ Dockerfile/docker-compose.yml をそのまま活用してセットアップしたい
- CIでも
bundle install
やrails s
やrspec
などのコマンドは全てdocker-compose {run,exec}
を介してコンテナ内で実行する(したい)- 「CI用のDockerfile」とか「CI時だけoverrideする為のdocker-compose.yml」は極力作りたくない
- GitHub ActionsではDockerイメージや依存ライブラリのキャッシュを有効活用してCIを高速化したい
Version
- Ruby 2.7.1
- gem 3.1.3
- bundler 2.1.4
- Rails 6.0.3 (API mode)
- Docker (Docker for Mac)
- Engine 19.03.8
- Compose 1.25.5
- MySQL 8.0.20
Contents
Dockerfile
./Dockerfile# このDockerfileとdocker-compose.ymlの書き方については、 # TechRachoさんの記事 https://techracho.bpsinc.jp/hachi8833/2019_09_06/79035 # で紹介されていた手法をベースにしています。(感謝) ARG ARG_RUBY_VERSION FROM ruby:${ARG_RUBY_VERSION}-alpine3.11 # hadolint ignore=DL3008,DL3018 RUN apk update && \ apk add --update --no-cache \ build-base \ bash \ curl \ git \ less \ tzdata \ mysql-client \ mysql-dev && \ rm -rf /var/cache/apk/* SHELL ["/bin/bash", "-eo", "pipefail", "-c"] ARG ARG_TZ=Asia/Tokyo RUN cp /usr/share/zoneinfo/${ARG_TZ} /etc/localtime ARG ARG_GEM_HOME=/bundle ENV GEM_HOME=${ARG_GEM_HOME} ENV BUNDLE_JOBS=4 \ BUNDLE_RETRY=3 \ BUNDLE_PATH=${GEM_HOME} \ BUNDLE_BIN=${GEM_HOME}/bin \ BUNDLE_APP_CONFIG=${GEM_HOME} \ LANG=C.UTF-8 \ LC_ALL=C.UTF-8 ARG ARG_BUNDLER_VERSION="2.1.4" RUN gem update --system && \ gem install bundler:${ARG_BUNDLER_VERSION} ENV APP_ROOT=/app RUN mkdir ${APP_ROOT} ENV PATH=${APP_ROOT}/bin:${BUNDLE_BIN}:${GEM_HOME}/gems/bin:${PATH} WORKDIR ${APP_ROOT} ENV RAILS_ENV=development ARG ARG_COMPOSE_WAIT_VER=2.7.3 ADD https://github.com/ufoscout/docker-compose-wait/releases/download/${ARG_COMPOSE_WAIT_VER}/wait /wait RUN chmod +x /waitRailsアプリのファイルをイメージ(Dockerfile)に含めない
CMD
やENTRYPOINT
による処理は定義せず、Railsアプリが動く環境を整える事にのみ特化してイメージのサイズを小さくしています。bundle install
やrails s
といった処理はdocker-compose {run,exec}
を介してコンテナ内で実行し、ライブラリやアプリのファイルはvolumesでマウントしてコンテナにコピーする(イメージには含めないようにする)事を意図しています。DB起動を待つ処理用途で ufoscout/docker-compose-wait を導入
ARG ARG_COMPOSE_WAIT_VER=2.7.3 ADD https://github.com/ufoscout/docker-compose-wait/releases/download/${ARG_COMPOSE_WAIT_VER}/wait /wait RUN chmod +x /waitDockerfileの末尾3行部分で ufoscout/docker-compose-wait をインストールしています。これはmysql等のミドルウェア・コンテナのポートがLISTEN状態になるのを待ってくれるRust製のツールで、名前の通りdocker-compose.ymlとの併用が意図されています。
依存ミドルウェア起動をどうやって待つか?については、netcatやdockerizeを使った例だったり、公式のPostgresの例ではシェルスクリプトを書いて頑張る例が紹介されていたりしますが(Badの反応が多いのが気になりますが...)、このツールはミドルウェアの追加・削除時もdocker-compose.ymlに少し記述を追加するだけで対応できますし動作も確実性が高くて使い勝手が良かったです。具体的な使い方は後述のdocker-compose.ymlやGitHub Actionsに関する節で解説します。docker-compose.yml
./docker-compose.ymlversion: "3.7" services: db: image: mysql:8.0.20 command: --default-authentication-plugin=mysql_native_password environment: - MYSQL_ROOT_PASSWORD - MYSQL_ALLOW_EMPTY_PASSWORD volumes: - mysql-data:/var/lib/mysql ports: - ${MYSQL_FORWARDED_PORT:-3306}:3306 - ${MYSQL_FORWARDED_X_PORT:-33060}:33060 base: &base build: context: . dockerfile: ./Dockerfile cache_from: - rails6api-development-cache args: ARG_RUBY_VERSION: ${ARG_RUBY_VERSION:-"2.7.1"} image: rails6api-development:0.1.0 tmpfs: - /tmp wait-middleware: &wait-middleware <<: *base environment: WAIT_HOSTS: db:3306 depends_on: - db command: /wait backend: &backend <<: *base stdin_open: true tty: true volumes: - ./:/app:cached - ${GEMS_CACHE_DIR:-bundle-cache}:/bundle - rails-cache:/app/tmp/cache depends_on: - db console: <<: *backend ports: - 3333:3000 command: /bin/bash server: <<: *backend ports: - 3333:3000 command: bash -c "rm -f tmp/pids/server.pid && rails s -b 0.0.0.0" volumes: mysql-data: bundle-cache: rails-cache:1つのDockerfileでdocker-composeの複数サービスを定義する
base: &base build: context: . dockerfile: ./Dockerfile cache_from: - rails6api-development-cache args: ARG_RUBY_VERSION: ${ARG_RUBY_VERSION:-"2.7.1"} image: rails6api-development:0.1.0docker-compose.ymlの
base
サービス以降の記述が先程のDockerfileを利用するサービスの設定になります。Dockerfileの冒頭で入力を期待しているARG_RUBY_VERSION
ARGについては、「環境変数で指定されていたらその内容を、未設定時のデフォルト値は2.7.1
を」指定するようにargs
にて定義しています。
base
サービスは、それ自体がcommandやentrypointによる処理を行ってはおらず、単にビルドする為だけのサービスとして定義しています。&base
とエイリアスを定義している事からも分かるように、これを後続サービスでマージして利用しています(後述)。ビルドに関する設定はこのbase
サービスにのみ集約してあるので、buildセクションの設定はこれ以降のサービスには出てきません。
base
サービスのポイントはcache_from
でrails6api-development-cache
を指定している事です。この設定は開発作業時ではなくCI時での利用を想定したものです。詳細は後述します。wait-middleware: &wait-middleware <<: *base environment: WAIT_HOSTS: db:3306 depends_on: - db command: /waitこのサービス定義が、Dockerfileの最後でインストールしたufoscout/docker-compose-waitを使って
db
サービスの起動を待つ為のサービスです。ymlの定義方法は公式を参照ください。先に定義したbase
サービスをmergeし、docker-compose-waitで必要な設定とdbサービスとの関連を定義しています。単独で実行したい場合はdocker-compose run
すればOKです。$ docker-compose run --rm wait-middleware Creating network "rails6api_default" with the default driver Creating rails6api_db_1 ... done -------------------------------------------------------- docker-compose-wait 2.7.3 --------------------------- Starting with configuration: - Hosts to be waiting for: [db:3306] - Timeout before failure: 30 seconds - TCP connection timeout before retry: 5 seconds - Sleeping time before checking for hosts availability: 0 seconds - Sleeping time once all hosts are available: 0 seconds - Sleeping time between retries: 1 seconds -------------------------------------------------------- Checking availability of db:3306 Host db:3306 not yet available... Host db:3306 is now available! -------------------------------------------------------- docker-compose-wait - Everything's fine, the application can now start!上記実行例は筆者のMacでのもので、ほとんど待ちが発生せずdbが立ち上がります。この速さならdepends_on で起動順さえ意識しておけば「DBが立ち上がっていない状態でアプリが動きそうになってエラー」という状況はほぼ発生しないのですが、GitHub ActionsのCI環境ではこの速さでは起動してくれず、
wait-middleware
の効果が大きくなります。これについても後述します。backend: &backend <<: *base stdin_open: true tty: true volumes: - ./:/app:cached - ${GEMS_CACHE_DIR:-bundle-cache}:/bundle - rails-cache:/app/tmp/cache depends_on: - db console: <<: *backend ports: - 3333:3000 command: /bin/bash server: <<: *backend ports: - 3333:3000 command: bash -c "rm -f tmp/pids/server.pid && rails s -b 0.0.0.0" volumes: mysql-data: bundle-cache: rails-cache:bashログインしてのプロンプト作業や
rails s
する為のサービス定義と、volumeの定義部分です。TechRachoさんの記事 で紹介されていた書き方を流用させてもらっています。
console
とserver
の両サービスがbackend
というサービス定義をマージしているのですが、このbackend
のvolumesで${GEMS_CACHE_DIR:-bundle-cache}:/bundle
と定義されているvolumeはbundle install
先のディレクトリで、「環境変数GEMS_CACHE_DIR
がセットされていればその内容で、セットされていなければbundle-cache
という名前のnamed volumeでマウント」する事を意図しており、CIの為にこのような設定を行っています。これも詳細は後述します。.env
./.envMYSQL_ROOT_PASSWORD=root MYSQL_ALLOW_EMPTY_PASSWORD=1 DB_HOST=db MYSQL_FORWARDED_PORT=3806 MYSQL_FORWARDED_X_PORT=38060docker-compose.ymlのdbサービス内の環境変数を.envの内容から展開 しています。
.github/workflows/ci.yml
./.github/workflows/ci.ymlon: push: branches: - master paths-ignore: - '**/*.md' - 'LICENSE' pull_request: paths-ignore: - '**/*.md' - 'LICENSE' env: DOCKER_BUILDKIT: 1 COMPOSE_DOCKER_CLI_BUILD: 1 APP_IMAGE_TAG: rails6api-development:0.1.0 APP_IMAGE_CACHE_TAG: rails6api-development-cache IMAGE_CACHE_DIR: /tmp/cache/docker-image IMAGE_CACHE_KEY: cache-image jobs: image-cache-or-build: strategy: matrix: ruby: ["2.7.1"] os: [ubuntu-18.04] runs-on: ${{ matrix.os }} env: ARG_RUBY_VERSION: ${{ matrix.ruby }} steps: - name: Check out code id: checkout uses: actions/checkout@v2 - name: Generate dotenv id: generate-dotenv run: cp .env.sample .env - name: Cache docker image id: cache-docker-image uses: actions/cache@v1 with: path: ${{ env.IMAGE_CACHE_DIR }} key: ${{ runner.os }}-${{ env.IMAGE_CACHE_KEY }}-${{ matrix.ruby }}-${{ hashFiles('Dockerfile') }} restore-keys: | ${{ runner.os }}-${{ env.IMAGE_CACHE_KEY }}-${{ matrix.ruby }}- - name: Docker load id: docker-load if: steps.cache-docker-image.outputs.cache-hit == 'true' run: docker image load -i ${IMAGE_CACHE_DIR}/image.tar - name: Docker build id: docker-build run: docker-compose build --build-arg BUILDKIT_INLINE_CACHE=1 base - name: Docker tag and save id: docker-tag-save if: steps.cache-docker-image.outputs.cache-hit != 'true' run: mkdir -p ${IMAGE_CACHE_DIR} && docker image tag ${APP_IMAGE_TAG} ${APP_IMAGE_CACHE_TAG} && docker image save -o ${IMAGE_CACHE_DIR}/image.tar ${APP_IMAGE_CACHE_TAG} test-app: needs: image-cache-or-build strategy: matrix: ruby: ["2.7.1"] os: [ubuntu-18.04] runs-on: ${{ matrix.os }} env: ARG_RUBY_VERSION: ${{ matrix.ruby }} GEMS_CACHE_DIR: /tmp/cache/bundle GEMS_CACHE_KEY: cache-gems steps: - name: Check out code id: checkout uses: actions/checkout@v2 - name: Generate dotenv id: generate-dotenv run: cp .env.sample .env - name: Cache docker image id: cache-docker-image uses: actions/cache@v1 with: path: ${{ env.IMAGE_CACHE_DIR }} key: ${{ runner.os }}-${{ env.IMAGE_CACHE_KEY }}-${{ matrix.ruby }}-${{ hashFiles('Dockerfile') }} restore-keys: | ${{ runner.os }}-${{ env.IMAGE_CACHE_KEY }}-${{ matrix.ruby }}- - name: Docker load id: docker-load if: steps.cache-docker-image.outputs.cache-hit == 'true' run: docker image load -i ${IMAGE_CACHE_DIR}/image.tar - name: Docker compose build id: docker-build run: docker-compose build --build-arg BUILDKIT_INLINE_CACHE=1 base - name: Wait middleware services id: wait-middleware run: docker-compose run --rm wait-middleware - name: Confirm docker-compose logs id: confirm-docker-compose-logs run: docker-compose logs db - name: Cache bundle gems id: cache-bundle-gems uses: actions/cache@v1 with: path: ${{ env.GEMS_CACHE_DIR }} key: ${{ runner.os }}-${{ env.GEMS_CACHE_KEY }}-${{ matrix.ruby }}-${{ hashFiles('Gemfile.lock') }} restore-keys: | ${{ runner.os }}-${{ env.GEMS_CACHE_KEY }}-${{ matrix.ruby }}- - name: Setup and Run test id: setup-and-run-test run: docker-compose run --rm console bash -c "bundle install && rails db:prepare && rspec" scan-image-by-trivy: needs: image-cache-or-build strategy: matrix: ruby: ["2.7.1"] os: [ubuntu-18.04] runs-on: ${{ matrix.os }} env: ARG_RUBY_VERSION: ${{ matrix.ruby }} TRIVY_CACHE_DIR: /tmp/cache/trivy steps: - name: Check out code id: checkout uses: actions/checkout@v2 - name: Cache docker image id: cache-docker-image uses: actions/cache@v1 with: path: ${{ env.IMAGE_CACHE_DIR }} key: ${{ runner.os }}-${{ env.IMAGE_CACHE_KEY }}-${{ matrix.ruby }}-${{ hashFiles('Dockerfile') }} restore-keys: | ${{ runner.os }}-${{ env.IMAGE_CACHE_KEY }}-${{ matrix.ruby }}- - name: Docker load id: docker-load if: steps.cache-docker-image.outputs.cache-hit == 'true' run: docker image load -i ${IMAGE_CACHE_DIR}/image.tar - name: Scan image id: scan-image run: docker container run --rm -v /var/run/docker.sock:/var/run/docker.sock -v ${TRIVY_CACHE_DIR}:/root/.cache/ aquasec/trivy ${APP_IMAGE_CACHE_TAG}BuildKitを有効化して(ちょっとだけ)ビルドを高速化
env: DOCKER_BUILDKIT: 1 COMPOSE_DOCKER_CLI_BUILD: 1グローバルな環境変数でdocker-compose向けにBuildKitを有効化しています。今回のDockerfileはmulti-stageでも無くBuildKitによる恩恵はそこまで大きくはないのですが、有効化した事でビルド時間が速くなった(12%程度削減)ので有効化しています。
Dockerイメージのキャッシュ・リストア, アプリのテスト, イメージの脆弱性スキャン, の3ジョブ構成
jobs: # Dockerイメージのキャッシュ・リストア image-cache-or-build: # アプリのテスト test-app: needs: image-cache-or-build # イメージの脆弱性スキャン scan-image-by-trivy: needs: image-cache-or-build最初に必ずDockerイメージのキャッシュリストア(キャッシュが無ければ新規ビルド→キャッシュ生成)を行い、後続のアプリテスト&イメージスキャンはこのキャッシュからリストアしたイメージを使って実行するようにします。アプリテストとイメージスキャンは並列実行でも構わないので並列にしています。
docker image save
+docker image load
+cache_from
with BuildKit を駆使したDockerイメージのキャッシュ・リストアjobs: image-cache-or-build: strategy: matrix: ruby: ["2.7.1"] os: [ubuntu-18.04] runs-on: ${{ matrix.os }} env: ARG_RUBY_VERSION: ${{ matrix.ruby }} steps: - name: Check out code id: checkout uses: actions/checkout@v2 - name: Generate dotenv id: generate-dotenv run: cp .env.sample .env - name: Cache docker image id: cache-docker-image uses: actions/cache@v1 with: path: ${{ env.IMAGE_CACHE_DIR }} key: ${{ runner.os }}-${{ env.IMAGE_CACHE_KEY }}-${{ matrix.ruby }}-${{ hashFiles('Dockerfile') }} restore-keys: | ${{ runner.os }}-${{ env.IMAGE_CACHE_KEY }}-${{ matrix.ruby }}-Dockerイメージのキャッシュリストアは、公式のキャッシュ処理用actionであるactions/cacheで行います。キャッシュのキーに
${{ hashFiles('Dockerfile') }}
を含めているのは、Dockerfileに変更があった際にキャッシュHITさせないようにする事を意図したものです。env: DOCKER_BUILDKIT: 1 COMPOSE_DOCKER_CLI_BUILD: 1 APP_IMAGE_TAG: rails6api-development:0.1.0 APP_IMAGE_CACHE_TAG: rails6api-development-cache # 略 - name: Docker load id: docker-load if: steps.cache-docker-image.outputs.cache-hit == 'true' run: docker image load -i ${IMAGE_CACHE_DIR}/image.tar - name: Docker build id: docker-build run: docker-compose build --build-arg BUILDKIT_INLINE_CACHE=1 base - name: Docker tag and save id: docker-tag-save if: steps.cache-docker-image.outputs.cache-hit != 'true' run: mkdir -p ${IMAGE_CACHE_DIR} && docker image tag ${APP_IMAGE_TAG} ${APP_IMAGE_CACHE_TAG} && docker image save -o ${IMAGE_CACHE_DIR}/image.tar ${APP_IMAGE_CACHE_TAG}上記step群の処理をまとめると、「ビルドされたイメージに
rails6api-development-cache
というタグを付与してtarに保存し、actions/cache のキャッシュ先ディレクトリに image.tar という名前で保存する」という処理を行っています。キャッシュHITの有無で処理の流れは下記のように変わります。
- キャッシュがHITしなかった場合
docker-build
のstepで新規にイメージがビルドされます
- ※
--build-arg BUILDKIT_INLINE_CACHE=true
オプションを指定している理由は、BuildKitが有効化された状態でビルドされたイメージをcache_from
で取り込むには、元のイメージがこのオプション付きでビルドされている必要がある為です- 実際に
cache_from
が指定されているのは、docker-compose.ymlのbase
サービスのbuild設定の箇所(=rails6api-development-cache
)- actions/cacheでキャッシュ先として指定した
${IMAGE_CACHE_DIR}
をmkdirします- ビルド結果のイメージに別途「キャッシュ用のタグ」を付与します
- =
APP_IMAGE_CACHE_TAG
=rails6api-development-cache
- 「キャッシュ用のタグ」 =
rails6api-development-cache
を付与したイメージをdocker image save
で保存します。この保存先にactions/cacheでのキャッシュ先ディレクトリを指定します(ファイル名は image.tar)- キャッシュがHITした場合
docker-load
のstepで、キャッシュからリストアされたimage.tarがdocker image load
によって展開されます
- 展開されるイメージには「キャッシュ用のタグ」 =
rails6api-development-cache
が付与されていますdocker-build
のstepでイメージがビルドされますが、先のloadのstepで展開されたイメージによってcache_from
rails6api-development-cache
の指定が効き、このビルドはすぐに終わります- tagとsaveのstepは
if: steps.cache-docker-image.outputs.cache-hit != 'true'
の指定によりSKIPされますキャッシュ済のイメージでアプリのCI(テスト)を実行する事で高速化を図る
env: DOCKER_BUILDKIT: 1 COMPOSE_DOCKER_CLI_BUILD: 1 APP_IMAGE_TAG: rails6api-development:0.1.0 APP_IMAGE_CACHE_TAG: rails6api-development-cache # 略 jobs: # 略 test-app: needs: image-cache-or-build # 略 - name: Cache docker image id: cache-docker-image uses: actions/cache@v1 with: path: ${{ env.IMAGE_CACHE_DIR }} key: ${{ runner.os }}-${{ env.IMAGE_CACHE_KEY }}-${{ matrix.ruby }}-${{ hashFiles('Dockerfile') }} restore-keys: | ${{ runner.os }}-${{ env.IMAGE_CACHE_KEY }}-${{ matrix.ruby }}- - name: Docker load id: docker-load if: steps.cache-docker-image.outputs.cache-hit == 'true' run: docker image load -i ${IMAGE_CACHE_DIR}/image.tar - name: Docker compose build id: docker-build run: docker-compose build --build-arg BUILDKIT_INLINE_CACHE=1 base先述のイメージのビルド&キャッシュjobが完了すると、アプリのテストを行う
test-app
が起動します。docker-load
のstepでは「キャッシュ用のタグ」 =rails6api-development-cache
が付与されているイメージが展開され、docker-build
のstepでこのイメージをcache_from
によって取り込んでbase
サービスのイメージ(=rails6api-development:0.1.0
タグが付与されたイメージ)をビルドします。ufoscout/docker-compose-wait でMySQLコンテナの起動を待つ
- name: Wait middleware services id: wait-middleware run: docker-compose run --rm wait-middleware - name: Confirm docker-compose logs id: confirm-docker-compose-logs run: docker-compose logs dbDockerfileの最後でインストールしてあるufoscout/docker-compose-waitを使って
db
サービスの起動を待ちます。Starting with configuration: - Hosts to be waiting for: [db:3306] - Timeout before failure: 30 seconds - TCP connection timeout before retry: 5 seconds - Sleeping time before checking for hosts availability: 0 seconds - Sleeping time once all hosts are available: 0 seconds - Sleeping time between retries: 1 seconds -------------------------------------------------------- Checking availability of db:3306 Host db:3306 not yet available... Host db:3306 not yet available... Host db:3306 not yet available... Host db:3306 not yet available... Host db:3306 not yet available... Host db:3306 not yet available... Host db:3306 not yet available... Host db:3306 not yet available... Host db:3306 not yet available... Host db:3306 not yet available... Host db:3306 not yet available... Host db:3306 is now available! -------------------------------------------------------- docker-compose-wait - Everything's fine, the application can now start! --------------------------------------------------------以前に「GitHub ActionsのCI環境ではこの速さでは起動してくれず、
wait-middleware
の効果が大きくなります」と書きましたが、上記ログがGitHub Actionsのrunnerインスタンス上での実行例で(Host db:3306 not yet available...
でsleepを1秒挟んでいます)、ポート3306のLISTENまでに10秒以上掛かっています。このログ例のみならず、何度実行しても平均的に10秒超は掛かっていました。仮にこの所要時間でwaitするstepを挟まない(depends_on
を指定するのみ)とすると、MySQL起動前に後続のRailsアプリに関するstepが走ってしまいエラーになるでしょう。余談: MySQLコンテナ起動プロセスのどの処理が遅いのか?
db_1 | 2020-05-25 16:36:40+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.20-1debian10 started. db_1 | 2020-05-25 16:36:40+00:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql' db_1 | 2020-05-25 16:36:40+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.20-1debian10 started. db_1 | 2020-05-25 16:36:40+00:00 [Note] [Entrypoint]: Initializing database files db_1 | 2020-05-25T16:36:40.990281Z 0 [Warning] [MY-011070] [Server] 'Disabling symbolic links using --skip-symbolic-links (or equivalent) is the default. Consider not using this option as it' is deprecated and will be removed in a future release. db_1 | 2020-05-25T16:36:40.990349Z 0 [System] [MY-013169] [Server] /usr/sbin/mysqld (mysqld 8.0.20) initializing of server in progress as process 45 db_1 | 2020-05-25T16:36:40.996092Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started. db_1 | 2020-05-25T16:36:42.100882Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended. db_1 | 2020-05-25T16:36:43.312805Z 6 [Warning] [MY-010453] [Server] root@localhost is created with an empty password ! Please consider switching off the --initialize-insecure option. db_1 | 2020-05-25 16:36:46+00:00 [Note] [Entrypoint]: Database files initialized db_1 | 2020-05-25 16:36:46+00:00 [Note] [Entrypoint]: Starting temporary server db_1 | 2020-05-25T16:36:46.494377Z 0 [Warning] [MY-011070] [Server] 'Disabling symbolic links using --skip-symbolic-links (or equivalent) is the default. Consider not using this option as it' is deprecated and will be removed in a future release. db_1 | 2020-05-25T16:36:46.494485Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 8.0.20) starting as process 92 db_1 | 2020-05-25T16:36:46.507413Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started. db_1 | 2020-05-25T16:36:46.819578Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended. db_1 | 2020-05-25T16:36:46.915827Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Socket: '/var/run/mysqld/mysqlx.sock' db_1 | 2020-05-25T16:36:47.015509Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed. db_1 | 2020-05-25T16:36:47.017398Z 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory. db_1 | 2020-05-25T16:36:47.034485Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.20' socket: '/var/run/mysqld/mysqld.sock' port: 0 MySQL Community Server - GPL. db_1 | 2020-05-25 16:36:47+00:00 [Note] [Entrypoint]: Temporary server started. db_1 | Warning: Unable to load '/usr/share/zoneinfo/iso3166.tab' as time zone. Skipping it. db_1 | Warning: Unable to load '/usr/share/zoneinfo/leap-seconds.list' as time zone. Skipping it. db_1 | Warning: Unable to load '/usr/share/zoneinfo/zone.tab' as time zone. Skipping it. db_1 | Warning: Unable to load '/usr/share/zoneinfo/zone1970.tab' as time zone. Skipping it. db_1 | db_1 | 2020-05-25 16:36:49+00:00 [Note] [Entrypoint]: Stopping temporary server db_1 | 2020-05-25T16:36:49.538277Z 10 [System] [MY-013172] [Server] Received SHUTDOWN from user root. Shutting down mysqld (Version: 8.0.20). db_1 | 2020-05-25T16:36:51.341063Z 0 [System] [MY-010910] [Server] /usr/sbin/mysqld: Shutdown complete (mysqld 8.0.20) MySQL Community Server - GPL. db_1 | 2020-05-25 16:36:51+00:00 [Note] [Entrypoint]: Temporary server stopped db_1 | db_1 | 2020-05-25 16:36:51+00:00 [Note] [Entrypoint]: MySQL init process done. Ready for start up. db_1 | db_1 | 2020-05-25T16:36:51.806387Z 0 [Warning] [MY-011070] [Server] 'Disabling symbolic links using --skip-symbolic-links (or equivalent) is the default. Consider not using this option as it' is deprecated and will be removed in a future release. db_1 | 2020-05-25T16:36:51.806498Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 8.0.20) starting as process 1 db_1 | 2020-05-25T16:36:51.816008Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started. db_1 | 2020-05-25T16:36:52.191532Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended. db_1 | 2020-05-25T16:36:52.286349Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Socket: '/var/run/mysqld/mysqlx.sock' bind-address: '::' port: 33060 db_1 | 2020-05-25T16:36:52.341936Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed. db_1 | 2020-05-25T16:36:52.345030Z 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory. db_1 | 2020-05-25T16:36:52.363785Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.20' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server - GPL.上記はMySQLコンテナ(
db
サービス)起動時のログをdocker-compose logs db
で確認した際の例です。
Initializing database files
からDatabase files initialized
で6秒Starting temporary server
からTemporary server stopped
で5秒この2処理で所要時間をほぼ半分ずつ要しています。
依存gemのキャッシュ・リストアで named volume のディレクトリをそのまま使わない(使えない)
env: ARG_RUBY_VERSION: ${{ matrix.ruby }} GEMS_CACHE_DIR: /tmp/cache/bundle GEMS_CACHE_KEY: cache-gems # 略 - name: Cache bundle gems id: cache-bundle-gems uses: actions/cache@v1 with: path: ${{ env.GEMS_CACHE_DIR }} key: ${{ runner.os }}-${{ env.GEMS_CACHE_KEY }}-${{ matrix.ruby }}-${{ hashFiles('Gemfile.lock') }} restore-keys: | ${{ runner.os }}-${{ env.GEMS_CACHE_KEY }}-${{ matrix.ruby }}-依存Gemのキャッシュリストアも、公式のキャッシュ処理用actionであるactions/cacheで行います。キャッシュのキーに
${{ hashFiles('Gemfile.lock') }}
を含めているのは、Gemfile.lock(Gemfile)に変更があった際にキャッシュHITさせないようにする事を意図したものです。./docker-compose.ymlbackend: &backend # 略 volumes: - ./:/app:cached - ${GEMS_CACHE_DIR:-bundle-cache}:/bundle - rails-cache:/app/tmp/cache # 略 volumes: mysql-data: bundle-cache: rails-cache:全てをDockerで行おうとしているので、
bundle install
もコンテナ内で行います。つまりインストールされたgemをキャッシュしたければ、volume mount先からインストール結果を取り出さなければなりません。↑のdocker-compose.ymlを紹介した際に「環境変数
GEMS_CACHE_DIR
がセットされていればその内容で、セットされていなければbundle-cache
という名前のnamed volumeでマウント」と書いたのですが、このcache-bundle-gems
のstepがまさに「環境変数GEMS_CACHE_DIR
がセットされていれば」なケースに該当します。これは「named volumeではなくマウント先のパスを環境変数で明示する」のが意図です。公式の
volumes
のSHORT SYNTAXによると、パスが指定されていればそのパスが、固定文字列が指定されていればその名前のnamed volumeが、それぞれマウントされます。開発作業時はGEMS_CACHE_DIR
を明示せずデフォルトの固定文字列(= named volume =bundle-cache
)を使用しても良いですが、actions/cache で内容をキャッシュしようとした場合、そのディレクトリとしてnamed volumeの実体(具体的には/var/lib/docker/volumes/xxx
というパス)を指定するとpermission deniedエラーでキャッシュに失敗してしまいます。なのでこれを回避する為にCI時のみGEMS_CACHE_DIR
として/tmp/cache/bundle
という(permission deniedにならない)ディレクトリを明示しています。これによりCI時のbundle install結果はこのGEMS_CACHE_DIR
ディレクトリに出力され、actions/cacheでディレクトリが丸ごとキャッシュされます。この記述は正直分かりやすいとは言えないので、LONG SYNTAXを利用して分かりにくさを軽減したいところなのですが、今回は「環境変数の中身によってvolume typeが変えられる」「1つのdocker-compose.ymlを開発作業とCIで併用しやすい」というメリットを優先してSHORT SYNTAXを採用しました。
アプリのセットアップ&テストもDockerコンテナ内で実行
- name: Setup and Run test id: setup-and-run-test run: docker-compose run --rm console bash -c "bundle install && rails db:prepare && rspec"このstepは構築するアプリの仕様ややりたい事次第で変わると思いますが、一応今回の例を紹介しておくと
bundle install
(結果は先述の通りキャッシュされる) →db:prepare
でDBセットアップ(参考) → テスト(今回使ったアプリではrspec
を使用しています)、という順にテストまで実施しています。それぞれのコマンドをrunnerインスタンス上で直接実行するのではなく、docker-compose run
でconsole
サービスを立ち上げてその中で実行するようにしています。アプリのテストと並行してDockerイメージの脆弱性スキャンも実行
env: DOCKER_BUILDKIT: 1 COMPOSE_DOCKER_CLI_BUILD: 1 APP_IMAGE_TAG: rails6api-development:0.1.0 APP_IMAGE_CACHE_TAG: rails6api-development-cache IMAGE_CACHE_DIR: /tmp/cache/docker-image IMAGE_CACHE_KEY: cache-image # 略 scan-image-by-trivy: needs: image-cache-or-build strategy: matrix: ruby: ["2.7.1"] os: [ubuntu-18.04] runs-on: ${{ matrix.os }} env: ARG_RUBY_VERSION: ${{ matrix.ruby }} TRIVY_CACHE_DIR: /tmp/cache/trivy steps: - name: Check out code id: checkout uses: actions/checkout@v2 - name: Cache docker image id: cache-docker-image uses: actions/cache@v1 with: path: ${{ env.IMAGE_CACHE_DIR }} key: ${{ runner.os }}-${{ env.IMAGE_CACHE_KEY }}-${{ matrix.ruby }}-${{ hashFiles('Dockerfile') }} restore-keys: | ${{ runner.os }}-${{ env.IMAGE_CACHE_KEY }}-${{ matrix.ruby }}- - name: Docker load id: docker-load if: steps.cache-docker-image.outputs.cache-hit == 'true' run: docker image load -i ${IMAGE_CACHE_DIR}/image.tar - name: Scan image id: scan-image run: docker container run --rm -v /var/run/docker.sock:/var/run/docker.sock -v ${TRIVY_CACHE_DIR}:/root/.cache/ aquasec/trivy ${APP_IMAGE_CACHE_TAG}開発〜CIをDockerで完結させようとしているので、折角なのでCI時にDockerイメージの脆弱性スキャンも行っておきたいです。今回は aquasecurity/trivy を使わせてもらいました。Docker完結を目指しているので、trivyによるスキャンも公式に提供されているDockerで行います。
needs: image-cache-or-build
によってイメージビルド&キャッシュが完了済なので、スキャンもそのキャッシュをload・展開したイメージに対して実施して高速化します(スキャン対象として${APP_IMAGE_CACHE_TAG}
=rails6api-development-cache
を指定)。
毎回 aquasec/trivy をpullする事でスキャンそのものの仕様を常に最新化しているので、Dockerfileに変更がなくてもスキャンを実行するようにしています。開発作業のユースケース例
新規参画エンジニアの環境構築手順は?
# ビルド docker-compose build base # セットアップ docker-compose run --rm console bash -c "bundle install && rails db:prepare && rails db:seed" # 起動 docker-compose up -d serverrailsを起動したい時は?
docker-compose up -d server
テスト(rspec)を実行したい時は?
起動中のserverサービスで
docker-compose exec server rspec [SPEC_FILES]consoleサービスで
docker-compose run --rm console rspec [SPEC_FILES]マイグレーションを追加・修正・適用したい時は?
起動中のserverサービスで
# マイグレーション新規生成 docker-compose exec server rails g migration MIGRATION_NAME # : # マイグレーションファイルを適宜修正 # : # マイグレーションの適用 docker-compose exec server rails db:migrate
- 投稿日:2020-05-26T12:23:25+09:00
【初心者向けハンズオン】WindowsにDockerでPHP/MySQL/Apache環境構築(第2回:PHP動かすところまで)
概要
前回の記事はこちら:
新人もベテランも本当にエンジニアによくある、「開発環境建てるのめんどくさい問題」を解決する糸口を模索していました。そこで行き当たったWindows環境でDockerを使って開発出来るようにする、という着地に対してアプローチしていくハンズオンです。
1回1時間程度の作業時間でやるルールにして、記事は細切れに書いて行くスタイル。あと参考にさせていただいた記事はこちらです。
※https://qiita.com/hirosnow/items/cbe2a90ba1c6921fea1a目標
- PHP/MySQL/Apacheで古き良きWEBアプリケーションの開発環境を作る
- つくった開発環境を他の人に配布できるようにする
- ↑の「配布された人が簡単に開発環境を作れる手順をまとめる」ところまでがゴール
では行ってみましょう。
本日の作業内容
作業ディレクトリの作成
参考にした記事に従って、「C:\Users\あなたのPCのユーザー名\home」というディレクトリを作成しました。
※参考との相違点①:「\work」になっているところを「\home」い変えました(好み)home/
├ html/
│ └ index.php
├ mysql/
├ php/
│ └ php.ini
└ docker-compose.yml設置したファイルの記述
ちなみにここに来て大変今更ですが「Dockerってなんや」「Dockerfile?docker-compose.yml?」あたりでそもそも理解に躓いたタイミングがあり、その解消になったyoutube動画を転載させていただきます。
https://www.youtube.com/watch?v=VIzLh4BgKck記述したdocker-compose.yml
version: '3' services: php: image: php:7.2-apache volumes: - ./php/php.ini:/usr/local/etc/php/php.ini - ./html:/var/www/html ports: - 32778:80 mysql: image: mysql:5.7 volumes: - ./mysql:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: test MYSQL_USER: test MYSQL_PASSWORD: test command: --innodb-use-native-aio=0 ports: - "32780:3306" phpmyadmin: image: phpmyadmin/phpmyadmin environment: PMA_ARBITRARY: 1 PMA_HOST: mysql PMA_USER: test PMA_PASSWORD: test restart: always links: - mysql:mysql ports: - '32779:80'記述したphp.ini(ホントはxamppでもともと使ってたやつを入れたんですが、恥ずかしいのでこちらの記述で)
[Date] date.timezone = "Asia/Tokyo" [mbstring] mbstring.internal_encoding = "UTF-8" mbstring.language = "Japanese"記述したindex.php(ここは別になんでもいいと思う)
<?php phpinfo();いざ、起動
該当フォルダに移動
cd (docekr-compose.ymlのあるディレクトリ)いざ、起動
docker-compose up -d接続してみる
終わり。
コマンド一発は超絶楽ですね。
- 投稿日:2020-05-26T12:18:33+09:00
【初心者向けハンズオン】WindowsにDockerでPHP/MySQL/Apache環境構築(第1回:DockerDesktopを入れるまで)
概要
新人もベテランも本当にエンジニアによくある、「開発環境建てるのめんどくさい問題」を解決する糸口を模索していました。何しろ、自分と共通の開発環境つくってもらうのがめんどくさくて仕方がない。
で、そういった課題への解決策としては当然のように「Docker」が提唱されることになるわけです。
「でも自分の開発環境Windowsなんだよなあ」
とはいえ行けるかどうか半信半疑で調べてみたら、結構参考記事が上がっていたので「よしじゃあこれも備忘つけながらやってみるか」と思い立った次第です。同じく例によって、1回1時間程度の作業時間でやるルールにして、記事は細切れに書いて行くスタイル。長くなると読む側もダレるかなあと思ったというのもあります。
目標
- PHP/MySQL/Apacheで古き良きWEBアプリケーションの開発環境を作る
- つくった開発環境を他の人に配布できるようにする
- ↑の「配布された人が簡単に開発環境を作れる手順をまとめる」ところまでがゴール
では行ってみましょう。
作業内容
dockerインストール
こちらからインストール → https://docs.docker.com/docker-for-windows/install/
その後ダウンロードしたexeファイルを使ってインストール。
↓↓↓
↓↓↓
まさかのエラー(でなければスルー)
意気揚々とインストールしたdockerを始めようとしたらいきなりエラーで出鼻をくじかれる。なぜ。。。
調べたらこんな記事が出てきました。
https://qiita.com/toro_ponz/items/d75706a3039f00ba1205手順通りやったらできました。良かった。PCの管理者、ユーザーの利用者で躓くかもしれない。
初期セットアップ
とりあえず指示通り青いボタンを押しながら「Start」「Next Step」
Docker Hubのアカウントが必要でした。
僕はたまたまもってましたが、ここで取得しましょう。
https://hub.docker.com/
で、Sign inしておしまいです。チュートリアルとか色々出るけど最後はこの画面でした。
終わり。
なんだかんだ読みながらやるとインストールだけで1時間とか立ちますよね。
- 投稿日:2020-05-26T12:03:47+09:00
Dockerの基本をまとめてみた
基本事項
- Linuxでのみ作動
- CLI
- カーネルをホストと共有
- インフラ(OS)とアプリケーションを同梱
メリット
- 起動時間が短い
= インフラ(=コンテナ)を毎回作りなおすImmutableInfrastructureと相性がいいライフサイクル
実行中
停止
- 異常終了時
- 正常終了時
停止状態では再実行が可能
かつ、停止直前の状態を保持している破棄
明示的に破棄しないとディスクに残る
Dockerfile/Docker image/Docker containerの関係
- Dockerfile ≒ クラス定義書
- Docker image ≒ クラス定義書から自動生成されたクラス
- Docker container ≒ 上記のクラスのインスタンス
- 投稿日:2020-05-26T10:53:19+09:00
転職したらKubernetesだった件
TL;DR
Kubernetes がどのように、人間の作業を自動化しているのかを、実際に Kubernetes がやっている作業を手作業で行なう ことで学びましょう。
このQiita の内容は、CloudNative Days Tokyo 2019 における発表の、「転職したらKubernetesだった件」を書き下ろし、実際にデモが行えるように修正を加えたものになります。
この物語はフィクションであり、登場する団体名・会社名・人名等は架空のもので、実在する団体・会社・人物等とは、一切、関係がありません。
これまでのあらすじ
ある日、某Z社に転職した稲津さんに与えられた仕事は、“Kubernetes の一員”になることだった!?
某Z社は、親会社からの依頼で Kubernetes クラスタを運用しなければならなくなりましたが、 「Kubernetes のアーキテクチャが難しすぎ、運用できない!」 と判断しました。しかし、世の中はクラウドネイティブです。とある団体曰く、 クラウドネイティブイコールKubernetesです。 世の中の流れ的に全てを拒否できないだろうと会社の上層部は判断しました。
「Kubernetes で一番重要なコンポーネントはなんだ?」
「kube-apiserverです。」と、こんな会話があったとかなかったとか。
こうして、某Z社では、kube-apiserverのみが導入され、その他を手作業とする業務フローが生まれたのでした。
それでは、あなたは某Z社に転職した inajob(稲津さん) となり、Z社の社員としてZ社の業務を実際に体験してみましょう!
事前準備
以下のレポジトリを clone して、環境をセットアップしてください。実際に業務体験をするためには Vagrant と VirtualBox が必要です (Mac OS Xで動作確認しています、その他の環境で動かなかった場合の対応は PR お待ちしております )
$ git clone https://github.com/zlabjp/k8s-the-heartful-way $ cd k8s-the-heartful-way/ $ vagrant upまた、以下のコマンドで、master ノードと、inajob のワーカーノードにログインできることを確認しておいてください。
$ vagrant ssh master01 $ vagrant ssh inajob第1話 「稲津さん、"Kubernetesネイティブ"企業に転職する」
今日は稲津さんの初出社日です。
稲津「この会社でKubernetesのスキルを身につけて、CKA,CKADを取得すればこの先しばらく引く手数多だ」と、この時はまだワクワクしていました。オリエンテーションを終えた稲津さん、そこにやってきた上司の須田さんから今後業務に使用するラップトップを受け取りました。
須田「稲津さん、あなたの仕事はそのラップトップを使って Kubernetesクラスタの一員になる ことです。そのラップトップでワーカノードとして必要なセットアップを実施してください。」
稲津さんは、何を言われたのかよくわかりませんでしたが、先輩社員に言われた通りにラップトップをセットアップすることにしました。
ネットワークのセットアップ
まず、稲津さんがワーカノードとして働くためには、ノード上で起動するPod同士が疎通するためのネットワークをセットアップしなければなりません。某Z社のPodネットワークはトラディショナルなhost-gwモデルを採用しており、
/16
で切られたPodネットワークをさらに/24
でノードごとに割り当て、それぞれのPod間のアドレス解決はノード上のルーティングテーブルで行うというものでした。整理しましょう、
- Pod ネットワーク: 10.244.0.0/16
- ノードのネットワーク: 192.168.43.0/24
- inajob さんのノード
- IP アドレス: 192.168.43.111
- ノードに割り当てられたPod Networkのサブネット: 10.244.1.0/24
- yuanying さんのノード
- IP アドレス: 192.168.43.112
- ノードに割り当てられたPod Networkのサブネット: 10.244.2.0/24
こんな環境で、inajob さんのノード上に起動したPod(10.244.1.0/24)から、他のノードのPodに(例えばyuanyingノードに起動したPodは10.244.2.0/24)にネットワーク到達可能でなければなりません。
須田「ということで、とりあえず inajob ノードの Pod ネットワークと yuanying ノードの Pod ネットワークを繋いでみましょう。まずはノードにログインしてください!」
$ vagrant ssh inajob
須田「うまくできましたね!」
須田「ノードにログインすると、yuanying ノードの Pod ネットワークはどのレンジが割り当たっているかについては、以下のコマンドで知ることができますよ。」
inajob$ kubectl get nodes -o custom-columns="Name:.metadata.name, PodCIDR:.spec.podCIDR, Address:.status.addresses[?(@.type=='InternalIP')].address" | grep --color -E "^|yuanying.+$"須田「
192.168.43.112
のマシンに10.244.2.0/24
という Pod ネットワークが割り当てられていることがわかりますね。それではこのネットワークに対してルーティングを追加しましょう。」inajob$ sudo ip route add 10.244.2.0/24 via 192.168.43.112須田「ちゃんと登録されたか確認してください。」
inajob$ ip route | grep --color -E "^|^10\.244\.2\.0.+$"須田「大丈夫そうですね!引き続き他のメンバーのルーティングも追加する必要がありますが、とりあえず今日のところは、これで大丈夫です。」
ノードの登録
須田「Z社の社員の一覧は、
kubectl
で確認することができます。色々な業務をこのコマンドで行いますので覚えていきましょう。」inajob$ kubectl get node
稲津「あら、まだ僕は社員一覧にいないんですね。」
須田「ということで自分の情報を YAML で書いて登録してください。社員一覧の登録もkubectl
を使います。」
稲津「新人ラベルっていうのがあるんですね。」
須田「最近じゃnode-role.kubernetes.io
系のラベルは非推奨だけどね。」inajobcat <<EOF | kubectl apply -f - --- apiVersion: v1 kind: Node metadata: name: inajob labels: node-role.kubernetes.io/newbie: "" spec: podCIDR: 10.244.1.0/24 EOF須田「毎回、ちゃんと登録できたか確認してください。」
稲津「えーっと、きゅーべしーてぃーえる げっと のーど、っと。」inajob$ kubectl get nodes -o wide | grep --color -E "^|inajob.+$"稲津「ちゃんと登録されていました!なんか STATUS NotReady が目立ちますね。」
須田「うちは結構時間に融通きくんだよ。」
稲津「へー、いいですね。」
須田「CONTAINER-RUNTIMEやKERNEL-VERSIONなどの情報も一緒に登録よろしく。これらは現状のステータスを表すのでステータスフィールドに書いてください。」
稲津「これは kubectl じゃなくて curl を使うんですね。」
須田「そうだね、変な仕様だよまったく。」STATUS フィールドはユーザによる変更を想定していないため、
kubectl
では修正することができません。
通常は、kubelet や kube-controller-manager などのコントローラがこのフィールドを変更します。
今回の場合は、稲津さんが kubelet そのものなので、自身で変更する必要があるわけですね。須田「まず、ステータスを JSON で書きます。」
inajobSTATUS=$(cat <<EOF { "status": { "nodeInfo": { "kubeletVersion": "v1.15.0", "osImage": "Human 1.0.new", "kernelVersion": "4.15.2019-brain", "containerRuntimeVersion": "docker://18.6.3" }, "addresses": [ { "type": "InternalIP", "address": "192.168.43.111" } ] } } EOF )稲津「書きました」
須田「ノードのステータスはノードリソースのサブリソースなので、サブリソース用のエンドポイントに対してパッチリクエストを投げて更新してください。
稲津「こうですね、えいやっ!」inajob$ curl -k -X PATCH -H "Content-Type: application/strategic-merge-patch+json" \ --key ~vagrant/secrets/user.key \ --cert ~vagrant/secrets/user.crt \ --data-binary "${STATUS}" "https://192.168.43.101:6443/api/v1/nodes/inajob/status"須田「user.keyは認証用の鍵なので他の人に教えないでくださいね。それでは、、」
稲津「はいはい、確認ですね。ポチッと。(はー、kubectl楽だわー)」inajob$ kubectl get nodes -o wide | grep --color -E "^|inajob.+$"須田「よしよし、できたね。今日はこれでいいかな。」
稲津「これで晴れて私も会社の一員ですかね?」
須田「お疲れ様でした。」第2話「稲津さん、ワーカノードになる」
今日は稲津さんはじめての業務の日です。稲津さんが会社に出勤してきました。
どんな会社でも最初にやることは出勤登録することです。 “Kubernetes ネイティブ”である某Z社における出勤登録は 担当するノードのステータスを更新することです。
稲津「おはようございます、今日もよろしくお願いします!」
須田「おはよう、それでは早速ですが勤怠登録お願いします。」
稲津「はい、わかりました。勤怠はどこのシステムにログインすれば。。。」
須田「うちは、勤怠もKubernetesで行なっているんだよ。」
稲津(何を言ってるんだこの人は。)
須田「はいはい、早速ノードにログインして。フォーマットはこんな感じのJSONで。」inajob$ STATUS=$(cat <<EOF { "status": { "conditions": [ { "lastHeartbeatTime": "$(date --utc +"%Y-%m-%dT%H:%M:%SZ")", "message": "今日からよろしくお願いします。", "reason": "稲津出社", "status": "True", "type": "Ready" } ] } } EOF )稲津「これでいいですか。」
須田「それじゃあ、昨日と同じようにステータスサブリソースにパッチを投げてください。」inajob$ curl -k -X PATCH -H "Content-Type: application/strategic-merge-patch+json" \ --key ~vagrant/secrets/user.key \ --cert ~vagrant/secrets/user.crt \ --data-binary "${STATUS}" "https://192.168.43.101:6443/api/v1/nodes/inajob/status"稲津「できました、こんな感じですか?」
須田「それj」
稲津「確認ですね。」inajob$ kubectl get node | grep --color -E "^|inajob.+$"稲津「お、ステータスがREADYになりました。」
須田「上司の俺は、これで出勤を確認するから。これからじゃんじゃん仕事を振るからよろしくね。」
稲津「ウゲー(はい、よろしくお願いします!)」
Kubernetes ノードはコンテナを実行できる準備ができている、ということをクラスタに対してそれを定期的に伝えています。稲津さんも出社して準備ができたので、それをクラスタに伝えます。Kubernetes はその報告をもとに仕事を各ノードに振り分けます。
また、ノードに障害が発生して、ノードからの定期的な連絡がなくなると、Kubernetes はそのノードで問題が発生したと判断して、そのノード上で実行しているコンテナを他のノード上で代わりに実行するようにします。このようにすることで、Kubernetes 上にデプロイされるサービスは高い可用性を得ることができます。
ちなみに、最近の Kubernetes では Lease オブジェクトを利用して Node のハートビートを行っているため、微妙にこの説明は古くなっています。第3話「稲津さん、Pod を実行する」
スケジューラ担当 須田
須田「さて、俺もそろそろ自分の仕事をするかな。」
稲津「須田さんは何の仕事をしているんですか?」
須田「俺か?俺は色々な仕事を任せられているが、今日はスケジューラーだ。」
稲津「す、スケジューラー?」
須田「要は調整係だな、この会社で実行しなくちゃいけないプログラム、俺たちはPodと呼んでいるが、それを誰に実行してもらうか決める役割だ。」
稲津「へー、(簡単そうですね。)」
須田「そうだ、お前、俺の仕事を横から見ていろ。(俺もいつまでこの会社にいるかわからないからな。)後任を育てる義務がある。」
稲津「あっ。」須田「まずは管理者ノードに入るぞ。これ間違えたらできないからな。」
$ vagrant ssh master01
須田「そして、まだノードに割り当てられていないプログラム(Pod)があるかどうかを確認する。」
master01$ kubectl get pods -o custom-columns=Name:.metadata.name,Node:.spec.nodeName稲津「お、nginxという名前のPodがありますね。」
須田「そうだ、ここのNodeが<none>
になっているだろう、これはまだ、この nginx がどのノードにも割り当てられていないってことだ。」
須田「次に、出勤している社員を調べる。」master01$ kubectl get node
須田「この中でReadyになっていて実行できそうな社員にこのPodの実行を任せることにするんだ。」
master01$ kubectl describe node inajob | head -n 14須田「適当にノードを選んで、describe などを駆使して大丈夫かどうかを調べたりする。非常に高度な仕事だ。」
master01$ cat <<EOL | tee nginx-binding.yaml apiVersion: v1 kind: Binding metadata: name: nginx target: apiVersion: v1 kind: Node name: inajob EOL須田「どのPodをどのノード(社員)に割り当てるのかを決める Binding リソースをYAMLで作成する。」
master01$ curl -k -X POST -H "Content-Type: application/yaml" \ --data-binary @nginx-binding.yaml \ --key /vagrant/kubernetes/secrets/admin.key \ --cert /vagrant/kubernetes/secrets/admin.crt \ "https://192.168.43.101:6443/api/v1/namespaces/default/pods/nginx/binding"須田「そしてこのYAMLをapiserverにポスト、、、これで先ほどのBindingリソースが、nginx Pod の binding サブリソースとして登録された。」
稲津(わけわからん、これは私に割り当てられたのか…?)ここでは、須田さんは Kubernetes でいう kube-scheduler の役割を演じていました。kube-scheduler は Node に割り当てられていない Pod を逐一監視し、ノード一覧からそのPodを実行するのに最も適したノードを選択し、Podをノードに割り当てます。
割り当て自体は Pod の binding サブリソースとして表現されいます。稲津さん、仕事を割り当てられていることを知る
稲津「えーっと、それでは私の日々のルーチンは何をすればいいのでしょうか。」
須田「基本的に稲津くんの仕事はPodを実行することなので、自分にPodが割り当てられているかを確認してください。kubectl で。とりあえず自分のPCにログインしてください。」$ vagrant ssh inajob
稲津(マニュアルによるとこのコマンドか…。)
inajob$ kubectl get pod \ --field-selector 'status.phase=Pending,spec.nodeName=inajob' -A稲津「あれ、さっきの nginx Pod が表示されましたよ。」
須田「そのコマンドは自分のノードに割り当てられていて、まだ実行されていないPodが表示されるんだ。」
稲津「それでどうすれば、、」
須田「マニュアル嫁」Podのネットワークを設定しよう
pause コンテナを起動せよ
稲津「pauseコンテナを起動しろ、とありますね。pauseコンテナって何ですか?」
須田「ああ、そもそもPodとはね、」
稲津(そもそも論が始まった、これは長そうだ。)
稲津「あ、やっぱいいです。」
須田「まあ、簡単にいうとコンテナ同士のネームスペースを共有するため用のコンテナだ。まあ、今回は nginx コンテナ一つを起動するだけなので必要ないんだけどね。」
稲津(必要ないならやらなければいいのに…。)Pod はコンテナの集合です。某Z社ではコンテナランタイムにDockerを使っているわけなのですが、Dockerには「コンテナの集合」という概念がないため、その機能差を埋めるために 「pause コンテナ」というグルーを利用しています。
The Almighty Pause Container - Ian Lewisinajob$ docker run -d \ --network none \ --name k8s_POD_default-nginx \ k8s.gcr.io/pause:3.1稲津「起動しました!」
Pod に IP アドレスを割り当てよう
須田「それじゃあ、その pause コンテナに Pod のアドレスを割り当てようか。この作業は root で行う必要がある。」
稲津「すーどぅーですね。」inajob$ sudo su須田「そしてさっき作った pause コンテナのネットワークネームスペースをメモっといてくれ。」
inajob@root$ PID=$(docker inspect -f '{{ .State.Pid }}' k8s_POD_default-nginx) $ NETNS=/proc/${PID}/ns/net稲津「メモりました。」
須田「そしてCNIコマンドを実行する。CNIコマンドは環境変数と設定ファイルを標準入力を実行時のパラメータとして取るんだ。」
稲津「えーっと、環境変数を設定、っと。。何かおまじない感ありますね。」inajob@rootexport CNI_PATH=/opt/cni/bin export CNI_COMMAND=ADD export CNI_CONTAINERID=k8s_POD_default-nginx export CNI_NETNS=${NETNS} export CNI_IFNAME=eth0 export PATH=$CNI_PATH:$PATH export POD_SUBNET=$(kubectl get node inajob -o jsonpath="{.spec.podCIDR}")須田「環境変数を設定したら CNI の bridge コマンドを実行してくれ。」
inajob@root/opt/cni/bin/bridge <<EOF { "cniVersion": "0.3.1", "name": "bridge", "type": "bridge", "bridge": "cni0", "isGateway": true, "ipMasq": true, "ipam": { "type": "host-local", "ranges": [ [{"subnet": "${POD_SUBNET}"}] ], "routes": [{"dst": "0.0.0.0/0"}] } } EOF稲津「な、何かずらずらと出てきました!」
須田「.ips[0].address
の値が割り当てられたPodのIPだ、忘れると大変だからこれもメモしておいてくれ。あ、以降の作業はまた一般ユーザで行うからexit
してからな。」inajob$ exit # root 作業終了 $ POD_IP=10.244.1.2稲津「メモりました!」
POD_IP
の値は環境ごとに違うので、実際の値をメモっておいてください。須田「ちゃんと設定されたか確認しておこう。」
稲津「えーっと、この場合の確認ってどうすればいいんですかね。」
須田「ping
で良いんじゃね。」inajob$ ping ${POD_IP}稲津「応答返ってきました!」
nginx コンテナを起動しよう
須田「それでは実際の nginx コンテナを起動しようか。」
稲津「えーっと、どんな spec かをちゃんと確認してっと。。。」inajob$ kubectl get pod nginx -o json | jq '.spec.containers'稲津「
nginx:1.14
のイメージを起動すれば良いんですね。そういえば他のフィールドは…。」
須田「。。。」
稲津(時間と尺の都合で割愛かな。)
須田「ネットワークにさっきのコンテナを指定すれば、ネットワークを共有することができるんだ。」
稲津(あ、無視された。)inajob$ docker run -d \ --network container:k8s_POD_default-nginx \ --name k8s_nginx_nginx_default \ nginx:1.14稲津「起動しました!」
須田「実際に nginx が起動したか確認してみようか。」inajob$ curl http://${POD_IP}稲津「nginx のデフォルトページが返ってきました!」
Pod のステータスを更新しよう
須田「それじゃあ、ちゃんとPodが実行できたことを俺に報告してくれ。」
稲津「なになに、Podのステータスを更新する、とありますね。この会社の報告は大体ステータスの変更で行うんですね。」
須田「Kubernetesネイティブ企業だからな。」inajobSTATUS=$(cat <<EOF { "status": { "conditions": [ { "lastProbeTime": null, "lastTransitionTime": "$(date --utc +"%Y-%m-%dT%H:%M:%SZ")", "status": "True", "type": "Initialized" }, { "lastProbeTime": null, "lastTransitionTime": "$(date --utc +"%Y-%m-%dT%H:%M:%SZ")", "status": "True", "type": "Ready" }, { "lastProbeTime": null, "lastTransitionTime": "$(date --utc +"%Y-%m-%dT%H:%M:%SZ")", "status": "True", "type": "ContainersReady" } ], "containerStatuses": [ { "containerID": "human://nginx-0001", "image": "nginx:1.14", "imageID": "docker-pullable://nginx@sha256:96fb261b66270b900ea5a2c17a26abbfabe95506e73c3a3c65869a6dbe83223a", "lastState": {}, "name": "nginx", "ready": true, "restartCount": 0, "state": { "running": { "startedAt": "$(date --utc +"%Y-%m-%dT%H:%M:%SZ")" } } } ], "hostIP": "192.168.43.111", "phase": "Running", "podIP": "${POD_IP}", "startTime": "$(date --utc +"%Y-%m-%dT%H:%M:%SZ")" } } EOF )稲津「ステータスのJSON書きました。何か妙に長いですね。」
須田「本当はもっと細かく状態が変更されるたびに報告してもらうんだが、新人だから簡略化しました。」
稲津(えらくマイクロマネジメントだな。)
須田「それでは apiserver に登録してください。」inajob$ curl -k -X PATCH -H "Content-Type: application/strategic-merge-patch+json" \ --key ~vagrant/secrets/user.key \ --cert ~vagrant/secrets/user.crt \ --data-binary "${STATUS}" "https://192.168.43.101:6443/api/v1/namespaces/default/pods/nginx/status"稲津「登録しました。」
inajob$ kubectl get pods -o wide | grep --color -E "^|Running"須田「PendingからRunningになりましたね、ちゃんと登録されているようです。」
第4話「吉田さん、ReplicaSet を処理する」
ここで須田さん、稲津さんの上司である吉田さんが初登場します。吉田さんは、コントロールプレーンとしてコントローラマネージャという管理業務を行なっています。
管理業務の1つは Pod の冗長化です。一般にアプリケーションは、複数のインスタンスで構成して可用性を担保します。Kubernetes ではこれを ReplicaSet で実現します。
今日は吉田さんが ReplicaSet コントローラとして活躍するお話です。
吉田さん、処理されていないReplicaSetを見つける
吉田(ゴソゴソ)
$ vagrant ssh master01
稲津(おや、あれは確か上司の吉田さんだ。)
稲津「吉田さん、何をしているんですか。」
吉田「何って、仕事だよ仕事。色々監視してるんだ。」
稲津「何かこの会社って、基本監視で仕事が回ってますよね。」
吉田「そうだな、自分の仕事内容については自分でapierverを監視して自主的に処理するんだ。」リコンシレーションループと呼ばれる、対象オブジェクトの監視と処理は、Kubernetesの第一原則となっています。
吉田「親会社の人間は、俺たちに何かを処理させたくなると大体、ReplicaSet か Deployment と呼ばれる書式で指示して来ることが多いんだ。」
master01$ kubectl get replicasets -o wide -A | grep --color -E "^|DESIRED|CURRENT"吉田「ほら、
web
という名前のアプリを二つ処理してくれって来たぞ。」
稲津「DESIREDが2つ、になっていますね。CURRENTがゼロですが。」
吉田「そりゃまだ俺たちが処理していないからな。」
吉田「まあ、一応、本当に処理していないか調べる。このReplicaSetの指示書によると、app=web
というラベルがついたPodが二ついないとダメらしい。」master01$ kubectl get pod -l app=web稲津「ありませんね。」
吉田「よし、それじゃあ Pod を二つ作るか。どんなPodを作るかは ReplicaSet の指示書に書いてある。」master01$ kubectl get rs web -o json | jq '.spec.template'稲津「この spec フィールドのテンプレートのところですね。」
吉田「そうだ、これを使ってチョチョイとkubectl create
っと…。」master01$ kubectl get rs web -o json | \ jq -r '.spec.template | .+{"apiVersion": "v1", "kind": "Pod"} | .metadata |= .+ {"name": "web-001"}' | \ kubectl create -f - $ kubectl get rs web -o json | \ jq -r '.spec.template | .+{"apiVersion": "v1", "kind": "Pod"} | .metadata |= .+ {"name": "web-002"}' | \ kubectl create -f -稲津「jq がキモいですね。」
吉田「これで Pod が二つ作られたはずだ。」master01$ kubectl get pod -l app=web稲津「どっちもPendingのままですね、担当者に割り当てもされていないみたいです。」
吉田「おう、俺の仕事はReplicaSetからPodの指示書を作るだけだからな、あとはスケジューラの須田とkubelet担当の誰かがやってくれるさ。」
稲津(この人の業務楽そうだな、、早く偉くなりたい。)
吉田「まだ終わりじゃないぞ、一応親会社の誰かさんに処理が進んだことを報告しないといけないからな。」
稲津「ステータスを更新するんですね。」
吉田「そうだ、よくわかっているじゃないか。須田もちゃんと教育しているようだな。」master01$ STATUS=$(cat <<EOF { "status": { "availableReplicas": 0, "fullyLabeledReplicas": 0, "observedGeneration": 1, "readyReplicas": 0, "replicas": 2 } } EOF )吉田「とりあえず、まだ何も動かないPodを二つ作ったので
replicas
に 2 を指定してっと。」master01$ curl -k -X PATCH -H "Content-Type: application/strategic-merge-patch+json" \ --key /vagrant/kubernetes/secrets/admin.key \ --cert /vagrant/kubernetes/secrets/admin.crt \ --data-binary "${STATUS}" "https://192.168.43.101:6443/apis/apps/v1/namespaces/default/replicasets/web/status"吉田「ReplicaSetのステータスサブリソースを更新しておく。」
master01$ kubectl get replicasets -o wide | grep --color -E "^|DESIRED|CURRENT"稲津「READYがまだゼロだけど、CURRENTが2になりました。」
吉田「うむ、これで俺の仕事は終わり。基本は自分の担当オブジェクトをkubectl watch
してればいい。」須田さんによる、いつものスケジューリング業務
$ vagrant ssh master01
須田「む、Podが更新されたな、俺の仕事の出番か。」
稲津「お疲れ様です、どうしたんですか?」master01$ kubectl get pods -o custom-columns=Name:.metadata.name,Node:.spec.nodeName須田「見ろ、ノードが割り当てられてないPodが二つ増えた。スケジューラとしての俺の出番だ。」
稲津「誰かに仕事を割り振るんですね。(自分に割り当てませんように)」
須田「そうだな、最初の1個目はリモート勤務の大塚(yuanying)に任せるか。」
稲津「この会社はリモート勤務もOKなんですね。」
須田「最近の流行りだな。いつも通りに指示書を作るぞ。」master01$ cat <<EOL | tee web-yuanying-binding.yaml apiVersion: v1 kind: Binding metadata: name: web-001 target: apiVersion: v1 kind: Node name: yuanying EOL須田「そしてapiserverに登録する。」
master01$ curl -k -X POST -H "Content-Type: application/yaml" \ --data-binary @web-yuanying-binding.yaml \ --key /vagrant/kubernetes/secrets/admin.key \ --cert /vagrant/kubernetes/secrets/admin.crt \ "https://192.168.43.101:6443/api/v1/namespaces/default/pods/web-001/binding"須田「リモートでもちゃんと仕事してるか確認するぞ。」
master01$ kubectl get pod -o wide稲津「わ、もう
Running
になってますよ!仕事早いですねー。」
須田「大塚はワーカーノード歴が長いからな、このくらいやってもらわないと困る。」
須田「まあ、念の為ちゃんと起動してるか確認するか。」master01$ curl http://$(kubectl get pod web-001 -o jsonpath='{.status.podIP}'):8080稲津「ちゃんと動いてますね。」
須田「それじゃあもう一個は稲津、お前に振るぞ。」master01$ cat <<EOL | tee web-inajob-binding.yaml apiVersion: v1 kind: Binding metadata: name: web-002 target: apiVersion: v1 kind: Node name: inajob EOL $ curl -k -X POST -H "Content-Type: application/yaml" \ --data-binary @web-inajob-binding.yaml \ --key /vagrant/kubernetes/secrets/admin.key \ --cert /vagrant/kubernetes/secrets/admin.crt \ "https://192.168.43.101:6443/api/v1/namespaces/default/pods/web-002/binding"稲津(他の人に振ってくれてもいいのに)
稲津さん、またまたPodを起動する
稲津「えーっと、ノードにログインしてっと。」
$ vagrant ssh inajob
稲津「さて、何の仕事が私に割り当たっていたんだっけかな。確認しよう。」
inajob$ kubectl get pod \ --field-selector 'status.phase=Pending,spec.nodeName=inajob' -A稲津「あったあった。人使い荒いなあ、この会社は。」
稲津「といっても優秀な稲津くんなので、すでにこの作業はスクリプトにしているのです。」
須田(何をブツブツ言ってるんだ。)inajob$ sudo bash /vagrant/scripts/create-pod.sh web-002稲津(できたできた、もう少し仕事してるフリしてから報告しよう。)
master01$ kubectl get pod -A須田「お、稲津くん、キミも仕事が早いじゃないか。」
稲津(しまった、ステータスの更新もスクリプト化してしまっていた。)吉田さん、ReplicaSet 業務の報告をする
吉田「さてと、そろそろ俺が指示した Pod の作成も終わっている頃かな。親会社への作業完了の報告はコントローラマネージャとしての大事な仕事だからな、ちゃんと監視を怠らないようにしないと。」
$ vagrant ssh master01
吉田「コントロールプレーンノードにログインしてっと、Podを確認するか。」
master01$ kubectl get pod -l app=web -o wide | \ grep --color -E "^|Running"吉田「お、ちゃんと二つのPodがReadyになっているな。それではReplicaSetのステータスを更新するか。」
master01$ STATUS=$(cat <<EOF { "status": { "availableReplicas": 2, "fullyLabeledReplicas": 2, "observedGeneration": 1, "readyReplicas": 2, "replicas": 2 } } EOF )吉田「
readyReplicas
を2に更新してっと。」master01$ curl -k -X PATCH -H "Content-Type: application/strategic-merge-patch+json" \ --key /vagrant/kubernetes/secrets/admin.key \ --cert /vagrant/kubernetes/secrets/admin.crt \ --data-binary "${STATUS}" "https://192.168.43.101:6443/apis/apps/v1/namespaces/default/replicasets/web/status"吉田「apiserver に登録っと。」
master01$ kubectl get rs | grep --color -E "^|DESIRED|CURRENT|READY"吉田「Desired/Current/Ready が一致した。完璧だ。」
稲津(この会社、独り言してる人が多いな。)Kubernetes コントローラは、API オブジェクトに定義された「望ましい状態」とクラスタの「現在の状態」を一致させるように「継続的に」動作します。これを調整ループ(reconciliation Loop)と呼びます。
第5話「稲津さん、Service を処理する」
ReplicaSet を使って複数の Pod レプリカを作成できましたが、 Pod にアクセスしたいときにそれぞれの IP に直接アクセスするのは不便です。
代表的なアドレスがあっていい感じに ロードバランスして欲しいですよね。
Kubernetes では通常、そのような場合 Service を使います。サービスは ClusterIP と呼ばれる仮想 IP とポートを使って、そこへのアクセスをバックエンドの Pod に分散します。バックエンドの Pod はラベルセレクタで選択されます。"Service" という名前はサービスの入り口となる機能となることから来ています。
この Service のロードバランサとしての機能の実現に責任を持つコンポーネントがkube-proxy
です。激務な(コントローラー)マネージャー吉田さん、Endpointを処理する
吉田「さてと、そろそろまた新しい指示が来てそうな時間だな、確認するか。」
$ vagrant ssh master01
吉田「うわ、やっぱり。さっきReplicaSetを処理したから来てると思ったんだ。」
稲津「どうしたんですか、吉田さん。」
吉田「お、稲津か。これを見てみろ。」
稲津「どれどれ?」master01$ kubectl get service -o wide稲津「web-service っていう名前の Service がありますね。何ですかこれ?」
吉田「CLUSTER-IP
が10.254.10.128
でSELECTOR
がapp=web
とあるだろ、要するにこれはさっき二つ作ったPodをいい感じにロードバランスしてくれる VIP10.254.10.128
が欲しい、っていう指示書だ。」
稲津「へ、じゃあ吉田さんはこれからその、ロードバランサの設定をするんですか?
吉田「いや、俺はこの指示をもう少し細かくブレークダウンするだけで、実際の作業は kube-proxy 担当だ。」
稲津「へー、そうなんですね。」
吉田「ひとごとのようだが、kube-proxy はノード担当の仕事だぞ。」
稲津「!?(やっぱりこの人の仕事は楽なんじゃない?!)」master01$ kubectl get pod -l app=web -o custom-columns=NAME:.metadata.name,IP:.status.podIP吉田「まず、やることは、さっきのラベルを持っているPodのIPアドレス一覧を取得する。」
master01$ cat <<EOF | kubectl apply -f - apiVersion: v1 kind: Endpoints metadata: name: web-service subsets: - addresses: - ip: $(kubectl get pod web-001 -o jsonpath='{.status.podIP}') - ip: $(kubectl get pod web-002 -o jsonpath='{.status.podIP}') ports: - port: 8080 protocol: TCP EOF吉田「そしてそのIPアドレスからEndpointsオブジェクトを作ってやる。あとはこのEndpointsオブジェクトを見てノード担当が実際の設定をするんだ。」
稲津さん、kube-proxy の初期設定をする
須田「そういえば稲津、お前のノードにロードバランサの初期設定をするのを忘れていた。」
稲津「なんかめんどくさそうですね。」
須田「そうだな、俺もいまだによくわからん。とりあえず一個ずつ設定していくぞ。ノードにログインして!」$ vagrant ssh inajob
稲津「はい、ログインしました。」
須田「ついでに sudo が面倒なので root になっておいて。」inajob$ sudo su稲津「はい、root になりました!」
さて、稲津くんはこれから kube-proxy の設定を行なっていきます。一つ目は Dummy interface の作成。二つ目は、外部からサービスのリクエストを受けたときのためのiptables のマスカレードの設定及びカーネルパラメータの設定です。これらがないと IPVS のリクエストを外部から受けたとき、パケットがどこかに吸い込まれていってしまいます。須田「うちは kube-proxy の処理を行うのに、IPVS を使っている。なのでまずは VIP を割り当てる dummy interface を作成する。」
稲津「こうですね!」inajob@root$ ip link add kube-ipvs0 type dummy須田「次に、iptablesの設定をしていくぞ。」
稲津「iptables...」
須田「iptables を触りたくないから IPVS にしたのだが、結局 iptables は必要なのだそうだ。」inajob@root$ ipset create KUBE-CLUSTER-IP hash:ip,port稲津「まずは ipset というのを作るんですね?」
須田「そうだ。これを作ることで iptables のルールを線形探索しなくてすむようになる。」須田「そして、その ipset を使う iptablesのルールを作っていくぞ。」
inajob@rootiptables -t nat -N KUBE-MARK-MASQ iptables -t nat -A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000 iptables -t nat -N KUBE-POSTROUTING # kubernetes service traffic requiring SNAT iptables -t nat -A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x4000/0x4000 -j MASQUERADE iptables -t nat -N KUBE-SERVICES # Kubernetes service cluster ip + port for masquerade purpose iptables -t nat -A KUBE-SERVICES ! -s 10.244.0.0/16 -m comment --comment "Kubernetes service cluster ip + port for masquerade purpose" -m set --match-set KUBE-CLUSTER-IP dst,dst -j KUBE-MARK-MASQ iptables -t nat -A KUBE-SERVICES -m set --match-set KUBE-CLUSTER-IP dst,dst -j ACCEPT # kubernetes service portals iptables -t nat -I PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES iptables -t nat -I OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES # kubernetes postrouting rules iptables -t nat -I POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING iptables -t filter -N KUBE-FORWARD # kubernetes forwarding rules iptables -t filter -A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT # kubernetes forwarding conntrack pod source rule iptables -t filter -A KUBE-FORWARD -s 10.244.0.0/16 -m comment --comment "kubernetes forwarding conntrack pod source rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT # kubernetes forwarding conntrack pod destination rule iptables -t filter -A KUBE-FORWARD -d 10.244.0.0/16 -m comment --comment "kubernetes forwarding conntrack pod destination rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT # kubernetes forwarding rules iptables -t filter -I FORWARD -m comment --comment "kubernetes forwarding rules" -j KUBE-FORWARD稲津「何だかたくさんありますね…。」
須田「これでも昔よりは楽になったんだ。少なくともこれ以上ルールが増えることがないからな。」Kubernetes の kube-proxy/ipvs パッケージ に、どのような場合に IPVS モードの kube-proxy が iptables にフォールバックするのかが書いてあります。
ちなみに、今回設定した iptables のルールは、ClusterIP に関するルールのみで、type: NodePort
やtype: Loadbalancer
のルールは省略されています。須田「最後に、カーネルパラメータの設定だ。」
須田「まずは、カーネルモジュールをロードして。」inajob@rootmodprobe -- ip_vs modprobe -- ip_vs_rr modprobe -- ip_vs_wrr modprobe -- ip_vs_sh modprobe -- nf_conntrack_ipv4須田「そしてカーネルパラメータを設定する。」
inajob@root$ echo 1 | sudo tee /proc/sys/net/ipv4/vs/conntrack稲津「これは一体何をしてるんですか…。」
須田「よく聞く気になったな、えらいえらい。」
須田「詳しくは 「IPVS (LVS/NAT) とiptables DNAT targetの共存について調べた に書いてあるから読むといいぞ。」
稲津「なるほど、IPVS により conntrack の情報が消されてしまってマスカレードができなくなってしまう問題を解決するための設定なんですね!」
須田(なんだ、いきなり賢くなったぞ。)稲津さん、IPVS の設定をする
須田「それじゃあ、まずは設定するVIPの値を調べよう、
kubectl get svc
して。」inajob$ kubectl get svc
稲津「はい、しました。どうやらVIPのアドレスは
10.254.10.128
のようですね。」
須田「そのアドレスを dummy interface に設定する。」inajob$ sudo ip addr add 10.254.10.128 dev kube-ipvs0須田「さらに、さっき作った ipset にも登録する。」
inajob$ sudo ipset add KUBE-CLUSTER-IP 10.254.10.128,tcp:80須田「そして、さらにさらに、ipvs の Virtual Server そのものを作成する。」
inajob$ sudo ipvsadm -A -t 10.254.10.128:80 -s rr稲津「何か、、いろんなところに同じ情報が散らばっていて気持ち悪いですね、、自動化したい気分…。」
kube-proxy
はこれらの作業を自動化しています。須田「何か言ったか?それじゃあ ipvs の Real Server を設定するぞ、endpoint を取得して!」
inajob$ kubectl get ep web-service -o yaml稲津「
.subsets[0].addresses
の値ですね。」
須田「その値で Real Server を二つ作る。」inajob$ sudo ipvsadm -a -t 10.254.10.128:80 -r $(kubectl get ep web-service -o jsonpath='{.subsets[0].addresses[0].ip}'):8080 -m $ sudo ipvsadm -a -t 10.254.10.128:80 -r $(kubectl get ep web-service -o jsonpath='{.subsets[0].addresses[1].ip}'):8080 -m稲津「設定しました!」
須田「それじゃあ確認して!」
稲津(何か投げやりになってきたな、面倒になってきたのか?)inajob$ sudo ipvsadm -Ln稲津「設定できているみたいです。」
須田「実際にcurl
してみようか。」inajob$ curl http://10.254.10.128 $ curl http://10.254.10.128稲津「ちゃんとラウンドロビンされているようです!」
須田「よくやった、、もうお前は立派なくべれっとだ。」
稲津「何かスタッフロールが流れてきました。」その後、須田さんは別会社へと旅立ち、稲津さんがスケジューラに昇進したとかしないとか。しかし、その後も某Z社はKubernetesネイティブ企業として長く繁栄したとのことです。
まとめ
Kubernetes はコントロールプレーンと呼ばれるマスタコンポーネントとノードコンポーネントで構成されています。今回いくつかの Kubernetes のコンポーネントの代わりに手作業で一通りの Kubernetes での作業を実施することで、そのコンポーネントの責任と何をやっているのかをあきらかにしました。
Kubernetes はブラックボックスでよくわからないものではなく、もちろん人間が実施できる作業です。Kubernetes がなにをやっているかを把握しておくことはなにか問題があったときの解決に役立つでしょう。
それでは、良い Kubernetes ライフを!
キャスト kubectl kubectl kube-apiserver kube-apiserver etcd etcd kube-scheduler 須田一輝 kube-controller-manager 吉田龍馬 kubelet1 稲津和磨(あなた) kubelet2 kubelet kube-proxy1 稲津和磨(あなた) kube-proxy2 kube-proxy container-runtime Docker スライド https://speakerdeck.com/superbrothers/ 動画 https://www.youtube.com/watch?v=_Bve-nkBr2E GitHub https://github.com/zlabjp/k8s-the-heartful-wayおわり
- 投稿日:2020-05-26T09:22:14+09:00
dockerの覚書
Dockerとは
dockerとは自分のパソコンのカーネルを使って、あたかも自分のパソコンにもう一つ別のパソコンがあるようにするものと認識しています。
もう一つ別のパソコンがあるようにしている技術のことをコンテナと呼ぶと認識しています。
これの何が良いかというと、共同開発していく上で、バージョンの違いや、各々のPCの環境に左右されないことです。Dockerをインストールする
公式ページからインストールします
https://www.docker.com/
ダウンロードするとdocker-composeも一緒インストールされます。docker-composeとは
dockerを使って複数のコンテナを作ることが可能です。この複数のコンテナをいちいち、設定を書いたりするのは面倒なので、一括で管理できるものがdocker-composeです
参考
https://knowledge.sakura.ad.jp/16862/composeをインストールするときに使うコマンド
# curl -L https://github.com/docker/compose/releases/download/1.21.2/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose # chmod +x /usr/local/bin/docker-compose
- 投稿日:2020-05-26T00:47:51+09:00
ほぼゼロから始めたdockerのお勉強の旅メモ
前段
dockerって超便利で今どき知らないとか自称ITエンジニアとしてもどうなの?という世界線で、dockerをよくわからずなんとなく使っていたが、ECSとかFargateとかついにDockerから逃れられなくなった青年が、広大なネットを一日さまよって得たDockerの知識をここに記す。
※つまり、ハチャメチャなことを書いている可能性があるということだ。気をつけてね。
とりあえずこれをやればいいと思った件
https://jdlm.info/articles/2019/09/06/lessons-building-node-app-docker.html
これすごく良かったような気がします。
いろんなサイト巡ってこれにたどり着いた。
Google翻訳でもたぶん読めるから英語だけど頑張れ!その上で、今日の気ずきをまとめていく
COPYとVolumeについて
今日の朝はVolumeでソースコードマウントして作ったイメージをFargateでデプロイしたら503エラーがでた〜とか騒いでいたんですがこれは当たりまえだのクラッカーなお話でした。
基本的にDocker開発をするときはソースコードを何回も何回も何回も何回も変更するので
docker run -v /host/path:/container/path hoge
みたいな感じでホストのディレクトリをコンテナのディレクトリにマウントして開発をします。
こうしないと、コンテナの中でプログラムを書くか、ホストでソースコードを書き換えるたびにdocker buildしなくてはいけなくなるからです。だかしかし、つまりこれはコンテナのなかにソースコードは入っていないということなのです。
当然イメージにソースコードは含まれていません。そんなイメージをFargateで展開したってなんも出てこないのは当たり前です。
ということでどこかしらで公開するということになったら、これまでVolumeでマウントしていたファイルたちをイメージの中に内包することが必要になってきます。
その時に使うのがCOPYです。
COPY . /app/src/みたいにDockerfileに書いて上げればイメージにソースコードが内包されます。
開発環境はVolumeでマウントすればいいのですが、デプロイするってなったらCOPYしなくちゃいけないよ。
とここで、僕は開発と本番でDockerfile2つ書かなくちゃいけないのかよめんどくさーって思いました。
次に繋がります。検証と本番でかき分けるマルチステージドサービス
検証と本番のDockerfile分けるのめんどくせーって思っていたし、どうやら2016年とかは分けるしかなかったみたいなんだけど、今は「マルチステージドサービス」というものがあって、分けなくていいんだとさ。
FROM node:10.16.3 AS development 略 FROM node:10.16.3-slim AS production 略この
FROM ・ AS hoge
ってのがポイント。
このhoge
ってところを指定することで開発用とか本番用とか分けられるんだって。指定は
target
でやればいいらしいよ。docker-compose.yamlversion: '3.7' services: chat: build: context: . target: development command: npx nodemon index.js ports: - '3000:3000' volumes: - .:/srv/chat - chat_node_modules:/srv/chat/node_modules上にtargetって書いてありますが、そこをASの後ろのワードにすればOK。
で多分これの本当の良さはここではないんだが、ぶっちゃけまだ良くわかってないのでなんとなく書くんだけれど
COPY --from=development --chown=root:root /srv/chat/node_modules ./node_modulesこういうことができるのですよ。
つまり、別のステージのビルド結果をコピーできるってことですね。
Dockerガリガリつかってないからなんとも言えないので詳しくはこのあたりを読むといいと思う。
https://qiita.com/minamijoyo/items/711704e85b45ff5d6405以上
まとめるとたいしたことないな。
- 投稿日:2020-05-26T00:45:42+09:00
Raspberry Piで、自分のBlynkサーバーを立ててWidgetをタダで使おう
Blynkはサーバーも要らなくていいね。
Blynkは、インターネット上にあるBlynk Serverを介して、デバイスと情報のやりとりをします(下の図)。スマホとIOTデバイスだけで簡単にリモート操作ができるので大変すばらいい。
ローカルサーバーを持つ利点は?
今回、紹介するのは、そのBlynk serverをお家のRaspberry Piに立てることです。うん?サーバーなしですぐできるのが長所なのになぜだ?と思うかも知りません。
- 応答が速い (お家の無線LANだけでやりとりするので)
- BlynkのWidgetが使い放題(タダです)
下記の図のようにBlynk mobileアプリにジョイスティックやボタンなどのWidgetをつけるのにお金がかかります。(Energyというポイントが必要で、最初に1000pointsまで無料です)自分のサーバーなら、それを自由に設定できるので、開発の際に自由度がぐーんと増します。そして、お家のサーバーなので、ネットを介すよりタイムラグがすくなく、例えば、リモコンでミニカーを操作する場合、操作性が断然違います。
今回使ったもの
- Raspberry Pi 3 Model B+
手順
- Raspberry piにDockerをインストール
- Blynk ServerカスタマイズしてImage作成
- 自動起動設定と起動
- Mobile AppのBlynkでユーザー作成
1. Raspberry piにDockerをインストール
下記の先人の知恵を借りました。
ただ、素早くやってみたいだけの方はこちらのコードでどうぞ
// 現在の Raspberry pi内の環境 $ uname -a Linux raspberrypi 4.14.98-v7+ #1200 SMP Tue Feb 12 20:27:48 GMT 2019 armv7l GNU/Linux $ lsb_release -a No LSB modules are available. Distributor ID: Raspbian Description: Raspbian GNU/Linux 9.8 (stretch) Release: 9.8 Codename: stretchRaspberry Piに Docker インストール
# Install $ curl -fsSL https://get.docker.com -o get-docker.sh $ sudo sh get-docker.sh $ sudo systemctl enable docker $ docker --version # Docker version 19.03.9, build 9d98839 # Raspbianの標準ユーザのpiにDockerの実行権限を付与します。 $ sudo usermod -aG docker piインストールと起動確認
インストールしたら、常にdockerが動くようになっています。# 確認 $ systemctl status docker.service ● docker.service - Docker Application Container Engine Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled) Active: active (running) since Sat 2020-05-23 21:56:03 JST; 1 day 14h ago $ ps aux | grep docker root 416 0.1 6.5 967836 58464 ? Ssl May23 2:48 /usr/bin/dockerd -H fd:// -- .... root 857 0.0 0.3 866456 3152 ? Sl May23 0:34 /usr/bin/docker-proxy -proto tcp -host-ip .... root 870 0.0 0.3 867224 3396 ? Sl May23 0:00 /usr/bin/docker-proxy -proto tcp -host-ip .... root 879 0.0 0.6 799176 5672 ? Sl May23 0:25 containerd-shim -namespace moby -workdir .... pi 12761 0.0 0.0 4340 564 pts/0 S+ 12:49 0:00 grep --color=auto docker2. Blynk ServerカスタマイズしてImage作成
blynkの公式Github repositoryにdockerを使った起動方法がありますが、そこの説明のままに実行したら毎回データーが初期化されるなど不便なところがあります。そこで、そのイメージを少しカスタマイズしたものを用意したので、このrepositoryからクローンして、Raspberry piで、実行してください。
ここでは、まず、自分のローカル# イメージビルドソースをクローン # ビルドしたイメージの中でblynk $ git clone https://github.com/yusonkim/localblynkserver.git $ cd localblynkserver # サーバー設定ファイルを開く(各自環境に合わせてeditorを使ってください) $ vim server.properties
initial.energy
は、新しいアカウントを作成した時に使えるWidget購入pointです。そのままでも十分ですが、ガンガン使うならもっと高くしても良いでしょう。
admin.email
とadmin.pass
は、ウェブからの管理画面にアクセスする時に使うものです。この管理画面のURLは、https://<Rasberry piのhostname>.local:9443/admin
で、お家のネットワーク内で、Macなど他のPCから接続できます。暗証番号等を新たに変更してください。server.properties...中略 #initial amount of energy initial.energy=100000 ...中略 admin.email=admin@blynk.cc admin.pass=adminそのあとは、Raspberry pi内でカスタムしたイメージを作成して、Rapberry piが再起動しても常に立ち上がるように設定するだけです。
# 設定ファイルを取り込んだカスタムBlynk Server Imageを作成 $ ./build.sh3. 自動起動設定と起動
下記を参照しました。
systemctlを使ってRaspberry Piが起動すると自動的にdocker runコマンドで起動するように設定しています。その時に、永続化するボリュームを指定してユーザーデーターがなくならないようにしています。実際には、下記のスクリプトを実行するだけでよいです。
# Blynk Serverが自動的に起動するように設定 $ ./deploy.sh # 再起動 $ sudo reboot # 再起動後確認 # 起動までに約1分ほどかかります。 $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 646b4dbdd298 rasbian-blynk:customlocal "entrypoint.sh" About a minute ago Up About a minute 0.0.0.0:8080->8080/tcp, 0.0.0.0:9443->9443/tcp, 8440/tcp blynk4. Mobile AppのBlynkでユーザー作成
アプリがなければ、AppStoreなどからダウンロードしてください。
もしログインしていれば、一度ログアウトして、新しくアカウントを作ります。
ローカルサーバーへの接続は、新しいアカウントを作る時に指定します。
Create New Account を選択して、EmailやPasswordを入力します。
そのあと、下にあるサーバー選択アイコンを選択します。
BlynkからCustomに設定を変更してから、住所にRaspberry piのIP、もしくは .localをいれ、ポート番号を9443に指定します。
プロジェクトを作って見ると、Energyが増えていることが確認できます。その他
管理画面から、token取得
IOTデバイスで動くプログラムを組むときは、PCだと思いますが、その時に、各プロジェクトごとにデバイスのtokenをいれなければなりません。結構長いので、モバイルのBlynkアプリからコピーするのは大変だと思いますが、先ほど出ていた管理画面に接続すれば、簡単に参照できます。
https://<RaspberryPiのIP>:9443/admin
に接続します。するとこんな画面が、でます。今回ssl証明書の設定をしてないので、怒られています。Advencedを押して、Proceed to ...を押して管理画面に入ることができます。sslの設定方法はblynkの公式Github repositoryにありますので、ソース上のserver.propertiesファイルも変更すれば、ご自身で設定できると思います。
設定したadmin情報でログインして見ると、
User欄からモバイルで登録したアカウントが確認できます。
そして、そのアカウントをクリックすれば、プロジェクトやデバイスなどの情報と、デバイスの情報欄にtokenが表示されていることがわかりますので、ここからコピーすれば良いでしょう。
サーバー設置方法検討
Blynk社でローカルサーバー用のBlynk Server公開しているので、それを使えば簡単にローカルサーバーが立てられます。
が、今回はDockerを使ってサーバーを立てます。その理由は、
- どんな環境(ハードウェアに関係なし)でも失敗せずに立ち上げられます。
- パッケージやプログラム言語をインストールする必要がない
- 起動・停止・削除が簡単で、綺麗に行える
Dockerを使うときは、ボリュームを適切にマウントして、ユーザーデーターを保持することに注意が必要です。そうしなければ、サーバーを起動する度に初期化状態になります。簡単にできますので、心配は要りません。
docker-composeやkubernetes(k8s)を使うのもいいですが、今回は必要性をあまり感じませんでした。postgress dbを使って、データー(IOTデバイスからの生データー)を保存するとなるとdocker-composeやk8sもいいオプションですが、今回は、小さな容量で動くRaspberryPiなので、dbは必要ありません。ユーザーデーターもファイルで保存する方法でやっていますので、Blynk server単体で十分です。そして、サーバーの台数を増やしたりするAutoscalingも必要ありませんので、k8sもスキップしました。
- 投稿日:2020-05-26T00:32:27+09:00
Deno+MySQL+DockerでAPIサーバーをさくっと作る
Deno
TypeScriptを標準サポートしているDenoのv1.0がリリースされたということで興味があったので触ってみました。
あと恐竜がかわいい。作るもの
deno + MySQL + docker-composeでローカル環境にシンプルなAPIサーバーです。
適当にどう森の村民を登録して編集するようなやつとします。全体がみたい方はこちら
https://github.com/rydein/deno_first_api
インストールとか下準備
公式に書いてある通りですが以下のコマンドを流すだけです
$ brew install denoエディタはVSCodeを使いますがdenoのimport記法に対応させるためこちらのプラグインを追加しましょう。
Docker + docker-compose
deno用のDockerfileとdocker-compose.ymlはこちらを使います
DockerfileFROM hayd/ubuntu-deno:1.0.0 EXPOSE 3000 WORKDIR /app USER deno ADD . . RUN deno cache index.ts CMD ["run", "--allow-net", "index.ts"]docker-compose.ymlversion: '3' services: deno: build: . ports: - "3000:3000" db: image: mysql:8 ports: - "3306:3306" environment: - MYSQL_ROOT_PASSWORD=Passw0rd - MYSQL_DATABASE=deno-dev volumes: - db-volume:/var/lib/mysql - ./mysql/conf:/etc/mysql/conf.d volumes: db-volume:ローカル環境立ち上げ
とりあえずMySQLをたちあげておきたいので動く環境だけ用意します
index.tsimport { Application, Router } from "https://deno.land/x/denotrain@v0.5.0/mod.ts"; const app = new Application(); const router = new Router(); app.get("/", (ctx) => { return {"hello": "world"}; }); await app.run();立ち上げます
$ docker-compose build $ docker-compose up -dDB migration
まずはDBのテーブル定義を作成していきます。
migrationにはこちらのツールを使います。init
# nessie.config.ts を作成してdb接続情報を追記する $ deno run --allow-net --allow-read --allow-write https://deno.land/x/nessie/cli.ts initMySQL以外のデフォルトコードが生成されますが今回は不要なので消してしまいましょう。
configMySql
にDB接続情報を追記します。nessie.config.tsconst configMySql = { migrationFolder: `./migrations`, connection: { hostname: "localhost", // hostからDockerのMySQLコンテナに繋ぐ port: 3306, username: "root", password: "Passw0rd", db: "deno-dev", }, dialect: "mysql", }; export default configMySql;接続情報が作成できたのでmigrationファイルを作成します。
create_villagers
の部分は作成するテーブルによって適宜読み替えてください。# 村民のmigration $ deno run --allow-net --allow-read --allow-write https://deno.land/x/nessie/cli.ts make create_villagers
migration/
ディレクトリにmigrationファイルが生成されるので必要な情報を追記していきます。
村民には名前、性別、性格、誕生日があるので追加します。migrations/1590336600981-create_villagers.tsimport { Schema } from "https://deno.land/x/nessie/mod.ts"; // migration ファイルにテーブル情報を追加する export const up = (schema: Schema): void => { schema.create("villagers", (table) => { table.id(); table.string("name", 100).nullable(); // 名前 table.string("gender", 100).nullable(); // 性別 table.string("personality", 100).nullable(); // 性格 table.string("birthday", 100).nullable(); // 誕生日 }); }; export const down = (schema: Schema): void => { schema.drop("villagers"); };migrationを実行しましょう。
#migration の実行 $ deno run --allow-net --allow-read https://deno.land/x/nessie/cli.ts migrateQuery Builder
DBからデータを参照したり登録したりするにはこちらのクエリビルダーを使います。
場所はどこでも良いですが
models/villagers.ts
にDBに登録、取得する処理を作成します。models/villagers.tsimport Dex from "https://deno.land/x/dex/mod.ts"; import client from "./config.ts"; let dex = Dex({client: "mysql"}); let table = "villagers"; interface Villager { id?: number, name: string, gender: string, personality: string, birthday: string } /// /// 新しい村民を追加して追加したデータを返す /// function addVillager(villager: Villager) { const insertQuery = dex.queryBuilder().insert([villager]).into(table).toString(); return client.execute(insertQuery).then((result: any) => { const getQuery = dex.queryBuilder().select().from(table).where({id: result.lastInsertId}).toString(); return client.execute(getQuery).then((result: any) => result.rows ? result.rows[0] : {}); }) } /// /// 全ての村民を返す /// function getAllVillagers() { const getQuery = dex.queryBuilder().select("*").from(table).toString(); return client.execute(getQuery); } /// /// 村民の更新を行う、更新されたデータを返す /// function editVillager(id: number, villager: Villager) { const editQuery = dex.queryBuilder().from(table).update(villager).where({id}).toString(); return client.execute(editQuery).then(() => { const getQuery = dex.queryBuilder.select().from(table).where({id}).toString(); return client.execute(getQuery).then((result: any) => result.rows ? result.rows[0] : {}); }); } /// /// 村民の削除 /// function deleteVillager(id: number) { const deleteQuery = dex.queryBuilder().from(table).delete().where({id}).toString(); return client.execute(deleteQuery) } export { addVillager, getAllVillagers, editVillager, deleteVillager }Http Server
controllers/villagers.ts
にコントローラーを作成し、APIのルーティングを作成してリクエストを処理していきます。controllers/villagers.tsimport { Router } from "https://deno.land/x/denotrain@v0.4.4/mod.ts"; import { addVillager, getAllVillagers, editVillager, deleteVillager } from "../models/villagers.ts"; const api = new Router(); api.get("/", (ctx) => { return getAllVillagers().then((result: any) => { return result.rows; }) }) api.post("/", (ctx) => { const body = { name: ctx.req.body.make, gender: ctx.req.body.gender, personality: ctx.req.body.personality, birthday: ctx.req.body.birthday, } return addVillager(body).then((villager: any) => { ctx.res.setStatus(201); return villager; }) }) api.patch("/:id", (ctx) => { const body = { name: ctx.req.body.make, gender: ctx.req.body.gender, personality: ctx.req.body.personality, birthday: ctx.req.body.birthday, } return editVillager(ctx.req.params.id as number, body).then((result: any) => { return result; }) }); api.delete("/:id", ctx => { return deleteVillager(ctx.req.params.id as number).then(() => { ctx.res.setStatus(204); return true; }) }); export default api;
index.ts
にサーバーのエントリーポイントを用意し、作成したRouterとURLをセットします。
/api/villagers
に作成したRouterをセットすることで、このURL配下に上記で作成したルーティングが用意されます。import { Application, Router } from "https://deno.land/x/denotrain@v0.4.4/mod.ts"; import api from "./controllers/villagers.ts"; const app = new Application({}); app.use("/api/villagers", api); app.run();起動
サーバーの用意ができたので起動しましょう、以下のURLでAPIが起動しているはずです。
http://localhost:3000/$ docker-compose build $ docker-compose up -dまずは最初のAPIを作ってみましたが、ライブラリを探すのも公式ページから検索できてドキュメントも読みやすくそろっているのでとくにハマることなく進みました。
Dockerのイメージも40MBぐらいで小さいので良さそうですね、次はクラウド環境にデプロイしてみたいと思います。