AWS Fargate BlueGreenDeployment

はじめに

AWS FargateはContainerインスタンスの管理をAWSにお任せすることができるサービスです。

現状、ECS(LaunchType EC2)を使っているのですが、JenkinsからECSにBlueGreenDeployするときにecs-deployを使っています。
ecs-deployはaws cliとjqには依存していますがshellだけで書かれてるので持ち運びが便利なんですね。

ecs-deployはFargateに対応していないので対応させてみました。

https://github.com/uzresk/ecs-deploy.git

使い方

1. aws cliはFargateに対応しているバージョンをお使いください。

ちなみに私の環境はこちら

aws-cli/1.14.7 Python/2.7.12 Linux/4.9.62-21.56.amzn1.x86_64 botocore/1.8.11

2. コマンドはecs-deployと全く同じです

./ecs-deploy -c [cluster-name] -n [service-name] -i [registry-url]:[tag] -t 300 -r us-east-1

デフォルトのタイムアウトは90秒なのですが、終わらないことが何回かあったので少し長めにしておくのがおススメです。

実行結果

Using image name: xxxx.dkr.ecr.ap-northeast-1.amazonaws.com/xxxx:0.0.1-SNAPSHOT
Current task definition: arn:aws:ecs:us-east-1:xxxx:task-definition/xxxx:25
Current requires compatibilities FARGATE
New task definition: arn:aws:ecs:us-east-1:xxxx:task-definition/xxxx:26
Service updated successfully, new task definition running.
Waiting for service deployment to complete...
Service deployment successful.

変更点

Fargateが追加されたことによりrequiresCompatibilitiesの指定を引き継ぐようにしたのと、
cpu, memoryの設定も合わせて引き継ぐようにしました。
LaunchTypeがEC2の場合はcpu,memoryは設定されません。

[root@ip-10-0-0-100 ecs-deploy]# git diff
diff --git a/ecs-deploy b/ecs-deploy
index 637e793..8ad1cb1 100755
--- a/ecs-deploy
+++ b/ecs-deploy
@@ -261,11 +261,17 @@ function createNewTaskDefJson() {
     fi

     # Default JQ filter for new task definition
-    NEW_DEF_JQ_FILTER="family: .family, volumes: .volumes, containerDefinitions: .containerDefinitions"
+    NEW_DEF_JQ_FILTER="family: .family, volumes: .volumes, containerDefinitions: .containerDefinitions, requiresCompatibilities: .requiresCompatibilities"

     # Some options in task definition should only be included in new definition if present in
     # current definition. If found in current definition, append to JQ filter.
-    CONDITIONAL_OPTIONS=(networkMode taskRoleArn placementConstraints)
+    LAUNCH_TYPE=$(echo "$TASK_DEFINITION" | jq -r '.taskDefinition.requiresCompatibilities[0]')
+    echo "Current requires compatibilities $LAUNCH_TYPE"
+    if [ $LAUNCH_TYPE == FARGATE ]; then
+      CONDITIONAL_OPTIONS=(networkMode taskRoleArn executionRoleArn placementConstraints memory cpu)
+    else
+      CONDITIONAL_OPTIONS=(networkMode taskRoleArn executionRoleArn placementConstraints)
+    fi
     for i in "${CONDITIONAL_OPTIONS[@]}"; do
       re=".*${i}.*"
       if [[ "$DEF" =~ $re ]]; then

おわりに

もう少し動作確認したらプルリクエスト送ろうと思いますが、だいぶメンテされていないようなので多分マージされない気がします。。。

続きを読む

[AWS]Hello Glacier! AWS Glacierの使い方を知るための始めの一歩

以前からGlacierを理解したいと思っていて軽く触ってみたのでまとめてみた。
しかしながら、現在はS3のGlacierアーカイブ機能があるためS3経由でならこの記事のように複雑なことをしなくても料金の圧縮が行える。
Glacierというサービスがどのようなものなのかという理解を深める上で手を動かして理解するにはこの記事はちょうどよいのかもしれない。

想定読者

以下のような方を想定している。

  • AWSを使っている
  • S3を使ったことがある or ある程度の知識を持っている
  • AWS Glacierを聞いたことはあるけどよくわからない
  • 使ってみたいけど使い方が分からない

Glacierとは?

おそらく知っていると思うが念のため。

一言で言うと 利便性を落とした代わりに値段が安く使えるS3
主に似ているAWSサービスのS3と比較すると特徴は以下。

  • ストレージ単価が安い
  • 取り出すまでに若干のタイムラグがある

用途

アクセス頻度が低いが消したくないデータを低価格 & 高可用性な環境で保持したい場合に使用する。

用語

Glacierを理解するために以下の用語を抑えておく。

  • Vault(ボールト)

    • S3でいうバケット
  • Archive(アーカイブ)
    • S3でいうオブジェクト
    • ただしS3と異なりIDが振られていてそれを使ってアクセスする
    • S3のようにファイル間隔で気軽にはアクセスできない
    • Jobを経由して一定期間後に取得可能になる
  • Inventory(インベントリー)
    • Archive一覧を保持している
    • 公式いわく一日間隔でArchive一覧が更新されるらしい
  • Job (ジョブ)
    • Vaultを対象にして行う処理
    • 主にArchive/Inventoryの取得作業に用いる(他にあるかは分からない)

動作環境

項目 バージョンなど
OS Mac Sierra
AWS CLI aws-cli/1.11.180 Python/3.6.3 Darwin/16.7.0 botocore/1.7.38

使い方

以下の工程を使って使い方を学ぶ。
* Vaultの作成
* Archiveのアップロード
* Archiveの取得
* Archiveの削除
* Vaultの削除

Glacierは2017-12-02現在でマネジメントコンソールから利用することが出来ない。
故に今回はAWS CLIを用いて解説していく。
ただしここではAWS CLIの使い方や導入については解説しない。

Vaultの作成

これはマネジメントコンソールからでも作成可能。
だがそれ以降はCLIを使っていくことになるのでここでもCLIを使用する。
vault-name には付けたい名前をつければ良いが今回は qiita-test という名前を使っていく。
account-id- を指定すればIAMユーザに紐付いたアカウントIDが使用される。

# {{account-id}}は伏せている
$ aws glacier create-vault --account-id - --vault-name qiita-test
{
    "location": "/{{account-id}}/vaults/qiita-test"
}

これで作成が完了した。
以下で確認して該当のVaultが作成できていれば完了していることを確認できる。

$ aws glacier list-vaults --account-id -
{
    "VaultList": [
        {
            "VaultARN": "arn:aws:glacier:ap-northeast-1:{{account-id}}:vaults/qiita-test",
            "VaultName": "qiita-test",
            "CreationDate": "2017-11-02T13:46:26.012Z",
            "NumberOfArchives": 0,
            "SizeInBytes": 0
        },
    ]
}

Archiveをアップロード

次に保存させたいデータをVaultにアップロードしていく。
コマンドは upload-archive を使用する。
ここで最も重要なのが出力された archiveId である。
以降のArchive取得時には必須パラメータなので忘れずに保存しておくこと。
ArchiveIdを忘れた場合は後述するInventoryを使うことになる。

# テスト用のファイル
$ echo 'Hello Glacier' > ~/simple.txt

$ aws glacier upload-archive --account-id - --vault-name qiita-test --body ~/simple.txt
{
    "location": "/{{account-id}}/vaults/qiita-test/archives/OQBATOmxpMSNOxXPrKLkGiISpNvtZySRe-Dg_PvVbg3zxfIRGa3o1et33LvKvZSJA2_nAHW9eMhclfJAbGLzSV3owDcUvNglvAEafu67wKsECismh1uRL_-Rz0rCsGD_4AoGB7UP8w",
    "checksum": "6b72933f6d83bb1294fef21c290d86ad4d5bacee89a0e34eca284c08b00e88c1",
    "archiveId": "OQBATOmxpMSNOxXPrKLkGiISpNvtZySRe-Dg_PvVbg3zxfIRGa3o1et33LvKvZSJA2_nAHW9eMhclfJAbGLzSV3owDcUvNglvAEafu67wKsECismh1uRL_-Rz0rCsGD_4AoGB7UP8w"
}

Archiveの取得

取得Jobの発行

GlacierではS3のように即時にデータにはアクセスできず、Jobを経由してデータを取得する。
データの取得Jobには速度・価格で3種類選べるので場合によって使い分けると良い。

image.png

まずはArchive取得Jobを投げる。
先程のArchiveIdを指定してあげる必要があるのだけど、パラメータがJSONなので一度ファイルに展開しておく。
jobのパラメータの説明は以下に記載。

  • SNSTopic

    • Jobが完了したときに通知するSNSのARNを指定する
    • これがないとJobが完了したときに通知をしてくれなくなる
    • SNSについての説明はここではしない
  • Tier
    • 上記の料金表に記載されている取得方法の種類を指定する
    • 左から順に Expedited|Standard|Bulk にそれぞれ対応している
$ cat ~/job.json
{
  "Type": "archive-retrieval",
  "ArchiveId": "OQBATOmxpMSNOxXPrKLkGiISpNvtZySRe-Dg_PvVbg3zxfIRGa3o1et33LvKvZSJA2_nAHW9eMhclfJAbGLzSV3owDcUvNglvAEafu67wKsECismh1uRL_-Rz0rCsGD_4AoGB7UP8w",
  "Description": "Hello Glacier Simple Message",
  "SNSTopic": "{{sns-arn}}",
  "Tier" : "Standard"
}

あとは上記のファイルを --job-parameters オプションで指定してあげれば良い。
出力される jobId は今後必要になるので覚えておこう。

# --job-parameter 'JSON' で直接記述することも可能
$ aws glacier initiate-job --account-id - --vault-name qiita-test --job-parameters file://~/job.json
{
    "location": "/{{account-id}}/vaults/qiita-test/jobs/3o5VRlLDKI7rc3kykAzvGqfHop1AbJbX5CjDAd9_GPaFtCVAcRDp6Rjp2Wigz5iVhQSZq8VAvy4xCNhjWVV_4Ie2Syci",
    "jobId": "3o5VRlLDKI7rc3kykAzvGqfHop1AbJbX5CjDAd9_GPaFtCVAcRDp6Rjp2Wigz5iVhQSZq8VAvy4xCNhjWVV_4Ie2Syci"
}

Jobの状態は以下のコマンドで確認できる。
もしJobIdを忘れた場合はこれで調べることも出来る。

$ aws glacier list-jobs --account-id - --vault-name qiita-test
{
    "JobList": [
        {
            "JobId": "3o5VRlLDKI7rc3kykAzvGqfHop1AbJbX5CjDAd9_GPaFtCVAcRDp6Rjp2Wigz5iVhQSZq8VAvy4xCNhjWVV_4Ie2Syci",
            "JobDescription": "Hello Glacier Simple Message",
            "Action": "ArchiveRetrieval",
            "ArchiveId": "OQBATOmxpMSNOxXPrKLkGiISpNvtZySRe-Dg_PvVbg3zxfIRGa3o1et33LvKvZSJA2_nAHW9eMhclfJAbGLzSV3owDcUvNglvAEafu67wKsECismh1uRL_-Rz0rCsGD_4AoGB7UP8w",
            "VaultARN": "arn:aws:glacier:ap-northeast-1:{{account-id}}:vaults/qiita-test",
            "CreationDate": "2017-12-02T14:30:24.038Z",
            "Completed": false,
            "StatusCode": "InProgress",
            "ArchiveSizeInBytes": 14,
            "SNSTopic": "{{snsのarn}}",
            "SHA256TreeHash": "6b72933f6d83bb1294fef21c290d86ad4d5bacee89a0e34eca284c08b00e88c1",
            "ArchiveSHA256TreeHash": "6b72933f6d83bb1294fef21c290d86ad4d5bacee89a0e34eca284c08b00e88c1",
            "RetrievalByteRange": "0-13",
            "Tier": "Standard"
        }
    ]
}

完了JobからArchiveをダウンロード

JOBが完了すると以下のようなメッセージがSNS経由で来る。

{
  "Action": "ArchiveRetrieval",
  "ArchiveId": "OQBATOmxpMSNOxXPrKLkGiISpNvtZySRe-Dg_PvVbg3zxfIRGa3o1et33LvKvZSJA2_nAHW9eMhclfJAbGLzSV3owDcUvNglvAEafu67wKsECismh1uRL_-Rz0rCsGD_4AoGB7UP8w",
  "ArchiveSHA256TreeHash": "6b72933f6d83bb1294fef21c290d86ad4d5bacee89a0e34eca284c08b00e88c1",
  "ArchiveSizeInBytes": 14,
  "Completed": true,
  "CompletionDate": "2017-12-02T18:24:32.585Z",
  "CreationDate": "2017-12-02T14:30:24.038Z",
  "InventoryRetrievalParameters": null,
  "InventorySizeInBytes": null,
  "JobDescription": "Hello Glacier Simple Message",
  "JobId": "3o5VRlLDKI7rc3kykAzvGqfHop1AbJbX5CjDAd9_GPaFtCVAcRDp6Rjp2Wigz5iVhQSZq8VAvy4xCNhjWVV_4Ie2Syci",
  "RetrievalByteRange": "0-13",
  "SHA256TreeHash": "6b72933f6d83bb1294fef21c290d86ad4d5bacee89a0e34eca284c08b00e88c1",
  "SNSTopic": "{{sns-arn}}",
  "StatusCode": "Succeeded",
  "StatusMessage": "Succeeded",
  "Tier": "Standard",
  "VaultARN": "arn:aws:glacier:ap-northeast-1:{{account-id}}:vaults/qiita-test"
}

これでArchiveからダウンロード出来る環境は整ったのでJobIdを指定してArchiveのダウンロードを行っていく。

$ aws glacier get-job-output --account-id - --vault-name qiita-test --job-id 3o5VRlLDKI7rc3kykAzvGqfHop1AbJbX5CjDAd9_GPaFtCVAcRDp6Rjp2Wigz5iVhQSZq8VAvy4xCNhjWVV_4Ie2Syci ~/job-output.txt
{
    "checksum": "6b72933f6d83bb1294fef21c290d86ad4d5bacee89a0e34eca284c08b00e88c1",
    "status": 200,
    "acceptRanges": "bytes",
    "contentType": "application/octet-stream"
}
$ cat ~/job-output.txt
Hello Glacier

無事Archiveをダウンロードすることが出来た。

Archiveの削除

Archiveおいておくだけでお金は掛かるのでそれが気になるという場合はArchiveを削除できる。
削除にはArchiveIdを指定すれば良い。
ちなみに削除してもすぐにはインベントリには反映はされず最大1日掛かる。

$ aws glacier delete-archive --account-id - --vault-name qiita-test --archive-id OQBATOmxpMSNOxXPrKLkGiISpNvtZySRe-Dg_PvVbg3zxfIRGa3o1et33LvKvZSJA2_nAHW9eMhclfJAbGLzSV3owDcUvNglvAEafu67wKsECismh1uRL_-Rz0rCsGD_4AoGB7UP8w
# 何も出力されない

Vaultの削除

Archiveが完全になくなったらVaultを削除することが出来る。
もしArchiveが存在する場合は下記のような例外が投げられる。
消した直後でおかしいと思うが、どうやらInventoryが更新されたタイミングでArchiveが存在しなくなるまではVaultの削除は出来ないようだ。
Inventoryの更新までおとなしく待つ(ちなみにInventoryの更新を手動で行う方法は調べた感じないようだった)

$ aws glacier delete-vault --account-id - --vault-name qiita-test

An error occurred (InvalidParameterValueException) when calling the DeleteVault operation: Vault not empty or recently written to: arn:aws:glacier:ap-northeast-1:{{account-id}}:vaults/qiita-test

Inentoryが更新されて空になったらVaultが削除できるようになっているので試してみよう。

$ aws glacier delete-vault --account-id - --vault-name qiita-test
# 上記の例外を除いて成功・失敗に関わらず何も表示されない

Vaultの一覧を取得してVaultがなくなっていることを確認してなくなっていれば削除完了。

$ aws glacier list-vaults --account-id -
{
    "VaultList": [
    ]
}

もしArchiveIdが分からなくなった場合

ArchiveIdがないとArchiveを取得することすら出来ない。
もしArchiveIdを保存し忘れていた場合はInventory経由でArchiveIdを知る必要がある。

以下のようにInventoryの取得Jobを流して一覧を取得することが出来る。
ただしこちらもArchive取得と同様に待たされる上にTierで速度も選べないので数時間の待ちが発生する。
そのため、基本ArchiveIdはupload時点で保存するようにしておくことをオススメする。

Inventoryを取得するJsonパラメータを以下に記載。

  • Format

    • 取得結果のファイル形式
    • JSON|CSV のどちらかが選べる
  • SNSTopic
    • Archive取得Jobと同義
  • InventoryRetrievalParameters
    • オプションで指定可能。Archiveの絞込が行える
    • .StartDate
      • 取得したいArchiveの開始期間
    • .EndDate
      • 取得したいArchiveの終了期間
    • .Limit
      • Archiveを最大何件表示するか
      • なければ限界の数を返す

以下のパラメータの場合は2017/11/23 ~ 2017/12/03 の間で1000件という条件を付けてInventoryからArchiveの検索を行っている(1件しかないから意味ないけど)。

$ cat ~/inventory_job.txt
{
  "Type": "inventory-retrieval",
  "Description": "Get Inventory",
  "Format": "JSON",
  "SNSTopic": "{{sns-arn}}",
  "InventoryRetrievalParameters": {
      "StartDate": "2017-11-23T00:00:00Z",
      "EndDate": "2017-12-03T00:00:00Z",
      "Limit": "1000"
   }
}

$ aws glacier initiate-job --account-id - --vault-name qiita-test --job-parameters file://~/inventory_job.json
{
    "location": "/{{account-id}}/vaults/qiita-test/jobs/CgRscKViRNU3XUgqxa8nu8GWVhmkw3yEWEZ089NZ0Hj-a9aL86QHHc5zav3VW2e5KUHm5AQwoMEd5QjNysg9EWYvhSin",
    "jobId": "CgRscKViRNU3XUgqxa8nu8GWVhmkw3yEWEZ089NZ0Hj-a9aL86QHHc5zav3VW2e5KUHm5AQwoMEd5QjNysg9EWYvhSin"
}

Jobが発行されていることをArchiveの時と同様に list-jobs で確認できる。
今回はオプションに --completed false を付けることで完了していないJobのみを表示するようにした。

$ aws glacier list-jobs --account-id - --vault-name qiita-test --completed false
{
    "JobList": [
        {
            "JobId": "CgRscKViRNU3XUgqxa8nu8GWVhmkw3yEWEZ089NZ0Hj-a9aL86QHHc5zav3VW2e5KUHm5AQwoMEd5QjNysg9EWYvhSin",
            "JobDescription": "Get Inventory",
            "Action": "InventoryRetrieval",
            "VaultARN": "arn:aws:glacier:ap-northeast-1:{{account-id}}:vaults/qiita-test",
            "CreationDate": "2017-12-03T01:28:54.814Z",
            "Completed": false,
            "StatusCode": "InProgress",
            "SNSTopic": "{{sns-arn}}",
            "InventoryRetrievalParameters": {
                "Format": "JSON",
                "StartDate": "2017-11-23T00:00:00Z",
                "EndDate": "2017-12-03T00:00:00Z",
                "Limit": "1000"
            }
        }
    ]
}

Archiveと同様にJobが完了するとSNS経由で通知が来る。
通知を確認したらArchiveの取得と同様に JobId をパラメータに渡してあげることでInventoryからArchiveの一覧を取得することが可能。

$ aws glacier get-job-output --account-id - --vault-name qiita-test --job-id CgRscKViRNU3XUgqxa8nu8GWVhmkw3yEWEZ089NZ0Hj-a9aL86QHHc5zav3VW2e5KUHm5AQwoMEd5QjNysg9EWYvhSin ~/output-get-inventory.json
$ cat ~/output-get-inventory.json  | jq .
{
  "VaultARN": "arn:aws:glacier:ap-northeast-1:{{account-id}}:vaults/qiita-test",
  "InventoryDate": "2017-12-02T17:21:20Z",
  "ArchiveList": [
    {
      "ArchiveId": "OQBATOmxpMSNOxXPrKLkGiISpNvtZySRe-Dg_PvVbg3zxfIRGa3o1et33LvKvZSJA2_nAHW9eMhclfJAbGLzSV3owDcUvNglvAEafu67wKsECismh1uRL_-Rz0rCsGD_
4AoGB7UP8w",
      "ArchiveDescription": "",
      "CreationDate": "2017-12-02T13:57:27Z",
      "Size": 14,
      "SHA256TreeHash": "6b72933f6d83bb1294fef21c290d86ad4d5bacee89a0e34eca284c08b00e88c1"
    }
  ]
}

おわりに

S3は使い方を分かっているけどGlacierをどう使っていいか分からない、という読者の疑問はこの記事を読んで多少は解消されることを期待している。
他にも分割アップロードや分割ダウンロードなどの機能があるようですが、あくまでも触りということで記載しなかった(し、自分も試してないので分からない)。
気になる方は公式の資料などを読むことでより理解が深まると思う。

参考

続きを読む

AWS CloudFormationを使ってAWS Fargateの環境を作成してみる

本記事は個人の意見であり、所属する組織の見解とは関係ありません。

こちらはAWS Fargate Advent Calendar 2017の6日目の記事です。
AWS Fargateが発表されて、一週間ぐらい経ちました。新しいサービス、機能を色々試してみるのは楽しいですよね!

今日は、Fargateを触ってみて、もう少し本格的に取り組んでみたいと感じた方向けにAWS CloudFormationを使ってAWS Fargateの環境を作成する流れについて確認してみたいと思います。

AWS CloudFormationとは

Cloudformationでは、AWSリソースの環境構築を設定テンプレートを元に自動化する事ができます。ECSで利用する場合、TaskdefinisionやServiceの設定なども記述する事ができます。Containerのデプロイをより簡単に行える様になり各種自動化を行いやすくなるメリットもあります。

今回はFargateのAdvent Calendarへの投稿ですので、詳細については、次のWebinerの資料を確認してみてください。

CloudFormationテンプレート作成

作成方針

Cloudformationのテンプレートは記載の自由度が高く、色々な記述の仕方ができるのですが、今回は分かりやすさを重視して次の様な構成で分割したテンプレートを作成してみました。

  • VPC作成用テンプレート

    • Fargate用のVPCを作成し、VPCの設定を行うテンプレート
    • PublicSubnetやPrivateSubnet、ルートテーブルなどを作成していきます。
  • SecurityGroup作成用テンプレート

    • TaskやALBで利用するSecurityGroupを作成します。
  • ECSクラスターを作成するテンプレート

  • ELBを設定するテンプレート

  • TaskDefinitionテンプレート

    • ECS上で起動するContainerに関する設定を行います。
  • Serviceテンプレート

分割の仕方も様々ですので、各自のユースケースにあわせて、色々と試してみてください。個人的には、ライフサイクルが異なるリソースは別テンプレートにするが好きです。逆に、開発環境やデモ環境を素早く立ち上げたい場合は1つのテンプレートの中に全て記載してしまうのもいいですよね。

VPC作成用テンプレート

テンプレートの一部は次の様な形になります。(折りたたんでいます)

Resources:
    VPC:
        Type: AWS::EC2::VPC
        Properties:
            CidrBlock: !Ref VpcCIDR
            Tags:
                - Key: Name
                  Value: !Ref EnvironmentName

    InternetGateway:
        Type: AWS::EC2::InternetGateway
        Properties:
            Tags:
                - Key: Name
                  Value: !Ref EnvironmentName


  <省略>


Outputs:

    VPC:
        Description: A reference to the created VPC
        Value: !Ref VPC
        Export:
          Name: !Sub ${EnvironmentName}-VPC

ポイントは、作成したリソースに関する情報を別リソースからもアクセスできる様に
OutpusセクションでExport属性をつけている事です。Export属性で定義しているNameを利用して、
別テンプレートからも対象リソースに対する参照を行う事ができます。

SecurityGroup作成用テンプレート

ALB用、Container用のSecurityGroupを作成し、必要なPortを許可しています。

こちらも長いので、折りたたんでいます。

Description: >
    This template deploys a security-groups.

Parameters:
  EnvironmentName:
    Description: An environment name that will be prefixed to resource names
    Type: String
    Default: advent-calendar-2017

Resources:
  LoadBalancerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId: !ImportValue advent-calendar-2017-VPC
      GroupDescription: SecurityGroup for ALB
      SecurityGroupIngress:
        - CidrIp: 0.0.0.0/0
          IpProtocol: tcp
          FromPort: 80
          ToPort: 80
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-LoadBalancers


  ContainerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId: !ImportValue advent-calendar-2017-VPC
      GroupDescription: Security Group for Task
      SecurityGroupIngress:
        -
          SourceSecurityGroupId: !Ref LoadBalancerSecurityGroup
          IpProtocol: -1
        -
          CidrIp: 0.0.0.0/0
          IpProtocol: tcp
          FromPort: 80
          ToPort: 80

      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-ContainerSecurityGroup

Outputs:
  LoadBalancerSecurityGroup:
    Description: A reference to the security group for load balancers
    Value: !Ref LoadBalancerSecurityGroup
    Export:
      Name: !Sub ${EnvironmentName}-LoadBalancerSecurityGroup

  ContainerSecurityGroup:
    Description: A reference to the security group for EC2 hosts
    Value: !Ref ContainerSecurityGroup
    Export:
      Name: !Sub ${EnvironmentName}-ContainerSecurityGroup

ECSクラスタ-を作成するテンプレート

非常にシンプルです。ただ、クラスタを定義して名前をつけるだけ。

Description: >
    This sample template deploys a ECS Cluster

Parameters:
  EnvironmentName:
    Description: An environment name that will be prefixed to resource names
    Type: String
    Default: advent-calendar-2017

Resources:
  ECSCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: !Sub ${EnvironmentName}-cluster

Outputs:
  ECSCluster:
    Description: A referenc
    Value: !Ref ECSCluster
    Export:
      Name: !Sub ${EnvironmentName}-Cluster

ELBを設定するテンプレート

ALB、Listener、Targetgroupを作成しています。

Description: >
    This template deploys an Application Load Balancer.


Parameters:
  EnvironmentName:
    Description: An environment name that will be prefixed to resource names
    Type: String
    Default: advent-calendar-2017


Resources:
  LoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: !Ref EnvironmentName
      Subnets:
        - !ImportValue advent-calendar-2017-PublicSubnet1
        - !ImportValue advent-calendar-2017-PublicSubnet2
      SecurityGroups:
        - !ImportValue advent-calendar-2017-LoadBalancerSecurityGroup
      Tags:
        - Key: Name
          Value: !Ref EnvironmentName
      Scheme: internet-facing

  LoadBalancerListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref LoadBalancer
      Port: 80
      Protocol: HTTP
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref DefaultTargetGroup

  DefaultTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: !Sub ${EnvironmentName}-targetgroup
      VpcId: !ImportValue advent-calendar-2017-VPC
      Port: 80
      Protocol: HTTP
      TargetType: ip

Outputs:

  LoadBalancer:
    Description: A reference to the Application Load Balancer
    Value: !Ref LoadBalancer
    Export:
      Name: !Sub ${EnvironmentName}-Loadbalancer

  LoadBalancerUrl:
    Description: The URL of the ALB
    Value: !GetAtt LoadBalancer.DNSName

  Listener:
    Description: A reference to a port 80 listener
    Value: !Ref LoadBalancerListener
    Export:
      Name: !Sub ${EnvironmentName}-Listener

  DefaultTargetGroup:
    Value: !Ref DefaultTargetGroup
    Export:
      Name: !Sub ${EnvironmentName}-DefaultTargetGroup

TaskDefinition設定

ようやくFargateに関連する設定が出てきました。ここでは、RequiresCompatibilities属性にFARGATEを指定し、
NetworkMode属性にawsvpcを指定しています。また、CPU、メモリの設定はContainerDefinitionsの外側で設定します。
Container Definitionsにおけるmemory/cpuの指定はオプションです。加えて、各Taskがログを出力するためのCloudwatch Logsの設定もここで行なっています。

Description: >
    This sample deploys a task

Parameters:
  EnvironmentName:
    Description: An environment name that will be prefixed to resource names
    Type: String
    Default: advent-calendar-2017

Resources:
  LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub '/ecs/logs/${EnvironmentName}-groups'

  ECSTask:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Cpu: 256
      ExecutionRoleArn: arn:aws:iam::XXXXXXXXXXXX:role/ecsTaskExecutionRole
      Family: !Sub ${EnvironmentName}-task
      Memory: 512
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      ContainerDefinitions:
        -
          Name: nginx
          Image: nginx:latest
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref LogGroup
              awslogs-region: us-east-1
              awslogs-stream-prefix: ecs
          MemoryReservation: 512
          PortMappings:
            -
              HostPort: 80
              Protocol: tcp
              ContainerPort: 80

Outputs:
  LogGroup:
      Description: A reference to LogGroup
      Value: !Ref LogGroup

  ECSTask:
    Description: A reference to Task
    Value: !Ref ECSTask

Service設定

ここではFargate上でTaskを起動させるために、LaunchType属性にFARGATEを指定しています。ここでTaskNameに指定しているXXの数字はTaskのRevisionに該当します。Taskの更新とともにここの数字を変える必要があるという点がポイントです。

Description: >
    This sample deploys a service

Parameters:
  EnvironmentName:
    Description: An environment name that will be prefixed to resource names
    Type: String
    Default: advent-calendar-2017

  TaskName:
    Description: A task name
    Type: String
    Default: advent-calendar-2017-task:XX

Resources:
  Service:
    Type: AWS::ECS::Service
    Properties:
      Cluster: !ImportValue advent-calendar-2017-Cluster
      DesiredCount: 2
      LaunchType: FARGATE
      LoadBalancers:
        -
          TargetGroupArn: !ImportValue advent-calendar-2017-DefaultTargetGroup
          ContainerPort: 80
          ContainerName: nginx
      NetworkConfiguration:
        AwsvpcConfiguration:
          SecurityGroups:
            -
              !ImportValue advent-calendar-2017-ContainerSecurityGroup
          Subnets:
            -
              !ImportValue advent-calendar-2017-PrivateSubnet1
            -
              !ImportValue advent-calendar-2017-PrivateSubnet2
      ServiceName: !Sub ${EnvironmentName}-service
      TaskDefinition: !Ref TaskName
Outputs:
  Service:
      Description: A reference to the service
      Value: !Ref Service

Cloudformation Stackを作成する

これで、Fargate環境の作成準備が整いました。ここからは順番にStackを作成していきます。

$ aws cloudformation create-stack --stack-name advent-calendar-2017-vpc 
--template-body file://Fargate-vpc.yml 
--region us-east-1

$ aws cloudformation create-stack --stack-name advent-calendar-2017-security-group 
--template-body file://Fargate-security-groups.yaml 
--region us-east-1

$ aws cloudformation create-stack --stack-name advent-calendar-2017-load-balancer 
--template-body file://Fargate-load-balancers.yaml 
--region us-east-1

$ aws cloudformation create-stack --stack-name advent-calendar-2017-cluster 
--template-body file://Fargate-cluster.yml 
--region us-east-1


$ aws cloudformation create-stack --stack-name advent-calendar-2017-task 
--template-body file://Fargate-taskdefinition.yml 
--region us-east-1

$ aws cloudformation create-stack --stack-name advent-calendar-2017-service 
--template-body file://Fargate-service.yml 
--region us-east-1

作成した環境を確認する

Cloudformationでの環境構築が終わりました。正しく構築できているか、ALB経由でアクセスして確認してみてください。
作成したALBのFQDNは、マネージメントコンソール上のEC2の画面>ロードバランサにアクセスして確認できます。
それ以外にも今回の例では、CLIでの次の様なコマンドで確認する事ができます。(少し無理やりですが。。。)

$ aws cloudformation describe-stacks --stack-name advent-calendar-2017-load-balancer  
--region us-east-1 | jq '.Stacks[].Outputs[] | select(.OutputKey == "LoadBalancerUrl")'

{
  "Description": "The URL of the ALB",
  "OutputKey": "LoadBalancerUrl",
  "OutputValue": "advent-calendar-2017-844241308.us-east-1.elb.amazonaws.com"
}


####awscli単独でやるなら、次の様にも書く事ができます。

aws cloudformation describe-stacks --stack-name advent-calendar-2017-load-balancer  
--region us-east-1 
--query 'Stacks[].Outputs[?OutputKey == `LoadBalancerUrl`].OutputValue'

ECSクラスター

次のコマンドで存在が確認できます。

$ aws ecs list-clusters --region us-east-1
{
    "clusterArns": [
        "arn:aws:ecs:us-east-1:925496135215:cluster/advent-calendar-2017-cluster"
    ]
}

サービスの状態

作成したサービスの状態は次の様なコマンドで確認できます。

aws ecs describe-services --services <service name> 
--cluster <cluster name> --region us-east-1

例えば、デプロイしているサービスの状況を確認する際には以下の様なコマンドで状態を取得可能です。
次のコマンド結果には、runningCountが2であり、desiredCountの設定通りにTaskが起動している事が確認できます。


$ aws ecs describe-services --services advent-calendar-2017-service 
--cluster advent-calendar-2017-cluster 
--region us-east-1 | jq .[][].deployments
[
  {
    "status": "PRIMARY",
    "networkConfiguration": {
      "awsvpcConfiguration": {
        "subnets": [
          "subnet-2541e678",
          "subnet-9297e0f6"
        ],
        "securityGroups": [
          "sg-326f1047"
        ]
      }
    },
    "pendingCount": 0,
    "createdAt": 1512499161.953,
    "desiredCount": 2,
    "taskDefinition": "arn:aws:ecs:us-east-1:XXXXXXXXXXXX:task-definition/advent-calendar-2017-task:3",
    "updatedAt": 1512500281.269,
    "id": "ecs-svc/9223370524355613851",
    "runningCount": 2
  }
]

デプロイしたServiceを更新する

Cloudformationを利用して作成していますので、更新もCloudformation経由で行います。

テンプレートを更新する。

今回はDesiredCountを修正してみました。

$ diff Fargate-service-update.yml Fargate-service.yml
20c20
<       DesiredCount: 4
---
>       DesiredCount: 2

Stackを更新する

次の様なコマンドでStackの更新が可能です。

$ aws cloudformation update-stack --stack-name advent-calendar-2017-service 
--template-body file://Fargate-service-update.yml 
--region us-east-1

しばらく待った後に再びServiceの状態を確認するとDsierdCount通りにTaskの数が増えている事が確認できます。

$ aws ecs describe-services --services advent-calendar-2017-service 
--cluster advent-calendar-2017-cluster 
--region us-east-1 | jq .[][].deployments
[
  {
    "status": "PRIMARY",
    "networkConfiguration": {
      "awsvpcConfiguration": {
        "subnets": [
          "subnet-2541e678",
          "subnet-9297e0f6"
        ],
        "securityGroups": [
          "sg-326f1047"
        ]
      }
    },
    "pendingCount": 0,
    "createdAt": 1512499161.953,
    "desiredCount": 4,
    "taskDefinition": "arn:aws:ecs:us-east-1:925496135215:task-definition/advent-calendar-2017-task:3",
    "updatedAt": 1512538215.582,
    "id": "ecs-svc/9223370524355613851",
    "runningCount": 4
  }
]

Taskをアップデートする。

テンプレートを更新する。

Taskが利用するメモリの容量を修正してみました。

$ diff Fargate-taskdefinition-update.yml Fargate-taskdefinition.yml 
25c25
<       Memory: 1024
---
>       Memory: 512

Stackを更新する

次の様なコマンドでTask用のStackの更新をします。

$ aws cloudformation update-stack --stack-name advent-calendar-2017-task 
--template-body file://Fargate-taskdefinition-update.yml 
--region us-east-1

TaskのRevisionが変化していますので、Serviceでも新しいRevisionを利用する様に、テンプレートを修正して、Service用のStackを更新します。

$ aws cloudformation update-stack --stack-name advent-calendar-2017-service 
--template-body file://Fargate-service.yml 
--region us-east-1

再度、サービスの状態を確認して、起動しているTaskが更新されている事を確認してみてください。

まとめ

Cloudformationを利用したECS,Fargateの操作はいかがだったでしょうか。今回の記事を書く為に、新規でCloudformationテンプレートを作成したのですが、これまでのECSで利用していたテンプレートとの違いは僅かでした。FargateをきっかけにECSに興味を持って頂けた方の参考になればうれしいです。

続きを読む

AWS Fargateのタスクでsshd、コンテナにシェルで入ってみる

とりあえずやってみたくなるやつ。

今回使用したDockerイメージのソース、タスク用のJSONなどはこちら。

一回ログインしたらセッション切断時にタスクも終了する(sshdが-dで起動している)ようにしています。

ログインしてみる

ssh root@52.90.198.xxx で。環境変数ROOT_PWをrun-taskで指定していなければパスワードroooootで。

一応amazonlinuxをベースにしており、ログインしたときの環境変数はこんな感じ。 (※ 元の環境変数からAWS_*のみ自動でexportするように指定済。)

$ ssh root@34.236.216.xxx
root@34.236.216.xxx's password: 
There was 1 failed login attempt since the last successful login.
debug1: PAM: reinitializing credentials
debug1: permanently_set_uid: 0/0

Environment:
  USER=root
  LOGNAME=root
  HOME=/root
  PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/aws/bin
  MAIL=/var/mail/root
  SHELL=/bin/bash
  SSH_CLIENT=xxxxxxxxx 57401 22
  SSH_CONNECTION=xxxxxxxxx 57401 10.0.1.188 22
  SSH_TTY=/dev/pts/0
  TERM=xterm-256color
  AWS_CONTAINER_CREDENTIALS_RELATIVE_URI=/v2/credentials/2bf9b4ff-70a8-4d84-9733-xxxxxxx
  AWS_DEFAULT_REGION=us-east-1
  AWS_REGION=us-east-1

-bash-4.2# 

普通のメタデータは取れません。

# ec2-metadata 
[ERROR] Command not valid outside EC2 instance. Please run this command within a running EC2 instance.

Task Roleの取得はOK

-bash-4.2# curl 169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI | jq .
{
  "RoleArn": "arn:aws:iam::xxxxxxxx:role/ecsTaskExecutionRole",
  "AccessKeyId": "ASIAXXXXXXXXX",
  "SecretAccessKey": "XXXXXXXXXX",
  "Token": "xxx==",
  "Expiration": "2017-12-04T11:37:56Z"
}

という程度の軽い動作チェック程度には使えるというのと、一応このまま何かを頑張って構築しても稼働はするはず。。※コンテナポートはtask-definitionに追加で。
ホストのファイルシステムをマウントとかできないのであまり探るところもないけれど、気になるところは各自でチェックという感じで。

ふろく: AWS-CLIでrun-task

CLIの実行例にTask Roleはつけていないので、必要ならregister-task-definitionか、run-taskのoverrideで付与します。

task-definitionを作成する。

$ EXEC_ROLE_ARN=<ロールのARN> # AmazonECSTaskExecutionRolePolicyが付いたなにかしらのRole
$ aws ecs register-task-definition 
  --execution-role-arn $EXEC_ROLE_ARN 
  --family ssh-login 
  --network-mode awsvpc 
  --requires-compatibilities FARGATE 
  --cpu 256 
  --memory 512 
  --container-definitions "`cat example/task-def-sshlogin.json`"

> {
>    "taskDefinition": {
>        "taskDefinitionArn": "arn:aws:ecs:us-east-1:xxxxxxxx:task-definition/ssh-login:2",
>        "containerDefinitions": [
...

taskを実行する。

$ CLUSTER_NAME=<クラスタの名前>
$ SUBNET_ID=<コンテナを実行するサブネット>
$ SG_ID=<TCP/22が開いているSG>
$ aws ecs run-task 
  --cluster $CLUSTER_NAME 
  --launch-type FARGATE 
  --task-definition ssh-login:1 
  --network-configurationawsvpcConfiguration="{subnets=[$SUBNET_ID],securityGroups=[$SG_ID],assignPublicIp=ENABLED}"

#=> タスクのIDが返ってくるで変数に入れておくと良い

ENIのPublicIPはrun-taskの時点ではすぐにわからないので、ENIのIDがわかるまで適当にdescribe。

$ TASK_ARN=arn:aws:ecs:us-east-1:xxxxxxxxxxx:task/677c0132-9086-4087-bd18-xxxxxxxxxxx
$ aws ecs --region us-east-1 describe-tasks 
  --cluster $CLUSTER_NAME 
  --tasks arn:aws:ecs:us-east-1:xxxxxxxxxxx:task/677c0132-9086-4087-bd18-xxxxxxxxxxx

# =>
...

                {
                    "id": "xxxxxxxxxxx",
                    "type": "ElasticNetworkInterface",
                    "status": "ATTACHED",
                    "details": [
                        {
                            "name": "subnetId",
                            "value": "subnet-xxxxxxx"
                        },
                        {
                            "name": "networkInterfaceId",
                            "value": "eni-xxxxxxxx"
                        },
...

ENIのIDが分かったらPublicIPをとります。

$ aws ec2 --region us-east-1 describe-network-interfaces --network-interface-ids eni-xxxxxx | jq .NetworkInterfaces[].Association.PublicIp
"52.90.198.xxx"

ENIあたりはManagementConsole見ながらのほうが楽ですけどね。

続きを読む

AWS Media ServicesをCLIから操作する

最近、AWSメディアサービスやってみています! https://qiita.com/tin-machine/items/73d1564eeeeda49a3f6c

でも、動画配信のパラメータを変更して検証するの… :tired_face: 超面倒なんですけど。おっさん辛い。

:persevere: そもそも、都度チャンネル作らなきゃならない設定はなんなの。辛み。
CLIから作りたくなってきた。aws-cliをアップデートするとメディアサービスも使えるようになります!

最初に aws-cliをアップデートする

pip install -U awscli

チャンネルidを取得する

先にチャンネルのIDを確認します。WebUIだと
[ MediaLive ]->[ Channels ]->作成したチャンネルのラジオボタンをクリック->[ ID ]の所の数字をメモ

aws medialive describe-channel --region ap-southeast-1 --channel-id チャンネルid > channel_sample.json

CLIからだと

aws medialive list-channels --region ap-southeast-1

で取得できます

チャンネル設定を変更する :relaxed:

上記の describe-channel はユニークであるべきidやチャンネルごとに振られるIPアドレス、ステータスなども出力されてしまいますので削除します。
* PipelinesRunningCount
* EgressEndpoints
* State
* Id
* Arn

既にある設定を削除する :smiling_imp:

注) 下記を実行すると、既にあるチャンネル設定が一つ消えます。高速に検証するため、チャンネル設定を消して作ってを行っていますが、サービスするようになったら変更する必要があるでしょう。

上記のチャンネルidを取得する方法を利用して、既存のチャンネル設定を削除しています。

aws medialive delete-channel --region ap-southeast-1 --channel-id $(aws medialive list-channels --region ap-southeast-1 | jq -r '.Channels[].Id')

jsonからチャンネルを作る :kissing_heart:

aws medialive create-channel  --region ap-southeast-1  --cli-input-json file://channel_sample.json

チャンネルをスタートする :movie_camera:

aws medialive start-channel --region ap-southeast-1 --channel-id $(aws medialive list-channels --region ap-southeast-1 | jq -r '.Channels[].Id')

チャンネルをストップする :no_entry_sign:

aws medialive stop-channel --region ap-southeast-1 --channel-id (aws medialive list-channels --region ap-southeast-1 | jq -r '.Channels[].Id')

続きを読む

KubernetesでAWS ALBを自動作成する〜ついでにRoute53 Record Setも

kube-ingress-aws-controllerを使います。

kube-ingress-aws-controllerとは

Zalandoが公開している、Kubernetes用のIngress Controllerの一つです。

ZalandoはKubernetes界隈では著名な、ヨーロッパでファッションECをやっている企業です。
Kubernetesコミュニティへの様々な形で貢献していて、今回紹介するkube-aws-ingerss-controllerや先日紹介したexternal-dnsもその一つです。

何かできるのか

KubernetesユーザがAWSを全く触らずとも

  • ALBの自動作成
  • ALBに割り当てるTLS証明書(ACM管理)を自動選択

をしてくれます。

使い方

KubernetesのIngressリソースを普段通りつくります。

kubectl create -f myingress.yaml

すると、1~2分ほどでIngressリソースに書いたホスト名でインターネットからアクセスできるようになります。

open https://myingress.exapmle.com

Ingress Controllerとは

KubernetesのIngressはL7ロードバランサのスペックのようなもので、そのスペックから実際にL7ロードバランサをセットアップするのがIngress Controllerの役割です。

coreos/alb-ingress-controllerとの違い

coreos/alb-ingress-controller

  • Ingressリソース一つに対して、1 ALBをつくります

zalando-incubator/kube-ingress-aws-controller

  • ACM証明書のドメイン一つに対して、ALBを割り当てます
  • 同じドメイン名に対するルートを含むIngressリソースは、一つのALBにまとめられます
  • ALBのターゲットグループにEC2インスタンスを割り当てるところまでしかやってくれない!ので、実際にIngressとして利用するためには他のIngress Controllerを併用する必要があります

kube-ingress-aws-controllerのセットアップ手順

Security Groupの作成

kube-ingress-aws-controllerはALBに割り当てるSecurity Groupまでは自動作成してくれないので、AWSコンソール等で作成します。

kube-ingerss-aws-controllerのドキュメントにはCloudFormationを使った手順がかいてあります。

同等のSecurity Groupをawscliで作る場合は以下のようなコマンドを実行します。

CLUSTER_NAME=...
VPC_ID=vpc-...

aws ec2 create-security-group \
  --description ${CLUSTER_NAME}-kube-aws-ingress-controller-alb \
  --group-name ${CLUSTER_NAME}-kube-aws-ingress-controller-alb \
  --vpc-id $VPC_ID | tee sg.json

SG_ID=$(jq -r '.GroupId' sg.json)

aws ec2 create-tags --resources $SG_ID --tags \
  "Key=\"kubernetes.io/cluster/$CLUSTER_NAME\",Value=owned" \
  "Key=\"kubernetes:application\",Value=kube-ingress-aws-controller"

aws ec2 authorize-security-group-ingress \
  --group-id $SG_ID \
  --ip-permissions '[{"IpProtocol": "tcp", "FromPort": 80, "ToPort": 80, "IpRanges": [{"CidrIp": "0.0.0.0/0"}]}, {"IpProtocol": "tcp", "FromPort": 443, "ToPort": 443, "IpRanges": [{"CidrIp": "0.0.0.0/0"}]}]'

aws ec2 describe-security-groups --group-id $SG_ID

# あとで: 不要になったらクラスタやVPCの削除前に以下のように削除
aws ec2 delete-security-group --group-id $SG_ID

IAMポリシーの割当

kube-ingerss-aws-controllerのドキュメントにIAM Policy Statementの一覧がかいてありますが、要約すると

  • CloudFormationスタックのCRUD権限
  • ACM証明書、VPC、RouteTable、Subnet、Security Group、AutoScalingGroup、EC2 InstanceのGet/List/Describe権限
  • ALBのCRUD権限

が必要です。

kube-awsのcluster.yamlの場合は、以下のように書きます。

worker:
  nodePools:
  - iam:
      policy:
        statements:
        - effect: Allow
          actions:
          - "autoscaling:DescribeAutoScalingGroups"
          - "autoscaling:AttachLoadBalancers"
          - "autoscaling:DetachLoadBalancers"
          - "autoscaling:DetachLoadBalancerTargetGroup"
          - "autoscaling:AttachLoadBalancerTargetGroups"
          - "elasticloadbalancing:AddTags"
          - "elasticloadbalancing:DescribeLoadBalancers"
          - "elasticloadbalancing:CreateLoadBalancer"
          - "elasticloadbalancing:DeleteLoadBalancer"
          - "elasticloadbalancing:DescribeListeners"
          - "elasticloadbalancing:CreateListener"
          - "elasticloadbalancing:DeleteListener"
          - "elasticloadbalancing:DescribeTags"
          - "elasticloadbalancing:CreateTargetGroup"
          - "elasticloadbalancing:DeleteTargetGroup"
          - "elasticloadbalancing:DescribeTargetGroups"
          - "elasticloadbalancingv2:DescribeTargetGroups"
          - "elasticloadbalancingv2:DescribeLoadBalancers"
          - "elasticloadbalancingv2:CreateLoadBalancer"
          - "elasticloadbalancingv2:DeleteLoadBalancer"
          - "elasticloadbalancingv2:DescribeListeners"
          - "elasticloadbalancingv2:CreateListener"
          - "elasticloadbalancingv2:DeleteListener"
          - "elasticloadbalancingv2:DescribeTags"
          - "elasticloadbalancingv2:CreateTargetGroup"
          - "elasticloadbalancingv2:DeleteTargetGroup"
          - "ec2:DescribeInstances"
          - "ec2:DescribeSubnets"
          - "ec2:DescribeSecurityGroup"
          - "ec2:DescribeRouteTables"
          - "ec2:DescribeVpcs"
          - "acm:ListCertificates"
          - "acm:DescribeCertificate"
          - "iam:ListServerCertificates"
          - "iam:GetServerCertificate"
          - "cloudformation:Get*"
          - "cloudformation:Describe*"
          - "cloudformation:List*"
          - "cloudformation:Create*"
          - "cloudformation:Delete*"
          resources:
          - "*"

kube-aws-ingress-controllerをデプロイ

$ kubectl apply -f kube-aws-ingress-controller.yaml
kube-aws-ingress-controller.yaml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: kube-ingress-aws-controller
  namespace: kube-system
  labels:
    application: kube-ingress-aws-controller
    component: ingress
spec:
  replicas: 1
  selector:
    matchLabels:
      application: kube-ingress-aws-controller
      component: ingress
  template:
    metadata:
      labels:
        application: kube-ingress-aws-controller
        component: ingress
    spec:
      containers:
      - name: controller
        image: registry.opensource.zalan.do/teapot/kube-ingress-aws-controller:latest
        env:
        - name: AWS_REGION
          value: ap-northeast-1

併用するIngress Controllerをデプロイ

今回はskipperを使ってみます。

apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
  name: skipper-ingress
  namespace: kube-system
  labels:
    component: ingress
spec:
  selector:
    matchLabels:
      application: skipper-ingress
  updateStrategy:
    type: RollingUpdate
  template:
    metadata:
      name: skipper-ingress
      labels:
        component: ingress
        application: skipper-ingress
    spec:
      hostNetwork: true
      containers:
      - name: skipper-ingress
        image: registry.opensource.zalan.do/pathfinder/skipper:latest
        ports:
        - name: ingress-port
          containerPort: 9999
          hostPort: 9999
        args:
          - "skipper"
          - "-kubernetes"
          - "-kubernetes-in-cluster"
          - "-address=:9999"
          - "-proxy-preserve-host"
          - "-serve-host-metrics"
          - "-enable-ratelimits"
          - "-experimental-upgrade"
          - "-metrics-exp-decay-sample"
          - "-kubernetes-https-redirect=true"
        resources:
          limits:
            cpu: 200m
            memory: 200Mi
          requests:
            cpu: 25m
            memory: 25Mi
        readinessProbe:
          httpGet:
            path: /kube-system/healthz
            port: 9999
          initialDelaySeconds: 5
          timeoutSeconds: 5

WorkerノードのSecurity Group設定変更

今回はskipperをつかうことにしたので、kube-ingress-aws-controllerが作成したALBからアクセスする先はskipper(がいるEC2インスタンス)になります。

Security GroupへALBからskipperがいるEC2インスタンスへの通信をブロックしたままだとGateway Timeoutになってしまいます。そうならないように、ALB用につくったSGから、WorkerノードのSGへの9999番ポート(kube-ingress-aws-controllerと組み合わせて使うskipperのhostPortに指定した)の通信を許可しましょう。

ALB側SGのOutboundを絞っていないのであれば、Worker側SGのInboundを追加すればOKです。

Ingressリソースの作成

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: nginx
spec:
  rules:
  - host: nginx-ingress.example.com
    http:
      paths:
      - backend:
          serviceName: nginx
          servicePort: http

---

apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  type: ClusterIP
  ports:
  - name: http
    port: 80
    targetPort: 80
  selector:
    app: nginx

---

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: nginx
spec:
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx
        name: nginx
        ports:
        - containerPort: 80

ログの確認

$ k logs kube-ingress-aws-controller-6bbd8f9d6c-bcqph
2017/12/06 12:33:52 starting /bin/kube-ingress-aws-controller
2017/12/06 12:33:54 controller manifest:
2017/12/06 12:33:54     kubernetes API server:
2017/12/06 12:33:54     Cluster ID: k8s3
2017/12/06 12:33:54     vpc id: vpc-12345678
2017/12/06 12:33:54     instance id: i-07e29f841f676ca00
2017/12/06 12:33:54     auto scaling group name: k8s3-Nodepool1-MMF7MXKI9350-Workers-BZWB5IAV7JW8
2017/12/06 12:33:54     security group id: sg-8368a6fa
2017/12/06 12:33:54     private subnet ids: []
2017/12/06 12:33:54     public subnet ids: [subnet-12345678 subnet-23456789]
2017/12/06 12:33:54 Start polling sleep 30s

30秒経過後、以下のようにCloudFormationスタックが作成される。

2017/12/06 12:34:24 Found 1 ingresses
2017/12/06 12:34:24 Found 0 stacks
2017/12/06 12:34:24 Have 1 models
2017/12/06 12:34:24 creating stack for certificate "arn:aws:acm:ap-northeast-1:myawsaccountid:certificate/64f33935-05ac-4e6b-b1ae-58973d556a74" / ingress ["kube-system/nginx"]
2017/12/06 12:34:25 stack "arn:aws:cloudformation:ap-northeast-1:myawsaccountid:stack/k8s3-b9dbfe3/caf1f3a0-da81-11e7-9e21-500c28b97482" for certificate "arn:aws:acm:ap-northeast-1:myawsaccountid:certificate/64f33935-05ac-4e6b-b1ae-58973d556a74" created
2017/12/06 12:34:25 Start polling sleep 30s

作成されたAWSリソースの確認

1〜2分待ってスタックがCREATE_COMPLETE状態になれば成功。

コンソールでCloudFormationスタックのResourcesの内容を見ると、何がつくられたのかがわかる。

image.png

つくられるのは以下の4つ。

  • HTTPListener: 80番ポート用のListener
  • HTTPSListener: 443番ポート用のListener
  • LB: ALB
  • TG: kube-ingress-aws-controllerがデプロイされているノードが登録されたTargetGroup

ALB

リスナー

443番ポート用のListenerには、Ingressリソースに書いたドメインに対応するACM証明書が選択されています。

今回は*.example.com用のワイルドカード証明書を事前に用意しておいたのですが、Ingressにnginx-ingress.example.comというホスト名を設定したところ、ちゃんとワイルドカード証明書を探し出してくれました(かしこい)。

image.png

ターゲットグループ

kube-aws-ingress-controllerがデプロイされたノードのASGをコンソールでみてみると、ターゲットグループに割り当てられていました。EC2インスタンスを直接TargetGroupに登録していくような方法だとインスタンスが落ちた場合などが怖いですが、ちゃんとしてますね。

image.png

Route53 RecordSetの作成

これだけだとALBが作成されただけなので、nginx-ingress.example.comでアクセスできないはずです。

しかし、昨日デプロイしたexternal-dnsがIngressリソースとALBを検知して、勝手にRecordSetをつくってくれていました。

stern_external-dns.log
external-dns-768686fd4c-zpnlx external-dns time="2017-12-06T12:43:53Z" level=info msg="Desired change: CREATE nginx-ingress.example.com A"
external-dns-768686fd4c-zpnlx external-dns time="2017-12-06T12:43:53Z" level=info msg="Desired change: CREATE nginx-ingress.example.com TXT"
external-dns-768686fd4c-zpnlx external-dns time="2017-12-06T12:43:53Z" level=info msg="Record in zone example.com. were successfully updated"

image.png

ちゃんとALBへのA(lias)レコードを作成してくれていますね。

 インターネットからアクセスしてみる

nginx-ingress.example.comにブラウザからアクセスしてみて、以下のようなnginxのウェルカムページが表示されてば成功です。おつかれさまでした。

image.png

まとめ

kube-ingress-aws-controllerを使うと、Kubernetesユーザはkubectl createするだけでALBとRecordSetをよしなにセットアップしてくれます。
ALBの作成・管理やRoute53 RecordSetの作成のためにいちいちインフラエンジニアを呼び出したくない!というようなセルフサービス好きの会社さんでは特に役立つのではないでしょうか?!

トラブルシューティング

unable to get details for instance “i-0346d738155e965d8”

IAMポリシーが足りないときに出るエラーです。

$ k logs kube-ingress-aws-controller-7f7974ff58-6bvv8
2017/12/06 07:11:51 starting /bin/kube-ingress-aws-controller
2017/12/06 07:11:53 unable to get details for instance "i-0346d738155e965d8": NoCredentialProviders: no valid providers in chain. Deprecated.
    For verbose messaging see aws.Config.CredentialsChainVerboseErrors

required security group was not found

Security Groupがないか、またはSecurityGroupのタグが間違っているか、EC2インスタンスにkubernetes.io/cluster/クラスタ名=ownedというタグがついていない場合のエラーです。

$ k logs kube-ingress-aws-controller-7f7974ff58-xqgrq
2017/12/06 08:10:40 starting /bin/kube-ingress-aws-controller
2017/12/06 08:10:41 required security group was not found

CloudFormationで「At least two subnets in two different Availability Zones must be specified」

KubernetesのWorkerノードのASGが単一のAZに割り当てられているときのエラー。ALBの仕様で、最低2つのAZが必要。

kube-ingress-aws-controller-7f7974ff58-rp7t8 controller 2017/12/06 12:01:06 Start polling sleep 30s
kube-ingress-aws-controller-7f7974ff58-rp7t8 controller 2017/12/06 12:01:36 Found 1 ingresses
kube-ingress-aws-controller-7f7974ff58-rp7t8 controller 2017/12/06 12:01:37 Found 0 stacks
kube-ingress-aws-controller-7f7974ff58-rp7t8 controller 2017/12/06 12:01:37 Have 1 models
kube-ingress-aws-controller-7f7974ff58-rp7t8 controller 2017/12/06 12:01:37 creating stack for certificate "arn:aws:acm:ap-northeast-1:myaccountid:certificate/64f33935-05ac-4e6b-b1ae-58973d556a74" / ingress ["kube-system/nginx"]
kube-ingress-aws-controller-7f7974ff58-rp7t8 controller 2017/12/06 12:01:37 stack "arn:aws:cloudformation:ap-northeast-1:myaccountid:certificate:stack/k8s3-b9dbfe3/360e1830-da7d-11e7-99f7-500c596c228e" for certificate "arn:aws:acm:ap-northeast-1:myaccountid:certificate:certificate/64f33935-05ac-4e6b-b1ae-58973d556a74" created

image.png

instance is missing the “aws:autoscaling:groupName” tag

ASG以外で作ったEC2インスタンスにkube-ingress-aws-controllerがデプロイされてしまったときのエラー。

$ k logs kube-ingress-aws-controller-7f7974ff58-m6ss2
2017/12/06 12:25:59 starting /bin/kube-ingress-aws-controller
2017/12/06 12:25:59 instance is missing the "aws:autoscaling:groupName" tag

kube-aws-ingress-controllerは、デフォルトではASGに設定されたSubnetをALBのSubnetに流用する。
そのためにASGを探すとき、EC2インスタンスについたaws:autoscaling:groupNameというASGが自動的につけてくれるタグをヒントにするため、ASG以外でつくったEC2インスタンスではこのエラーが出てしまう。

Ref: Spot Fleet support · Issue #105 · zalando-incubator/kube-ingress-aws-controller

Issueも出ているが、まだASG以外は対応していない。ワークアラウンドとしては、kube-ingress-aws-controllerのaffinityでASGでつくったノードにだけスケジュールされるようにすることが考えられる。

kube-awsの場合、awsNodeLabels機能をオンにすると、ASGでつくったノードには”kube-aws.coreos.com/autoscalinggroup”というラベルが付与されるので、それを前提にすると以下のようなaffinityをかけばOK。

              affinity:
                nodeAffinity:
                  requiredDuringSchedulingIgnoredDuringExecution:
                    nodeSelectorTerms:
                    - matchExpressions:
                      - key: "kube-aws.coreos.com/autoscalinggroup"
                        operator: "Exists"

504 Gateway Time-out

ALB経由でnginxにアクセスしようとしてこのエラーがかえってきた場合、ALB用につくったセキュリティグループからkube-aws-ingress-controllerが動いているEC2インスタンスへのアクセスを許可できていない可能性があります。

EC2インスタンス側のSGに、ALB用SGからの9999番ポート(kube-ingress-aws-controllerと組み合わせて使うskipperのhostPortに指定した)への通信をを許可するようなInboundルールを追加しましょう。

続きを読む

AWS Batchで速く/安くやるデータセットの前処理

OpenStreamアドベントカレンダーの一日目です。

結構前からやっている趣味DeepLearningですが、最近(実際は結構前から)次のような問題に当たり始めました。

  • データセットが大きくなってきてHDDが厳しい
  • データセットが大きくなってきて前処理がやばい

小さいデータセット+Augmentationでなんとかなるものはいいんですが、現在最大のデータセットは 画像33万枚、220GB弱 あります。
んで、これを前処理したり何だりしていると、最終的に学習で利用するデータを作成するだけで、HDDが500GBくらい利用されてしまう状態です。

容量も当然厳しいんですが、一番厳しいのは処理時間です。現状の前処理を行うと、大体 12時間くらい かかります。趣味でやるので基本的に自分のPCでやっていると、HDDが悲鳴を上げる上に、実行している間はレイテンシが悪すぎて他の作業もできないって状態になってしまっていました。

前処理の中でGPUを利用しているので、GPUを使うような作業も出来ないという。

一部のデータセットで試して〜ってやれば出来ないこともないんですが、結局最後には全部やらないとならないので、この機会に AWS Batch を使って、一気に処理出来ないかどうかを試してみました。

Azure Batchとかもありますが、とりあえずはAWSで。Azure Batchでも同じようなことは出来るかと
GCPのDataflowのだとApache Beamに縛られるので、今回は対象外としました

前提

今回作成するジョブは大きく2つです。

  • 画像のリサイズ
  • エッジの抽出

また、今回は前処理だけ出来ればいいので、GPUインスタンスは使いません。

後述するように、基本的に AWS CloudFormation を利用します。基本的にAWS CLIは構築時点では利用しません。

では行ってみましょう。

AWS Batchについて

日本(の特にSIer的な人々)にとって、Batchと言えばJP1とかああいったフロー制御を想像してしまいますが、それとは異なります。

こちら でわかりやすくまとめられています。Batchって言われると脊髄反射的にGUIが・・・とか思ってしまうのはなんとかしたいです。

上記の記事で説明はされているので、ここではAWS Batchが何かということについては記述しません。

ジョブ実行用コンテナの作成

まずはジョブ実行用のDocker imageを作成します。今回はサイズにこだわって、次のような構成にしてみました。

  • Alpine Linux 3.6
  • Python 3.6.3
  • OpenCV 3.2.0

で、実際に利用しているDockerfileがこんな感じになります。

FROM alpine:3.6

ENV OPENCV_VERSION 3.2.0
ENV PYTHON_VERSION 3.6.3

RUN apk add --update --no-cache 
    build-base 
    openblas 
    openblas-dev 
    unzip 
    curl 
    cmake 
    libjpeg 
    libjpeg-turbo-dev 
    libpng-dev 
    jasper-dev 
    tiff-dev 
    libwebp-dev 
    clang-dev 
    openssl 
    ncurses 
    libstdc++ 
    linux-headers && 
    mkdir -p /opt && 
    apk add --no-cache --virtual .build-python 
      openssl-dev 
      ncurses-dev 
      xz && 
    curl -LO https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tar.xz && 
    unxz Python-${PYTHON_VERSION}.tar.xz && 
    tar xf Python-${PYTHON_VERSION}.tar -C /opt && 
    cd /opt/Python-${PYTHON_VERSION} && 
    ./configure --enable-shared && make -j > /dev/null && make install > /dev/null && 
    cd / && 
    rm -rf /opt/Python-${PYTHON_VERSION} Python-${PYTHON_VERSION}.tar && 
    pip3 install numpy==1.13.3 && 
    apk del --virtual .build-python && 
    curl -LO https://github.com/opencv/opencv/archive/${OPENCV_VERSION}.zip && 
    unzip ${OPENCV_VERSION}.zip -d /opt && 
    rm -rf ${OPENCV_VERSION}.zip && 
    mkdir -p /opt/opencv-${OPENCV_VERSION}/build && 
    cd /opt/opencv-${OPENCV_VERSION}/build && 
    cmake 
      -D CMAKE_BUILD_TYPE=RELEASE 
      -D CMAKE_INSTALL_PREFIX=/usr/local 
      -D WITH_FFMPEG=NO 
      -D WITH_IPP=NO 
      -D WITH_OPENEXR=NO 
      -D WITH_TBB=YES 
      -D BUILD_TESTS=NO 
      -D BUILD_EXAMPLES=NO 
      -D BUILD_ANDROID_EXAMPLES=NO 
      -D INSTALL_PYTHON_EXAMPLES=NO 
      -D BUILD_DOCS=NO 
      -D BUILD_opencv_python2=NO 
      -D BUILD_opencv_python3=ON 
      -D PYTHON3_EXECUTABLE=/usr/local/bin/python3 
      -D PYTHON3_INCLUDE_DIR=/usr/local/include/python3.6m/ 
      -D PYTHON3_LIBRARY=/usr/local/lib/libpython3.so 
      -D PYTHON_LIBRARY=/usr/local/lib/libpython3.so 
      -D PYTHON3_PACKAGES_PATH=/usr/local/lib/python3.6/site-packages/ 
      -D PYTHON3_NUMPY_INCLUDE_DIRS=/usr/local/lib/python3.6/site-packages/numpy/core/include/ 
      .. && 
    make VERBOSE=1 && 
    make -j2 && 
    make install && 
    cd / && 
    rm -rf /opt && 
    apk del gcc build-base openblas-dev clang-dev linux-headers cmake unzip

全部一つのRUNに押し込めているので、失敗すると悲惨ですが、これで作成したimageは 273MB 程度と、それなりに扱いやすいサイズになります。

このimageは、実際に作業をさせるimageのBase imageになるので、小さいにこしたことはないです。

実際に利用するImage

実際に利用するImageは、以下で管理しています。

https://github.com/derui/painter-tensorflow/tree/master/batch/image-converter

単純に必要なPythonスクリプトをコピーして、pipで依存のインストールをしているだけです。

AWS Batchの環境作成

AWS Batchは、Managed環境とUnmanaged環境の二通りが選べますが、基本的にはManagedでいいかと思います。しかし、ManagedでもJobを動かすまでには以下のようなリソースが必要になります。

AWS Batchが様々なサービスを組み合わせて構築されている証左ですが、手動で作ってると色々とめんどくさいです。

  • VPC/Subnet

    • ECSが使われるので、PublicかPrivate+NAT Gatewayがセットアップされてないと厳しいです
  • SecurityGroup
  • ECSのService Role
    • InstanceProfileも必要です
  • AWS BatchのService Role
  • JobのRole
    • ECSのInstanceProfileとは別で必要です
  • ECR
    • Publicなimageでよければ、Dockerhubとかでもいいんですが、大抵はECRが必要かと

ということで、CloudFormationのテンプレートを作りました。

https://github.com/derui/painter-tensorflow/tree/master/batch/cfn.yml

ただ、必要なリソースを全部入れているので、結構量の多いテンプレートになっています。そこで、Batch系のリソースだけ抜粋します。

  ComputeEnvironment:
    Type: AWS::Batch::ComputeEnvironment
    Properties:
      Type: MANAGED
      ServiceRole: !Sub "arn:aws:iam::${AWS::AccountId}:role/AWSBatchServiceRole"
      ComputeEnvironmentName: C4OnDemand
      ComputeResources:
        MaxvCpus: 128
        SecurityGroupIds:
          - !Ref InstanceSecurityGroup
        Type: EC2
        Subnets:
          - !Ref PrivateSubnet
        MinvCpus: 0
        ImageId: ami-a1b2c3d4
        InstanceRole: ecsInstanceRole
        InstanceTypes:
          - c4.large
          - c4.xlarge
          - c4.2xlarge
          - c4.4xlarge
          - c4.8xlarge
        Ec2KeyPair: batch-compute
        Tags: {"Name": "Batch Instance - C4OnDemand"}
        DesiredvCpus: 48
      State: ENABLED

  MyRepository: 
    Type: "AWS::ECR::Repository"
    Properties: 
      RepositoryName: "image-convert"
      RepositoryPolicyText:
        Version: "2012-10-17"
        Statement: 
          - 
            Sid: AllowPushPull
            Effect: Allow
            Principal:
              AWS: !Sub "arn:aws:iam::${AWS::AccountId}:user/${User}"
            Action: 
              - "ecr:GetDownloadUrlForLayer"
              - "ecr:BatchGetImage"
              - "ecr:BatchCheckLayerAvailability"
              - "ecr:PutImage"
              - "ecr:InitiateLayerUpload"
              - "ecr:UploadLayerPart"
              - "ecr:CompleteLayerUpload"
          - 
            Sid: AllowPull
            Effect: Allow
            Principal:
              AWS: !GetAtt JobRole.Arn
            Action: 
              - "ecr:GetDownloadUrlForLayer"
              - "ecr:BatchGetImage"
              - "ecr:BatchCheckLayerAvailability"

  JobDefinitionForResize:
    Type: 'AWS::Batch::JobDefinition'
    Properties:
      Type: container
      JobDefinitionName: !Sub
          - ${Service}-resize-image
          - { Service: !Ref ServiceName}
      Parameters:
        Bucket: !Ref Bucket
        ExcludeFiles: exclude.txt
        Size: 128
      ContainerProperties:
        Command:
          - python3
          - -m
          - resize_fix_size
          - -b
          - "Ref::Bucket"
          - -d
          - resized
          - -e
          - "Ref::ExcludeFiles"
          - -s
          - "Ref::Size"
          - --crop
          - "Ref::Prefix"
        Memory: 1000
        JobRoleArn: !Ref JobRole
        Vcpus: 1
        Image: !Sub
          - "${AWS::AccountId}.dkr.ecr.ap-northeast-1.amazonaws.com/${Repository}/image-converter"
          - {"Repository": !Ref MyRepository}
      RetryStrategy:
        Attempts: 1

  JobQueue:
    Type: AWS::Batch::JobQueue
    Properties:
      ComputeEnvironmentOrder:
        - Order: 1
          ComputeEnvironment: !Ref ComputeEnvironment
      State: ENABLED
      Priority: 1
      JobQueueName: HighPriority

これでも全体の1/3くらいです。AWS Batchは、Compute Environmentでリソースの総量を管理し、Job Queueで流量を制御し、Job Definitionで流れるJobの内容を制御します。

実はCloudFormationのドキュメントにある AWS::Batch::JobDefinition のサンプルには罠があり、RetryStrategyが無いため、真似するともれなくエラーになります。(記述時点:2017/12)

大抵コピーで済ます人(自分)は間違いなく踏む地雷かと。

AWS BatchのServiceRoleについて

AWS BatchのServiceRoleは、恐らくCLI/CloudFormationで作成する人だけが引っかかる地雷があります。ドキュメントのTroubleShootingにわざわざ書いてあるくらいなので、よくあったんでしょう・・・私も見事に踏みました。

単純に言うとARNの指定間違いなのですが、これをやってしまうと1時間程度消すことも消す準備をすることもできず、もれなく待ち状態になります。

Important
Do not attempt to delete a compute environment that is in an INVALID state due to a misconfigured AWS Batch service role. This could cause your environment to get stuck in a DELETING state for up to an hour, and you cannot update the compute environment until the operation times out and fails back to INVALID.
http://docs.aws.amazon.com/batch/latest/userguide/troubleshooting.html#invalid_service_role_arn から引用

Management ConsoleからRoleを作成していて、それをそのまま利用していれば基本的には起こりません。AWS Batchを手動で作るのめんどいなーって人や自動化バンザイって人はご注意を。

Jobを実行する

今回対象にしているテータセットは、全部sha256 hashを名前にしています。それを利用して、prefixをJobのパラメータに渡すようにしてみました。

実際にprefixの分け方=並列度で、どれくらい実行時間が変わるかを試してみました。試すのは画像のリサイズ処理です。

ちなみにローカルでは、8並列で平均して100枚/5秒です。無視するデータを抜いた25万枚だと250分=4時間くらいかかります。

  • prefixを0000〜ffffにした時(65535個のJob)

    • 1jobあたり平均して5から6枚を処理
    • 途中で断念
    • Jobのsubmitだけで4時間くらいかかる(4job/秒)
    • Jobあたりの枚数が少なすぎて、起動→終了のオーバーヘッドの方が大きい
  • prefixを000〜fffにした時(4096個のJob)
    • 1jobあたり平均して80枚を処理
    • 実行時間は40分ほど
    • 25万枚/40分=100枚/秒
    • vCPUの利用率は大体15〜20%くらい
    • Single threadだったので、Networkが明らかにボトルネックになっている
    • 金額はだいたい以下の通り

      c4.8xlarge * 3 = 0.6 * 3 = $1.8/h
      c4.4xlarge * 2 = 0.18 * 2 = $0.36/h
      $2.16/h * 45/60 = $1.54
  • prefixを00〜ffにした時(256個のJob)
    • 1jobあたり平均して1300枚を処理
    • 25万枚/7分=600枚/秒
    • 実行時間は7分ちょっと(!)
    • ただし、各コンテナで8threadで処理するように
    • vCPUの利用率は劇的に改善。大体70〜80%くらい
    • ネットワークの利用度合いが明らかによくなっている
    • 金額はだいたい以下の通り

      c4.8xlarge * 3 = 0.6 * 3 = $1.8/h
      c4.4xlarge * 2 = 0.18 * 2 = $0.36/h
      $2.16/h * 7/60 = $0.24

S3→Instance→S3という経路を辿るため、Networkがボトルネックになります。そのため、1vCPUしかわたしていないコンテナでも、threadを利用したほうがいいと判断した所、大当たりでした。

まさか一回あたり30円くらいまで下げられるとは思いませんでした。

この辺はバッチコンピューティングの経験が問われそうです・・・

依存関係のあるJobを実行する

実際には、リサイズ以外にも処理が必要になります。そこで、こんなスクリプトを作ってみました。

# submit-all-jobs.sh
#!/bin/bash
JOB_QUEUE=xxxxxx

RESIZE_JOB_ARN=$(aws batch describe-job-definitions --job-definition-name image-converter-resize-image --status ACTIVE | jq -r '.jobDefinitions | max_by(.revision).jobDefinitionArn')
EDGE_JOB_ARN=$(aws batch describe-job-definitions --job-definition-name image-converter-extract-edge --status ACTIVE | jq -r '.jobDefinitions | max_by(.revision).jobDefinitionArn')

MAKE_SEQ=$(cat <<EOF
for i in range(0, 0xff):
    print("{:0>2x}".format(i + 1))
EOF
        )

SEQ=$(python -c "$MAKE_SEQ")
echo "$SEQ" | xargs -P 8 -I {} -n 1 
                    ./submit-jobs.sh $JOB_QUEUE $RESIZE_JOB_ARN $EDGE_JOB_ARN {}

# submit-jobs.sh
#!/bin/bash
JOB_QUEUE=$1
RESIZE_JOB_ARN=$2
EDGE_JOB_ARN=$3
SEQ=$4

resize_job=$(aws batch submit-job --job-name "resize-$SEQ-$now" --job-queue $JOB_QUEUE 
    --job-definition $RESIZE_JOB_ARN 
    --parameters Prefix=full/$SEQ
          )

resize_job_id=$(jq '.jobId' "$resize_job")

aws batch submit-job --job-name "edge-$SEQ-$now" --job-queue $JOB_QUEUE 
    --job-definition $RESIZE_JOB_ARN 
    --parameters Prefix=resized/$SEQ 
    --depends-on jobId=$resize_job_id

xargsでお手軽にparallel実行するためのスクリプトです。8並列で行うと、256×2のジョブ登録に数分といったとこ
ろです。

AWS Batchでの依存関係は、submit-jobの depends-on パラメータで指定します。実際にはListなので、複数のJobが終わってから実行する、みたいなことも出来ます。

ここでは、リサイズが終わってからエッジ抽出をしたいので、そういうふうに指定しています。

なお、リサイズ+エッジ抽出を全部やっても 10分 くらいで終わります。今までの苦労は何だったんや・・・。

前処理の要求は止まらない

30万枚の画像が30円くらいでリサイズ出来るようになり、前処理が捗るようになりました。とりあえず現状は概ね満足しています。もっと前処理が必要となってもなんとかなりそうって感覚を得られましたし。

ただ、まだ色々と課題はあります。

  • TFRecordへの変換がめんどくさい

    • 現状はローカルにダウンロードしてきて変換スクリプトかましてます
    • TFRecordにするまでが前処理なので、そのへんが難しいところです
  • 前処理で別のNNを利用する場合どうするか
    • GPUを使うと10倍以上速いので利用したいところですが、個人でやるレベルかって・・・

TFRecordまで一連のJobで出来ると学習が捗るんですが・・・。できそうなので、後で試してみようかと思います。

AWS Batchを今回初めてまともに触ってみましたが、なんとなく既存の仕組みの延長にある感じなので、理解はしやすいように思いました。
EC2が秒単位課金になったこともあり、一気にInstanceを立ち上げてさっさと終わらせるってことができるようになったのも大きいですね。

EMRを使うほどじゃないとか、EMRだと使いにくい言語で大量のデータ処理をしないといけないってなった時には検討してみてはどうでしょうか。

(番外)AWS Batchの注意点

今回試してみて、いくつか?ってなったりハマった点がありました。いずれも2017/12時点です。

  • DashBoardのJob Queueには1000以上は表示できない

    • 1000を超えると 100+ って表示になります。1000超えてるのに何故100?ってなることうけあいです
  • Jobの一覧で検索できない
    • 失敗したJobが100を超えると悲惨です
  • Jobの一覧で並べ替えが出来ない
    • Started atで並べ替えしたい・・・
  • まれにJobがRUNNINGでStuckする
    • TerminateもCancelも効かず、最終的にはJob Queueを削除しました
    • 多分30分くらいすると解消される感じです
  • vCPU/Memoryのバランスに注意
    • ECSと一緒ですね
  • Instanceのスケールダウンが結構早い
    • 5分とかjobが投入されないと全部消す勢いです

GAになってからまだ一年経っていないので、これからもっと使いやすくなっていくんじゃないかと思います。

明日は @pegass85 さんです。

続きを読む

AWS WAFのIP match conditionsに多量のアドレスを登録する

この記事は Relux Advent Calendar 2017 1日目の記事です。

AWS WAFについて

弊社ではAWS WAFを利用しています。
CloudFrontやALBに設置することのできる、AWSマネージドなWAFサービスです。

AWS WAFにはIPアドレスのリストによるホワイトリストまたはブラックリストを設定することができます。
登録するアドレスが少なければ良いのですが、登録するアドレスが多い場合、WebのコンソールやCLIどちらを使っても設定が大変です。
大変なのは主に次の2つの理由によります。

  • 登録できるアドレスは /8, /16, /24, /32 のCIDRアドレスのみ
  • CLIから登録する場合、リストの作成やリストへの登録操作のたびにトークン発行が必要

例えば /25 のネットワークを登録したい場合には 127個の /32 のアドレスを登録するという操作が必要です。
そしてCLIで操作をする場合、1つのIP match conditionを作成するためには

  1. get-change-token でトークン発行
  2. create-ip-set でリストを作成
  3. get-change-toke でトークン発行
  4. update-ip-set でリストにアドレスを追加

と、最低で4回のコマンドを実行する必要があります。

登録作業を楽にするスクリプトを作成した

複数のリストにたくさんのIPアドレスを登録する、という作業が必要だったため、簡略化するためにスクリプトを作成しました。
作成したいリストの名前とCIDRアドレスを列挙したファイルの名前を引数に指定して実行すると、
IP match conditionsが作成できます。

  1. ファイルからCIDRアドレスを読み込む
  2. CIDRアドレスをWAFに登録できるブロックに分解する
  3. 登録するためのJSONを生成
  4. 作成したJSONを用い、リストにアドレスを登録

といったフローで動作をします。

IPアドレスの計算をする箇所はこちらの記事を参考にしました。
https://qiita.com/harasou/items/5c14c335388f70e178f5

JSONの生成にはPHPのjson_encode()関数を使用しています。

create-ip-set.bash
#!/bin/bash

# AWS APIからChangeTokenを取得する
function get-change-token () {
    echo `aws waf get-change-token | jq -r '.ChangeToken'`
}

# WAFに空のIP-Setを作成する
function create-ip-set () {
    local changeToken=`get-change-token`
    echo `aws waf create-ip-set --name ${1} --change-token ${changeToken} | jq -r '.IPSet.IPSetId'`
}

# WAFのIP-SetにCIDRアドレスのリストを登録する
function update-ip-set () {
    local changeToken=`get-change-token`
    local addressList=''

    for row in `cat ${2}`
    do
        local cidrAddress=''
        echo $row | grep -oE '/(8|16|24|32)' > /dev/null
        if test $? -eq 0; then
            cidrAddress="['Action'=>'INSERT','IPSetDescriptor'=>['Type'=>'IPV4', 'Value'=> '${row}']],"
        else
            cidrAddress=`disassembleCidrAddress $row`
        fi
        addressList="${addressList}${cidrAddress}"
    done

    aws waf update-ip-set --ip-set-id ${1} --change-token ${changeToken} --updates `php -r "echo json_encode([${addressList}]);"`
}

# /8, /16, /24, /32 以外のCIDRアドレスを/8, /16, /24, /32に分解する
function disassembleCidrAddress () {
    local baseAddress=''
    local networkLength=''
    local step=''
    local disassembledLength=''
    local numberOfAddrs=''
    local count=0

    baseAddress=`echo ${1} | sed -E 's//[0-9]+//'`
    baseAddressDecimal=`ip2decimal $baseAddress`
    networkLength=`echo ${1} | grep -oE '/[0-9]+' | sed -E 's////'`

    if test $networkLength -lt 8; then
        step=16777216
        disassembledLength=8
        numberOfAddrs=`expr $disassembledLength - $networkLength`
    elif test $networkLength -lt 16; then
        step=65536
        disassembledLength=16
    elif test $networkLength -lt 24; then
        step=256
        disassembledLength=24
    else
        step=1
        disassembledLength=32
    fi
    numberOfAddrs=`expr $disassembledLength - $networkLength`
    numberOfAddrs=`echo $numberOfAddrs' ^ 2 -1' | bc`

    while test $count -le $numberOfAddrs
    do
        echo -n "['Action'=>'INSERT','IPSetDescriptor'=>['Type'=>'IPV4', 'Value'=> '"`decimal2ip baseAddressDecimal`'/'$disassembledLength"']],"
        baseAddressDecimal=`expr $baseAddressDecimal + $step`
        count=`expr $count + 1`
    done
}

#
# IPアドレス計算
# https://qiita.com/harasou/items/5c14c335388f70e178f5
#
# IPアドレス表記 -> 32bit値 に変換
function ip2decimal () {
    local IFS=.
    local c=($1)
    printf "%sn" $(( (${c[0]} << 24) | (${c[1]} << 16) | (${c[2]} << 8) | ${c[3]} ))
}

# 32bit値 -> IPアドレス表記 に変換
function decimal2ip () {
    local n=$1
    printf "%d.%d.%d.%dn" $(($n >> 24)) $(( ($n >> 16) & 0xFF)) $(( ($n >> 8) & 0xFF)) $(($n & 0xFF))
}

IpSetId=`create-ip-set ${1}`
update-ip-set $IpSetId $2

使用方法

addresslist.txt
192.168.0.0/30
172.20.0.0/21

登録したいアドレス一覧を、このようなフォーマットで用意します。
test-address-listというリストを作成する場合、下記のように実行します。

create-ip-set.bash test-address-list addresslist.txt

作成したリストは、Webのマネジメントコンソールでこのように確認できます。

Screen Shot 0029-12-01 at 0.28.43.png

明日は

iOSエンジニア @wootan による「iOS FirebaseRemoteConfigをつかって強制アップデートを実現する方法について」です。
お楽しみに!

続きを読む

AWSの利用料金をサっと確認するワンライナー

概要

  • AWSの利用料金をサっと確認したい
  • AWS CLIとjqでサッと見やすく

準備

  • AWSアカウント
  • AWS CLIと利用準備
  • jq

awscliのバージョン

$ aws --version
aws-cli/1.12.2 Python/2.7.12 Linux/4.9.58-18.51.amzn1.x86_64 botocore/1.8.2

もちろんCygwinでも大丈夫です

$ aws --version
aws-cli/1.12.2 Python/2.7.13 CYGWIN_NT-10.0/2.9.0(0.318/5/3) botocore/1.8.2

jsonフォーマットでサッと

コマンド

aws ce get-cost-and-usage \
--region us-east-1 \
--time-period Start=`date '+%Y-%m'`-01,End=`date -d 'next month' '+%Y-%m'`-01 \
--granularity MONTHLY \
--metrics BlendedCost \
--group-by Type=DIMENSION,Key=SERVICE \
| jq -c '.ResultsByTime[].Groups[] | {(.Keys[]): .Metrics.BlendedCost.Amount}'

結果

{"AWS CloudTrail":"0"}
{"Amazon Elastic Block Store":"2.6873538247"}
{"Amazon Elastic Compute Cloud - Compute":"5.9323839592"}
{"Amazon Route 53":"0.5563328"}
{"Amazon Simple Email Service":"0"}
{"Amazon Simple Notification Service":"0.0000107185"}
{"Amazon Simple Queue Service":"0.0000004264"}
{"Amazon Simple Storage Service":"0.0428270202"}
{"AmazonCloudWatch":"0"}
{"Tax":"0.73"}

CSVでサッと

コマンド

aws ce get-cost-and-usage \
--region us-east-1 \
--time-period Start=`date '+%Y-%m'`-01,End=`date -d 'next month' '+%Y-%m'`-01 \
--granularity MONTHLY --metrics BlendedCost \
--group-by Type=DIMENSION,Key=SERVICE \
| jq -r '.ResultsByTime[].Groups[] | [(.Keys[]), .Metrics.BlendedCost.Amount] | @csv'

結果

"AWS CloudTrail","0"
"Amazon Elastic Block Store","2.6873538247"
"Amazon Elastic Compute Cloud - Compute","5.9323839592"
"Amazon Route 53","0.5563328"
"Amazon Simple Email Service","0"
"Amazon Simple Notification Service","0.0000107185"
"Amazon Simple Queue Service","0.0000004264"
"Amazon Simple Storage Service","0.0428270202"
"AmazonCloudWatch","0"
"Tax","0.73"

メモと振り返り

  • エンドポイントは us-east-1
  • 期間指定(–time-period)のEndは範囲の次の日を指定します
    • Endの指定日は期間に含まれません(The end date is exclusive.)
    • 式で書くと 日付 >= Start AND 日付 < End みたいに評価されるということですね
  • 日付までコマンドに埋め込んだのは、やり過ぎたかもしれないと反省している(見づらい)

リンク

AWS

jq

続きを読む

EC2上でアタッチされたroleを使用してaws-cliを行う

やりたいこと

  • EC2上で動くjenkinsでaws-cliを使う
  • EC2にアタッチされているroleを使う
  • Access KeyやSecret Keyは持たない
  • AWSのAPIを使って行う

実現方法

シェルファイルでAPIを叩き、
レスポンスを環境変数に入れて一時tokenを使えるようにする

credential.sh
# bin/bash

url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE_NAME
curl $url > credential.json
export AWS_ACCESS_KEY_ID=`cat credential.json | jq .AccessKeyId | tr -d "`
export AWS_SECRET_ACCESS_KEY=`cat credential.json | jq .SecretAccessKey | tr -d "`
export AWS_SESSION_TOKEN=`cat credential.json | jq .Token | tr -d "`

ちなみにJenkinsfile

作ったlambda関数を自動的にS3に上げてくれるジョブです
lambdaを更新するコマンドでやらないとS3に上げるだけになっちゃうので要改良

Jenkinsfile
#!groovy
pipeline {
    agent any
    triggers {
        pollSCM('H/3 * * * 1-5')
    }
    //environment {}
    stages {
        stage('Develop') {
            when {
                branch 'PR-*'
            }
            steps {
                ansiColor('xterm') {
                    echo '<<< start develop >>>'
                    sh '''
                        source `pwd`/credential.sh
                        Lambdadir="`pwd`/Lambdafiles/*"
                        for filepath in $Lambdadir; do
                            dirname=`basename $filepath`
                            zip `pwd`/$dirname.zip $filepath/*
                            aws s3 cp `pwd`/$dirname.zip s3://BUCKET_NAME/
                            aws lambda update-function-code --function-name dev_$dirname --zip-file fileb://$dirname.zip
                        done
                    '''
                }
            }
        }
    }
}
folder構成
.
├── Jenkinsfile
├── Lambdafiles
│   └── MODULE1_NAME_FOLDER #中にlamda用の関数を入れておく
│   └── MODULE2_NAME_FOLDER #中にlamda用の関数を入れておく
├── README.md
└── credential.sh

続きを読む