Realize から Air に移行し、 Go(echo) + Air + docker-compose でホットリロードを利用して開発を行う。
この記事は Go 2 Advent Calendar 2020 の4日目の記事です。
tl;dr
- Goでホットリロード(a.k.a Live reload)を利用する際には今までは Realize というライブラリを使っている人が多いが、開発が止まっている。
- issueも放置され、go modの対応もされておらず、2020/10/25現在のモダンな環境で構築を行うと詰まる点が多い。
- 代わりに Air というライブラリを利用してホットリロードを行う。
Go + docker-composeを利用してホットリロードを利用した開発を行いたいと検索をして一番多かったのがrealizeというライブラリだった。
が、開発が停滞しているようで、issueも溜まっており、go modの対応もされておらず、2020/10/25 現在の環境で新規に作ろうとすると、いくつもハマる点があった。
他にライブラリは無いかといくつか調べたところ、Airというライブラリが開発も頻繁にされており、star数も多く筋が良さそうだったので、こちらを利用することにした。
利用方法
Usageにもあるが、
- .air.toml ファイルを作成する
- repositoryの.air_example.toml をコピーペーストし、自分の環境に合わせる
- コマンドを実行する
air
.air.tomlの設定について
repositoryにexampleがあるので、それを参考にする。
air/air_example.toml at 71d8bf298b426e631c7f51947e8d706246b4ff16 · cosmtrek/air · GitHub
設定ファイルもそんなに難しくなく、ある程度はわかると思う。特に重要な点は
cmd
bin
cmd
は自分のGo Appをbuildするコマンドを記述する。
bin
は cmd
でbuildしたbinaryのpathを記述する
実際に利用するシーンを見たほうが早いので、GitHubにpushしたコードを参考に構築する。
ディレクトリ構成は下記
$ tree -I frontend . ├── LICENSE ├── README.md ├── backend │ ├── Dockerfile │ ├── cmd │ │ └── app │ │ └── main.go │ ├── go.mod │ ├── go.sum │ ├── handlers │ │ ├── handler.go │ │ ├── root.go │ │ └── routes.go │ ├── models │ └── repositories └── docker-compose.yml 6 directories, 10 files
実際にホットリロードを行う。今回の場合の.air.tomlの構成は下記
$ cd backend/ $ go get -u github.com/cosmtrek/air $ air -c .air.toml __ _ ___ / /\ | | | |_) /_/--\ |_| |_| \_ v1.12.1 // live reload for Go apps, with Go1.14.0 watching . watching cmd watching cmd/app watching handlers watching models watching repositories !exclude tmp building... running... ____ __ / __/___/ / ___ / _// __/ _ \/ _ \ /___/\__/_//_/\___/ v4.1.16 High performance, minimalist Go web framework https://echo.labstack.com ____________________________________O/_______ O\ ⇨ http server started on [::]:5000 $ curl localhost:5000 {"result":true}
handlers/root.go
に手を入れてみる。
$ air -c .air.toml __ _ ___ / /\ | | | |_) /_/--\ |_| |_| \_ v1.12.1 // live reload for Go apps, with Go1.14.0 watching . watching cmd watching cmd/app watching handlers watching models watching repositories !exclude tmp building... running... ____ __ / __/___/ / ___ / _// __/ _ \/ _ \ /___/\__/_//_/\___/ v4.1.16 High performance, minimalist Go web framework https://echo.labstack.com ____________________________________O/_______ O\ ⇨ http server started on [::]:5000 handlers/root.go has changed building... running... ____ __ / __/___/ / ___ / _// __/ _ \/ _ \ /___/\__/_//_/\___/ v4.1.16 High performance, minimalist Go web framework https://echo.labstack.com ____________________________________O/_______ O\ ⇨ http server started on [::]:5000 $ $ curl localhost:5000 {"result":false}
とJSON Responseが変わっていることがわかる。
docker-compose
ここまではローカル環境で行ってきたが、目的としてContainer上で行いたいのでdocker-composeで行ってみる。
Dockerfile
docker-compose.yml
$ docker-compose build api Building api Step 1/7 : FROM golang:1.15-alpine as base ---> b3bc898ad092 Step 2/7 : WORKDIR /app/go/base ---> Using cache ---> 99b11653d662 Step 3/7 : COPY go.mod . ---> 8d6ca0f99b03 Step 4/7 : COPY go.sum . ---> 72e5d889c991 Step 5/7 : RUN go mod download ---> Running in 5b61f9b803ee Removing intermediate container 5b61f9b803ee ---> 05068d1b4923 Step 6/7 : RUN go get -u github.com/cosmtrek/air ---> Running in dbefc40ddd77 go: downloading github.com/cosmtrek/air v1.21.2 go: github.com/cosmtrek/air upgrade => v1.21.2 go: downloading github.com/fsnotify/fsnotify v1.4.9 go: downloading github.com/creack/pty v1.1.9 go: downloading github.com/imdario/mergo v0.3.8 go: downloading github.com/pelletier/go-toml v1.6.0 go: downloading github.com/fatih/color v1.7.0 go: github.com/pelletier/go-toml upgrade => v1.8.1 go: github.com/mattn/go-colorable upgrade => v0.1.8 go: github.com/mattn/go-isatty upgrade => v0.0.12 go: github.com/imdario/mergo upgrade => v0.3.11 go: github.com/fatih/color upgrade => v1.9.0 go: github.com/creack/pty upgrade => v1.1.11 go: golang.org/x/sys upgrade => v0.0.0-20201022201747-fb209a7c41cd go: downloading github.com/fatih/color v1.9.0 go: downloading github.com/imdario/mergo v0.3.11 go: downloading github.com/creack/pty v1.1.11 go: downloading golang.org/x/sys v0.0.0-20201022201747-fb209a7c41cd go: downloading github.com/pelletier/go-toml v1.8.1 go: downloading github.com/mattn/go-colorable v0.1.8 Removing intermediate container dbefc40ddd77 ---> 7a9749558df6 Step 7/7 : COPY . . ---> 37c82f03a414 Successfully built 37c82f03a414 Successfully tagged go-nextjs-playground_api:latest $ docker-compose up api Recreating go-nextjs-playground_api_1 ... done Attaching to go-nextjs-playground_api_1 api_1 | api_1 | __ _ ___ api_1 | / /\ | | | |_) api_1 | /_/--\ |_| |_| \_ v1.12.1 // live reload for Go apps, with Go1.14.0 api_1 | api_1 | mkdir /app/go/base/tmp api_1 | watching . api_1 | watching cmd api_1 | watching cmd/app api_1 | watching handlers api_1 | watching models api_1 | watching repositories api_1 | !exclude tmp api_1 | building... api_1 | running... api_1 | api_1 | ____ __ api_1 | / __/___/ / ___ api_1 | / _// __/ _ \/ _ \ api_1 | /___/\__/_//_/\___/ v4.1.16 api_1 | High performance, minimalist Go web framework api_1 | https://echo.labstack.com api_1 | ____________________________________O/_______ api_1 | O\ api_1 | ⇨ http server started on [::]:5000
$ curl localhost:5000 {"result":true}
また handler/root.go
のコードを変える。今回はJSON Responseの値をtrueからfalseに変更する。
api_1 | handlers/root.go has changed api_1 | building... api_1 | running... api_1 | api_1 | ____ __ api_1 | / __/___/ / ___ api_1 | / _// __/ _ \/ _ \ api_1 | /___/\__/_//_/\___/ v4.1.16 api_1 | High performance, minimalist Go web framework api_1 | https://echo.labstack.com api_1 | ____________________________________O/_______ api_1 | O\ api_1 | ⇨ http server started on [::]:5000
$ curl localhost:5000 {"result":false}
Containerを起動したまま、レスポンスが変わっていること(ホットリロードされていること)がわかる。
まとめ
- Realizeは開発が停滞しており、今後扱うと負債になる可能性が高い。
- 代わりにAirというライブラリを利用することで、ホットリロード (a.k.a ライブロード) を利用することが出来るのでそちらを利用する。
ref
参考Project
各種PR
階層構造(a.k.a ツリー構造・ディレクトリ構造・フォルダ)をDBでどう設計すべきか
階層構造とは
いわゆるTree Strcture(木構造)。
例として上げるのであれば、
ここではフォルダを想定する。
隣接リスト(Adjacency List)
Tree-Structure/adjacency_list at main · teitei-tk/Tree-Structure · GitHub
単純に考えると、各フォルダに親のフォルダを参照させるやり方が想定される。 しかし、このままだとフォルダ構成が深い場合、単一のSQLで取得することが難しい。実際に見てみる。
フォルダ構成は下記
table
CREATE TABLE IF NOT EXISTS `adjacency_list` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `parent_id` bigint(20), `name` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
ノードデータ
INSERT INTO `adjacency_list` ( parent_id, name ) VALUES ( null, "Home" ), ( 1, "Downloads" ), ( 2, "20201126" ), ( 1, "Desktop" ), ( 1, "Documents" ), ( 5, "Adobe" ), ( 6, "PremierePro" ), ( 6, "PhotoShop" )
このデータを一括で取得する場合は下記のSQLになる。
SELECT * FROM adjacency_list as al LEFT OUTER JOIN adjacency_list as al2 ON al.parent_id = al2.id LEFT OUTER JOIN adjacency_list as al3 ON al2.parent_id = al3.id LEFT OUTER JOIN adjacency_list as al4 ON al3.parent_id = al4.id
結果は下記の通りになる。
id | parent_id | name | id | parent_id | name | id | parent_id | name | id | parent_id | name |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | NULL | Home | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL |
2 | 1 | Downloads | 1 | NULL | Home | NULL | NULL | NULL | NULL | NULL | NULL |
3 | 2 | 20201126 | 2 | 1 | Downloads | 1 | NULL | Home | NULL | NULL | NULL |
4 | 1 | Desktop | 1 | NULL | Home | NULL | NULL | NULL | NULL | NULL | NULL |
5 | 1 | Documents | 1 | NULL | Home | NULL | NULL | NULL | NULL | NULL | NULL |
6 | 5 | Adobe | 5 | 1 | Documents | 1 | NULL | Home | NULL | NULL | NULL |
7 | 6 | PremierePro | 6 | 5 | Adobe | 5 | 1 | Documents | 1 | NULL | Home |
8 | 6 | PhotoShop | 6 | 5 | Adobe | 5 | 1 | Documents | 1 | NULL | Home |
クエリを見ての通り階層分 n回 LEFT OUTER JOINで直近の子と親を取得しているが、 このクエリのままではすべてのフォルダを一括で取得することが出来ない。やるとしても大量かつ複雑なSQLが必要になる。
この隣接リストモデルの代替となるものには、下記のような設計がある。
- 経路列挙モデル(Path Enumeration)
- 入れ子集合モデル(Nested Set)
- 閉包テーブルモデル(Closure Table)など
順に解説をする
経路列挙 (Path Enumeration)
Tree-Structure/path_enumeration at main · teitei-tk/Tree-Structure · GitHub
経路列挙モデル(Path Enumeration)は先祖の系譜を表す文字列をノードとして扱うことで解決している。
具体的にはUnixのディレクトリ構造。eg. /bin/sh
テーブル定義
CREATE TABLE IF NOT EXISTS `path_enumeration` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `path` varchar(1000), `name` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
ノードデータ
INSERT INTO `path_enumeration` ( path, name ) VALUES ( "1/", "Home" ), ( "1/2/", "Downloads" ), ( "1/2/3/", "20201126" ), ( "1/4/", "Desktop" ), ( "1/5/", "Documents" ), ( "1/5/6/", "Adobe" ), ( "1/5/6/7/", "PremierePro" ), ( "1/5/6/8/", "PhotoShop" );
結果
id | path | name |
---|---|---|
1 | 1/ | Home |
2 | 1/2/ | Downloads |
3 | 1/2/3/ | 20201126 |
4 | 1/4/ | Desktop |
5 | 1/5/ | Documents |
6 | 1/5/6/ | Adobe |
7 | 1/5/6/7/ | PremierePro |
8 | 1/5/6/8/ | PhotoShop |
例として、PhotoShopディレクトリのすべての先祖を取得するクエリは下記のようになる。
SELECT pe.*, CONCAT(pe.path, "%") as LikeQuery FROM path_enumeration AS pe WHERE "1/5/6/8" LIKE CONCAT(pe.path, "%");
結果
id | path | name | LikeQuery |
---|---|---|---|
1 | 1/ | Home | 1/% |
5 | 1/5/ | Documents | 1/5/% |
6 | 1/5/6/ | Adobe | 1/5/6/% |
8 | 1/5/6/8/ | PhotoShop | 1/5/6/8% |
CONCAT(pe.path, "%")
が味噌。クエリ結果にLikeQuery
を表示しているのでわかると思うが先祖のパスがLIKEパターンマッチしている。具体的には下記のノードと一致する。
1/%
1/5/%
1/5/6/%
子孫を取得する場合にはLIKE句の引数を逆にする。
SELECT pe.*, CONCAT("1/5/", "%") AS LikeQuery FROM path_enumeration AS pe WHERE pe.path LIKE CONCAT("1/5/", "%");
結果
id | path | name | LikeQuery |
---|---|---|---|
5 | 1/5/ | Documents | 1/5/% |
6 | 1/5/6/ | Adobe | 1/5/% |
7 | 1/5/6/7/ | PremierePro | 1/5/% |
8 | 1/5/6/8/ | PhotoShop | 1/5/% |
1/5/%
は下記のノードと一致する。
1/5/
1/5/6/
1/5/6/7/
1/5/6/8/
ノードの挿入
Home
以下にMusic
というノードを挿入する。
Query
INSERT INTO `path_enumeration` ( name ) VALUES ( "Music" ); Update `path_enumeration` SET path = CONCAT(( SELECT x.path FROM ( SELECT path FROM path_enumeration WHERE id = 1 ) as x ), LAST_INSERT_ID()) WHERE id = LAST_INSERT_ID();
主キーがAutoIncremntの場合、挿入時点では自分のIDがわからないのでpathを構築出来ない。一旦pathを除いたノードを挿入し、Update文でpathを更新する。
デメリット
- pathの管理はアプリケーションのコードに依存することになる。validationが複雑になりメンテンス性の心配が残る。
- pathの型はvarchar型で、無限に保存出来るわけではない。
入れ子集合(Nested Set)
Tree-Structure/nested_set at main · teitei-tk/Tree-Structure · GitHub
入れ子集合(Nested Set)は、直近の親ではなく、子孫の集合に関する情報を各ノードに保存する。各ノードのnsleftおよびnsrightと呼ばれる数値で階層を表現する。
テーブル定義
CREATE TABLE IF NOT EXISTS `nested_set` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `nsleft` integer NOT NULL, `nsright` integer NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
ノードデータ
INSERT INTO `nested_set` ( name, nsleft, nsright ) VALUES ( "Home", 1, 15 ), ( "Downloads", 2, 5 ), ( "20201126", 3, 4 ), ( "Desktop", 6, 7 ), ( "Documents", 8, 14 ), ( "Adobe", 9, 12 ), ( "PremierePro", 10, 11 ), ( "PhotoShop", 12, 13 )
id | name | nsleft | nsright |
---|---|---|---|
1 | Home | 1 | 15 |
2 | Downloads | 2 | 5 |
3 | 20201126 | 3 | 4 |
4 | Desktop | 6 | 7 |
5 | Documents | 8 | 14 |
6 | Adobe | 9 | 12 |
7 | PremierePro | 10 | 11 |
8 | PhotoShop | 12 | 13 |
例として、Documents
以下のノードを取得する場合は下記のクエリになる。
SELECT s2.* FROM nested_set as s1 INNER JOIN nested_set as s2 ON s2.nsleft BETWEEN s1.nsleft AND s1.nsright WHERE s1.id = 5
BEETWEEN
で範囲指定しているところが味噌。この例で言うと、id = 5
であるDocuments
のnsleft
.8とnsright
.14の間にある、nsleft
の値を持っているすべての子孫ノードを取得することが出来る。
id | name | nsleft | nsright |
---|---|---|---|
5 | Documents | 8 | 14 |
6 | Adobe | 9 | 12 |
7 | PremierePro | 10 | 11 |
8 | PhotoShop | 12 | 13 |
先祖を取得する場合は逆にする。
SELECT s1.*, s2.* FROM nested_set as s1 INNER JOIN nested_set as s2 ON s1.nsleft BETWEEN s2.nsleft AND s2.nsright WHERE s1.id = 5
id = 5
であるDocuments
のnsleft
.8の値が nsleft
とnsright
の間にあるすべてのノードを取得出来る。この例でいうと、nsleft
が1でnsright
が15であるHome
が該当する。
デメリット
- ノードの挿入や移動などの際にツリー構造の操作が他のモデルに比べて複雑になる。そのためノードの挿入が頻繁に求められる場合は最適解にはならない。
- ノードの挿入では新しいノードの左(nsleft)の値より大きいすべての左右のノードの値を計算し直す必要がある。
閉包テーブル(Closure Table)
Tree-Structure/closure_table at main · teitei-tk/Tree-Structure · GitHub
閉包テーブル(Closure Table)は今までもモデルとは違い直接の親子関係だけではなく、ツリー全体のパスをテーブルに格納する。
テーブル定義
今までのようなフォルダの定義に加えて、TreePath
というテーブル定義する。
CREATE TABLE IF NOT EXISTS `closure_table` ( `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `tree_paths` ( `ancestor` BIGINT(20) UNSIGNED NOT NULL, `descendant` BIGINT(20) UNSIGNED NOT NULL, PRIMARY KEY (`ancestor`, `descendant`), FOREIGN KEY (`ancestor`) REFERENCES closure_table(`id`), FOREIGN KEY (`descendant`) REFERENCES closure_table(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
閉包テーブルでは今までモデルとは違い、ツリー構造のデータをTreePathsテーブルに保存する。このテーブルの各行には先祖(ancestor)/子孫(descendant)を共有するノードの組み合わせを保存する。これはツリー状の離れた位置にあるノードも含めたすべてのノードが対象になる。また、各行のノードには自分自身を参照するレコードも追加する。
ノードデータ
INSERT INTO `closure_table` ( name ) VALUES ( "Home" ), ( "Downloads" ), ( "20201126" ), ( "Desktop" ), ( "Documents" ), ( "Adobe" ), ( "PremierePro" ), ( "PhotoShop" ); INSERT INTO `tree_paths` ( ancestor, descendant ) VALUES ( 1, 1 ), ( 1, 2 ), ( 1, 3 ), ( 1, 4 ), ( 1, 5 ), ( 1, 6 ), ( 1, 7 ), ( 1, 8 ), ( 2, 2 ), ( 2, 3 ), ( 3, 3 ), ( 4, 4 ), ( 5, 5 ), ( 5, 6 ), ( 5, 7 ), ( 5, 8 ), ( 6, 6 ), ( 6, 7 ), ( 6, 8 ), ( 7, 7 ), ( 8, 8 );
closure_table
id | name |
---|---|
1 | Home |
2 | Downloads |
3 | 20201126 |
4 | Desktop |
5 | Documents |
6 | Adobe |
7 | PremierePro |
8 | PhotoShop |
tree_paths
ancestor | descendant |
---|---|
1 | 1 |
1 | 2 |
2 | 2 |
1 | 3 |
2 | 3 |
3 | 3 |
1 | 4 |
4 | 4 |
1 | 5 |
5 | 5 |
1 | 6 |
5 | 6 |
6 | 6 |
1 | 7 |
5 | 7 |
6 | 7 |
7 | 7 |
1 | 8 |
5 | 8 |
6 | 8 |
8 | 8 |
ノードを取得するクエリは入れ子集合よりも更に簡単になる。ID=2
のDownloads
以下を取得するにはtree_paths
をJoinして、anestor
(先祖)が2の行を探すだけ。
SELECT c.* FROM closure_table as c INNER JOIN tree_paths as t ON c.id = t.descendant WHERE t.ancestor = 2
result
id | name |
---|---|
2 | Downloads |
3 | 20201126 |
先祖の取得も簡単で、先程のクエリのancestor
とdescendant
を逆に書くだけ。ID=8
の先祖を取得する。
SELECT c.* FROM closure_table as c INNER JOIN tree_paths as t ON c.id = t.ancestor WHERE t.descendant = 8
result
id | name |
---|---|
1 | Home |
5 | Documents |
6 | Adobe |
8 | PhotoShop |
新たにノードを挿入する場合、例えばID=4
のDesktop
以下にノードを挿入するには、まずclosure_table
にフォルダのデータを挿入する。
INSERT INTO `closure_table` ( name ) VALUES ( "Macintosh HD" )
次にID=4
を子孫として参照する行の集合を取得し、子孫をLAST_INSERT_ID()
で置き換えて新たに挿入する。
INSERT INTO `tree_paths` ( ancestor, descendant ) SELECT t.ancestor, LAST_INSERT_ID() as descendant FROM tree_paths as t WHERE t.descendant = 4 UNION ALL SELECT LAST_INSERT_ID(), LAST_INSERT_ID()
INSERT句で利用しているSELECT句の結果は下記になる。
ancestor | descendant |
---|---|
1 | 9 |
4 | 9 |
9 | 9 |
- 先祖が1(Home)で、子孫が9(Macintosh HD)
- 先祖が4(Desktop)で、子孫が9(Macintosh HD)
- 先祖が9(Macintosh HD)で、子孫が9(Macintosh HD)
となっていることがわかる。
ノードを削除する場合、例えば先程挿入した`ID=9'を削除する場合には、子孫として9を参照するすべての行を削除する。具体的に削除するレコードは上記の結果である。
DELETE FROM `tree_paths` WHERE descendant = LAST_INSERT_ID();
サブツリー全体、例えばID=6
とその子孫をすべて削除するには、子孫としてID=6
を参照する全ての行と、ID=6
の子孫を子孫として参照するすべての行を削除する。
具体的には下記のSQLになる
DELETE FROM `tree_paths` WHERE descendant IN ( SELECT x.id FROM ( SELECT descendant as id FROM `tree_paths` WHERE ancestor = 6 ) as x )
DELETE文の条件に利用しているSELECT文の結果は下記
SELECT x.id FROM ( SELECT descendant as id FROM `tree_paths` WHERE ancestor = 6 )
結果
id |
---|
6 |
7 |
8 |
IDが6, 7, 8と、ID=6
以下のツリーが削除されることがわかる。
結局どの設計を使うべきなのか
設計 | 子へのクエリ実行 | ツリーへのクエリ実行 | 挿入 | 削除 | 参照整合性維持 |
---|---|---|---|---|---|
隣接リスト | 簡単 | 難しい | 難しい | 簡単 | 可能 |
経路列挙モデル | 簡単 | 簡単 | 簡単 | 簡単 | 不可 |
入れ子集合モデル | 難しい | 難しい | 難しい | 難しい | 不可 |
閉包テーブルモデル | 簡単 | 簡単 | 簡単 | 簡単 | 可能 |
個人的な意見
仕様にもよりますが、消去法で閉包テーブル設計かなとなります。
隣接リスト
単純に筋が悪い設計だと考えている。要件がかなり簡易な場合(一つの親しか持たない)にはありかもしれないが、そのような仕様は時間が経つに連れて変わっていくし、すぐに新しく親を設計したいと出てくる。それを考えると別の手法を設計すべきと考える。
経路列挙
筋は良いが、経路列挙のpathの管理が辛そうと考えている。アプリケーション側でpathのValidationなどが長大になりそうなのを考えるとメンテンス性に欠ける。メンテンス性に欠けるということはバグの温床になりやすい。そう考えると閉包テーブルモデルの方が良いと考える。
また、pathに複数の値(フォルダの経路)を格納する事はSQLアンチパターンのジェイウォークに当てはまる。
www.slideshare.net
入れ子集合モデル
ノードの挿入や移動などの際にツリー構造の操作が他のモデルに比べて複雑になるのが欠点。挿入や更新が多い要件には向いていない。レコード数が大量にあると更新に時間がかかる。個人的にはクエリが複雑になる事が一番の欠点。コードもクエリも設計もシンプルに収めたほうが良いと考える人間なので、筋は良いが、それなら閉包テーブルモデルを採用したほうが良いと判断。
閉包テーブル
ツリー構造を格納するテーブルのレコードが爆発的に増えるが、その分取得、挿入、更新、削除もシンプルに出来る。トレードオフだが、複雑性が増して不具合の温床になるよりはレコード数が増えるデメリットを受け入れたほうが長期的に見て良いと思っている。
Docker with Multi Stage Buildを利用したNext.jsのDockerイメージを作る
tl;dr
- Next.jsのSSG(Static Site Generator)を利用したDocker Imageを作る際にMulti Stage BuildでImageを作ると、利用しない場合に比べて453MB変わる
Dockerfile
以前とは違い、Production環境でも利用出来るように構築をした。SSGを利用する前提で書いている。 ただ、実際にはSSGをProductionで利用する場合はNetlifyやVercel、S3など静的コンテンツを置くのに適したプラットフォームを利用すると思うので、あまり意味はないかもしれない。
それぞれbase
, builder
, production
とステージを分けている。
base
依存関係にあるnpm packageをインストールする。開発環境ではbase stageを利用し、docker build時の引数でnext
コマンドを指定するだけで良い。
$ docker build -t next-app:base --target base . Sending build context to Docker daemon 177.8MB Step 1/7 : FROM node:14-alpine as base ---> fa2fa5d4e6f4 Step 2/7 : WORKDIR /app/js/base ---> Using cache ---> 3bcd585979e7 Step 3/7 : COPY package.json . ---> Using cache ---> 73e62066ac85 Step 4/7 : COPY yarn.lock . ---> Using cache ---> d6ed6b01b3ed Step 5/7 : COPY tsconfig.json . ---> Using cache ---> 7a8ae367da60 Step 6/7 : RUN yarn install ---> Using cache ---> dd874aced5fe Step 7/7 : COPY . . ---> Using cache ---> 5ad130f7577a Successfully built 5ad130f7577a Successfully tagged next-app:base $ docker run -it --rm -p 3000:3000 next-app:base yarn next yarn run v1.22.5 $ /app/js/base/node_modules/.bin/next ready - started server on http://localhost:3000 info - ready on http://localhost:3000 event - build page: / wait - compiling... A codemod is available to fix the most common cases: https://nextjs.link/codemod-ndc info - ready on http://localhost:3000
builder
アプリケーションをbuildする層。この層でアプリケーションbuildしproduction stageへは不要なもの(node_modules, ソースコード etc)を持ち込まないようにする。
production
Production環境に適応したイメージを作る層。ここで不要なファイルが作成されるとその分Imageサイズが大きくなるので不要なものは入れてはいけない。 このstageでbuildをしてしまうとソースコードなど不要なものが必要になるので、一つ前のbuilder stageから必要なものをコピーする。
最後のyarn install --production
でproductionに必要な依存関係のみをインストールする。
結果
Multi Stage Buildを利用した場合と利用しない場合で比較をしてみる。
Multi Stage Buildを利用する
$ docker build -t next-app:base . --target base Sending build context to Docker daemon 194.2MB Step 1/7 : FROM node:14-alpine as base ---> fa2fa5d4e6f4 Step 2/7 : WORKDIR /app/js/base ---> Using cache ---> 7a04d844b125 Step 3/7 : COPY package.json . ---> Using cache ---> ab5127932992 Step 4/7 : COPY yarn.lock . ---> Using cache ---> e8ba5b6cb1ab Step 5/7 : COPY tsconfig.json . ---> Using cache ---> 10b359198a18 Step 6/7 : RUN yarn install ---> Using cache ---> affbace5d646 Step 7/7 : COPY . . ---> Using cache ---> ce9e93809dc2 Successfully built ce9e93809dc2 Successfully tagged next-app:base
builder stage
$ docker build -t next-app:builder . --target builder Sending build context to Docker daemon 194.2MB Step 1/12 : FROM node:14-alpine as base ---> fa2fa5d4e6f4 Step 2/12 : WORKDIR /app/js/base ---> Running in ad2141be97fb Removing intermediate container ad2141be97fb ---> 6d182a28bf59 Step 3/12 : COPY package.json . ---> 4a44d0936af9 Step 4/12 : COPY yarn.lock . ---> 489f697cba57 Step 5/12 : COPY tsconfig.json . ---> b155046ac03c Step 6/12 : RUN yarn install ---> Running in 937d5fb38b0e yarn install v1.22.5 [1/4] Resolving packages... [2/4] Fetching packages... info fsevents@1.2.13: The platform "linux" is incompatible with this module. info "fsevents@1.2.13" is an optional dependency and failed compatibility check. Excluding it from installation. info fsevents@2.1.3: The platform "linux" is incompatible with this module. info "fsevents@2.1.3" is an optional dependency and failed compatibility check. Excluding it from installation. [3/4] Linking dependencies... warning " > file-loader@6.0.0" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0". warning " > url-loader@4.1.0" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0". [4/4] Building fresh packages... Done in 49.13s. Removing intermediate container 937d5fb38b0e ---> 27a2d7a30d34 Step 7/12 : COPY . . ---> c19dc009a6a6 Step 8/12 : FROM node:14-alpine as builder ---> fa2fa5d4e6f4 Step 9/12 : ENV NODE_ENV=production ---> Running in 3bd3364115b5 Removing intermediate container 3bd3364115b5 ---> 0cfcd40518b3 Step 10/12 : WORKDIR /app/js/builder ---> Running in 0e20dd40600f Removing intermediate container 0e20dd40600f ---> 9dac22ae0e11 Step 11/12 : COPY --from=base /app/js/base /app/js/builder ---> 208200318cf4 Step 12/12 : RUN ["yarn", "build"] ---> Running in ef55ba375411 yarn run v1.22.5 $ next build && next export Creating an optimized production build... Compiled successfully. Automatically optimizing pages... Page Size First Load JS ┌ ● / 9.31 kB 90.9 kB ├ /_app 1.9 kB 60 kB ├ ○ /404 3.25 kB 63.2 kB └ ● /articles/[id] 344 B 81.9 kB ├ /articles/1 └ /articles/2 + First Load JS shared by all 60 kB ├ static/pages/_app.js 1.9 kB ├ chunks/444ecc389d1b2833cd1c90b9f6d2e962a7bb15f6.c2e7b8.js 10.7 kB ├ chunks/62e1b0f2.e05b9c.js 64 B ├ chunks/framework.4dd100.js 40.3 kB ├ runtime/main.9f98e5.js 6.28 kB ├ runtime/webpack.c21266.js 746 B ├ css/7ed58071346528c353bf.css 97.4 kB └ css/cfb837a2694c3ad390b0.css 108 B λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps) ○ (Static) automatically rendered as static HTML (uses no initial props) ● (SSG) automatically generated as static HTML + JSON (uses getStaticProps) > using build directory: /app/js/builder/.next copying "static build" directory > No "exportPathMap" found in "next.config.js". Generating map from "./pages" launching 1 workers Exporting (0/2) copying "public" directory Exporting (1/2) Exporting (2/2) Export successful Done in 42.64s. Removing intermediate container ef55ba375411 ---> ef487ea233e3 Successfully built ef487ea233e3 Successfully tagged next-app:builder
production stage
$ docker build -t next-app:production . Sending build context to Docker daemon 194.2MB Step 1/18 : FROM node:14-alpine as base ---> fa2fa5d4e6f4 Step 2/18 : WORKDIR /app/js/base ---> Running in 141707b10d1b Removing intermediate container 141707b10d1b ---> 7a04d844b125 Step 3/18 : COPY package.json . ---> ab5127932992 Step 4/18 : COPY yarn.lock . ---> e8ba5b6cb1ab Step 5/18 : COPY tsconfig.json . ---> 10b359198a18 Step 6/18 : RUN yarn install ---> Running in 25c5e101426f yarn install v1.22.5 [1/4] Resolving packages... [2/4] Fetching packages... info fsevents@1.2.13: The platform "linux" is incompatible with this module. info "fsevents@1.2.13" is an optional dependency and failed compatibility check. Excluding it from installation. info fsevents@2.1.3: The platform "linux" is incompatible with this module. info "fsevents@2.1.3" is an optional dependency and failed compatibility check. Excluding it from installation. [3/4] Linking dependencies... warning " > file-loader@6.0.0" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0". warning " > url-loader@4.1.0" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0". [4/4] Building fresh packages... Done in 78.52s. Removing intermediate container 25c5e101426f ---> affbace5d646 Step 7/18 : COPY . . ---> ce9e93809dc2 Step 8/18 : FROM node:14-alpine as builder ---> fa2fa5d4e6f4 Step 9/18 : ENV NODE_ENV=production ---> Running in 05b08f765682 Removing intermediate container 05b08f765682 ---> 43348a423c2a Step 10/18 : WORKDIR /app/js/builder ---> Running in e924b7124b7e Removing intermediate container e924b7124b7e ---> d872eca49f96 Step 11/18 : COPY --from=base /app/js/base /app/js/builder ---> 5aa288be27aa Step 12/18 : RUN ["yarn", "build"] ---> Running in c1762e58a430 yarn run v1.22.5 $ next build && next export Creating an optimized production build... Compiled successfully. Automatically optimizing pages... Page Size First Load JS ┌ ● / 9.31 kB 90.9 kB ├ /_app 1.9 kB 60 kB ├ ○ /404 3.25 kB 63.2 kB └ ● /articles/[id] 344 B 81.9 kB ├ /articles/1 └ /articles/2 + First Load JS shared by all 60 kB ├ static/pages/_app.js 1.9 kB ├ chunks/62e1b0f2.e05b9c.js 64 B ├ chunks/9a9e7c821a17e6790c99026b07c1c46382c721c2.c2e7b8.js 10.7 kB ├ chunks/framework.4dd100.js 40.3 kB ├ runtime/main.9f98e5.js 6.28 kB ├ runtime/webpack.c21266.js 746 B ├ css/7ed58071346528c353bf.css 97.4 kB └ css/cfb837a2694c3ad390b0.css 108 B λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps) ○ (Static) automatically rendered as static HTML (uses no initial props) ● (SSG) automatically generated as static HTML + JSON (uses getStaticProps) > using build directory: /app/js/builder/.next copying "static build" directory > No "exportPathMap" found in "next.config.js". Generating map from "./pages" launching 1 workers Exporting (0/2) copying "public" directory Exporting (1/2) Exporting (2/2) Export successful Done in 51.92s. Removing intermediate container c1762e58a430 ---> d264d497a0a5 Step 13/18 : FROM nginx:latest as production ---> f35646e83998 Step 14/18 : WORKDIR /app/js/src ---> Running in 89ce96f55a15 Removing intermediate container 89ce96f55a15 ---> fd0fb921aa75 Step 15/18 : RUN rm -rf /etc/nginx/conf.d ---> Running in d73698e7006d Removing intermediate container d73698e7006d ---> 85cc19c895e4 Step 16/18 : ADD conf/nginx/conf.d /etc/nginx/conf.d ---> 27ee1c3cfac8 Step 17/18 : COPY --from=builder /app/js/builder/out ./out ---> 5d4d1bd4881e Step 18/18 : EXPOSE 80 ---> Running in ef6862c2ccd5 Removing intermediate container ef6862c2ccd5 ---> 46fc04190def Successfully built 46fc04190def Successfully tagged next-app:production
Multi Stage Buildを利用しない
$ cat Dockerfile_not_msb FROM alpine:latest RUN apk update RUN apk add --update nodejs nodejs-npm yarn WORKDIR /app/js COPY package.json . COPY yarn.lock . COPY tsconfig.json . RUN yarn install COPY . . RUN ["yarn", "build"] RUN apk add nginx openrc RUN rm -rf /etc/nginx/conf.d ADD conf/nginx/conf.d /etc/nginx/conf.d EXPOSE 80 $ docker build -t next-app:not_msb -f Dockerfile_not_msb . Sending build context to Docker daemon 224MB Step 1/14 : FROM alpine:latest ---> a24bb4013296 Step 2/14 : RUN apk update ---> Using cache ---> 5a65f08f5db7 Step 3/14 : RUN apk add --update nodejs nodejs-npm yarn ---> Using cache ---> c97ad0a3e846 Step 4/14 : WORKDIR /app/js ---> Using cache ---> a37951715a8d Step 5/14 : COPY package.json . ---> Using cache ---> 6faddfc45d20 Step 6/14 : COPY yarn.lock . ---> Using cache ---> bd166cc22fbc Step 7/14 : COPY tsconfig.json . ---> Using cache ---> f827439198ba Step 8/14 : RUN yarn install ---> Using cache ---> 46c450c588da Step 9/14 : COPY . . ---> Using cache ---> 7b18d341a97a Step 10/14 : RUN ["yarn", "build"] ---> Using cache ---> 578c525c3ce1 Step 11/14 : RUN apk add nginx openrc ---> Using cache ---> 94434ea28089 Step 12/14 : RUN rm -rf /etc/nginx/conf.d ---> Using cache ---> 571eedb7cbb5 Step 13/14 : ADD conf/nginx/conf.d /etc/nginx/conf.d ---> Using cache ---> 1a765adec9ca Step 14/14 : EXPOSE 80 ---> Using cache ---> 5df8f8cc2b74 Successfully built 5df8f8cc2b74 Successfully tagged next-app:not_msb
結果
$ docker images | grep next-app next-app not_msb 5df8f8cc2b74 7 minutes ago 614MB next-app builder ef487ea233e3 About an hour ago 327MB next-app production 46fc04190def About an hour ago 161MB next-app base ce9e93809dc2 About an hour ago 599MB
まとめ
- Multi Stage Buildを利用することで一つのDockerfileで開発環境・Production環境をすぐに作ることが出来る。
- Multi Stage Buildを利用した場合、利用しない場合に比べて453MB変わる。
GitLab CIでTrivyを利用しDocker Imageの脆弱性を検知し、Slackに投稿する
tl;dr
Trivyについて
昔書いたのでそちらを参照してください。 teitei-tk.hatenablog.com
コードを見せろ
Sample Repository
Merge Request
工夫したところ
gitlab-ci.yml
は特殊な事は書いておらず、ほとんどGitHubと、GitLabのサンプルコードをコピーしただけです。
工夫した点としては、Trivyの検知した結果のレポートをJUnitのフォーマットにしています。 GitHubのサンプルコードではGitLabのContainer scanningフォーマットで書かれていますが、Container ScanningはGold/Ultimate Planでないと利用できないため、JUnitフォーマットを利用しています。
今回はあえて、サンプルのため脆弱性のあるRack Gemなど、古いgemを利用しています。
JUnitフォーマットを利用した場合、MergeRequestの画面ではこのように表示されます。
CVE-2019-16782などが表示されていることが確認できると思います。 リンクをクリックすると、CVEの詳細が表示されます。
Schedule Pipelineを利用して一定周期でImageのScanを行う
新しくSchedule Pipelineを設定します。特に難しい点は無いと思うのでので設定は省略。
gitlab-ci.yml
の内容は下記です。特に変な点は書いていませんが、only: schedules
でSchedule Pipelineでのみ実行されるようにしています。
image_scan: image: docker:stable services: - name: docker:dind variables: DOCKER_HOST: tcp://docker:2375/ DOCKER_DRIVER: overlay2 # See https://github.com/docker-library/docker/pull/166 DOCKER_TLS_CERTDIR: "" IMAGE: trivy-ci-test:$CI_COMMIT_SHA before_script: - export TRIVY_VERSION=$(wget -qO - "https://api.github.com/repos/aquasecurity/trivy/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/') - echo $TRIVY_VERSION - wget --no-verbose https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz -O - | tar -zxvf - script: # Build image - docker build -t $IMAGE . # Fail on high and critical vulnerabilities - ./trivy --exit-code 1 --cache-dir .trivycache/ --severity CRITICAL,HIGH,MEDIUM --no-progress $IMAGE cache: paths: - .trivycache/ only: - schedules
設定すると、CI/CDページにてSchduleと表示されていることが確認できると思います。
SlackへScan結果を通知する。
ここも特に難しいことはないです。SlackにてApps -> Incoming WebHooksを追加。WebHook URLをコピーし、GitLabRepositoryのSettings -> CI/CD -> Slack notificationに移動。
Pipelineにチェックを入れ、通知するSlack Channelを記入するだけ。
その後、Schedule Pipelineを実行すると、このようにSlackに通知されます。
Trivy を利用した Docker Container・Image の脆弱性検知入門
tl;dr
- trivyを利用することで簡単にDocker コンテナの脆弱性の検知をすることが出来る。
- OSライブラリからアプリケーション依存のライブラリまで検知が可能
- SimpleかつREADMEが豊富で導入の敷居が低い。
- CIに組み込むことで DevOps から DevSecOps への移行を加速することが可能。
コンテナの脆弱性
2020/08/06 現在、Dockerを利用するしてプロダクトの環境を作る事がデファクトスタンダードになっている。 どの会社に行ってもセキュリティは最重要視されていると言っても過言ではないが、Docker イメージを生成 or 取得し、そのまま利用を続け、時間が経ち、利用しているライブラリに不具合が生じているのにも関わらず、知らないうちに脆弱性があるContainerを利用しているケースは少なくない。
脆弱性を放置して起きたインシデントは具体的に下記のような事例がある。
コンテナに仮想通貨の採掘を行うマルウェアイメージをダウンロードさせるキャンペーンが頻繁に行われており、ピーク時は 2,000以上の Dockerホストがマルウェア感染したとの報告もあります。
脆弱性の検知
コンテナの脆弱性検知を行うツールは複数ある。
Docker Bench Security
clair
anchore
今回はOSSのTrivyを利用する。
Trivyとは
AquaSectiry社が提供するOSS。特徴として、A Simple and Comprehensive Vulnerability Scanner for Containers, Suitable for CI と書かれているように非常にシンプルかつ、とても簡単にCIに組み込むことが出来る。
製作者は日本の方で、OSSで作成したものをAquaSecutiry社が買収し、そのまま就職をして仕事でOSS開発をするというエンジニアには夢のような記事を書かれていたことを覚えている。
READMEがとても豊富にかかれており、各プラットフォームでのインストールの方法が書かれており、気軽に手元で実践することが出来ることも非常に魅力的。
https://github.com/aquasecurity/trivy#installation
特に利用していきたいと決めた点は、OSパッケージの脆弱性だけではなく、各アプリケーションの依存ライブラリの脆弱性の検知も出来る。
アプリケーションの依存ライブラリというのは、RubyでいうGemfile.lock
、NodeJSでいうpackage-lock.json, yarn.lock
。
メジャーなアプリケーションの依存ライブラリはサポートされている印象で、業務でも使いやすい。
https://github.com/aquasecurity/trivy#application-dependencies
Trivyで脆弱性を検知する
実際に検知をしてみる。今回は3年前に作ったきり更新していないDocker Imageを利用する。
$ trivy image docker.io/teitei/docker-chrome-stable:latest 2020-08-07T00:30:14.481+0900 [33mWARN[0m You should avoid using the :latest tag as it is cached. You need to specify '--clear-cache' option when :latest image is changed 2020-08-07T00:30:46.674+0900 [34mINFO[0m Detecting Ubuntu vulnerabilities... docker.io/teitei/docker-chrome-stable:latest (ubuntu 16.04) =========================================================== Total: 930 (UNKNOWN: 0, LOW: 462, MEDIUM: 446, HIGH: 22, CRITICAL: 0) +--------------------------+------------------+----------+-------------------------------------+---------------------------------------+----------------------------------------------------------+ | LIBRARY | VULNERABILITY ID | SEVERITY | INSTALLED VERSION | FIXED VERSION | TITLE | +--------------------------+------------------+----------+-------------------------------------+---------------------------------------+----------------------------------------------------------+ | apt | CVE-2019-3462 | HIGH | 1.2.20 | 1.2.29ubuntu0.1 | Incorrect sanitation of the | | | | | | | 302 redirect field in HTTP | | | | | | | transport method of... | + +------------------+----------+ +---------------------------------------+----------------------------------------------------------+ | | CVE-2020-3810 | MEDIUM | | 1.2.32ubuntu0.1 | Missing input validation in | | | | | | | the ar/tar implementations of | | | | | | | APT before version 2.1.2... | +--------------------------+------------------+----------+-------------------------------------+---------------------------------------+----------------------------------------------------------+ | bash | CVE-2019-18276 | LOW | 4.3-14ubuntu1.2 | | bash: when effective UID is | | | | | | | not equal to its real UID | | | | | | | the... | + +------------------+ + +---------------------------------------+----------------------------------------------------------+ | | CVE-2019-9924 | | | 4.3-14ubuntu1.4 | bash: BASH_CMD is writable in | | | | | | | restricted bash shells | +--------------------------+------------------+ +-------------------------------------+---------------------------------------+----------------------------------------------------------+ | bsdutils | CVE-2016-2779 | | 2.27.1-6ubuntu3.2 | | util-linux: runuser tty hijack | | | | | | | via TIOCSTI ioctl | + +------------------+ + +---------------------------------------+----------------------------------------------------------+ | | CVE-2016-5011 | | | | util-linux: Extended partition | | | | | | | loop in MBR partition table | | | | | | | leads to DOS | +--------------------------+------------------+ +-------------------------------------+---------------------------------------+----------------------------------------------------------+ | coreutils | CVE-2016-2781 | | 8.25-2ubuntu2 | | coreutils: Non-privileged | | | | | | | session can escape to the | | | | | | | parent session in chroot | +--------------------------+------------------+----------+-------------------------------------+---------------------------------------+----------------------------------------------------------+ .... .... ....
Total: 930 (UNKNOWN: 0, LOW: 462, MEDIUM: 446, HIGH: 22, CRITICAL: 0)
と画面に収まりきれないほど検知出来ている。
アプリケーション依存のライブラリに存在する脆弱性の検出も出来る。これは私が雑に作ったものを入れているRepositoryで、Sinatraを利用しているが、rackに脆弱性がある。
$ trivy image teitei-tk/zatsu/ruby-rsa 2020-08-07T00:21:22.325+0900 WARN You should avoid using the :latest tag as it is cached. You need to specify '--clear-cache' option when :latest image is changed 2020-08-07T00:21:28.282+0900 INFO Detecting Alpine vulnerabilities... 2020-08-07T00:21:28.290+0900 INFO Detecting bundler vulnerabilities... teitei-tk/zatsu/ruby-rsa (alpine 3.12.0) ======================================== Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0) app/Gemfile.lock ================ Total: 3 (UNKNOWN: 0, LOW: 0, MEDIUM: 3, HIGH: 0, CRITICAL: 0) +---------+------------------+----------+-------------------+---------------+--------------------------------+ | LIBRARY | VULNERABILITY ID | SEVERITY | INSTALLED VERSION | FIXED VERSION | TITLE | +---------+------------------+----------+-------------------+---------------+--------------------------------+ | rack | CVE-2019-16782 | MEDIUM | 2.0.7 | 2.0.8, 1.6.12 | rubygem-rack: hijack sessions | | | | | | | by using timing attacks | | | | | | | targeting the session id | + +------------------+ + +---------------+--------------------------------+ | | CVE-2020-8161 | | | 2.1.3 | rubygem-rack: directory | | | | | | | traversal in Rack::Directory | + +------------------+ + +---------------+--------------------------------+ | | CVE-2020-8184 | | | 2.2.3, 2.1.4 | rubygem-rack: percent-encoded | | | | | | | cookies can be used to | | | | | | | overwrite existing prefixed | | | | | | | cookie names... | +---------+------------------+----------+-------------------+---------------+--------------------------------+ .... .... ....
Rackに脆弱性があることがわかる。
フィルタリング
サーベイ
CRITICAL
, HIGH
など、サーベイによってフィルタリングも出来る。
$ trivy image --severity CRITICAL,HIGH docker.io/teitei/docker-chrome-stable:latest 2020-08-07T00:32:37.159+0900 WARN You should avoid using the :latest tag as it is cached. You need to specify '--clear-cache' option when :latest image is changed 2020-08-07T00:32:37.216+0900 INFO Detecting Ubuntu vulnerabilities... docker.io/teitei/docker-chrome-stable:latest (ubuntu 16.04) =========================================================== Total: 22 (HIGH: 22, CRITICAL: 0) +--------------------+------------------+----------+-------------------+-------------------+--------------------------------+ | LIBRARY | VULNERABILITY ID | SEVERITY | INSTALLED VERSION | FIXED VERSION | TITLE | +--------------------+------------------+----------+-------------------+-------------------+--------------------------------+ | apt | CVE-2019-3462 | HIGH | 1.2.20 | 1.2.29ubuntu0.1 | Incorrect sanitation of the | | | | | | | 302 redirect field in HTTP | | | | | | | transport method of... | +--------------------+ + + + + + | libapt-pkg5.0 | | | | | | | | | | | | | | | | | | | | +--------------------+------------------+ +-------------------+-------------------+--------------------------------+ | libc-bin | CVE-2018-1000001 | | 2.23-0ubuntu7 | 2.23-0ubuntu10 | glibc: realpath() buffer | | | | | | | underflow when getcwd() | | | | | | | returns relative path allows | | | | | | | privilege escalation... | +--------------------+ + + + + + | libc6 | | | | | | | | | | | | | | | | | | | | | | | | | | | + .... .... ....
OS or Library
表示する脆弱性の種類をOS or アプリケーション依存ライブラリを選ぶことも出来る
$ trivy image --vuln-type os --s CRITICAL,HIGH docker.io/teitei/docker-chrome-stable:latest 2020-08-07T00:33:44.006+0900 WARN You should avoid using the :latest tag as it is cached. You need to specify '--clear-cache' option when :latest image is changed 2020-08-07T00:33:44.061+0900 INFO Detecting Ubuntu vulnerabilities... docker.io/teitei/docker-chrome-stable:latest (ubuntu 16.04) =========================================================== Total: 22 (HIGH: 22) +--------------------+------------------+----------+-------------------+-------------------+--------------------------------+ | LIBRARY | VULNERABILITY ID | SEVERITY | INSTALLED VERSION | FIXED VERSION | TITLE | +--------------------+------------------+----------+-------------------+-------------------+--------------------------------+ | apt | CVE-2019-3462 | HIGH | 1.2.20 | 1.2.29ubuntu0.1 | Incorrect sanitation of the | | | | | | | 302 redirect field in HTTP | | | | | | | transport method of... | +--------------------+ + + + + + | libapt-pkg5.0 | | | | | | | | | | | | | | | | | | | | +--------------------+------------------+ +-------------------+-------------------+--------------------------------+ | libc-bin | CVE-2018-1000001 | | 2.23-0ubuntu7 | 2.23-0ubuntu10 | glibc: realpath() buffer | | | | | | | underflow when getcwd() | | | | | | | returns relative path allows | | | | | | | privilege escalation... | +--------------------+ + + + + + | libc6 | | | | | | | | | | | | | | | | | | | | | | | | | | | + .... .... ....
脆弱性の修正方法
Docker ImageをBuildをし直すだけ。
$ docker build -t teitei/docker-chrome-stable:new . $ trivy image teitei/docker-chrome-stable:new 2020-08-07T00:39:12.168+0900 INFO Detecting Ubuntu vulnerabilities... teitei/docker-chrome-stable:new (ubuntu 16.04) ============================================== Total: 275 (UNKNOWN: 0, LOW: 244, MEDIUM: 31, HIGH: 0, CRITICAL: 0) +-----------------------+------------------+----------+---------------------------------------+---------------+------------------------------------------------+ | LIBRARY | VULNERABILITY ID | SEVERITY | INSTALLED VERSION | FIXED VERSION | TITLE | +-----------------------+------------------+----------+---------------------------------------+---------------+------------------------------------------------+ | bash | CVE-2019-18276 | LOW | 4.3-14ubuntu1.4 | | bash: when effective UID is | | | | | | | not equal to its real UID | | | | | | | the... | +-----------------------+------------------+ +---------------------------------------+---------------+------------------------------------------------ .... .... ....
これはDockerfileのBaseImageに ubuntu:16.04
と古いImageを利用しているためLOWとMEDIUMが消えていないが、先程に比べてHIGH
が0件になり、MEDIUMの検知数が415件減っていることがわかる。
CIへの組み込み
参考PR
今回はGitHubActionを利用してtrivyを実行している。trivyはtrivy-actionというGitHubActionに組み込むことが出来るActionを公開しているので、そちらを利用した。
更にGitHubActionはSchedule機能があるので、一定間隔にチェックすることも出来る。
その他のCI
CircleCIやTravisCIを使っている場合でもREADMEにサンプルがあるので、それを参考にすることですぐに組み込むことが出来る。
まとめ
- コンテナを作成してそのまま放置し続けるのは脆弱性を放置することになり危険。
- trivyを利用することで簡単にコンテナの脆弱性を検知することが出来る。
- trivyはREADMEも豊富に書かれているのと、SimpleでCIに組み込みやすいので、比較的導入がしやすい。
- OSのライブラリだけでなく、アプリケーション依存のライブラリの脆弱性検知も出来る。
ref
www.slideshare.net