ksaitoの日記

日々試したことの覚え書き

dockerのクラスタとスケジューラ

AWS ECS、AWS Fargate、Docker Swarm、Kubernetesの違いについての覚書です。 Kubernetesの人たちがAWS ECSよりKubernetesのほうが賢いといっていたり、Docker純正のSwarmよりKubernetesが注目されている理由が何となく納得できました。

AWS ECS

ECSは、Auto Scaleで複数のEC2インスタンスクラスタを構成できます。 サーバリソースの状況によって、Auto Scaleがサーバを増減させてくれるので負荷のピークとコストは最適に調整することができます。コンテナもAuto Scaleで増減され、ELBの動的ポートマッピングは、利用者からシームレスにバックエンドのコンテナの入れ替えを可能にしてくれる素晴らしい機能です。

ECSでネットワークモードがdefaultの場合、TaskDefinitionで定義された複数のコンテナは、1つのインスタンスでセットで起動されるためクラスタのスケールアウトの恩恵を受けることはできません。また、TaskDefinitionの中の特定のコンテナだけスケールアウトすることもできません。Linksは、相互参照をサポートしないので、コンテナ間で双方向の通信がある場合、DNSやconsulといった、何らかのサービスディスカバリが必要になります。

ECSでネットワークモードにawsvpcを使う場合、コンテナがVPC上のホストのように見えます。コンテナレベルでスケールアウトするには良さそうですが、実装は、EC2インスタンスにENIを追加してコンテナのNICに割り当てているため、EC2インスタンスに追加できるENIの数に制約を受けます。

ECSは、素晴らしいサービスですが、上記のような制約を管理する必要があり、なかなか大変です。

AWS Fargate

Fargateは、ECSのPaaSでEC2インスタンスの管理をなくても良く、課金モデルもEC2インスタンスではなく、コンテナの稼働に対する課金となります。ECSのEC2でAuto Scale、コンテナでもAuto Scaleの二段階のスケールアウトに違和感を感じていたので、リリースされたときには、すごく魅力的に感じました。

実際に動かしてみて、docker exec できないと分かって、使う気持ちがなくなりました。コンテナを使うのに、CloudWatch経由でログを見るしかないのは、ちょっとしんどい... そのためにsshサーバをたてると言うのは本末転倒

Docker Swarm

Docker純正のクラスタでDocker環境に設定すれば利用できるので非常に手軽です。スタックの設定は、docker-composeが、ほぼそのまま使えます。 現時点では、コンテナのスケールを動的に行うスケジューラがないので非常に残念です。 swarm modeは、cap_addをサポートしていないので権限を必要とするコンテナを動かすことはできません。(concourse.ciのworkerは残念ながら動きませんでした。)

kubernetes

Kubernates環境を作成するのが大変そうで、なかなか踏み出せません。 使っていないので、なんともいませんが、ECSやSwarmの制約がないのかな。 GCPやEKSで試す前に、一度、やってみなければ...

シェルでファイルの差分があった場合に処理を実行したい。

シェルでファイルの差分があった場合のみ処理を実行したいユースケースがあります。 diffで差分は、検出できますが終了ステータスは0以外が返されます。 このステータスは、異常終了ではなく、差分があるか・ないかを判定したいだけなので無視して欲しいです。

いろいろな実装方法はあると思いますが、下記で意図する実装をすることができました。 シェルの||の使い方を理解する必要があるため、コードを見て意図を理解することが難しい実装なので、もっと良い方法があるような気もします。

sts=0
diff -uwB file1 file2 || sts=$?
echo Status=$sts

実行例は、以下の通りです。

$ cat sample.sh 
#! /bin/bash

sts=0
diff -uwB file1 file2 || sts=$?
echo Status=$sts
$ ./sample.sh 
Status=0
$ echo bbb > file2
$ ./sample.sh 
--- file1       2018-02-12 21:24:46.161302088 +0900
+++ file2       2018-02-12 21:26:03.922402102 +0900
@@ -1 +1 @@
-aaa
+bbb
Status=1
$ 

下記の2行目で||は、diffが失敗(終了ステータスが0以外)を返した時に実行されます。sts=$?は、必ず成功するので4行目全体として失敗することはありません。

diffが成功(終了ステータスが0)を返すとsts=$?は実行されません。 2行目のsts=0とすることで結果的にstsが未定義とならず、正常終了の0が設定されます。

ちょっと、トリッキーなので、もっと良い方法があるはずなのですが...

$ cat -n sample.sh 
     1  #! /bin/bash
     2
     3  sts=0
     4  diff -uwB file1 file2 || sts=$?
     5  echo Status=$sts
$ 

Dockerのお掃除

Dockerの使っていないオブジェクトを掃除するための手順です。

イメージ

タグのついていないイメージを削除します。

docker rmi `docker images -qf dangling=true`

コンテナ

停止しているコンテナを削除します。

docker rm `docker ps -a -q`

ボリューム

コンテナに紐付いていないボリュームを削除します。 必要なコンテナが立ち上がっていないと削除対象となってしまうので注意が必要です。

docker volume rm `docker volume ls -qf dangling=true`

bosh-cliのインストール

bosh-cliのインストール手順です。

bosh.ioからプラットフォームに応じたバイナリのURLを確認して、下記の一連のコマンドでインストールできます。

sudo curl -o /usr/local/bin/bosh-cli-2.0.45-linux-amd64 https://s3.amazonaws.com/bosh-cli-artifacts/bosh-cli-2.0.45-linux-amd64
sudo chmod +x /usr/local/bin/bosh-cli-2.0.45-linux-amd64
sudo ln -s /usr/local/bin/bosh-cli-2.0.45-linux-amd64 /usr/local/bin/bosh

バージョンを確認したらインストール完了です。

$ bosh --version
version 2.0.45-d208799-2017-10-28T00:31:53Z

Succeeded
$

bashのretry

下記のコードを$HOME/.bashrcに追加するとコマンドが失敗した場合に、一定時間待って、指定した回数リトライするretry関数を追加できます。

オリジナルは、ここのソースを参考に改良しました。(コメントしようと思いましたが、50以上の評価がないとコメントできませんでした。)

aws cliでリソースを作成した場合、awsコマンドは、リソース作成の終了を待たずに終わってしまいます。リソース作成が終わってから後続の処理をしたい場合に、適切なタイムアウトを設けてリトライするのに便利です。

コード

function retry() {
    if [ $# -lt 3 ]; then
        echo 'usage: retry <num retries> <wait retry secs> "<command>"'
        return 1
    fi

    retries=$1
    wait_retry=$2
    shift 2
    command=$@

    for i in `seq 1 $retries`; do
        echo "$command"
        $command
        ret_value=$?
        [ $ret_value -eq 0 ] && break
        echo "> failed with $ret_value, $wait_retry sec waiting to retry..."
        sleep $wait_retry
    done

    return $ret_value
}

使い方

成功する場合

$ retry 5 1 test 0 -eq 0
test 0 -eq 0
$ echo $?
0
$ 

失敗する場合

$ retry 5 1 test 0 -eq 1
test 0 -eq 1
> failed with 1, 1 sec waiting to retry...
test 0 -eq 1
> failed with 1, 1 sec waiting to retry...
test 0 -eq 1
> failed with 1, 1 sec waiting to retry...
test 0 -eq 1
> failed with 1, 1 sec waiting to retry...
test 0 -eq 1
> failed with 1, 1 sec waiting to retry...
$ echo $?
1
$ 

concourseを使ったビルド

concourse.ioは、日本語情報とても少ないです。 とてもクールなCIツールです。

パイプライン、ジョブ、リソース、タスクといった考え方があり、CI/CDのパイプラインを構築できますが、実装しようと思うと意外と手が止まります。 パイプラインの起点は、S3やgitなどで、Jenkinsのようにとりあえず実行といったことができないからです。

とりあえず実行をするには、タスクを作るのがお勧めです。文書だけでは、うまく伝わらないと思いますが、日常の開発でも地味に便利です。

ユースケース

Javaの開発をしていたとします。対応する必要のあるJDKのバージョンはたくさんあるとします。

従来のビルド環境であれば、複数のバージョンをインストールして環境変数で切り替えるといったことが考えられますが、以外と面倒なことになります。

タスクの作成

concourseは、タスクはdockerコンテナで実行されます。 下記のようなタスクを準備してJDK1.6の環境を準備できます。

DockerHubのjavaオフシャルコンテナを使っています。tagの指定を変えれば、好みのバージョンを使うことができます。

$ cat sample.yml
---
platform: linux

image_resource:
  type: docker-image
  source: {repository: java, tag: 6}

run:
  path: sh
  args:
  - -exc
  - |
    java -version
$ fly -t test execute -c sample.yml
executing build 41 at http://172.17.0.1:8080/builds/41 
initializing
running sh -exc java -version

+ java -version
openjdk version "1.8.0_111"
OpenJDK Runtime Environment (build 1.8.0_111-8u111-b14-2~bpo8+1-b14)
OpenJDK 64-Bit Server VM (build 25.111-b14, mixed mode)
succeeded
$

パラメータを指定する

任意のパラメータを環境変数で渡すことができます。コンパイルオプションの指定に便利です。

$ cat sample.yml
---
platform: linux

image_resource:
  type: docker-image
  source: {repository: java, tag: 6}

params:
  JAVA_OPTS: -version

run:
  path: sh
  args:
  - -exc
  - |
    java $JAVA_OPTS
$ JAVA_OPTS=-fullversion fly -t test execute -c sample.yml
executing build 43 at http://172.17.0.1:8080/builds/43 
initializing
running sh -exc java $JAVA_OPTS

+ java -fullversion
java full version "1.6.0_38-b38"
succeeded
$

ファイルの受け渡し

下記のようなプログラムをコンパイルする場合、ソースコードを渡して、コンパイル結果を受け取る必要があります。

$ cat Hello.java 
public class Hello {
    public static void main(String[] args)
    {
        System.out.println("Hello, world.");
    }
}
$ 

タスクにファイルを渡すためのinputsとファイルを受け取るためのoutputsを定義します。

$ cat sample.yml
---
platform: linux

image_resource:
  type: docker-image
  source: {repository: java, tag: 6}

params:
  JAVA_OPTS: -version
inputs:
  - name: src
outputs:
  - name: obj

run:
  path: sh
  args:
  - -exc
  - |
    java $JAVA_OPTS
    javac src/Hello.java
    java -cp src Hello
    cp -p src/Hello.class obj

下記のように実行するとカレントディレクトリのファイルがsrcディレクトリにコピーされてタスク内で使用できるようになります。また、タスク内でobjディレクトリにコピーしたファイルは、/tmp/objで受け取ることができます。

$ fly -t test execute -c sample.yml -i src=. -o obj=/tmp/obj
executing build 44 at http://172.17.0.1:8080/builds/44 
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1337    0  1337    0     0   1337      0 --:--:-- --:--:-- --:--:--  435k
initializing
running sh -exc java $JAVA_OPTS
javac src/Hello.java
java -cp src Hello
cp -p src/Hello.class obj

+ java -version
java version "1.6.0_38"
OpenJDK Runtime Environment (IcedTea6 1.13.10) (6b38-1.13.10-1~deb7u1)
OpenJDK 64-Bit Server VM (build 23.25-b01, mixed mode)
+ javac src/Hello.java
+ java -cp src Hello
Hello, world.
+ cp -p src/Hello.class obj
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   455    0     0    0   455      0    455 --:--:-- --:--:-- --:--:-- 45500
succeeded
$ ls /tmp/obj
Hello.class
$ 

パイプラインは、タスクに定義されたinputsとoutputsをつなぎ合わせることで成果物の受け渡しを行います。非常によく考えられていて便利です。