Realize から Air に移行し、 Go(echo) + Air + docker-compose でホットリロードを利用して開発を行う。

この記事は Go 2 Advent Calendar 2020 の4日目の記事です。

tl;dr

  • Goでホットリロード(a.k.a Live reload)を利用する際には今までは Realize というライブラリを使っている人が多いが、開発が止まっている。
  • 代わりに Air というライブラリを利用してホットリロードを行う。

Go + docker-composeを利用してホットリロードを利用した開発を行いたいと検索をして一番多かったのがrealizeというライブラリだった。

github.com

が、開発が停滞しているようで、issueも溜まっており、go modの対応もされておらず、2020/10/25 現在の環境で新規に作ろうとすると、いくつもハマる点があった。

他にライブラリは無いかといくつか調べたところ、Airというライブラリが開発も頻繁にされており、star数も多く筋が良さそうだったので、こちらを利用することにした。

github.com

利用方法

github.com

Usageにもあるが、

  1. .air.toml ファイルを作成する
  2. repositoryの.air_example.toml をコピーペーストし、自分の環境に合わせる
  3. コマンドを実行する air

.air.tomlの設定について

repositoryにexampleがあるので、それを参考にする。

air/air_example.toml at 71d8bf298b426e631c7f51947e8d706246b4ff16 · cosmtrek/air · GitHub

設定ファイルもそんなに難しくなく、ある程度はわかると思う。特に重要な点は

  1. cmd
  2. bin

cmd は自分のGo Appをbuildするコマンドを記述する。 bincmd でbuildしたbinaryのpathを記述する

実際に利用するシーンを見たほうが早いので、GitHubにpushしたコードを参考に構築する。

github.com

ディレクトリ構成は下記

$ 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の構成は下記

go-next.js-playground/.air.toml at 0aaad99873a2b42821912c9d2c96cfa1260d8eaf · teitei-tk/go-next.js-playground · GitHub

$ 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

github.com

各種PR

github.com

github.com

階層構造(a.k.a ツリー構造・ディレクトリ構造・フォルダ)をDBでどう設計すべきか

SQLアンチパターン第2章を自分なりに噛み砕く。

階層構造とは

いわゆるTree Strcture(木構造)。

例として上げるのであれば、

ここではフォルダを想定する。

隣接リスト(Adjacency List)

Tree-Structure/adjacency_list at main · teitei-tk/Tree-Structure · GitHub

単純に考えると、各フォルダに親のフォルダを参照させるやり方が想定される。 しかし、このままだとフォルダ構成が深い場合、単一のSQLで取得することが難しい。実際に見てみる。

フォルダ構成は下記

f:id:teitei_tk:20201126221016p:plain

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であるDocumentsnsleft.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であるDocumentsnsleft.8の値が nsleftnsrightの間にあるすべてのノードを取得出来る。この例でいうと、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

f:id:teitei_tk:20201128185710p:plain

ノードを取得するクエリは入れ子集合よりも更に簡単になる。ID=2Downloads以下を取得するには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

先祖の取得も簡単で、先程のクエリのancestordescendantを逆に書くだけ。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=4Desktop以下にノードを挿入するには、まず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

となっていることがわかる。

ノードを削除する場合、例えば先程挿入した`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以下のツリーが削除されることがわかる。

結局どの設計を使うべきなのか

SQLアンチパターンに記述があるのでそちらを引用します

設計 子へのクエリ実行 ツリーへのクエリ実行 挿入 削除 参照整合性維持
隣接リスト 簡単 難しい 難しい 簡単 可能
経路列挙モデル 簡単 簡単 簡単 簡単 不可
入れ子集合モデル 難しい 難しい 難しい 難しい 不可
閉包テーブルモデル 簡単 簡単 簡単 簡単 可能

個人的な意見

仕様にもよりますが、消去法で閉包テーブル設計かなとなります。

隣接リスト

単純に筋が悪い設計だと考えている。要件がかなり簡易な場合(一つの親しか持たない)にはありかもしれないが、そのような仕様は時間が経つに連れて変わっていくし、すぐに新しく親を設計したいと出てくる。それを考えると別の手法を設計すべきと考える。

経路列挙

筋は良いが、経路列挙の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など静的コンテンツを置くのに適したプラットフォームを利用すると思うので、あまり意味はないかもしれない。

gist.github.com

それぞれ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

  • Pipeline Schedule を利用し一定周期にscanを実行することで開発を止めずに脆弱性を検知する。
  • Slackに投稿することで脆弱性の検知をメンバーの目に入れることができる。

Trivyについて

昔書いたのでそちらを参照してください。 teitei-tk.hatenablog.com

コードを見せろ

Sample Repository

gitlab.com

Merge Request

gitlab.com

工夫したところ

gitlab-ci.ymlは特殊な事は書いておらず、ほとんどGitHubと、GitLabのサンプルコードをコピーしただけです。

工夫した点としては、Trivyの検知した結果のレポートをJUnitのフォーマットにしています。 GitHubのサンプルコードではGitLabのContainer scanningフォーマットで書かれていますが、Container ScanningはGold/Ultimate Planでないと利用できないため、JUnitフォーマットを利用しています。

今回はあえて、サンプルのため脆弱性のあるRack Gemなど、古いgemを利用しています。

JUnitフォーマットを利用した場合、MergeRequestの画面ではこのように表示されます。 f:id:teitei_tk:20201003212704p:plain

CVE-2019-16782などが表示されていることが確認できると思います。 リンクをクリックすると、CVEの詳細が表示されます。

f:id:teitei_tk:20201003213625p:plain

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と表示されていることが確認できると思います。

f:id:teitei_tk:20201003213848p:plain

SlackへScan結果を通知する。

ここも特に難しいことはないです。SlackにてApps -> Incoming WebHooksを追加。WebHook URLをコピーし、GitLabRepositoryのSettings -> CI/CD -> Slack notificationに移動。

Pipelineにチェックを入れ、通知するSlack Channelを記入するだけ。

その後、Schedule Pipelineを実行すると、このようにSlackに通知されます。

f:id:teitei_tk:20201003214742p:plain

Trivy を利用した Docker Container・Image の脆弱性検知入門

tl;dr

  • trivyを利用することで簡単にDocker コンテナの脆弱性の検知をすることが出来る。
    • OSライブラリからアプリケーション依存のライブラリまで検知が可能
  • SimpleかつREADMEが豊富で導入の敷居が低い。
  • CIに組み込むことで DevOps から DevSecOps への移行を加速することが可能。

コンテナの脆弱性

2020/08/06 現在、Dockerを利用するしてプロダクトの環境を作る事がデファクトスタンダードになっている。 どの会社に行ってもセキュリティは最重要視されていると言っても過言ではないが、Docker イメージを生成 or 取得し、そのまま利用を続け、時間が経ち、利用しているライブラリに不具合が生じているのにも関わらず、知らないうちに脆弱性があるContainerを利用しているケースは少なくない。

脆弱性を放置して起きたインシデントは具体的に下記のような事例がある。

コンテナに仮想通貨の採掘を行うマルウェアイメージをダウンロードさせるキャンペーンが頻繁に行われており、ピーク時は 2,000以上の Dockerホストがマルウェア感染したとの報告もあります。

www.netone.co.jp

脆弱性の検知

コンテナの脆弱性検知を行うツールは複数ある。

Docker Bench Security

github.com

clair

github.com

anchore

anchore.com

今回はOSSのTrivyを利用する。

Trivyとは

AquaSectiry社が提供するOSS。特徴として、A Simple and Comprehensive Vulnerability Scanner for Containers, Suitable for CI と書かれているように非常にシンプルかつ、とても簡単にCIに組み込むことが出来る。

github.com

製作者は日本の方で、OSSで作成したものをAquaSecutiry社が買収し、そのまま就職をして仕事でOSS開発をするというエンジニアには夢のような記事を書かれていたことを覚えている。

knqyf263.hatenablog.com

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を利用する。

github.com

hub.docker.com

$ 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に脆弱性がある。

github.com

$ 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

github.com

今回はGitHubActionを利用してtrivyを実行している。trivyはtrivy-actionというGitHubActionに組み込むことが出来るActionを公開しているので、そちらを利用した。

github.com

更にGitHubActionはSchedule機能があるので、一定間隔にチェックすることも出来る。

docs.github.com

その他のCI

CircleCIやTravisCIを使っている場合でもREADMEにサンプルがあるので、それを参考にすることですぐに組み込むことが出来る。

まとめ

  1. コンテナを作成してそのまま放置し続けるのは脆弱性を放置することになり危険。
  2. trivyを利用することで簡単にコンテナの脆弱性を検知することが出来る。
  3. trivyはREADMEも豊富に書かれているのと、SimpleでCIに組み込みやすいので、比較的導入がしやすい。
  4. OSのライブラリだけでなく、アプリケーション依存のライブラリの脆弱性検知も出来る。

ref

github.com

www.slideshare.net

qiita.com