20190521のdockerに関する記事は13件です。

Docker を使う(docker build でオリジナルのイメージを作成する)

前回の記事 の続きです。
今回は、 docker build を使っていきたいと思います。
とは言っても、一から説明するととても大変なので、事例を紹介しつつ、かいつまんで説明しようと思います。

何ができるのか?

要するに、オリジナルの docker イメージを作ります。
Docker Hub では、たくさんのイメージが公開されています。
当然それらを使うのですが、公開されている docker イメージをそのまま使っても、自分が書いたソースコードは docker イメージ内にないのでそのままでは動かせないわけです。

そこで、自分のソースコードをあらかじめ埋め込んだイメージを作成し、そのイメージでコンテナを起動することによって、高速に、何度でも同じ環境を構築できる、ということですね。

docker build コマンド

ここでようやく、 docker build コマンドが出てきます。
ただ、 docker build をするためには Dockerfile というファイルが必要になります。
この Dockerfile 内に、作成するイメージをどう構築するかを記述します。

今回のテーマ

今回は、 CentOS のイメージに httpd をインストールして Web サーバーとして起動したいと思います。
本当はそんなことしないとは思いますが・・・
いいサンプルが思いつかず、こうなりました。

で、今回用意するのは以下の3つです。

  • Dockerfile
  • entrypoint.sh
  • index.html

順を追って説明します。

Dockerfile

docker build を使う際に必須のものです。
ファイル名はこれじゃなくてもいいのですが、デフォルトがこの名称なのでこのままいきます。
詳しい説明は自分もあまり理解していないためできないので、参考書を購入し勉強しましょう・・・

今回使用する Dockerfile の中身はこちら。

# ベースイメージの指定
FROM centos:centos7.5.1804
# ディレクトリ作成を実行
RUN mkdir /var/html > /dev/null 2>&1
# httpd のインストール
RUN yum install -y httpd
# ファイルのコピー
COPY index.html /var/html
COPY entrypoint.sh /usr/bin
# docker のネットワーク上でコンテナのポートを開放
EXPOSE 8000
# docker run 時に絶対に実行されるコマンドを定義
ENTRYPOINT ["bin/bash", "/usr/bin/entrypoint.sh"]
# docker run 時の引数でコマンドを省略した時に実行されるコマンドを定義
CMD ["/usr/sbin/httpd", "-D", "FOREGROUND"]

なるべく色んなものを詰め込んでみました。
分かる範囲で説明します。

FROM

まあ、書いてある通りです。
Docker Hubで公開されているイメージを基にしています。
常に最新のものを使用する場合は
FROM centos:latest と書きますが、そんなことをする人はいないと思います。
ベースのバージョンが変わるとその後に実行するものが動かなくなる可能性がありますからね。

RUN

ビルド時に実行されるものです。
ここが間違っていたりするとイメージは作成できません。
なお、上記の例では Shell 形式で記述しています。
この形式で記述すると、 /bin/sh で実行されるそうです。

COPY

ファイルをコンテナ内に配置する際に使用します。
上記の書き方だと、 docker build を実行したカレントディレクトリに entrypoint.sh と index.html が存在している必要があります。

EXPOSE

コンテナのポートを開放します。
このポートを介してコンテナと通信をします。
ここでは、 8000 番ポートを開けています。

ENTRYPOINT

イメージ作成後、実際に docker run でコンテナを起動する際に実行されるコマンドです。
上記の例では、 Exec 形式で記述しています。
この形式では、実行したいコマンドを JSON 配列で指定します。
また、シェルを介さずに直接実行されるそうです。
そのため上記では /bin/bash を用いて entrypoint.sh を実行するように記載しています。

CMD

イメージ作成後、実際に docker run でコンテナを起動する際に実行されるコマンドです。
ENTRYPOINT と異なる点は、こちらのコマンドは docker run 時に引数でコマンドを指定すると、 CMD で指定したコマンドは実行されない点です。

次は entrypoint.sh の説明です。

entrypoint.sh

Dockerfile 内で呼び出して実行しているシェルですが、中身は以下のように書いています。

#!/bin/bash
set -e

conf () {
    echo "ServerRoot \"/etc/httpd\""
    echo "Listen 8000"
    echo "ServerName `hostname`"
    echo "Include conf.modules.d/*.conf"
    echo "User apache"
    echo "Group apache"
    echo "ServerAdmin root@localhost"
    echo "<Directory />"
    echo "    AllowOverride none"
    echo "    Require all denied"
    echo "</Directory>"
    echo "DocumentRoot \"/var/html\""
    echo "<Directory \"/var/www\">"
    echo "    AllowOverride None"
    echo "    Require all granted"
    echo "</Directory>"
    echo "<Directory \"/var/html\">"
    echo "    Options Indexes FollowSymLinks"
    echo "    AllowOverride None"
    echo "    Require all granted"
    echo "</Directory>"
    echo "<IfModule dir_module>"
    echo "    DirectoryIndex index.html"
    echo "</IfModule>"
    echo "<Files \".ht*\">"
    echo "    Require all denied"
    echo "</Files>"
    echo "ErrorLog \"logs/error_log\""
    echo "LogLevel warn"
    echo "<IfModule log_config_module>"
    echo "    LogFormat \"%h %l %u %t \\\"%r\\\" %>s %b \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\"\" combined"
    echo "    LogFormat \"%h %l %u %t \\\"%r\\\" %>s %b\" common"
    echo "    <IfModule logio_module>"
    echo "      LogFormat \"%h %l %u %t \\\"%r\\\" %>s %b \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\" %I %O\" combinedio"
    echo "    </IfModule>"
    echo "    CustomLog \"logs/access_log\" combined"
    echo "</IfModule>"
    echo "<IfModule alias_module>"
    echo "    ScriptAlias /cgi-bin/ \"/var/www/cgi-bin/\""
    echo "</IfModule>"
    echo "<Directory \"/var/www/cgi-bin\">"
    echo "    AllowOverride None"
    echo "    Options None"
    echo "    Require all granted"
    echo "</Directory>"
    echo "<IfModule mime_module>"
    echo "    TypesConfig /etc/mime.types"
    echo "    AddType application/x-compress .Z"
    echo "    AddType application/x-gzip .gz .tgz"
    echo "    AddType text/html .shtml"
    echo "    AddOutputFilter INCLUDES .shtml"
    echo "</IfModule>"
    echo "AddDefaultCharset UTF-8"
    echo "<IfModule mime_magic_module>"
    echo "    MIMEMagicFile conf/magic"
    echo "</IfModule>"
    echo "EnableSendfile on"
    echo "IncludeOptional conf.d/*.conf"
}
# 既存のコンフィグを書き換え
conf > /etc/httpd/conf/httpd.conf

exec "$@"

別にシェルで書く必要はないのですが・・・
httpd.conf を書き換えるためのシェルです。
シェルでやることのメリットとしては、内部で hostname のコマンド結果を使って動的に ServerName を変更できることですかね・・・
もっといいシチュエーションがあると思いますが、今回はこんなこともできるよ、という紹介です。
Listen だけ、 8000 番ポートを指定するように変更しています。
これは、コンテナで開放するポートを 8000 番と指定しているので、合わせてあります。

また、最後に exec "$@" と記載していますがこれがミソで、実際にビルドしたイメージを使って docker run を実行した際、 Dockerfile に記載した通りだとすると以下のようなコマンドでコンテナが起動します。

bin/bash /usr/bin/entrypoint.sh /usr/sbin/httpd -D FOREGROUND

そう、 ENTRYPOINT と CMD はどちらも実行されるので、横並びに実行されてしまいます。
そのため、 entrypoint.sh の引数として /usr/sbin/httpd -D FOREGROUND を受け取り、これをシェルの中で実行するような形としています。
他にもやりようはあるのかもしれませんが、なぜかこのように書いています。
一般的ではないのかも。。。
あくまで ENTRYPOINT を使う例として無理矢理シェルを実行しているための処置だと思ってください。

index.html

こちらはただ自前で用意しただけの HTML です。
entrypoint.sh で DocumentRoot を変更しているので、そこに合わせて配置しているだけにすぎません。
今回の例では、中身は以下のようにしてみました。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
</head>
<body>
    <h1>こんにちは、世界!</h1>
</body>
</html>

ビルドしてみる

ではでは、3つのファイルを用意したので実際にビルドしてみましょう。
/var/docker/centos 配下にファイルを配置しているものとして説明します。
こんな感じ。

[root@localhost centos]# pwd
/var/docker/centos
[root@localhost centos]# ls
Dockerfile  entrypoint.sh  index.html

コマンドを実行します。

[root@localhost centos]# docker build -t testcent .

-t は、名前とタグを決めます。
書式は name:tag となります。今回は testcent という名前にしました。
タグは指定していないので、 latest に勝手になります。
最後のドット . は、カレントディレクトリにある Dockerfile を使うときにこうなります。
Dockerfile の場所が異なる場合は、そのディレクトリを指定します。
なお、ファイル名が Dockerfile ではない場合は、 -f オプションを使って指定すればよいはずです。
(試してはいないです)

上手くできれば、最後に Successfully うんちゃらと出ているはず。
ここで、 docker images を実行してみます。

[root@localhost centos]# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
testcent            latest              f7e4a3bceea6        3 minutes ago       326MB
centos              centos7.5.1804      cf49811e3cdb        2 months ago        200MB

ベースとなった centos の docker イメージと、自分でビルドした testcent があることが分かります。

ビルドしたイメージでコンテナを起動してみる

以下のようにコマンドを実行します。

[root@localhost centos]# docker run -d -p 80:8000 testcent

-d オプションは、バックグランドでコンテナを実行し続けます。
-p {ホストのポート}:{コンテナのポート} と指定することで、ホストのポートへのアクセスをコンテナのポートに飛ばすことができます。
Dockerfile でコンテナに 8000 番ポートをあけるように記載しているので、ホストの 80 番ポートをコンテナの 8000 番ポートに繋げました。

コンテナの状態を確認してみます。

[root@localhost centos]# docker ps --no-trunc
CONTAINER ID                                                       IMAGE               COMMAND                                                           CREATED             STATUS              PORTS                  NAMES
2570dda5f952162c34eb397ff8f934c23cb0ebf7b603dde166bad54150c6b22d   testcent            "bin/bash /usr/bin/entrypoint.sh /usr/sbin/httpd -D FOREGROUND"   4 minutes ago       Up 4 minutes        0.0.0.0:80->8000/tcp   cranky_blackwell

--no-trunc オプションで省略せずに表示させているため横長ですが・・・
Dockerfile で指定した ENTRYPOINTCMD が実行されていますね。
この状態で、 localhost:80 へアクセスしてみましょう。

[root@localhost centos]# curl http://localhost:80
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
</head>
<body>
    <h1>こんにちは、世界!</h1>
</body>
</html>

ホストの 80 番ポートはコンテナの 8000 番ポートへ飛び、8000 番ポートで httpd が Listen しているので、 index.html を表示することができました。

最後に

長い間下書き状態で放置していたので、端折ってしまっている部分があるかも・・・
また、 httpd が分からないと難しい部分もあったかもしれません。
正直なところ、自分で LAMP 環境を構築できる程度の知識は必要です。
ちゃんと学ぼうと思ったら、 Virtualbox に CentOS の ISO イメージファイルを使って一からインストールをしてみたり yum install で httpd を入れてみたり、 iptables やら SELinux やら、 systemctl やら firewalld やら、ifconfig やら ip addr show やら、よく使うような基本的なところは勉強しておきたいですね。
自分もそんなにやってませんが・・・(´・ω・`)

次回も記事を書こうかと思いますが、今度は Docker Compose か Docker Swarm あたりにしようかと思います。
もう次からは超絶難しくなるので、 Docker Compose はすっ飛ばして Docker Swarm にするかも。
Kubernetes は・・・職場で使ってないので使ったことがなく・・・
Docker Swarm の記事が書けたら、 Kubernetes を勉強してみたいなぁと思ってます。

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

Android開発者のためのOCR入門(tess-two編)

はじめに

AndroidアプリでOCRを実装する際、TessaractのAndroid用ラッパーライブラリtess-twoを利用するか、またはML Kit for Firebaseを利用する方法があります。
今回は、前者のtess-twoを使ってギャラリーから画像を選択してその画像にある文字を読み取るアプリを作ってみたいと思います。

サンプルコードはここに置いてあります。

インストール

まずはtess-twoを入れます。また、画像をギャラリーから読み込む際にExifを読み取って画像の向きを正しい方向に直したいのでexifinterfaceも入れておきます。

app/build.gradle.kts
// 略
dependencies {
    implementation("com.rmtheis:tess-two:9.0.0")
    implementation("androidx.exifinterface:exifinterface:1.0.0")
}

続いてappディレクトリ以下に/assets/tessdata/ディレクトリを作成して言語データを配置します。
今回はこんな感じにしました。

app - assets - tessdata
                  |-- jpn.traineddata
                  `-- eng.traineddata

言語データは下記からダウンロードしてください。tess-twoはtesseract3.0.5以下で作成された言語データが必要です。
https://github.com/tesseract-ocr/tessdata/tree/3.04.00

実装

tess-twoの使い方自体は非常に簡単です。わずか5行程度です。

OCRUtil.kt
        val baseApi = TessBaseAPI()
        // initで言語データを読み込む
        baseApi.init(context.getFilesDir().toString(), "jpn")
        // ギャラリーから読み込んだ画像をFile or Bitmap or byte[] or Pix形式に変換して渡してあげる
        baseApi.setImage(bitmap)
        // これだけで読み取ったテキストを取得できる
        val recognizedText = baseApi.utF8Text
        baseApi.end()

ですが注意点が2つあります。

1つ目はbaseApi.initはの中では、言語データをFileでopenしているためFileで読み込める場所にコピーしてあげる必要があります。

OCRUtil.kt
    private val TESS_DATA_DIR = "tessdata" + File.separator
    private val TESS_TRAINED_DATA = arrayListOf("eng.traineddata", "jpn.traineddata")
    private fun copyFiles(context: Context) {
        try {
            TESS_TRAINED_DATA.forEach {
                val filePath = context.filesDir.toString() + File.separator + TESS_DATA_DIR + it

                // assets以下をinputStreamでopenしてbaseApi.initで読み込める領域にコピー
                context.assets.open(TESS_DATA_DIR + it).use {inputStream ->
                    FileOutputStream(filePath).use {outStream ->
                        val buffer = ByteArray(1024)
                        var read = inputStream.read(buffer)
                        while (read != -1) {
                            outStream.write(buffer, 0, read)
                            read = inputStream.read(buffer)
                        }
                        outStream.flush()
                    }
                }

                val file = File(filePath)
                if (!file.exists()) throw FileNotFoundException()
            }
        } catch (e: FileNotFoundException) {
            e.printStackTrace()
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }

2つ目はtess-twoで画像を読み込む場合、画像の向きが正しくないと当然読み込めません。ギャラリーから読み込む場合、画像が回転してしまっている場合があるのでExifを見て正しい方向に直す必要があります。

MainActivity.kt
    // 画像のuri
    uri?.let {
        contentResolver.openFileDescriptor(it, "r").use {parcelFileDescriptorNullable ->
            parcelFileDescriptorNullable?.let {parcelFileDescriptor ->
                val fileDescriptor = parcelFileDescriptor.fileDescriptor
                bitmapOrigin = BitmapFactory.decodeFileDescriptor(fileDescriptor)
                contentResolver.openInputStream(it).use {
                    it?.let {
                        // ExifInterfaceを初期化
                        val exifInterface = ExifInterface(it)
                        val orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)
                        val degrees = when (orientation) {
                            // 正しい方向の場合は回転させない
                            1 -> { 0f }
                            // 逆向きなので180度回転させる
                            3-> { 180f }
                            // 左向きの画像になってるので90度回転させる
                            6 -> { 90f }
                            // 右向きの画像になってるので270度回転させる
                            8 -> { 270f }
                            else -> { 0f }
                        }
                        val matrix = Matrix()

                        val imageWidth = bitmapOrigin?.getWidth() ?: 0
                        val imageHeight = bitmapOrigin?.getHeight() ?: 0
                        matrix.setRotate(degrees, imageWidth.toFloat() / 2, imageHeight.toFloat() / 2)
                        bitmap = Bitmap.createBitmap(bitmapOrigin!!, 0, 0, imageWidth, imageHeight, matrix, true)
                    }
                }
            }
        }
    }

ここまで実装したら早速動かしてみましょう。今回はこちらの画像から文字を読み取ってみます。

早速アプリを起動して画像を読み込んでみると…

無事、文字を読み取ることが出来るようになりました!が、、精度がいまいちです…

日本語言語データをカスタマイズ

先程の画像を見てみると数字が全然読み取れていないことに気がつくと思います。今回はこの数字の精度を改善してみたいと思います。

数字の誤変換の原因ですが、こちらのサイトを見てみると認識結果の変換マッピングが原因のようです。なのでjpn.unicharambigsを修正してみます。

今回、tesseract-ocrの環境をDockerで用意しました。こちらの環境を使ってカスタマイズします。
https://github.com/tarumzu/OCRSampleAndroid/tree/master/containers

まずは下記の手順でDockerコンテナにログイン

cd containers
docker-compose build
docker-compose up

docker-compose exec tesseract bash

ログインしたら、jpn.traineddataをダウンロード、次にcombine_tessdataコマンドのeオプションでjpn.traineddataからjpn.unicharambigsを取り出します。

wget https://github.com/tesseract-ocr/tessdata/raw/3.04.00/jpn.traineddata
combine_tessdata -e jpn.traineddata jpn.unicharambigs

取り出したらjpn.unicharambigsをvimで開いて下記の行を削除します。これは左側のキャラクタを右側のものに変換するというルールなのですが正直必要ない変換だと思います。

1   l   1   ー 1
1   | 1   ー 1
1   I   1   ー 1
1   1   1   ー 1
1   |   1   ー 1
1   O   1   。 1
1   °  1   。 1

ここまでできたらjpn.traineddataを再作成します。

combine_tessdata -o jpn.traineddata jpn.unicharambigs

では、先程作成した言語データはcontainersに保存されます。この言語データを使って再度画像を読み込んでみましょう。

たったこれだけで数字の精度が劇的に改善できました!

終わりに

簡単にOCRが実装できましたが言語データのカスタマイズは必須ですね…
次はML Kit for FirebaseでOCRを実装してみて違いなどを記事にしてみたいと思います。

参考

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

Dockerfileの作成の留意点備忘録

はじめに

急遽Dockerが必要になった。
体系的なまとめては度返しして、とにかく今得た内容を残す。

留意点殴り書き

  • apt-get時は-yを忘れずに
  • RUN cd xxxは次のRUNへ持ち越せない
    • RUN cd /home/hoge && yyyとするかWORKDIR /home/hogeとする
  • RUN lsを使えばbuild失敗時も対象コンテナのファイルシステムを観察できる
  • RUNの途中に# コメントを挟むと警告が出るが動作はする
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

まじか! docker の volume にパスの長さが影響するなんて (gitlab)

結論

  • gitlab を docker で立ち上げる時、 --volume のパスの長さが、長過ぎる時は気をつけろ。

正常起動

  • 正常起動とエラーとの違いは、 volume のパス。
    • 正常起動: ${HOME}/Downloads
    • エラー: ${PWD}
  • HOME と PWD を展開した時の長さが全然違う。(今回の場合の桁数)
    • 正常起動(HOME): 14 バイト
    • エラー(PWD): 83 バイト
docker run --detach \
  --hostname localhost \
  --publish 10080:80 \
  --publish 10022:22 \
  --name gitlab \
  --restart always \
  --volume ${HOME}/Downloads/gitlab/config:/etc/gitlab \
  --volume ${HOME}/Downloads/gitlab/logs:/var/log/gitlab \
  --volume ${HOME}/Downloads/gitlab/data:/var/opt/gitlab \
  gitlab/gitlab-ce:latest

エラー

docker run --detach \
  --hostname localhost \
  --publish 10080:80 \
  --publish 10022:22 \
  --name gitlab \
  --restart always \
  --volume ${PWD}/gitlab/config:/etc/gitlab \
  --volume ${PWD}/gitlab/logs:/var/log/gitlab \
  --volume ${PWD}/gitlab/data:/var/opt/gitlab \
  gitlab/gitlab-ce:latest
  • エラー内容
Error executing action `run` on resource 'bash[migrate gitlab-rails database]'
There was an error running gitlab-ctl reconfigure:

bash[migrate gitlab-rails database] (gitlab::database_migrations line 51) had an error: Mixlib::ShellOut::ShellCommandFailed: Expected process to exit with [0], but received '1'
---- Begin output of "bash"  "/tmp/chef-script20190521-24-51lpum" ----
STDOUT: rake aborted!
PG::ConnectionBad: could not connect to server: No such file or directory
        Is the server running locally and accepting
        connections on Unix domain socket "/var/opt/gitlab/postgresql/.s.PGSQL.5432"?
/opt/gitlab/embedded/service/gitlab-rails/lib/tasks/gitlab/db.rake:55:in `block (3 levels) in <top (required)>'
/opt/gitlab/embedded/bin/bundle:23:in `load'
/opt/gitlab/embedded/bin/bundle:23:in `<main>'
Tasks: TOP => gitlab:db:configure
(See full trace by running task with --trace)
STDERR: 
---- End output of "bash"  "/tmp/chef-script20190521-24-51lpum" ----
Ran "bash"  "/tmp/chef-script20190521-24-51lpum" returned 1
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerはコンテナ内プログラムの終了ステータスもホスト側に渡してくれるよというお話

タイトルの通りです。実際に確認してみましょう。
Pythonのコンテナを立ち上げて、終了ステータスを114に設定して終了させてみます。

$ docker run -it python  # pythonコンテナを起動
Python 3.7.3 (default, May  8 2019, 05:28:42) 
[GCC 6.3.0 20170516] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> exit(114)  # 終了ステータスを114に設定してPythonを終了させる


$ echo $?  # 終了ステータスを見てみる
114

無事にホスト側で終了させるを確認してみると、114となっていますね。

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

フロントエンドでもtrivyを使って脆弱性対策したい!

伝えたいこと

  • フロントエンドもコンテナ経由でアクセスさせよう
  • コンテナならtrivyを使ってヤバそうな脆弱性を知ることができる
  • trivyならCIに組み込むことも簡単
  • multi-stage buildの場合は工夫が必要
  • いまの状況でできる範囲の脆弱性対策からはじめよう

今回の記事でできること

trivy_flow.png

CI上で、本番用のフロントエンドのコンテナをmulti-satge buildでつくり、
trivyを利用して脆弱性を検知するところまでを目指します。

最後に、trivyを利用したレベル別の運用イメージも提案します。

導入

週末、trivyというコンテナ向けの脆弱性検知ツールが正式リリースされました。公開が5日目の5/21 12:00時点で 900star以上獲得しています。

trivyの詳細は原作者である @knqyf263 さんの「CIで使えるコンテナの脆弱性スキャナ」という記事を参照ください。

ただフロントエンドの方は「脆弱性はインフラ側が対応してくれるから、自分には関係ない」と思っていないでしょうか。
そのような方をメインに、今回は以下の内容を紹介します。

  • コンテナを使ってフロントエンドの本番環境を構築する
  • trivyを利用して、脆弱性が含まれていないかチェックする
  • trivyを通じた脆弱性の運用方法を学ぶ

僕は普段はフロントエンドも担当するエンジニアで、セキュリティに対して体系だって学んだことはありません。ただ、trivyのコミッタでもあり、脆弱性の運用も少し知っているので、フロントエンドでもDevSecOpsを定着させるきっかけになるといいなと考えています。

ソースコード

今回使うコードは、すべて以下のレポジトリにあります。

https://github.com/tomoyamachi/trivy-with-react
reactrivy.png

この記事では触れませんが、開発環境でもコンテナ経由でホットリロードするような仕組みになっています。

実践 : プロジェクト作成~trivyの導入

ここからはプロジェクトの作成と、CIにtrivyを組み込むまでの手順を書きます

1. プロジェクトの作成

今回はCreate React Appを利用して、Reactのプロジェクトをまず作成します。
そして、デフォルトではパッケージ管理ツールにyarnが利用されているので、今回はプロジェクトのパッケージ管理をnpmで実行します。

$ npx create-react-app trivy-with-react
$ cd trivy-with-react
$ rm -fr node_modules yarn.lock
$ npm install

2. 本番用コンテナの作成

  1. 本番用のコードを生成
  2. nginxコンテナに生成したコードを移し、nginxを起動

という手順をとります。

通常であれば1ファイル

そして通常2つのステップは、multi-stage buildを利用して、1つのDockerfileでコンテナを作成します。

Dockerfile
# build environment
FROM node:12.2.0-alpine as build
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
COPY package.json /app/package.json
RUN npm install --silent
RUN npm install react-scripts@3.0.1 -g --silent
COPY . /app
RUN npm run build

# production environment
FROM nginx:1.16.0-alpine
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

from Dockerizing a React App

1ファイルでmulti-stage buildの問題点

multi-stage buildは1つのDockerfileで最終成果物のimageだけを出力してくれるので、通常はベストプラクティスです。
その場合、以下のように、builder側のイメージがIMAGE IDでしか特定できない形で作成されているのがわかります。

$ docker images
REPOSITORY         TAG    IMAGE ID ...
trivy-with-react   test   96c4fdfac287 # nginx側
<none>             <none> 41fd97016d25 # builder側

しかし、今回、脆弱性はbuilder側のコンテナにもあることに気がついた方もいるでしょう。package-lock.json もそうですし、一般的ではないですが、Static Linkでビルドする場合も考えられます。

trivyはREPOSITORY:TAGの形式でコンテナイメージを指定してスキャンするので、multi-stage buildにしてしまうと、builder側の脆弱性が放置されてしまいます。

そのため、builder側と、nginx側のコンテナイメージを独立させて、参照可能にしたいと思います。具体的には、Dockerfileを分けることで、それぞれのイメージにタグを付けます。
また、2回コマンド実行するのも微妙なのでmakefileを作成します。

※ もしも1つのDockerfileでmulti-stage buildをし、builder側のイメージ名を指定することができるのであれば、切実に情報がほしいです!

trivy用の構成

コードを見てもらえばわかりますが、Dockerfileをbuilder用(Dockerfile.builder)と、nginx用(Dockerfile.prod)に分けて、makefileは以下のようにしています。

Dockerfile.builder
FROM node:12.2.0-alpine as builder
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
COPY package.json /app/package.json
RUN npm install --silent
RUN npm install react-scripts@3.0.1 -g --silent
COPY . /app
RUN npm run build
Dockerfile.prod
ARG tag
FROM local-builder:${tag} as builder

FROM nginx:1.16.0-alpine
ARG tag
COPY --from=builder /app/build /usr/share/nginx/html
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx/nginx.conf /etc/nginx/conf.d
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
makefile
.PHONY: build all

TAG := $(shell git rev-parse HEAD)

all: build
build:
    docker build . -f Dockerfile.builder -t local-builder:$(TAG)
    docker build . --build-arg tag=$(TAG) -f Dockerfile.prod -t built:$(TAG)

このため、以下のようにデフォルトではコミットハッシュごとにコンテナを作成し、変数TAGが指定されていればそれを利用します。

$ make build  # 最新のコミットハッシュをコンテナのタグに利用
$ make build TAG=hoge # hogeをコンテナのタグに指定

実際のレポジトリでは、SPA用にnginx.confを置いたりしているので、必要な方はご確認ください。

1つのDockerfileで、同様のことができる方法をご存知の方いたら教えてください...

ビルドしたコンテナのテスト

では、さっそくビルドしたコンテナにアクセスしてみましょう。

$ make build TAG=test
$ docker run -p 8000:80 built:test

以上を実行し、 http://localhost:8000 を開くと、画面が表示されることが確認できました。

localhost.png

ついでに、nginxでSPA対応できているかを確認するため、 http://localhost:8000/spa/test が開けることを確認します。

localhost_spa

3. trivyでスキャン

では、builder側とnginx側をスキャンしましょう。
trivyはインストールしてある前提ですが、makefileに以下のコードを追加します。

buildと同じようにTAGを指定することで、指定したイメージをスキャンできるようになります。

ci-scan:
    trivy --exit-code 1 --severity HIGH,CRITICAL  --quiet --auto-refresh local-builder:$(TAG)
    trivy --exit-code 1 --quiet --auto-refresh built:$(TAG)

--exit-codeオプションで脆弱性を検知した場合の終了コードを指定できるので、スキャンはしたいけど、CIは止めたくない人は --exit-code 0 (デフォルトなので省略可) を利用しましょう。

4. CircleCIで実行

CircleCIの場合は以下のようにします。
trivyの脆弱性データの取得が重いので、脆弱性データなどはキャッシュに保存すると2回目以降のスキャンが高速になります。

version: 2
jobs:
  build:
    docker:
      - image: docker:18.09-git
    steps:
      - checkout
      - setup_remote_docker
      - restore_cache:
          key: vulnerability-db
      - run:
          name: Build image
          command: |
            apk add --update --no-cache make curl
            make build TAG=${CIRCLE_SHA1}
      - run:
          name: Install latest trivy
          command: |
            VERSION=$(
              curl -I https://github.com/knqyf263/trivy/releases/latest | \
              grep -o '/tag/v[0-9]\+.[0-9]\+\.[0-9]\+' | \
              sed -E 's:/tag/v([0-9\.]+):\1:'
            )
            wget https://github.com/knqyf263/trivy/releases/download/v${VERSION}/trivy_${VERSION}_Linux-64bit.tar.gz
            tar zxvf trivy_${VERSION}_Linux-64bit.tar.gz
            mv trivy /usr/local/bin
      - run:
          name: Scan local images with trivy
          command: |
            make ci-scan TAG=${CIRCLE_SHA1}
      - save_cache:
          key: vulnerability-db
          paths:
            - $HOME/.cache/trivy
workflows:
  version: 2
  release:
    jobs:
      - build

正常終了の場合、以下のようになります。
builder側とnginx側のどちらにも脆弱性がなかったということがわかりました。

ci-success.png

脆弱性があった場合CIを止めたい

脆弱性が見つかった場合に、CIを止めたい場合は--exit-code 1オプションを利用します。このオプションにより、脆弱性が見つかった場合、異常終了(終了コードが1)になります。
また、 severityオプションを利用することで、検知したい脆弱性の危険度を指定できます。

trivy --exit-code 1 --severity HIGH,CRITICAL \
  --quiet --auto-refresh \ 
  local-builder:$(TAG)

この状態で脆弱性のあるパッケージを追加して結果を見ると、CIが失敗することが確認できました!

ci-error.png
https://circleci.com/gh/tomoyamachi/trivy-with-react/8

trivyを使った運用スタイル

さて、では脆弱性が見つかった場合、どのようにすればよいでしょうか?

こうしたほうがいい、というのはありますが、それぞれの運用方針にそって決めればいいと思います。

以下に、「とりあえず導入」「ちょっと気になる」「がんばる」のコースとそれぞれによさそうな運用方針をまとめました。

とりあえず導入 ちょっと気になる がんばる
検知時のCI とめない(--exit-code 0) とめる(--exit-code 1) とめる(--exit-code 1)
対応方針 対応せずSlackなどに通知 バージョンアップして、駄目なら無視する (.trivyignore) 脆弱性を調べて、必要なら対応
検知タイミング ビルド時 ビルド時 ビルド時&定期実行

全く興味なくても、導入するだけ導入したい、という人がいてもいいと思うので、できる範囲ですすめられるといいかなと思います。

以下は、「とりあえず導入」以上の興味がある人に対しての方法論になります。

1. 脆弱性を調べ、本当にCRITICALかをチェック

まず、その脆弱性が自分のアプリケーションにとって本当に問題になるのかを理解しましょう。
NVDなどで脆弱性の内容を調べる方法もあるし、まわりの詳しい人に聞くのもありです。
自分で調べる場合、脆弱性が危険かどうかの判断は このスライドの「トリアージ」以降の章が役立つかと思います。

2. 重要ではない場合は、その脆弱性を無視する

プロジェクトに .trivyignore を作成し、無視したい脆弱性ID(以下、VULNERABILITY ID)を記載すると、そのIDを無視してくれます。
さきほどの例でいうと、以下のように指定すれば、それ以降同じVULNERABILITY IDは検知されなくなります。

.trivyignore
NSWG-ECO-328
CVE-2019-5428
CVE-2019-11358

この対応で先程停止していたワークフローがとおるようになったのがわかります。
https://circleci.com/gh/tomoyamachi/trivy-with-react/10

3. 重要な場合は...

重要な場合、バージョンアップをすれば良いと思うのですが、そうではない場合、自分で修正したコードを利用する方法などもあります。

アプリケーションパッケージの場合

たとえば、npmのパッケージであれば、 この記事の方法で でforkした自分のレポジトリを入れることが可能です。

ただし、その場合、バージョン情報が以下のようになり、そのパッケージの脆弱性チェックができなくなる点に留意ください。

"jquery": {
      "version": "git+https://<token>:x-oauth-basic@github.com/tomoyamachi/jquery.git#438b1a3e8a52d3e4efd8aba45498477038849c97",
      "from": "git+https://<token>:x-oauth-basic@github.com/tomoyamachi/jquery.git"
    },

OSのパッケージの場合

検出されるパッケージバージョン自体は変わらないので、patchfileを当てるコマンドを実行するのと同時に、.trivyignore に対象のVULNERABILITY IDを追加してください。

まとめ

今回は、以下のことを書きました。

  • フロントエンドをコンテナで運用する
  • trivyで脆弱性対策できる
  • 脆弱性のおおまかな運用方法

なお、コンテナになっていない場合は、Vulsという素晴らしい脆弱性スキャナがあるので、こちらをご利用ください。動作中のコンテナもスキャン可能です。
すこし導入に準備が必要ですが、あなたのサーバは本当に安全ですか?今もっともイケてる脆弱性検知ツールVulsを使ってみた を参考にお試しください。

謝辞

記事を書く際に、 @knqyf263 さん、 @sadayuki-matsuno さん、 @codehex さんにご意見をいただきました。ありがとうございました!!

以上です。

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

フロントエンドでもtrivyを使って脆弱性対策したい!

伝えたいこと

  • フロントエンドもコンテナ経由でアクセスさせよう
  • コンテナならtrivyを使ってヤバそうな脆弱性を知ることができる
  • trivyならCIに組み込むことも簡単
  • multi-stage buildの場合は工夫が必要
  • いまの状況でできる範囲の脆弱性対策からはじめよう

今回の記事でできること

trivy_flow.png

CI上で、本番用のフロントエンドのコンテナをmulti-satge buildでつくり、
trivyを利用して脆弱性を検知するところまでを目指します。

最後に、trivyを利用したレベル別の運用イメージも提案します。

導入

週末、trivyというコンテナ向けの脆弱性検知ツールが正式リリースされました。公開が5日目の5/21 12:00時点で 900star以上獲得しています。

trivyの詳細は原作者である @knqyf263 さんの「CIで使えるコンテナの脆弱性スキャナ」という記事を参照ください。

今回は、フロントエンドをメインに、今回は以下の内容を紹介します。

  • コンテナを使ってフロントエンドの本番環境を構築する
  • trivyを利用して、脆弱性が含まれていないかチェックする
  • trivyを通じた脆弱性の運用方法を学ぶ

僕も、普段はフロントエンドも担当するエンジニアで、セキュリティに対して体系だって学んだことはありません。ただ、trivyのコミッタでもあり、脆弱性の運用も少し知っているので、フロントエンドでもDevSecOpsを定着させるきっかけになるといいなと考えています。

ソースコード

今回使うコードは、すべて以下のレポジトリにあります。

https://github.com/tomoyamachi/trivy-with-react
reactrivy.png

この記事では触れませんが、開発環境でもコンテナ経由でホットリロードするような仕組みになっています。

実践 : プロジェクト作成~trivyの導入

ここからはプロジェクトの作成と、CIにtrivyを組み込むまでの手順を書きます

1. プロジェクトの作成

今回はCreate React Appを利用して、Reactのプロジェクトをまず作成します。
そして、デフォルトではパッケージ管理ツールにyarnが利用されているので、今回はプロジェクトのパッケージ管理をnpmで実行します。

$ npx create-react-app trivy-with-react
$ cd trivy-with-react
$ rm -fr node_modules yarn.lock
$ npm install

2. 本番用コンテナの作成

  1. 本番用のコードを生成
  2. nginxコンテナに生成したコードを移し、nginxを起動

という手順をとります。

通常であれば1ファイル

そして通常2つのステップは、multi-stage buildを利用して、1つのDockerfileでコンテナを作成します。

Dockerfile
# build environment
FROM node:12.2.0-alpine as build
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
COPY package.json /app/package.json
RUN npm install --silent
RUN npm install react-scripts@3.0.1 -g --silent
COPY . /app
RUN npm run build

# production environment
FROM nginx:1.16.0-alpine
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

from Dockerizing a React App

1ファイルでmulti-stage buildの問題点

multi-stage buildは1つのDockerfileで最終成果物のimageだけを出力してくれるので、通常はベストプラクティスです。
その場合、以下のように、builder側のイメージがIMAGE IDでしか特定できない形で作成されているのがわかります。

$ docker images
REPOSITORY         TAG    IMAGE ID ...
trivy-with-react   test   96c4fdfac287 # nginx側
<none>             <none> 41fd97016d25 # builder側

しかし、今回、脆弱性はbuilder側のコンテナにもあることに気がついた方もいるでしょう。package-lock.json もそうですし、一般的ではないですが、Static Linkでビルドする場合も考えられます。

trivyはREPOSITORY:TAGの形式でコンテナイメージを指定してスキャンするので、multi-stage buildにしてしまうと、builder側の脆弱性が放置されてしまいます。

そのため、builder側と、nginx側のコンテナイメージを独立させて、参照可能にしたいと思います。具体的には、Dockerfileを分けることで、それぞれのイメージにタグを付けます。
また、2回コマンド実行するのも微妙なのでmakefileを作成します。

※ もしも1つのDockerfileでmulti-stage buildをし、builder側のイメージ名を指定することができるのであれば、切実に情報がほしいです!

trivy用の構成

コードを見てもらえばわかりますが、Dockerfileをbuilder用(Dockerfile.builder)と、nginx用(Dockerfile.prod)に分けて、makefileは以下のようにしています。

Dockerfile.builder
FROM node:12.2.0-alpine as builder
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
COPY package.json /app/package.json
RUN npm install --silent
RUN npm install react-scripts@3.0.1 -g --silent
COPY . /app
RUN npm run build
Dockerfile.prod
ARG tag
FROM local-builder:${tag} as builder

FROM nginx:1.16.0-alpine
ARG tag
COPY --from=builder /app/build /usr/share/nginx/html
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx/nginx.conf /etc/nginx/conf.d
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
makefile
.PHONY: build all

TAG := $(shell git rev-parse HEAD)

all: build
build:
    docker build . -f Dockerfile.builder -t local-builder:$(TAG)
    docker build . --build-arg tag=$(TAG) -f Dockerfile.prod -t built:$(TAG)

このため、以下のようにデフォルトではコミットハッシュごとにコンテナを作成し、変数TAGが指定されていればそれを利用します。

$ make build  # 最新のコミットハッシュをコンテナのタグに利用
$ make build TAG=hoge # hogeをコンテナのタグに指定

実際のレポジトリでは、SPA用にnginx.confを置いたりしているので、必要な方はご確認ください。

1つのDockerfileで、同様のことができる方法をご存知の方いたら教えてください...

ビルドしたコンテナのテスト

では、さっそくビルドしたコンテナにアクセスしてみましょう。

$ make build TAG=test
$ docker run -p 8000:80 built:test

以上を実行し、 http://localhost:8000 を開くと、画面が表示されることが確認できました。

localhost.png

ついでに、nginxでSPA対応できているかを確認するため、 http://localhost:8000/spa/test が開けることを確認します。

localhost_spa

3. trivyでスキャン

では、builder側とnginx側をスキャンしましょう。
trivyはインストールしてある前提ですが、makefileに以下のコードを追加します。

buildと同じようにTAGを指定することで、指定したイメージをスキャンできるようになります。

ci-scan:
    trivy --exit-code 1 --severity HIGH,CRITICAL  --quiet --auto-refresh local-builder:$(TAG)
    trivy --exit-code 1 --quiet --auto-refresh built:$(TAG)

--exit-codeオプションで脆弱性を検知した場合の終了コードを指定できるので、スキャンはしたいけど、CIは止めたくない人は --exit-code 0 (デフォルトなので省略可) を利用しましょう。

4. CircleCIで実行

CircleCIの場合は以下のようにします。
trivyの脆弱性データの取得が重いので、脆弱性データなどはキャッシュに保存すると2回目以降のスキャンが高速になります。

version: 2
jobs:
  build:
    docker:
      - image: docker:18.09-git
    steps:
      - checkout
      - setup_remote_docker
      - restore_cache:
          key: vulnerability-db
      - run:
          name: Build image
          command: |
            apk add --update --no-cache make curl
            make build TAG=${CIRCLE_SHA1}
      - run:
          name: Install latest trivy
          command: |
            VERSION=$(
              curl -I https://github.com/knqyf263/trivy/releases/latest | \
              grep -o '/tag/v[0-9]\+.[0-9]\+\.[0-9]\+' | \
              sed -E 's:/tag/v([0-9\.]+):\1:'
            )
            wget https://github.com/knqyf263/trivy/releases/download/v${VERSION}/trivy_${VERSION}_Linux-64bit.tar.gz
            tar zxvf trivy_${VERSION}_Linux-64bit.tar.gz
            mv trivy /usr/local/bin
      - run:
          name: Scan local images with trivy
          command: |
            make ci-scan TAG=${CIRCLE_SHA1}
      - save_cache:
          key: vulnerability-db
          paths:
            - $HOME/.cache/trivy
workflows:
  version: 2
  release:
    jobs:
      - build

正常終了の場合、以下のようになります。
builder側とnginx側のどちらにも脆弱性がなかったということがわかりました。

ci-success.png

脆弱性があった場合CIを止めたい

脆弱性が見つかった場合に、CIを止めたい場合は--exit-code 1オプションを利用します。このオプションにより、脆弱性が見つかった場合、異常終了(終了コードが1)になります。
また、 severityオプションを利用することで、検知したい脆弱性の危険度を指定できます。

trivy --exit-code 1 --severity HIGH,CRITICAL \
  --quiet --auto-refresh \ 
  local-builder:$(TAG)

この状態で脆弱性のあるパッケージを追加して結果を見ると、CIが失敗することが確認できました!

ci-error.png
https://circleci.com/gh/tomoyamachi/trivy-with-react/8

trivyを使った運用スタイル

さて、では脆弱性が見つかった場合、どのようにすればよいでしょうか?

こうしたほうがいい、というのはありますが、それぞれの運用方針にそって決めればいいと思います。

以下に、「とりあえず導入」「ちょっと気になる」「がんばる」のコースとそれぞれによさそうな運用方針をまとめました。

とりあえず導入 ちょっと気になる がんばる
検知時のCI とめない(--exit-code 0) とめる(--exit-code 1) とめる(--exit-code 1)
対応方針 対応せずSlackなどに通知 バージョンアップして、駄目なら無視する (.trivyignore) 脆弱性を調べて、必要なら対応
検知タイミング ビルド時 ビルド時 ビルド時&定期実行

全く興味なくても、導入するだけ導入したい、という人がいてもいいと思うので、できる範囲ですすめられるといいかなと思います。

以下は、「とりあえず導入」以上の興味がある人に対しての方法論になります。

1. 脆弱性を調べ、本当にCRITICALかをチェック

まず、その脆弱性が自分のアプリケーションにとって本当に問題になるのかを理解しましょう。
NVDなどで脆弱性の内容を調べる方法もあるし、まわりの詳しい人に聞くのもありです。
自分で調べる場合、脆弱性が危険かどうかの判断は このスライドの「トリアージ」以降の章が役立つかと思います。

2. 重要ではない場合は、その脆弱性を無視する

プロジェクトに .trivyignore を作成し、無視したい脆弱性ID(以下、VULNERABILITY ID)を記載すると、そのIDを無視してくれます。
さきほどの例でいうと、以下のように指定すれば、それ以降同じVULNERABILITY IDは検知されなくなります。

.trivyignore
NSWG-ECO-328
CVE-2019-5428
CVE-2019-11358

この対応で先程停止していたワークフローがとおるようになったのがわかります。
https://circleci.com/gh/tomoyamachi/trivy-with-react/10

3. 重要な場合は...

重要な場合、バージョンアップをすれば良いと思うのですが、そうではない場合、自分で修正したコードを利用する方法などもあります。

アプリケーションパッケージの場合

たとえば、npmのパッケージであれば、 この記事の方法で でforkした自分のレポジトリを入れることが可能です。

ただし、その場合、バージョン情報が以下のようになり、そのパッケージの脆弱性チェックができなくなる点に留意ください。

"jquery": {
      "version": "git+https://<token>:x-oauth-basic@github.com/tomoyamachi/jquery.git#438b1a3e8a52d3e4efd8aba45498477038849c97",
      "from": "git+https://<token>:x-oauth-basic@github.com/tomoyamachi/jquery.git"
    },

OSのパッケージの場合

検出されるパッケージバージョン自体は変わらないので、patchfileを当てるコマンドを実行するのと同時に、.trivyignore に対象のVULNERABILITY IDを追加してください。

まとめ

今回は、以下のことを書きました。

  • フロントエンドをコンテナで運用する
  • trivyで脆弱性対策できる
  • 脆弱性のおおまかな運用方法

ところで、 記事を書いているときにnpmを使うのであれば、npm auditで簡単に脆弱性を取得できることを知りました… が、環境側の脆弱性もチェックできるので、コンテナ化しているのであれば、trivyを使ってみてください。
S3などにホストしている場合は、npm auditだけでよさそうです。

なお、コンテナになっていない場合で実行環境をチェックしたい場合、Vulsという素晴らしい脆弱性スキャナがあるので、こちらをご利用ください。動作中のコンテナもスキャン可能です。
すこし導入に準備が必要ですが、あなたのサーバは本当に安全ですか?今もっともイケてる脆弱性検知ツールVulsを使ってみた を参考にお試しください。

謝辞

記事を書く際に、 @knqyf263 さん、 @sadayuki-matsuno さん、 @codehex さんにご意見をいただきました。ありがとうございました!!

以上です。

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

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

Docker Swarmでコンテナのオーケストレーションを試したときの備忘です。
Docker環境が複数必要です。

Docker Swarmとは

Docker に対応するネイティブなクラスタリング用ツールです。Docker Swarm は標準 Docker API で操作できます。そのため、Docker ホスト群を集め、1つの仮想 Docker ホストとして扱えます。

参考
http://docs.docker.jp/swarm/overview.html

Docker Swarmを使ってクラスタ作成

//node1
$ docker swarm init --advertise-addr eth0
Swarm initialized: current node (4qmd5zm5h002y8ww8lwz5g07v) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-31gbv0gje7royyy3x9i09iyd2h00hwtx8a21vlq23x2fxrje5n-48j2paz9czuz9fl2u4coo9euy 192.168.65.3:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

各nodeをクラスタに組み込む

上記コマンドでプリントアウトされたswarmの文をコピー(赤枠部分)
image.png

各nodeで実行

//node2
$docker swarm join --token SWMTKN-1-31gbv0gje7royyy3x9i09iyd2h00hwtx8a21vlq23x2fxrje5n-48j2paz9czuz9fl2u4coo9euy 192.168.65.3:2377
This node joined a swarm as a worker.

//node3
$docker swarm join --token SWMTKN-1-31gbv0gje7royyy3x9i09iyd2h00hwtx8a21vlq23x2fxrje5n-48j2paz9czuz9fl2u4coo9euy 192.168.65.3:2377
This node joined a swarm as a worker.

//node1でクラスタが組まれているか確認 
docker nodes ls
$ docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
811 *   node1               Ready               Active              Leader              19.03.0-beta2
z08     node2               Ready               Active                                  19.03.0-beta2
0xj     node3               Ready               Active                                  19.03.0-beta2

Node1にコンテナ起動

//Nginxを起動
$docker service create nginx nginx

//起動確認
$docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE               PORTS
v80vj        nginx              replicated          5/5                 nginx:1.12          *:80->80/tcp

Ngingxをスケール

//クラスタ内に5つレプリカを作成する
$docker service update --replicas=5 --detach=true nginx

//起動の確認 1
$docker service ps nginx
    :

//起動の確認 2
$watch -n 1 docker service ps nginx

→nginxがスケールされました。swarmがルーティングの制御もしています。
※curlでアクセスしてみるとレスポンスするnodeが変わるのが確認できます。

所感

AWSのオートスケーリングみたいですねー。ただしswarmはport80しか対応していないらしい。。
からK8Sなのかー

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

[環境開発]Rails で scss が廃止された 話 by @penguin_fuyuno

結論

書き換える

# gem 'sass-rails', '~> 5.0'
gem 'sassc-rails'

理由

ただし、2019年3月に廃止予定であるため、乗り換えなければならないとのこと

作業向上labo
link: http://creative-agure.conohawing.com/efficiency_labo/?p=82

Github
link: https://github.com/sass/sassc-rails

エラー内容

Ruby Sass has reached end-of-life and should no longer be used.

* If you use Sass as a command-line tool, we recommend using Dart Sass, the new
  primary implementation: https://sass-lang.com/install

* If you use Sass as a plug-in for a Ruby web framework, we recommend using the
  sassc gem: https://github.com/sass/sassc-ruby#readme

* For more details, please refer to the Sass blog:
  https://sass-lang.com/blog/posts/7828841

環境

ruby '2.5.3'
gem 'rails', '~> 5.2.2'
gem 'mysql2', '>= 0.4.4', '< 0.6.0'

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

docker imageを直接pullしてみた

https://github.com/cielavenir/pulldockerimage

背景

userlandの仮想化基盤としてだけでなく、ソフトウェアの直接導入にもdockerは使われてきている。dockerはimageという単位で取り扱うことが多いが、このimageをリモートリポジトリからpullしたり、リモートリポジトリにpushしたりすることが可能である。これによりimageを共有することができるのである。
ところが一部、このpullを直接はできない環境があり得る。当該マシンが直接インターネットに繋がっていないことや、イントラネットにすら繋がっていないこともあるだろう。そのような場合、docker loadにより標準入出力を介してimageをやり取りすることになる。この場合に使われるのはpullしてきたファイル群をTarで固めたものである。これは手元のdocker daemonからdocker saveすることで作成することができるが、一旦手元にdocker pullしなければ、docker saveは実行できない。バッチ処理に組み込む場合等、不便なことがあるかもしれない。
今回は、リモートからダウンロードしたdocker imageのTarを直接標準出力に生成することを試みる。[1]はそうした試みの先駆けであるが、単体ではログイン機構を有しておらず、docker.ioで使うには力不足である。これをBashからRuby/Pythonで書き直し、再利用をしやすくしたい。

方法

docker pullはHTTPSを用いており、認証 -> マニフェストダウンロード -> レイヤーダウンロード -> メタファイル生成という順序で行われる。
まずはマニフェストをダウンロードする。ここで401になったら、WWW-Authenticateヘッダの情報を用いて認証を行う。この認証はBasic認証であり、docker loginが済んでいれば、認証文字列は.docker/config.jsonから直接得ることができる。ここで帰ってきたtokenをBearer認証に用いる。レイヤーはマニフェストに書いてある順番で重ねれば良い。レイヤーのIDは[1]にならいSHA256(parentId+"\n"+layerDigest+"\n")とした。

結果

モジュール

Pythonは標準ライブラリで完結させることができた。Rubyはminitarとmechanizeについて外部ライブラリを利用する。minitarはTarライブラリで、mechanizeはプログラムベースブラウザとも言うべきウェブ操作ライブラリである。

WWW-Authenticateヘッダの取扱い

Pythonはurllib内にparse_keqv_listやparse_http_listといった関数群があり、これでparseすることができる。Rubyは標準ライブラリでは処理できないため、mechanizeを使用する。ただし、mechanizeはsavonというSOAPライブラリが導入されている環境に導入しようとするとruby-ntlm関係でエラーが発生することが知られているため、今回はMechanize::HTTP::WWWAuthenticateParserの中身を埋め込むこととした。

30xステータスコードの取扱い

mechanizeであれば30xステータスコードが来たら自動でLocationを読みに行ってくれるが、HTTPを直接触る限りそういうわけにも行かない。30xが来たら、今のレスポンスを捨てたあと、locationを読みに行き30xが来る限りレスポンスを捨てることを繰り返せば良い。この繰り返しはブロック(Pythonの場合はcontextmanagerを使う)を用いて行い、HTTP bodyはyieldで渡すようにすると、後処理が楽である。

Tarの取扱い

Tar(に限らずアーカイブファイル一般)はヘッダにメンバサイズを書かなければならない(またはアーカイブに追加したあとでシークが必要であるが、標準出力を扱うため認められない)。HTTPヘッダのContent-Lengthをこのメンバサイズとして書き込めば良い。Rubyの場合は、tar.add_fileではなくtar.add_file_simpleを使う必要があった。add_fileはメンバ内に書き込んだサイズを自動判別(しシーク)するバージョンだからである。

結論

今回はRuby/Pythonを用いてdocker imageをtarとしてpullすることができた。
https://github.com/cielavenir/pulldockerimage にて利用可能である。

参考

[1] https://raw.githubusercontent.com/moby/moby/master/contrib/download-frozen-image-v2.sh

余談

(英語で書いたとしても)Linux Journalとかに載せるレベルじゃないですよねー肝心な場所のアルゴリズムは既存の技術ですしorz
7bgzfは違う領域だからある程度の価値があるけども。

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

Docker for WindowsでEnabled Kubernetesが出来なくてハマったときの解決方法

問題

  • Docker for WindowsでEnabled KubernetesをチェックしApplyしたが、インストールが終わらなかったときの解決方法

2019-05-21_01h23_48.png

対応内容

  • firewallのoutboundルール追加

2019-05-21_01h40_13.png

2019-05-21_01h30_16.png

  • 「.kube」ファイルを削除
  • デフォルトに戻す

2019-05-21_01h27_23.png

  • 各種設定を変更

2019-05-21_01h25_11.png

2019-05-21_01h32_27.png

最後に

これで私は起動しました。参考 issuesは以下

Kubernetes fails to start (hangs in starting phase)
https://github.com/docker/for-win/issues/1962

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

Dockerfileを使用したコンテナ環境の作成ついて

Dockerfileを使用したコンテナ環境の作成について

  • dockerを使って、素早く環境構築したい
  • dockerfileをもっとスラスラ描きたいという思いから、基本的なことをまとめてみました。
  • 下記の書籍を参考にしました。
  • Docker/Kubenetes実践コンテナ開入門
  • 間違いなどありましたら、コメントにて教えて頂けると、嬉しいです。

Dockerとは

  • コンテナ型仮想化技術を実現、操作、管理するためのプロダクト
  • コマンドラインで操作
  • 良い特徴として
    • インストールの難しいツールなどを使う場合でも、ホストを汚さずにDockerコンテナとして実行できる。
    • 様々なライブラリやツールをコードで管理することができ、チームで開発環境を統一できる。
    • Kubernetesなどを使ってコンテナを管理することにより、より簡単にデプロイやスケールを実現できる。 などなど

DockerイメージとDockerコンテナ

Dockerイメージ

  • Dockerコンテナを構成するためのファイルシステムや実行するアプリケーション、設定をまとめたもの。コンテナを作成するためのテンプレート。

DockerHub

  • Docker社が管理しているDockerレジストリで、Docker公式のイメージはDockerHubで公開されており、そこからpull(取得)して使う。自分で作成したイメージをDockerHubにpushすることで世界中のユーザに使用してもらうことも可能です。
# イメージ pull
# タグ名は省略可能ですが、省略するとデフォルト(多くの場合latest)が指定されます。
# docker image pull [options] リポジトリ名[:タグ名]
docker image pull golang:1.12
# イメージ push
docker image push [options] リポジトリ名[:タグ]

Dockerコンテナ

  • Dockerイメージを基に作成され、アプリケーションが実行されている状態。Dockerによって作成されるゲストOS。

Dockerによる大まかなアプリケーション実行までの流れ

  1. Dockerイメージを定義...dockerfileを作成
  2. Dockerイメージを作成...dockerイメージをビルド
  3. 作成されたDockerイメージを使って、コンテナを作成実行
  4. コンテナを操作する
  5. コンテナの停止と開始、再起動と削除
  6. イメージの削除

1.Dockerイメージを定義...dockerfileの作成

Dockerfileとは

  • Dockerイメージを作成するためのスクリプトのことです。イメージを作成するために必要な手順を記載していきます。
# Dockerfile
FROM golang:1.12

RUN mkdir /echo_test
COPY main.go /echo_test

CMD ["go", "run", "/echo_test/main.go"]
// コンテナ実行される、main.goファイル
package main

import (
  "fmt"
  "log"
  "net/http"
)

func main(){
  http.HandleFunc("/",func(w http.ResponseWriter, r *http.Request){
    log.Println("received request")
    fmt.Fprintf(w, "Hello Docker!!")
  })
  log.Println("start server")
  sv := &http.Server{Addr: ":8080"}
  if err := sv.ListenAndServe();err != nil{
    log.Println(err)
  }
}
  • FROMRUNと行ったキーワードを「インストラクション(命令)」と言います。
  • Dockerfileではインストラクションを使って、イメージの構成を定義します。
  • 上記のDocerfileでやっていること
    1. FROM で作成するDockeイメージのベースとなるイメージを指定しています。この場合、公式のgolang:1.12が指定されていて、golang:1.12のイメージがまだpullされていなければ、DockerHubからpullし、すでにpullされていれば既存のgolang:1.12のイメージを使用します。
    2. RUN でDockerイメージをビルドする時に、Dockerコンテナ内で実行するコマンドを定義しています。この場合、/echo_testというディレクトリをコンテナ内に作成しています。
    3. COPY で、ホストマシン上のファイルやディレクトリをDockerコンテナ内にコピーしています。この場合、Dockerfileと同じディレクトリ内にあるmain.goというファイルをコンテナ内の/echo_testディレクトリにコピーしています。
    4. CMD でDockerコンテナとして実行する際にコンテナ内で実行するプロセスをしています。 CMDはコンテナ起動時に1度実行されます。この場合、以下のようなコマンドを実行してgoのプロセスを実行します。
go run /echo_test/main.go
  • Dockerfileに記載した各項目(この場合、FROM, RUN, COPY, CMD)が、それぞれ1つのレイヤーという単位で、そのレイヤーが重なって、イメージが作成されます。FROMで指定したイメージがベースとなるというのは、このような仕組みのためです。

2.Dockerイメージを作成...Dockerイメージをビルド

  • Dockerfileを使用して、Dockerイメージをビルドします。
# docker image build -t イメージ名[:タグ名] Dockerfileのあるディレクトリ
docker image build -t example_go/echo:latest .
  • docer image build でイメージをビルドします。
  • -t オプションを必ずつけたほうが良いです。-tオプションにより、イメージ名を指定することができます。
  • -tオプション無しでもイメージの作成はできますが、イメージ名(REPOSITORY)とタグ(TAG)の値が<none>になります。ハッシュ値(IMAGE ID)での管理となり、わかりづらくなります。
$ docker image ls # docker imageの一覧を表示
REPOSITORY        TAG      IMAGE ID       CREATED             SIZE
<none>            <none>   a086e6cb87f0   53 seconds ago      774MB
example_go/echo   latest   806faed5d0a8   About an hour ago   774MB
  • docker image ls コマンドでexample_go/echoのイメージが作成されていることが確認できました。

3. 作成されたDockerイメージを使って、コンテナを作成実行

  • 作成したイメージからコンテナを実行します。
  • コマンドはdocker container run イメージ名となりますが、いくつかオプション指定したほうが良いです。
# docker container run [options] イメージ名:[タグ名]
docker container run -d -p 9000:8080 example_go/echo:latest
  • 上記のコマンドを実行後、localhost:9000にアクセスするとHello Docker!!が表示されます。
    スクリーンショット 2019-05-20 0.54.44.png

  • -d オプションで、コンテナをバックグラウンドで実行します。今回のようなサーバアプリケーションだとバックグラウンドで実行したほうが勝手が良いことが多いと思います。ただrailsなどをコンテナで実行する時に、あえて-dオプション無しで実行し、logを確認しやすくするようなこともあります。

  • -pオプションでポートフォワーディングを設定します。今回のアプリケーションは8080のポートで実行されていますが、それはコンテナ内のことで、ポートフォワーディングの設定が無い場合、コンテナの外からポート8080にアクセスしても、コンテナ内のアプリケーションに到達できません。

  • コンテナ外からきたリクエストをコンテナ内で実行しているアプリケーションに到達させるために、-p {ホスト側ポート}:{コンテナ側ポート} で、ホスト側の指定のポートにアクセスすると、コンテナ側の指定したポートでリクエストを送ることができます。

  • また、-pオプションで、ホスト側ポートを指定しなかった場合、どのポートにアクセスするば良いのか調べる方法もあります。

$ docker container run -d -p 8080 example_go/echo:latest
2bdfce9e69c6b1ed81a7d6ea0e493e0867effa233d3f89aa8e27a793e1f29f90
$ docker container ls
CONTAINER ID  IMAGE             COMMAND                 CREATED        STATUS        PORTS 
 NAMES
2bdfce9e69c6  example_go/echo:latest  "go run /echo_test/m…"  3 seconds ago  Up 2 seconds  0.0.0.0:32769->8080/tcp  elated_roentgen
  • PORTSに記載されている、0.0.0.0:32769 がホスト側のポートになります。この場合、localhost:32769にアクセスすると、Hello Docker!!と表示されます。

4.コンテナを操作する

コンテナで任意のコマンドを実行する

# docker container exec [options] コンテナIDまたはコンテナ名 実行するコマンド
# 今回は例として、先ほど実行したコンテナに入ります。
docker container exec -it 2bdfce9e69c6 bash
root@2bdfce9e69c6:/go#
root@2bdfce9e69c6:/go# pwd
/go
root@2bdfce9e69c6:/go#
  • docker container exec コマンドは実行しているDockerコンテナの中で任意のコマンドを実行できます。
  • 標準入力を開いたままにする-iオプションと仮想ターミナルを割り当てる-tオプションを組み合わせて、対話的にシェルコマンド(bash)を実行しています。
  • コンテナの中に入るときは-itをつけると覚えておくと便利です。

コンテナとホスト間でファイルのコピーをする

# docker container cp [options] コンテナIDまたはコンテナ名:コンテナ内のコピー元 ホストのコピー先
# コンテナ内の/echo_test/main.go を ホストの ~/以下にコピー
docker container cp 2bdfce9e69c6:/echo_test/main.go ~/
# docker container cp [options]  ホストのコピー元 コンテナIDまたはコンテナ名:コンテナ内のコピー先
# ホストの ~/.ssh/id_rsa をコンテナ内の/echo_test以下にコピー
docker container cp ~/.ssh/id_rsa 2bdfce9e69c6:/echo_test
  • 上記のようにcpコマンドでコンテナとホスト間でファイルのコピーができます。
  • 例にあるように、ホストssh_keyをコンテナにコピーして、コンテナからGitHubのリポジトリにアクセスするなども可能です。

5.コンテナの停止と開始、再起動と削除

  • 先ほど使用した、docker container ls コマンドでコンテナのIDやコンテナ名を確認することができます。
  • 確認したコンテナIDやコンテナ名を引数にして下記のコマンドを実行することで、コンテナの停止と開始、再起動と削除ができます。
  • コンテナの停止
docker container stop コンテナIDまたはコンテナ名
  • コンテナの開始
docker container start コンテナIDまたはコンテナ名
  • コンテナの再起動
docker container restart コンテナIDまたはコンテナ名
  • コンテナの削除
  • コンテナの削除はコンテナが停止している状態でないと基本的には実行できません。
  • ただdocker rm -f コンテナIDまたはコンテナ名のように、-fオプションで強制削除することもできます。
docker container rm コンテナIDまたはコンテナ名

6.イメージの削除

  • docker image ls または docker images コマンドでイメージの一覧を確認できます。
  • そこで表示された対象のイメージのIDを引数に指定することで、イメージを削除します。
docker image rm イメージID
# または
docker rmi イメージID

最後に

  • dockerfileだけでも簡単に環境構築ができます。
  • 他にも便利なコマンドがありますので、公式ドキュメントや、docker helpコマンドで確認できます。
  • ComposeやSwarm、Kubernetesなどについても、まとめていきます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Windows10のWSLでdocker-compose

概要

  1. docker使いたい
  2. コマンドプロンプトとかPowerShellきらい
  3. WSLあるし使おう

マウントがうまく行かずハマる

TL;DR

マウントのパスは相対パスで書かないこと.絶対パスで書く
Windowsのパス形式で書かないこと.

やりたいこと

WSLでdocker-compose up できるようにする

WSLが有効化されている状態で始めます

  1. WSLでdockerのインストールからdocker-composeまで動かす
    を参考にWSLにdocker-composeのインストールまで済ませる

  2. Docker DesktopのSettingsでShare DrivesのCドライブを一旦チェック外してApply

  3. もう一回CドライブにチェックいれてApply 参考

  4. docker-compose.ymlのvolumesの中でパスを指定するときに絶対パスで指定する

参考

  • 相対パスだと,永続化はされるがホスト側の見当違いなところにマウントされる
  • Windowsのパス形式で書いてもWSLのdocker-composeは理解できない
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む