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

aws lambdaで ec2の local ipを r53へ自動登録. Contribute to h-imaoka/aws-lambda-internaldns development by creating an account on GitHub.

github.com

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

このエントリでは、今回蔵出ししたこのツールを通して 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が権限を持っている必要があるは直感でわかるはfずですが、これは逆に 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 を公開しています。

Containers for Serverless framework. Contribute to h-imaoka/sls-builder development by creating an account on GitHub.

github.com

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

まとめ・感想

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

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

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

元記事はこちら

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