続 カッコの付け方

AWSを始めとしたクラウドコンピューティング全般と、唯一神emacsにおける()の付け方についてだらだら書きます

Healthchecksで定期実行ジョブの着火もれを検出する

日次でも月次でも、まず殆どのシステムで何かしらのバッチジョブというものを動かしていると思いますが、 そのバッチが確かに実行された ことを保証・監視することは結構難しいです。ジョブが起動しましたのタイミングでログを書き、終わってもログを書くとすれば、ジョブが成功したか失敗したかはわかりますが、そもそもジョブが着火すらしかなったはわかりません。*1
例えば、「このジョブが終わるとここにファイルが出ているはずだ!このDBのレコードが更新されているはずだ!」などと、そのジョブ固有の結果を観測して判定することはできますが、汎用的な手法ではありません。
私は詳しくはないですが、商用のジョブ管理ツールはそれぐらいは見てくれるのかもしれませんが、もっと簡単にこれを実現できます、しかも今風に。

Healthcheckesとは

f:id:iga-ninja:20190323151509p:plain

ググりにくく、ネーミングセンスとしては最悪ですが、上記に挙げたような機能を実現するSaaSはもう結構あります。が、私が 2017年頃に一通り評価したところ、healthchecks.io に勝るものはありませんでした。

healthchecks.io

SaaSとして提供していて、価格もかなり安いのですが、OSSでも公開されており、IaaSなどで動かすことも可能です。今回はOSS版を動かします。

OSS版の動かし方

Healthchecksを選んだ理由はOSS版があることと、Python製であったことも一員です。ただデプロイ方法は結構複雑で、Djangoをそこそこわかってないと難しいです。じゃあSaaSを使えばいいんですが。。
実はこれを見つけた当初、こいつをDocker化するのは手頃でそこそこ複雑で丁度いいと思い、いつかやると心に決めていましたが、2019年の今、世界のすごく優秀な人がCoolなDocker Imageを作ってくれていました。今回はここは深く触れませんが、このイメージに気づくまで自分なりに結構な時間、試行錯誤しましたがここで得たものは大きかった。

必要なもの

ローカル環境(クライアントPC)のDocker

とりあえず入れてください。docker-compose も入れておくこと。ちなみに私のようにLinuxをDesktopとして使っている人は知っていると思いますが念の為 docker-composeはdockerパッケージに同梱じゃないです。別途いれてください。

Outbound Port 25が許可されているネットワーク

Healthchecksを使うには、メールアドレスが必要です。メールでユーザー登録するというアクションが、OSS版でも必要になります。このときに、受信できるメールアドレスはないという人は居ないと思いますが、メールを送信する機能が必要となり、OP25が遮断されているAzureやGCPでは動かせません(もちろん回避方法はあります)、AWSはOP25が許可されていますが、理由は割愛しますがおすすめしません。 OP25を切っているクライアントPCはそうそう無いと思いますが、一応。まず普段お使いの自宅用PCとネット回線を使えばまず問題ないはず

gmailアカウント

インフラの説明がメインじゃないので、SMTPサーバとしては gmailを使います。G Suiteじゃなくて無料の方でOKです。わかる人はアレンジしてください。

構築手順

Gmail のアプリケーションパスワードを取得する

account.google.comへアクセスして取得します。2段階認証が設定されていないと取得できません。ここでアプリケーションパスワードを取得

f:id:iga-ninja:20190323120025p:plain

docker pull する

飛ばしてもOKですが、Dockerイメージが玉石混在なので、イケてるやつをポイントします。

https://hub.docker.com/r/linuxserver/healthchecks

github.com

DockerHubでDownload数最大のやつはあきまへん。

docker-compose.ymlを書いて動かす

version: '3'

services:
  hc:
    image: linuxserver/healthchecks
    environment:
      - PUID=1000
      - PGID=1000
      - SITE_ROOT=http://localhost:8000
      - SITE_NAME=test-hc
      - EMAIL_HOST=smtp.gmail.com
      - EMAIL_PORT=587
      - EMAIL_HOST_USER=<あなたのGmailアドレス>
      - EMAIL_HOST_PASSWORD=<上記で取得したアプリケーションパスワード>
      - EMAIL_USE_TLS=True
    ports:
      - "8000:8000"

docker-compose up -d

ユーザー登録

ユーザー登録とはいうもののSaaS版とは関係ありません、OSS版で動かしているものに対して登録が必要です。 早速 http://localhost:8000/ を開きましょう そこから sign up ボタンを押して、メールアドレスを入れます。メアドは何でもOKです、composeファイルに書いたgmailのメアドでもOK。メールが届くので本文中のリンクからサクッと飛んでログイン完了。

いよいよ使ってみる

使うだけならSaaSのほうがいいかもな、とここまで書いて思いました。結構簡単になったとは思うけど、メールがやっぱ面倒だよな。。

まず、APIキーを発行します。これがないとどうにもならない。

f:id:iga-ninja:20190323124429p:plain f:id:iga-ninja:20190323124529p:plain f:id:iga-ninja:20190323124547p:plain

次にcheckを登録します。この処理はAPI Keyが必要です。 詳細はDocsのリンクから見てください。すぐわかると思います。timeout/grace については解ると思いますが、channels はアラート判定時もしくはOK判定時にどのIntegrationに送信するかのコントロールに使います。ここでは "*" を指定することにより、定義済みIntegration全部に対してアラート発砲します。

$ curl -H "X-Api-Key: KGYO7W0-7MCKh6s076X6zyjQwbUOVgew" \
 http://localhost:8000/api/v1/checks/ \
 --data '{"name": "test", "tags": "test", "timeout": 60, "grace": 60, "channels": "*"}' | jq .

{
  "name": "test",
  "ping_url": "http://localhost:8000/ping/8f894e90-cb77-45e7-971b-a8a8639edc68",
  "update_url": "http://localhost:8000/api/v1/checks/8f894e90-cb77-45e7-971b-a8a8639edc68",
  "pause_url": "http://localhost:8000/api/v1/checks/8f894e90-cb77-45e7-971b-a8a8639edc68/pause",
  "tags": "test",
  "grace": 60,
  "n_pings": 0,
  "status": "new",
  "channels": "81281b62-2619-42e2-84fc-8ae2801659c9",
  "last_ping": null,
  "next_ping": null,
  "timeout": 60
}

ブラウザでchecks一覧を確認するとこんな感じ f:id:iga-ninja:20190323124646p:plain

上のHTTPリクエストの戻りにあるping_url を叩きます。こちらは API-Keyは不要です。

curl http://localhost:8000/ping/8f894e90-cb77-45e7-971b-a8a8639edc68

そうすると下記のようにWebインターフェースに表示されます。

f:id:iga-ninja:20190323143126p:plain

このまま timeout および grace の時間が経過する2分経過まで放置しましょう。そうすると

f:id:iga-ninja:20190323143558p:plain

アラートが発砲されました!

実践でどう使うのか

順番が逆になりますが、そもそもこれをどう実践で使うかについて。 具体的にバックアップジョブを監視したいとします。仮にこれは1日1回、昼の12時に実行しているとします。このジョブの実行方法自体は変わりません、CronならCronで他のJob管理ツールならそれで良いです。変更するのは、バッチジョブ完了時に ping_url を叩くだけです、http(s) クライアントであれば何で叩いてもOK。
Healthcheckの動きに準じて説明します

  1. ジョブの定期実行頻度など仕様にあわせて check を create する APIを叩きます。timeout: 1 日 grace: 半日などで
  2. ping_url が払い出されるのでメモる
  3. バックアップジョブの処理の最後に ping_url を叩く
  4. 仮に次の日にバックアップジョブが走ったら、そのpingからまた1日延長(猶予)される
  5. その次の日にもしもバックアップジョブが失敗したら・それどころか何かが原因でcronすらコケていたら1日と半日経過した時点でアラート発砲

優れている点

なんと言ってもAPIで全部完結している点です。AutoScalingなどでインスタンスが複数立ち上がったり消えたりする、それらすべてで定期実行ジョブがあるのを見張るとなったとき、

  • 起動(Scale-Out)時に監視対象として追加する
  • 削除(Scale-In)時に監視対象から削除する

までを簡単にコントロールできる点です。その他Integration(= alart の発砲先)の多さ・グループ機能・タグ付けなど これぐらい常識だよね な機能は全部入っています。まあ、四の五の言わずに使えばわかります、それぐらいシンプルです。

感想

汎用的な手法で、この「ジョブは確かに実行された」を保証するには、必ず何らかの外部の観測者が必要となります。ジョブが走るサーバで自己完結してしまうとこの主題である「そもそも着火してないやん!」を見つけることはできない。この観測者の実装ですが、簡単そうで意外と難しい。

類似するSaaSは実は結構ありますが、私が数年前に一通り使ったところ healthchecks が最善手でした。他のものはAPIでのコントロール域が狭かったり、柔軟性が乏しかったり。

そもそも、第一感として、こんなのサーバーレスでやればいいやん! だったんですが、NG判定時に即座にアラートを飛ばすことが重要なのでDaemon化が必ずどこかに必要、よってコスト度外視しない限りサーバーレス(FaaS)での実装は理論的に無理。

あと、インフラについてですが、上で紹介したDockerイメージ(のDockerfile)はかなり良いです。勉強になるので興味がある人は見てみて。インフラ視点だと実は結構難しいことがわかり、気付きもたくさんあったので、そのうち書くかもしれない。

*1:ログの突き合わせをやるにしても、さらに定期実行ジョブを足すことになり、またそのジョブがちゃんと着火したかを誰かが見張らなければならないという無間地獄に陥ります。

最新版 Google Cloud Storage + L7LB で独自ドメインで静的サイトホスティング SSLもイケます

GCPGoogle Cloud Storage (GCS) & 独自ドメインで httpサーバを立てるというのを以前やりました。もはや2015年の古いエントリですが今も結構な閲覧数があるので、2019年版のリファインとして、もっといいやり方を紹介します。

iga-ninja.hatenablog.com

なお、この上記の方法は未だに通用はします。httpだけでOK, httpsは不要であれば上記の方法を今でもおすすめします。
また、本エントリーは深い解説はせず、手順だけをサラッと書きます

今回作るもの

の静的Webホスティングです。

AWSでいう CloudFront + S3 + ACM の組み合わせです。

手順

例によってフル GCP

材料

前回のエントリはGCSとCloudDNSのみでしたが、今回はL7LB + External IPアドレスが追加です。

ドメインの取得 + CloudDNSまで連携

これは以前の記事と同じです。割愛します。

Webマスターツールでドメイン所有権の証明は不要

朗報です。かなり楽になります。
正確には FQDNと同じ名前でバケットを作らないのならば 不要です。今回はGCS単体ではなくL7LB経由でアクセスさせるので、バケット名は不問になりました!

GCSでバケット作成

以前はgs と書いていましたがGCSのほうが主流のようなので、改めます。上記の通り FQDNと同じ名前のバケット名である必要はありません
好きな名前で作れればOKです

GCSへコンテンツアップロードと、ACLの調整

index.htmlをアップロードしますが、そのままでは閲覧できません。パブリック公開状態にする必要があります。以前はポチッと押すボタンがあったのですが、変わっています。

2019の手法としては、Bucket Policy で allUsers に対して Legacy Object Reader をつける方法が最善手です。本ブログでも触れています。

静的IPアドレスの取得

f:id:iga-ninja:20190216102416p:plain

f:id:iga-ninja:20190216113610p:plain

CloudDNSレコード登録

上記で取得したIPアドレスをAレコードに登録します。 f:id:iga-ninja:20190216120409p:plain

L7LBの作成

f:id:iga-ninja:20190216113615p:plain

f:id:iga-ninja:20190216113633p:plain

LBの設定はかなり複雑ですので、3パートに分かれています。各パートごとにクリックして設定していきます。 f:id:iga-ninja:20190216113641p:plain

まずはバックエンドの設定です。Backend bucketsをもちろん指定します。 f:id:iga-ninja:20190216113645p:plain

ホスト・パス ルーティングをする場合に指定しますが、今回は特に変更しません。今回はシンプルにバケットの構造をそのまま使います。 f:id:iga-ninja:20190216113648p:plain

フロントエンドとはListenerのことです。この例ではhttp 80も通します f:id:iga-ninja:20190216113651p:plain

次に本命のhttps側の設定です。証明書の設定が必要です f:id:iga-ninja:20190216115226p:plain

証明書は即時発行ではありません。しばらく時間がかかります(Let's Encryptなんで) f:id:iga-ninja:20190216113705p:plain

見直してCreateしましょう f:id:iga-ninja:20190216113707p:plain

注意点

GCSに置くファイルは 公開アクセス とする必要あり

これは必須です、S3のような Origin Access Identity は存在しないからです。結構深い話なので、続きは別エントリで

証明書の問題

すぐには使えません。Let's Encryptを代行してくれるだけなので、そのつもりで。

2019年版 Google Cloud Storage (gcs) のアクセスコントロールがより簡単に

以前に書いたエントリ、結構な更新が入っているので新規エントリとしました。

iga-ninja.hatenablog.com

導入時期は不明ですが、2つ更新項目を書きます

Bucket policy として Legacy な権限を指定可能

前回のエントリで、Default ACLを使わないと危険と書きましたが、ここが解消されています。重要なポイントなのでスクリーンショットで示します。

安全な指定方法はこちら f:id:iga-ninja:20190216150557p:plain

危険なのはこちら f:id:iga-ninja:20190216150601p:plain

危険か安全かはこれで確認 f:id:iga-ninja:20190216150602p:plain

仕組みの説明

一般公開向けであっても、Legacyを使わなければディレクトリ構造が見えてしまうという弱点を以前書きましたが、Bucket全体にたいしてLegacyの権限も適応可能になりました。(いつから可能になったのかはわかりません)
この時点でもうObject ACL (& Default ACL) はお役御免です。お疲れ様でした。 定方法はこちら

Bucket Policy Only?

もう一つまだベータのようですが、機能が追加されていました。Bucket Policy Onlyという機能ですが、こちらはObject ACLを封じkめてBucket Policy つまり Bucket全体でのみ権限を管理できます。AWSでもあるあるパターンですが、Bucket単位では非公開設定だけど、Object単位では公開にしていた、というパターンを抑制できます。まあ、AWSのS3にも追加された機能とほぼ同じ狙いですね。

このあたり、FTP/SFTP などのツールでサーバにアップロードしたら、パーミッションは弄るものだ!という昔からの習慣が残っている限り、絶対に発生しうるミスなのでシステムで封じ込めるのは致し方なしだと思います。

Bucket Policy という名前の語弊

AWS識者には大きな語弊を含みます。単刀直入に

  • アクセス元IPアドレスで絞ったりとかはできない
  • JSONで好きなルールを書いたりとかもできない

AWSでGCEぽく、Lambdaでインスタンス起動時にDNS自動登録する

GCEでインスタンスを起動するときに、先にホスト名を決める必要があります。AutoScaling等の場合は、勝手にプリフィックスがついて名前被りを避けます。これによりAWSのようなIPアドレスベースの使いにくいFQDN(Internal)ではなく、直感的な名前で名前解決ができます。
AWSでもUserData,cloud-initによる機構などで実現することは可能ですが、私の直感として「そんなことはインフラだけで完結しろよ!」と思うので、GCEぽい動きをするLambdaを約2年前に作成していたので、今更蔵出し公開します。
github.com

使いたいだけの人は使い方はすべてGitHub上に書いていますので、そちらを見てください。

f:id:iga-ninja:20181216103803j:plain

このエントリでは、今回蔵出ししたこのツールを通して serverlessフレームワークの歴史と使い方について振り返り書きます。書きたいのはこっちですが、タイトル詐欺ではないので許して。

その前にこのツールの意義

前述の通り、このツールは2年前に作成していました。現在と状況が違う点としては、Route53 auto naming (service discovery) が今は存在しますが、一応このツールとは競合していないです。

R53 auto naming との違い

結論から言うと、やっぱり ecs (eksも?) service discoveryとして使うべきものであり、名前解決が面倒だから楽したいという意図で使うべきではないと、一度使った人なら思うはず。

  • service discovery = サービス(web とか smtp とか)の生死を名前解決に反映させる
  • 本ツール = ただ、内部ホスト間の名前解決を楽したい

のように目的が根本的に違います。そのためservice discovery は見た目こそroute53でHostedZoneも見えるのですが、そのHostedZoneがAWS管轄となり、ユーザーが自力でレコードを一切いじれない となります。

例として、ただ単に名前解決したいと思い、Private Hostedzone .hoge. を作りました。
そこにはすでにいくつかFQDNを登録しています。
その .hoge. に service discovery で追加される分も混ぜることはできないです

言葉で書いてもピンとこないと思いますが、一発作ればよくわかります、ただ単に名前解決を楽にしたいだけという低い意識に対して service discovery (auto naming) を使うは完全にオーバースペックで悪手です。

UserDataなどを使う方法との違い

本ツールはEC2イベントとLambdaを使いますので、EC2インスタンス側への組み込みは一切不要です。UserData等の場合は、Powershellなのか、Shellなのか、AWSCLIなのか、Powershellのモジュールなのかいろいろ考える必要があります。もしくはGo lang win/linux 共通のバイナリを作るとか。でもそんなのめんどくさくてやってられない。

terraform/Cfnとかでインスタンスと同じタイミングにレコード追加するなどとの違い

上と一緒でめんどくさい。それよりなにより、名前解決ごときでterraformの実行反映がとろいのが嫌。IAMとRoute53のレコードは特に反映までの待ちが長いので、こんなしょうもないこと(ホスト間の名前解決)のためにいちいち待たされんのがいや。それと、AutoScaling等を導入する場合を考慮すると、結局UserDataなどを使わないといけないのでこちらのほうが守備範囲が狭い。

serverless framework(sls)

本エントリの本題に入ります。ダラダラ書きますが、基本は分かっている人向けで、入門的な話は書きません。

概要・存在意義

serverless version 1.x 系のリリースのころから私は始めました。当時の私の認識が間違っていたので、ここでも改めて書きますが

  • あくまでもLambdaが中心 (API Gatewayが中心じゃない)
  • プラグイン型にしたことに先見の明あり
  • nodejs製なので、serverless framework自体のバージョン固定必須

Lambdaを中心として、そのLambdaが必要とするIAMロールと、そのLambdaの導火線(着火剤)となるイベントを定義できます。Webに特化したものではないです、Webにも使えますが。AWSの場合はこの点が非常に重要で、特にインフラ屋が使うツールはたくさんのサービスに対してアクセスすることが多いです。これらを1元管理できるのは便利。
serverlessフレームワークは前身があったはずで、その頃はプラグイン型ではなかったはず。特にAWSのように目まぐるしき新しいサービス・サービス関連系が増えるものは明らかにこのモデルが優秀で、プラグインから本体へ取り込まれたものもいくつかあるはず。
最後に悪い点としてnodejsでできていること。ともかくアップデートのプレッシャーがきつく、nodejs本体のバージョンにも注意が必要。なので、ここはコンテナ(docker)化する。

EC2イベントから slsの本質を知る

2年前の実装をリファインするにあたり、まず最初にいじったのがここ。EC2イベントは当時はsls本体のイベントとしては存在しなかった。じゃあ、どうやって表現していたのか?slsの実装を忖度して、実行時=deploy に生成される Cfnテンプレートをイメージしながら追加リソースを書く となります。なんのとこやらわからないと思うので

# you can add CloudFormation resource templates here
resources:
 Resources:
   RegisterRule:
     Type: AWS::Events::Rule
     Properties:
       EventPattern:
         {
           "source": [
           "aws.ec2"
           ],
           "detail-type": [
             "EC2 Instance State-change Notification"
           ],
           "detail": {
              "state": [
                 "running"
              ]
           }
         }
       Name: register-rule
       Targets:
         -
           Arn:
             Fn::GetAtt:
               - "RegisterLambdaFunction"
               - "Arn"
           Id: "TargetFunc1"
   RegisterPermissionForEventsToInvokeLambda:
     Type: "AWS::Lambda::Permission"
     Properties:
       FunctionName:
         Ref: "RegisterLambdaFunction"
       Action: "lambda:InvokeFunction"
       Principal: "events.amazonaws.com"
       SourceArn:
         Fn::GetAtt:
           - "RegisterRule"
           - "Arn"
   UnRegisterRule:
     Type: AWS::Events::Rule
     Properties:
       EventPattern:
         {
           "source": [
           "aws.ec2"
           ],
           "detail-type": [
             "EC2 Instance State-change Notification"
           ],
           "detail": {
              "state": [
                 "terminated"
              ]
           }
         }
       Name: unregister-rule
       Targets:
         -
           Arn:
             Fn::GetAtt:
               - "UnregisterLambdaFunction"
               - "Arn"
           Id: "TargetFunc1"
   PermissionForEventsToInvokeLambda:
     Type: "AWS::Lambda::Permission"
     Properties:
       FunctionName:
         Ref: "UnregisterLambdaFunction"
       Action: "lambda:InvokeFunction"
       Principal: "events.amazonaws.com"
       SourceArn:
         Fn::GetAtt:
           - "UnRegisterRule"
           - "Arn"

というふうに書きました。slsはコード以外のデプロイつまりAWSリソースはすべてCfnで行われます。slsの優れているところは、このCfnに対してほぼべた書きでserverless.yml に追記できる点です。これより2年前の私はEC2イベントを実現していました。これは言葉を変えれば、今本体がサポートしていないイベントでも、Cfnがわかる人ならば自分で追加できるということになります。この点は本体slsのマージが遅いならばとっととプラグインとして公開し、みんな早くハッピーになれるので、先見の明と先に書いたのはこの点です。

解説します。

RegisterRule:
     Type: AWS::Events::Rule

はCloudWatch Events を定義しています、注目ポイントは

           Arn:
             Fn::GetAtt:
               - "UnregisterLambdaFunction"
               - "Arn"
           Id: "TargetFunc1"

で、イベント時に着火するLambdaを指定していますが、この名前は deploy 時に反映されるCfnのテンプレートやリソース名を確認して忖度し適合させています。みなさんがやる場合も生成されるCfnを読んでslsに忖度してあげましょう。

次に

   PermissionForEventsToInvokeLambda:
     Type: "AWS::Lambda::Permission"

の部分です。これはManagementConsoleでしかLambdaをいじったことがないと一生理解できないかもしれないLambda側のパーミッションです。Lambda関数の中で必要となる = APIコールするAWSリソースに対して、Lambdaが権限を持っている必要があるは直感でわかるはずですが、これは逆に S3やCloudWatch Events のようなものから対象Lambdaが呼ばれてもいいよ という許可設定です。この点は重要なので詳しく書いておきます。
例えばAWSリソース(A)から別のAWSリソース(B)へAPIコールする場合に、AにCredentialなどが持てるのであれば、A -> B の権限を、Aが持っていれば良いとなります。しかし CloudWatch EventsにはCredentialは仕込めません。私はこの手のものを俺用語で公共物と言ってます、具体的にはS3やCloudWatch Events,SQS 。対して、Credentialを仕込ませられるものは占有物と勝手にいい、代表はEC2やLambdaです。
つまり、占有物 -> 公共物ならば 占有物にCredential+権限があればOK。しかし公共物 -> 占有物には Credentialが使えない、「どうしよう、公共物ってことなので、だれでも名前さえ知ってたら呼べちゃう」を解決するのがこのLambda Permission。簡単にいうと、受ける側で送信元を限定する、A -> B の呼び出しをB側で許可するということになります。最近は公共物でもロールを当てることができたりします e.g. Cfnロール

EC2のイベントが本体に取り組まれたため、今回はこんなにスッキリしています。たったこれだけでCloudWatch Eventsの登録 & Lambdaパーミッションの設定ができます。

functions:
  register:
    handler: register.handle
    memorySize: 128
    timeout: 30
    events:
      - cloudwatchEvent:
          event:
            source:
              - "aws.ec2"
            detail-type:
              - "EC2 Instance State-change Notification"
            detail:
              state:
                - running

nodejs(sls)を固定化させる方法

最近のnpm は lockが可能なので少し立ち位置が微妙ですが yarn(hadoop関係ない方)で固定化させていました。とりあえず今回は npmでlockするので、yarnは無しにしました。 次にnodejs本体のバージョン固定と、ネイティブバイナリのビルドについての考慮です。

nodejs本体のバージョン固定

slsが入ったDockerイメージも誰かが作っているので、これに乗っかるのもありです。が、訳あってこれは使いません。

lambda向けネイティブバイナリビルド

じつは今回公開したLambdaは、ネイティブバイナリのビルドはありません。のでこの機能は必要ないのですが。ネイティブバイナリとは、まあ一般的にはC/C++ で書かれるそのOS/CPUでしか動かないコンパイル済みのバイナリを指します。これはmacでもWindowsでも引っかかる問題です。仮にローカルPCとしてmacでビルドした場合、mac用のバイナリができます。当然これをLinuxに持ち込んでも動かない。
Python使いなら requirements.txt というのを書くと思いますが、これをDocker でビルドして、バイナリをクライアントPCに持ってくるプラグインがあります serverless-python-requirements
これはrequirements.txtを扱えるだけではなく、Dockerを使ってのクロスコンパイル(というかDockerコンテナがLinuxなのでそこで普通にビルドするだけ)が可能です。
どうせやるならこの点も解消しようと思いました。

slsコマンド本体はDockerでやるのは決めていたので、そのまま何も考えないならば slsのDockerから、更にbuild用Dockerを作りそこでビルドです。ちなみにこれはDinDを使わなくとも一応実現可能ですが、macでしか使えないはずです。今回は採用しませんでしたが、一応残しておくと

  • slsコンテナに -v で docker.socket(UNIXドメインソケット)を渡す
  • slsコンテナに dockerコマンドを突っ込む
  • 上記で完璧と思ったが、slsコンテナからビルド用コンテナに行くとき、さらに -v でソース=requirements.ext がある場所をバインドしている。よって -v する mac上のパスと、slsコンテナのパスを完全に一致させる必要がある。

となります。が、この方法ははっきり言ってアホです。そもそもslsコンテナの時点でLinuxなので、だったらそこで普通にビルドしたら良いだけです。serverless-python-requirementsはDockerを使わないビルドも可能ですので、今回はこちらでやりました。方針としてはslsのblogと同じように、serverless-python-requirementsはグローバルインストールしない、dockerizePip: falseで使うことにします。こちらもpython37/36 だけですが Dockerfile & image を公開しています。 github.com

https://hub.docker.com/r/himaoka/sls-builder

まとめ・感想

2年たった今でもslsは優秀だなと改めて感じた。プラグイン型や、Cfnによるデプロイは本当に先見の明があり。

  • ド素人は本体にあるイベントで我慢して
  • ちょっと分かる人は、Cfnのリソースを直接書き足して(2年前の私)
  • もっと分かる人は、プラグインにして公開して
  • プラグインでももうこれは本体に取り込んで良いんじゃないかと思うものは、PRして

こうすることで、素人に優しい環境を提供しつつ、玄人の尊厳も忘れないというOSSの理想の形が体現できているように思う。ま、samは使ったことないんだけどね。

運用ベースのecs-cli利用手引き

f:id:iga-ninja:20181104124902p:plain
ecsへのアプリケーションデプロイはecs-cliが最も優れていると思いつつも、結構わかりにくいツールであるのは間違いないです。その解説をするのですが、この投稿ははっきり言って上級者向けで

  • docker-compose かなり使いこなせる
  • AWS ECS の基礎がわかっている

ことを前提にしています。上記を説明しだすとキリがないので、今は「ちょっと、何言ってるかわからないですね〜」な人も、ecsを使うならば近い将来役立つと思うので、あとで読むなどしておいてもらえればと思います。

ecs-cliのコンセプト

まずは ecs-cliがどんなものかを、これもある程度上級者向けに雰囲気を掴んでもらうように伝えるとこんなかんじ

  • docker-compose でローカル実行するよね普通
  • docker-compose.yml の内容って ecsのタスク定義 と似てるよね
  • じゃあ、docker-compose.ymlをヒントに ecsのタスク定義 を作ってついでにタスク実行・サービス定義までできるようにしたろ

できるけど、やらせないこと

ecs-cliはかなりの機能を持っています。ですが、大事なことなので最初に書きますが 全部の機能を使う必要はありません ほしいところだけつまみ食いできます。具体的にこの場では触れないこととして
ecs clusterそのものの構築 (コンテナインスタンスの起動とか)
があります。すでに定義済みの ecs クラスタを使うことは可能です。が、むしろクラスタの作成から ecs-cliを使うことは稀です。AWS公式ドキュメントのチュートリアルはだいたいクラスタ作成から入っていますが、本運用でクラスタから作り直すはそんなに無いし、というかecs-cliでそれ(デプロイの度にクラスタから新設する)をやるのは結構厄介なので、無視します。

やること・やらせたいこと

  • docker-build と ECRへのpush
  • ecs タスク定義の作成 & 更新
  • ecs タスクの実行
  • ecs サービスの定義
  • ecs サービスの更新

冒頭で機能はつまみ食いできると書いたものの、結局ほとんどecs-cliにやらせます。が、繰り返しますが、もちろん一部だけ使うもできますので、パーツごとに読んでもらって良いです。

docker-build と ECRへのpush

正確にはこのタスクにおけるecs-cliの役割はECRへのpushのみです、が ecs-cliが利用する docker-comose.yml に buildとタグ付けまでを任せることができます。これが後々の ecsのタスク定義 まで連結できます、ここが美味しい。
まず、実際のアプリ・システムで docker build が発生しないことはほぼ無いはずです。docker build は必須でかつ、そこでECRにpushした docker imageはすぐにタスク定義で参照することになります。
具体的にはdocker-compose.ymlはこのように書きます

version: "3"

services:
  web:
    build: .
    image: [aws-account-id].dkr.ecr.[ecr-region].amazonaws.com/web:1.0
    ports:
      - 80

Dockerファイルはあくまで参考に、皆さんの必要なアプリに合わせてください、docker-compose.ymlと同じ場所に配置します。

FROM nginx

RUN echo 'hello world this version is 1.0' > /usr/share/nginx/html/index.html

これで docker-compose build をすると [aws-account-id].dkr.ecr.[ecr-region].amazonaws.com/web:1.0 というタグがついたイメージが ローカルにできますecs-cliではなく docker-compose の機能です。これにより buldした imageにタグを付けて、かつ後々タスク定義にそのタグを使い回せます
引き続き ecs-cli でpushします

ecs-cli push --region <your ecr region>  --cluster-config <cluster config name> [aws-account-id].dkr.ecr.[ecr-region].amazonaws.com/web:1.0

docker コマンドでpushした場合、ECRへのpushの場合は docker login が必要ですが、これが不要になります。さらにECRのリポジトリ自体も先に作っておく必要がありません、pushしたときに無かったら勝手に作ってくれます。これだけでも便利。

お前は ecs-cli push の忖度を無視しているぞ! いえ、わざとです。

version: "3"

services:
  web:
    build: .
    image: web:1.0

として、docker-compose build ののち

ecs-cli push --region <your ecr region>  --cluster-config <cluster config name> web:1.0

とした場合、ecs-cliが忖度して、ECRのリポジトリ名にあったtagでpushしてくれます。なので、わざわざ docker-compose.ymlの image: で完全なtagをつけなくていいんですが、こうしておかないと後述のタスク定義を作成するときに、この世におそらく存在しない web:1.0 というECRではない、dockerHubのイメージを指すことになります。
このあたりは -f オプションで docker-compose.ymlを複数指定&マージや、環境変数を compose自体に吸わせたりと、いろいろ回避する方法はあるのですが、リポジトリにECRを使う場合は、ECRのURLをあえて抽象化しないほうが、事故のリスクも減らせるのでいいかなと思っています。

ecs タスク定義の作成 & 更新

次にecsのタスク定義を作成します。このタスク定義は2つのファイルで内容を指定します。

  • docker-compose.yml
  • ecs-params.yml

docker-compose単体では、ecsのタスク定義のすべてを表現しきれません。なので大部分はdocker-compose.ymlから、ecs specificな部分は ecs-params.ymlで指定し、2つがくっついてタスク定義になります。
各々のファイルの詳細は公式ドキュメントを読んでください。注意点は docker-compose のリファレンスにある機能がすべて ecs-cli でも指定できるわけではない点です。

ファイルの準備ができたら、ecs-cliでタスクを定義しましょう

ecs-cli compose create --cluster-config <hoge>

compose create は 作成も更新も行います。そもそもタスク定義は更新不可能ですので、新しいバージョンが作成されます。気をつけてほしいのは、このコマンドは タスク定義の作成(新バージョンも) のみですので、これを叩いただけではデプロイされません

ecs タスクの実行

次にECS用語のタスク実行です。ECS用語のサービスとは違い、一発もので死んでもrespawnしません。

ecs-cli compose up --cluster-config <hoge>

バッチジョブ等一発ものならばこれでOKです。もしもデーモンプロセスで、サービス化するまえにタスクで試したいだけ、確認が終わり止めたくなったら。

ecs-cli compose down --cluster-config <hoge>

docker-composeのコマンドと一緒です。ECSのタスクは、一発走ると死んでしまうので startとかはできません。

ちなみにFargateを使う場合は、SGやSubnetは ecs-params.ymlで指定します。ここで参照するSG等は、先に作っておく必要があります。ecs-cli は 1タスク定義 = 1 docker-compose.yml という構成になるので、(ある程度)マイクロサービス化しているならば何個もdocker-compose.ymlを作成するはずで、ここで利用するSGが互いに依存するような(e.g. sg-a -> sg-b の 80は accept)場合は、依存関係を解消できずに作成できなくなります。なので、ecs-cli でこれらAWSリソースを作成できるようにするのはちょっと無理筋です。

ecs サービスの定義

サービスの設定は少々癖があります。

  1. どのALBにぶら下げるとかは、コマンドラインオプションでしか指定できない
  2. 新規作成更新のコマンドが違う

サービス実行時のオプション等

ecs-cliはタスク定義が中心ですので、タスクで動かすのか、サービスで動かすかは抽象化、というかどっちでもいいようにできています。サービスとタスクの大きな違いは、とくにWebなら

  • タスクはALBにぶら下げられない
  • サービスはALBにぶら下げられる

ので、どのALBにぶら下げるかは、サービス定義時にコマンドオプションで指定します。ecs-params.ymlで指定で、タスクとして実行したときは無視してくれれば良さそうなもんですが(ここは変わるかもしれない)。また、ここで指定するALBやTGも先に作っておく必要があります。サービス定義時に指定するコマンド具体的にはこんな感じ

ecs-cli compose service create \
--deployment-max-percent 200 --deployment-min-healthy-percent 50 \
--target-group-arn <target-group's ARN> \
--container-name web --container-port 80 --cluster-config <hoge>

ecs-cliには ALBもTGも作る事はできないので、ALB/TG/Lister-rules/SG(バックのEC2と兼ね合い) は予め作っておく必要があります。このあたりも含めてどうしても一括で管理したいインフラ寄りの貴方には、ここまで書いといてどんでん返しですが、terraformをおすすめします。

新規作成と更新

実際は compose service ... の解説を熟読してほしいのですが、私の運用ルールを書きます。

  1. compose service create で初期定義&タスク数0
  2. compose service scale でタスク数調整
  3. compose service up で新バージョンデプロイ

最初の create と scale は特に違和感ないと思いますが、新バージョンのデプロイのみ up を使うとしています。初期定義時にいきなり up としてもタスク数1で立ち上がるのですが、この場合タスク定義だけ更新し、事前チェックしたいだけでもいきなりデプロイしてしまうためです(文法エラー時はデプロイしません)。そのうち dry-runもできるようになりそうですが、create だけならデプロイ無し、 up ならデプロイありと意識付けしたほうが良いでしょう。なお、タスク定義がただしいかどうかをチェックしたいだけなら、 compose create でOKです。サービスは新しいタスク定義バージョンができたからと言ってそれにつられて勝手にデプロイはされません。

ecs-cliで前バージョンへ切り戻し

ecs-cliには、タスク定義のバージョンを指定してタスク・サービスを実行することができません。が、一応ecs-cliの特性を使えば前のバージョンへ切り戻すことは可能です、

ecs-cli のタスク定義は、良くも悪くも docker-compose.yml および ecs-params.yml の主張をきちんと受け入れます。例えば

  • タスク定義 hoge:10 で指定していた imageのタグが ver-10
  • hoge:11 新たに imageタグ ver-11 を push しタスク定義でも ver-11 を参照した
  • その後 ver-11(hoge:11) にバグがあったことに気づき、ver-10(hoge:10) に戻したいとなった
  • docker-compose.yml で指定している image: のtag を ver-10 にもどした(ほかはいじっていない)
  • この状態で ecs-cli compose service up とすると、 hoge:10 で再デプロイされる なんと hoge:12が作成されずに hoge:10が再利用される

この機能は賛否両論あると思います。素直にECSのapiを使えば、サービスの hoge:11 への参照を hoge:10に変えればいいだけだからです。ですが、よく考えてみたら hoge:10 が tag:ver-10 だというのはきれいに連番で運用していたらそうなるだけで、タスク定義のバージョンだけの情報では、そこで使っているdocker-imageのtagはなんなのかは正確にわかりません。
今現時点で動いているタスク定義のバージョンは常に最新という鉄の掟を守れればよいですが、切り戻したあとに忘れず最新版=つまり問題があったバージョンを削除を徹底するなど、機械=ECSというシステムに人間を歩み寄らせる必要があるということです。

これに対してecs-cliお前の書いた docker-compose.yml (& ecs-params.yml) の内容、全部尊重したる。タスク定義の番号は使い回すか、新設するかは俺に任せろ、お前の言う通りにやったるわと言わんばかりに人間の主張をecsに合わせこませます。

私の考えとしては、タスク定義のバージョン番号だけ見ても、それがいつのdocker-imageを指しているかわからない。だったらいっその事タスク定義の番号のことは人間は意識せず(ecsの都合なんか知らない)にecs-cliに忖度させればいいと思います。
ただし、ecs-cliもdocker本体の進化、それに付随してecs自体の進化をどんどん吸収して新バージョンが頻繁にでます。よってそのecs-cliのバージョンアップには要注意で、都度タスク定義のバージョンの動きは確認したほうが良いでしょう。

latestタグの扱いをどうするか

latestというタグは特殊で、docker-imageのタグを省略した場合は latest が指定されているとみなします。この latestに依存すれば、タスク定義の書き換えは常に不要で、最新のdocker-imageを指し続けることができます。よって、通常オペレーションでは、 docker push ののち compose service up--force-deployment と指定すれば良いです。が、こうしてしまうと切り戻しが煩雑です。旧バージョンの docker-imageにlatestタグを付け替えて force-deployment とすればよいのですが、latestタグしかつけていない場合は、明確な印がないので、pushした日付から「多分この時期Pushしたバージョンはこうだったはず」と推測するしかありません。
これも賛否両論だと思いますが、latestタグによる運用は、切り戻しのことを考えるとあまりおすすめしません。latestを使うにしても、latest以外のユニークなタグをすべてのイメージに合わせてつけておくべきです。

ローカル実行との兼ね合い

docker-compose は -f オプションを書くことで、composeファイルをマージした状態で実行できます。環境変数なども使えるのですが、私は -f をつかって、ECS向けとローカル実行向けを切り替えるのが良いと思います。docker-compose.yml は ECS向け、 docker-compose-local.yml はローカル実行向けとした場合

docker-compose -f docker-compose.yml -f docker-compose-local.yml up 等
version: "3"

services:
  nginx:
    image: nginx
    logging:
      driver: "awslogs"
      options:
        awslogs-region: "ap-northeast-1"
        awslogs-group: "/ecs/test"
        awslogs-stream-prefix: "nginx"
version: "3"

services:
  nginx:
    logging:
      driver: json-file

これで走らせると、ecs向けでは logdriver は awslogs(cloiudwatch logs) でローカル実行ならば普通のjson-file (docker-compose logs ngixで読める) となります。
これをうまく使うと、ecs向けは DBは RDSをつかう、ローカル実行は、おなじ docker network に mysqlのコンテナを立ち上げる、ということもできます。もちろん ymlファイルを複数指定せずに、大本から2つに割ることもできますが、割ってしまうと 1枚の docker-compose.yml で 2度美味しい ecs-cliの利点が薄まります。
これ以外にも docker-compose の機能として、違うyamlを吸わせたり、環境変数を docker-compose.yml自体に吸わせたりする方法がたくさんあるので、自分にあったものを探して選んでください。

その他の注意点

タスク定義で指定している docker-imageが実在するかの裏取りはしてくれない

docker push忘れを予防するには、私達でなにか確認プロセスの準備が必要です。

ecs-cli logs の対象は cloudwatch logs (awslogs) のみ

Fargate以外なら ecs-agentからローカルログ(json-file)が取得できてもよさそうですがダメなようです。私としては、docker-compose logs をガンガン使うひとで且つecs-cli使うなら 「awslogsにはとりあえず投げとけ」を推奨します。

SSM Agentとか無視して Windowsで CloudWatch Agentを使う

もう4年前の話ですが、CloudWatch Logsへのログ送信をWindowsで行う場合の記事を書きました。

CloudWatch Logs on Windows 2012 Server - 続 カッコの付け方

もうこの内容は古くなって久しいですが、なるべく昔ながらのやり方で ファイルを書いてサービス起動する単純さ を貫くやり方を書きます

歴史と概要

WindowsからのCloudWatch (Logs)へのデータ送信の方法は2回変わっています。(当たり前ですが、直接APIを叩けば送信できるのは変わりません。)

  1. EC2Config単体時代
    これは上記の記事と同じです。EC2Configサービスが、CloudWatch (Logs)への送信機能も持っていた

  2. EC2Config + SSM Agentの時代
    非常に便利なSSMですが、SSM の管轄として CloudWatch (Logs) の設定が組み込まれました。設定はSSMで行うものの、サービスの実体はどっちが受け持っていたかは、よく知りません。

  3. CloudWatch Agentの時代
    SSM依存も解消されて、完全に CloudWatch だけで完結するようになりました。どう考えても、これがあるべき姿です。

CloudWatch Agent単体での設定を説明します。

インストール

  1. https://s3.amazonaws.com/amazoncloudwatch-agent/windows/amd64/latest/AmazonCloudWatchAgent.zip をダウンロード
  2. unzip
  3. 管理者権限で install.ps1 を実行

これで C:\Program Files\Amazon\AmazonCloudWatchAgent\ にコピーされるので、これから先の作業はこっちで行う

設定ファイルの生成

ちょっとややこしいです。それっぽい名前の amazon-cloudwatch-agent-config-wizard.exe というのがあり、対話してファイル生成できますが、お世辞にもわかりやすいとは言えないので、とっととファイルフォーマットのリファレンス読んだ方がマシです、どうせあとから読むことになるんだし。

Manually Create or Edit the CloudWatch Agent Configuration File - Amazon CloudWatch

何らかの形で config.json というファイルを作ります。ここからがキーポイントで、ここで作る config.json というファイルは そのまま利用するのではなくコンバートして使います 。ですので、場所はどこにおいても問題ないのですが、 C:\Program Files\Amazon\AmazonCloudWatchAgent\config.json に置いた前提ですすめます。

新フォーマットからのコンバート処理

新フォーマットという言葉は今は無視して。先程かいたコンバートは、専用コマンドで行います。管理者権限を持つPowershellターミナルにて

cd "C:\Program Files\Amazon\AmazonCloudWatchAgent"

.\amazon-cloudwatch-agent-ctl.ps1 -a fetch-config -c file:config.json

-c file:config.json でファイルから吸うという意味です。 file: で始めているのに file:// って書かないんだ。。これでだいぶハマったまじかよ。。

これで文法エラーとかあれば怒られますので、ちゃんとエコーを見てください。 なお、コンバート済みのファイルは C:\ProgramData\Amazon\AmazonCloudWatchAgent\amazon-cloudwatch-agent(json|toml) となります。

旧フォーマットからのコンバート処理

このコンバート処理は過信しないこと!

PS C:\Program Files\Amazon\AmazonCloudWatchAgent> .\amazon-cloudwatch-agent-config-wizard.exe -configFilePath .\AWS.EC2.Windows.CloudWatch.json -isNonInteractiveWindowsMigration

このコマンドで旧形式のjson(前述の 1,2 の時代の設定フォーマット) を新形式のconfig.jsonに変換します。ですので、実際にこれから稼働させるCloudWatch Agentに反映するには更に -a fetch-config にて (json|toml) への変換が必要です。

本コマンドは EC2config でやっていた人でも、SSMでやっていた人でも(この場合は上記とオプションは違う)どちらも通用するようです。が、100%完全コンバートではないので結局自分でちゃんと調べて1から書いたほうがマシです。 WindowsのEventログからの取得が一部、エラーすら出ずにうまく変換されませんでした。

サービスの起動・停止・設定変更の手順

  • 起動 .\amazon-cloudwatch-agent-ctl.ps1 -a start
  • 停止 .\amazon-cloudwatch-agent-ctl.ps1 -a stop
  • ステータス .\amazon-cloudwatch-agent-ctl.ps1 -a status

設定変更する場合は

.\amazon-cloudwatch-agent-ctl.ps1 -a fetch-config -c file:config.json
.\amazon-cloudwatch-agent-ctl.ps1 -a stop
.\amazon-cloudwatch-agent-ctl.ps1 -a start

fetch-configは加工済みの設定ファイルを吐き出すだけなので、restartは自動で行われませんので、stop/startする必要あり。

まとめ

  • CloudWatch Agent 単体で完結するようになった SSM Agentは別にいらない
  • 設定ファイルは json と toml の2本で、ジェネレートするコマンドを使う
  • サービスとしては CloudWatch Agent という名前で動いている、ただし確認用PSを使うことが推奨されている

なんでSSM管轄だと嫌なの?

最後に批判です。批判の先に言いたいのは CloudWatch Agent の独立万歳!

  • 本来、CloudWatchとSSMは依存関係に無いのに、どうしてわざわざSSMに依存しなければならない?
  • ファイルに書いてサービスリスタートで反映完了と言う単純明快な方法をなぜわざわざSSM経由のみに絞る必要があるのか
  • 同じファイルを複数台のEC2にばらまくときは確かに便利かもしれないが、サーバーごとに個別設定を入れ込みたい、{instance-id} のような変数だけでは実現できないとき、ファイル生成してリスタートのほうがはるかに簡単だし、同じ設定をばら撒くなら、定義をS3かどこかに置いて、それをCloudWatch Agentに (ファイル形式で) 反映させるSSMドキュメントを作ればいいだけの話
  • AWSの開発者リソースの話だが(私の勝手なおせっかい)、CloudWatchAgentの機能を受け持つのは EC2Configを作っている人なの? それともSSM Agentの開発者? CloudWatchAgent は、 CloudWatchの開発チームが面倒見るべき話じゃないの? これらが混ざっている意味も利点も利用者にはまったくない

2018年なぜ私達はコンテナ/Dockerを使うのか

2017年にもうコンテナの未来・一つのカタチはもう確定したと言え、今更感があるものの、改めてDockerとコンテナについて。 今更こんなことを書くのは、情報が溢れてくる今こそ、正しく理解し、正しい順序で学習することが重要だと切に思うから。

https://www.docker.com/sites/default/files/vertical.png

内容についてのお断り

  • How Toはかきません
  • あくまでも2018年時点の私見
  • 目新しい情報はない、2016年頃に書けたレベル

Dockerをこう使えとか、こうするのがいいとかの話ではなく、コンテナとDockerに関して大きな視点で現時点で私の考えを書きます。また、私自身はかなりのコンテナ推進派です。 Dockerをよくわかっている人には意味のない記事となります。

コンテナ(Docker)のメリット

何故コンテナがいいのか、コンテナをある程度の学習コストを払ってでもやる理由

コンテナとDocker

コンテナ技術はDockerが生まれる前から存在する技術ですが、今のコンテナブームは確実にDockerによるもの です。Dockerそのものについては後述します。 この記事のコンテナという表記はほとんどDocker(によるコンテナ)と読み替えて差し障りありません。

Docker Image

Dockerの最大の利点は ポータブルなDocker Imageが使えるということです。
本当にこの1点に尽きます。
Docker Image は言い換えるとベンダーロックインの無い AMI (OSイメージ)といえます。

なぜ、Docker Imageが便利なのか

ローカル開発環境の統一

恐らく最初のDockerの使われ方がこれだと思います。複数人でアプリケーションを開発する場合、開発者のマシンの数だけ開発環境を立てる必要があります(VDIでというのは無視)。まあ、大変ですよね、何人かにもよりますが、phpでもrubyでも、composerなりgemなり使えば参照ライブラリは合わせられますが、phprubyのバージョンまで合わせたり、もっと大きく考えればローカル環境がWindowsLinuxでも異なりますね。

これが dockerのサービス/デーモンさえ動いていれば、1つの共通 Docker Imageを挿すだけで全く同じ環境が、開発者全員に行き渡ります。開発環境を構築するのが趣味の人もいると思いますが、突き詰めると全員が同じバージョンで開発するのは非常に厳しい事がわかります。普段気にしないですが OS + ランタイム(phpとかruby) + ライブラリ すべて一緒ではじめて完全に同じ開発環境といえるのです。

本番環境 CI/CDでも使える

コンテナ(Docker)オーケストレーションツールが出揃った昨今では、こちらのほうが目立っている感じがします。開発で使ったImageをそのまま本番へ、更にはCI(テスト)もやってしまおうとする考え方です。もちろん開発と本番で環境差異(エンドポイントなど)を吸収する仕組みは必要ですが、開発で動いたものがまるっきりそのまま本番、しかもCIもその開発で使っていたImageなので、如何に安定性・リスク回避からも有利であることがわかると思います。

OSSの playgroundとして

Docker以前は、Vagrantとかでやっていた、とりあえずローカルで試してみようは、全てコンテナでみんなハッピーです。実際にそこらじゅうのOSSがDockerイメージで提供されています。

https://hub.docker.com/r/graphiteapp/graphite-statsd/

https://hub.docker.com/_/redmine/

https://hub.docker.com/r/ceph/demo/

特に単コンテナで完結しないものも、docker-compose でお手軽に一瞬で作れます。

https://docs.docker.com/compose/wordpress/#define-the-project

では、なぜVagrantではだめなのか?ですが、2つ理由があります

  • Vagrantをはじめとして、他プロビジョニングツールについて知っていなければ動かせない
    ここはDockerの功罪とも言えますが、VagrantにくらべてDockerはツールの使い方を覚えなくても、そのOSSのReadme.mdに書いている Dockerコマンドを一発叩けばいとも簡単に構築完了です。Vagrantは少なくともrubyの知識などが少しは無いとこんなにすんなり動かせません。Docker run 一発の楽さになれると Vagrantを今更やろうとは思えない。特に複数マシンでクラスタを組む(=複数コンテナでクラスタ構成)場合はdocker-composeの安心感には到底かなわない。
  • Boxファイル (OSイメージ) が大きい
    この点もDockerと大きく違う点です。詳しくは後述しますがDockerが流行る理由は、Docker Imageデータストレージの扱いが極めて賢いからです。対してBoxファイルはかなりサイズが大きく、Docker Imageのように賢いストレージエンジンではないです。

インフラ屋のテスト

これは実際に私が使っています。単純に AmazonLinuxやubuntuなどでのサーバ構築手順などを探るために、素のDocker Imageでサクッと起動させてテストします。当たり前ですが、Docker Imageのキャッシュがあれば一瞬で起動します、Vagrantなんてトロくてもう使ってないです。AWSでEC2を建てることすらウザいです。新しいOSのイメージを探す必要すらないのです。普段使いという点では、インフラ屋が受ける恩恵のほうが大きいと感じています。

コンテナにまつわる誤解

すごく残念なことですが、巷には間違ったコンテナにまつわる must/should が蔓延しています。

オーケストレーションツールが主体という考え方

人として、エンジニアとして、本番環境での実践を考えることは間違ってはいませんが、コンテナをやると聞いて、オーケストレーションから入るというのはどうも気に食わないです。
もっと自由に考えて、気軽に使うべきだと思います。そもそもオーケストレーションツールが存在しない時代を経験している人にとって、オーケストレーションほしいから使う ツールであって、オーケストレーションが無いとコンテナは使えないというような must/should としてオーケストレーションを捉えるのは間違いです。

コンテナの用途はそんな矮小化したものではないです。もっと広い用途に使えるもので、どこぞの権威が言っているからオーケストレーションわなきゃ ではなく、 どうしたいかは自分で決めて、そして自分で選ぶべきです。

Dockerfileが必須 / IaC必須

必須ではない。Dockerfileが便利であること、IaCもできることには誤りではないですが、Dockerfileは所詮 Docker Imageを作る道具に過ぎないです。使わなければならないではなく、使いたいから使うのです。 実際、run /bin/bash で直接 apt install でパッケージを入れてもからの docker commit でもいいですし、Dockerfileをつかってもいい、Dockerfileが非力なら、Packerから作ってもいいです。

https://blog.james-carr.org/build-docker-images-with-packer-and-ansible-3f40b734ef4f

こうしなければならないなんて、なにもない。あなたが思うように、使いたいものを選んで使えばよいのです。

マイクロサービス化が必須

マイクロサービス化と相性が良いのは間違いないですが、必須ではないです。マイクロサービスではなとも、コンテナは有利です。1コンテナ1プロセスとかいう標語も、場合によっては無視してもいいと思います。supervisordでプロセス束ねが正解の場合もあります。コンテナに置けるベストプラクティス1なんてものは存在しない、自分たちの欲しいものを自分たちで実装すればいいだけです。コンテナにはそれを受け入れる器の大きさがあります。

永続データは入れちゃだめ

この考えは概ねあっていますが、ほとんどの人が誤解しているはず。k8sやECSなどのオーケストレーションではコンテナは使い捨てですが、素のDockerではコンテナは使い捨てではありません。正確には使い捨てもできるし、stop状態としてデータを維持することもできます。これもコンテナの使い方を矮小化しないで欲しいのであえて書きました。オーケストレーションが必要ない小規模な単発コンテナなら、本番環境だろうが、Docker run で良いです。

この点は今現時点でもDockerの課題と言えるので、逆に時代を切り開く度量のある、反骨精神溢れる若人にとってはとてもおもしろいテーマと言えます。実際にこのテーマに挑戦している先駆者もすでにいます。

https://github.com/vitessio/vitess

常識を疑え

コンテナで何が変わるか

デプロイ高速化

オーケストレーションの利用は事実上、前提になりますが、多台数マシンへのアプリケーションのデプロイについて、コンテナによるデプロイは圧倒的に早くて確実です。デプロイ方法も今は選択肢がたくさんあり、デプロイと一口に言ってもやることは様々ですが、コンテナによるデプロイは単純明快で圧倒的なスピードです。新しいコンテナを立ち上げて古いコンテナを消す。 たったそれだけです。
docker pull がどれほど早いのかは体験すればすぐに分かるはずです。しかも、pull して run するだけで新しいバージョンのデプロイ終わりです。古いバージョンの殺しなどはオーケストレーションが大体やってくれます。

IaCの最適化と役割ベースのサーバ管理の終焉

世の中の大体のサーバーは、役割ごとに1台ずつサーバを分離していると思います。 e.g. webサーバ、applicationサーバ、DBサーバなど
設計が必要ではあるもののコンテナ、現実解として、オーケストレーションは必須になりますが、この考え方を崩すことができます。
サーバはもはや只の箱で、それをどう使うかは走らせるコンテナでフレキシブルに変える考え方です。 e.g. 3台でECSクラスタ構成、昼間は web:4,batch:1 夜は web:1,batch:8 このコンテナが動く
サーバをどう使うか、どの役割をもたせるかを、今まではサーバを分離して考えていたのが、それをすべて無しにして、機能を、使いたいときに使いたいだけ使うという考え方に変えることができます。
今までインフラ屋が「このサーバはWebです、このサーバはAppです」などと管理のしやすさだけで決めていた(大半の場合それは無駄の温床)がアプリ屋の主権に戻ると言っても差し支えないです。(もちろん、主権には責任が伴う)

もう一つ、IaC (Infrastracture as Code) にも良い影響があります。コンテナ時代になり、構成管理は楽になった、というよりほとんど不要になったと言えます。
具体的に chef なり ansibleなりで構成管理をしたときに、面倒なのは、すでに動いているサーバに対して「あとからこの機能足して!」です。例えばすでに ruby2.0 系が入っているのに 2.3系でないと動かないアプリだった場合、rbenv入れますか?それともサーバー新設しますか?(動いている本番に手を入れたくない心理が強く働くので、ほとんどこちらになるはず)になると思いますが、コンテナであればrubyのバージョンまで含めて1つのコンテナなので、他のコンテナに入っている rubyのことなんて全く関係ありません。また、1台のサーバーに機能を足せば足すほど構成管理の辻褄わせなど、本来は楽するための構成管理がイライラの元になることも私は経験しています。コンテナは単純明快 別機能がほしければ、別コンテナでやればいい

もっと簡単にAutoScaling

これもオーケストレーション、ECS前提ですが、EC2+CodeDeploy+AutoscalingによるAS対応に比べて、ECS (fargate前提)によるAutoScalingは遥かに簡単で実装がたやすいです。Autoscalingの実装は簡単そうに見えて難しいです。具体的にはスケールアウト時の最新版デプロイを確実にする方法と、スケールイン時の後始末(ログをS3に投げるなど)です。ECSはこの問題に対するユーザー実装を大幅に減らしてくれます。

サーバーレスとコンテナ

そもそも異質なものなので、比較自体がナンセンスですが、私が考える用法用量について

そもそもサーバーレスって何?

ここで言っている サーバーレス については、勝手意味を狭めて定義します。
サーバーレス = AWS Lambdaのようなリソース(CPU/RAM)の完全時間課金
と限定します。PaaSもサーバーレスの一部とも考えられますが、違うものとして、バッサリ切ります。

サーバーレスは直接コストが安い

サーバーレスの最大のメリットは (ほとんどの場合において)直接コストが安いという点です。
文字通りサーバーレスとはサーバーを建てないことです。対して従来型の一般的なWebサーバの場合、使っていようがいまいが、(クエストがあろうが、なかろうが)Listenしていなきゃいけないので、サーバは動いてないといけないです。Lambdaはサーバーを動かしっぱなしにしないで、API-GWでリクエストを受けて、必要なときだけ処理が走るので、ひっきりなしにリクエストが発生するWebサイト出ない限り、ほとんどの場合はサーバー有りの従来型に比べてコストは圧倒的に安いです。もちろんLambdaはWeb以外にも使えます。

サーバー構築は不要だが、ビジネスロジックに集中できるは嘘

これはほとんどのPaaSも同じですが、よく言われる「サーバーのお守りをしなくていいので、ビジネスロジックに集中できる」は半分あたりで半分外れです。
確かにサーバーレスの文字通り、サーバーの構築とお守りは不要です。スケールアウトも自動でしますが、逆にいうと、やりたくても自分ではできないとさえ言えます。
じゃあビジネスロジックだけに集中できるかと言ったら、そんなこと無いです。
サーバーのお守りは意識しなくていいですが、PaaSなりサーバーレスなりが強要してくるレギュレーション

にあなたの実装方法をあわせなければならないです。それでビジネスロジックだけに集中とはとても言えない、少なくとも従来型ではできていたことができないので、サーバーレスにしてみんなハッピーというような単純な話ではないです。

用途の狭さ

レギュレーションもそうですが、明らかにマイクロサービスを前提としたものです。時代の流れとして、マイクロサービス化そのものは正しいと思いますが、逆にそこでしか使えない。対してコンテナはサーバーレスどころか、モロにサーバー有りですが(Fargateとかは無視)、マイクロサービスだけに用途がとどまりません、GPUだって使える。

サーバーレスについてまとめ

  1. サーバーレス自体には、等しくみんなをハッピーにする力は無い = 用途が限られる
  2. サーバーというものは意識しなくていいが、レギュレーションへ合わせ込みが必要 = マイクロサービス化ほぼ必須
  3. しかしそれらを受け入れたとしても、圧倒的にサービス利用料が安い

暴論ですが 安いは正義

API-GW + Lambda のサーバーレスアーキテクチャと言われるものは、GAE(google)がもう10年以上前に通過した道です、技術的には。
http://write.kogus.org/articles/Y2Rtpp

GAEが流行らなくて、対するLambdaがわーわー言われている理由は100ms単位の完全従量課金にしたという1点です。

あともう一つ

  • コンテナとサーバーレスは競合関係に無いので、いいとこ取りすればいい

Dockerについて

Dockerについてです、コンテナという意味よりも狭い道具としての意味で書いています。

Dockerの2つの機能

1つはランタイムとして、つまりはDVDプレイヤーのような再生装置としての機能、ここについては特に触れません。 もう1つはDocker Imageの管理機能、たとえるとDVDそのものを作ったり、管理したりする機能について書きます。

dockerは、gitの OSイメージ版

git と dockerはコマンドとしてそっくりです。

こう考えるとdockerコマンドの本質がよく理解できます。

  • docker build = Dockerfileの中身をもって、コンテナを作成し、stopして docker commitする
  • docker commit = stop状態のコンテナから docker imageを作成する
  • docker push = 指定したローカルにある docker imageを docker-hub や ECRに pushする
  • docker pull = docker-hub や ECRにある docker imageをローカルにコピーする

docker imageソースコード に読み替えると、ほぼ gitと同じ、というか明らかに gitを意識した作りです。 ここで重要なのは、docker imageは差分(レイヤー)で管理され、差分でダウンロード可能である点です。実にエコです。

http://enakai00.hatenablog.com/entry/20140802/1406958412

Docker hub & github

同じく Docker hub も github をもろに意識したものです。何度も言いますが、 Docker hub は Dockerfile の差分を保持しているものではなく、 docker imageの差分を保持しています。大事なのは docker image です。 Dockerfileは docker imageをつくるためのただのヘルパーです。

上記にも触れましたが、OSSお試し環境として docker image の提供は極めて有益で、使い方ビデオを youtoube に上げるぐらいなら、 docker image を Docker hub に pushしたほうがより利用者にわかりやすいです。ものにもよりますが、今後のOSSの提供者としては、githubでソース公開 & バクトラッキングは当然、 dockerhubで機能・追加機能の紹介とやるのが良いでしょう2

Git & GitHub がやったことをOSイメージ = 本当の意味でのランタイムで実現したのがDockerです。このDocker Imageの取り回しのしやすさが起爆剤となり

  • CI/CD
  • IaC
  • 高速デプロイ
  • 開発・本番の共通化
  • ローカル開発

という数えきれないメリットを生み出した。

コンテナ化のデメリット

正直言って無いです。
あるにはありますが、メリットに対してデメリットが薄過ぎる。
マイクロサービス化の弊害は無視します、前述の通り コンテナ化 ≠ マイクロサービス化 なんで。

パフォーマンス劣化

パフォーマンスは軽微とはいうものの、当然劣化します。 ただし、私の世の中に対する感覚から言えば、ここで言われるパフォーマンス劣化は語ることすらナンセンスです。

何故か?
現状を見れば答えはあります。まだオンプレとかいう言葉がなかった頃、仮想環境(VMWareなど)で動かすほうがマイノリティーだった頃があります(この自体は体験している)。クラウドコンピューティングなんて影もカタチもなかった。
それから仮想環境が動かすのが当たり前になった。 理由はパフォーマンスの劣化よりも得るもののほうが大きかったから。
クラウドコンピューティングが出始めたころも、「こんなもの、開発でしか使えねー」と言われていたらしい。一見仮想環境よりも更にコスト効率悪いしね。
だけど、そんな上っ面のコストよりも深層で得られる見えづらいコストに対するメリットがあまりにデカかった。高いけど売れる という本当の価値。
コンテナ(Dockerと言い換えられる)も全く同じ、「開発はいいんだけどねー」だった、始まった頃は。だけど今はどう?

もちろんすべてのコンピューティング分野において、パフォーマンス劣化を受け入れられるわけはないのは知っているけど、ほぼ無視できるレベル。
むしろ仮想化のコストはバカ高だけど、コンテナならイケるというパターンも(正直レアだと思うけど)考えられる。

学習コスト

Dockerは低いと思います。問題はオーケストレーションかなと。予備知識なしだと複雑すぎるので、苦戦するところもあると思う。
まずはローカル環境のDockerを弄り倒すことから、それだけでも十分利を得られるはずです。

まとめ

  1. Docker Image が起爆剤となり、現存するいろんな問題を解決していった (= Docker Image をまず知る)
  2. Dockerだから〇〇しなければいけない制約は何一つない (= やりたいからやる、やりたい方法を選ぶ)
  3. Dockerが開発環境だけなんてもったいない!

最後に
オーケストレーションは置いといて、今現在、Dockerを使わない手はない。


  1. 私はベストプラクティスという言葉は大嫌い、なんか偉そうに感じませんか?「何がベストかは俺が決めるんや、アホ扱いするな!」といつも思う

  2. 私の提言みたいに書きましたが、すでに世の中がそうなってます。