ようへいの日々精進XP

よかろうもん

冬休みの自由研究 (1) 〜 LocalStack と CircleCI を使って Lambda の動作確認環境を割と苦労して作った 〜

追伸

書いた後で気付いたけど, Node.js のランタイムは 12.x もサポートしていたので, Node.js 10.x ではなく, Node.js 12.x にアップデートしたほうが良さそう...

という時にも, このような動作確認環境を作っておくとシュッと確認出来るので良いよねというお話でした.

tl;dr

冬休みに本を三冊読む目標を立てていますが, たまたま年明けから Lambda の Node.js 8.10 を Node.js 10.x にアップデートする作業を控えているので, 念の為の動作確認環境を作ろうと LocalStack に手を出してしまったのでメモです.

やりたいことは, LocalStack で Lambda の疑似環境を起動して, その Lambda 実行環境に対して, 既存の Node.js 8.10 ランタイムで動かしている関数をデプロイして動作確認を行うところまでです. せっかくなので, テストは CircleCI でも回せるようにして, 今後の追加開発等の際の動作確認でも使えるようにしたいと思います.

教材

教材は, 以下のリポジトリにアップしています.

github.com

このプロジェクトは, 以下のプロジェクトをフォークさせて頂いております.

github.com

このプロジェクトは, CloudWatch Alarm やいくつかのサービスについて SNS を介して Slack に通知する Lambda ファンクションです. YAMAP 社内でも利用させて頂いております.

今回は, このプロジェクトに以下の機能を追加します.

  • docker-compose を利用して LocalStack を起動し, ローカル環境に Lambda の実行基盤を起動出来るようにする
  • 同じく, docker-compose を利用し, Slack の疑似エンドポイントとして httpbin.org 環境を起動出来るようにする
  • Lambda を LocalStack の Lambda 基盤にデプロイし, AWS CLIaws lambda invoke を利用して関数を実行出来るようにする
  • aws lambda invoke は, Bash のテストフレームワークである bats を利用して実行出来るようにする

最終的には, bats でのテストを Node.js 8.10 と Node.js 10.x 環境で実行してみて, 双方でちゃんと動くのかの確認を取ることになります.

詳細

リポジトリを見てね

と, 言いたいところですが, 個人的に苦労した部分等を書いていきたいと思います.

LocalStack

以下のような docker-compose.yml でシュッと起動します.

version: '2.1'

services:
  localstack:
    image: localstack/localstack
    ports:
      - "4567-4597:4567-4597"
      - "18080:8080"
    environment:
      - TMPDIR=${TMPDIR- }
      - SERVICES=
      - DEFAULT_REGION=us-east-1
      - AWS_XRAY_SDK_ENABLED=true
      - DATA_DIR=${DATA_DIR- }
      - PORT_WEB_UI=${PORT_WEB_UI- }
      - LAMBDA_EXECUTOR=docker
      - LAMBDA_REMOTE_DOCKER=true
      - LAMBDA_DOCKER_NETWORK=project_my_net
      - DOCKER_HOST=unix:///var/run/docker.sock
      - DEBUG=1
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
        my_net:
          ipv4_address: 192.168.1.10
    dns:
      - 8.8.8.8
...  略 ...

networks:
  my_net:
    driver: bridge
    ipam:
     driver: default
     config:
       - subnet: 192.168.1.0/16
         gateway: 192.168.1.1

注意点としては, Lambda を実行する場合には, 以下の環境変数を定義してあげる必要がありました.

  • LAMBDA_EXECUTOR=docker
  • LAMBDA_REMOTE_DOCKER=true

また, LocalStack 内では, Lambda は別のコンテナ上で動作する (LocalStack から更に Docker コンテナが起動される) しますが, 今回は, そのコンテナ自体も同じ Docker ネットワーク内で動作させる必要があったので, 以下のように Docker ネットワークを固定することにしました.

  • LAMBDA_DOCKER_NETWORK=project_my_net

httpbin.org

lambda-cloudwatch-slack は SNS からサブスクライブされたメッセージを Slack に通知する Lambda ファンクションです. ということは, 動作確認として Slack に通知されるところまでを確認することが必要だと考えています. しかし, テストの度に Slack に通知が飛ぶのもなんだかなあと思ったので, Slack に通知する代わりに, 擬似的は HTTP エンドポイントに対して, Payload が正しく POST 出来たら正常と判断するようにしました.

そこで, 擬似的な HTTP エンドポイントとして, httpbin.org というサービスがあることを知り, その Docker イメージを利用することにしました.

httpbin.org

このサービス, 以下のように POST した場合, オウム返しのようにリクエストしたデータやヘッダの情報を返してくれるだけのとてもシンプルなサービスです.

$ curl -X POST -H "Content-Type: application/json" -d '{"text": "John"}' https://httpbin.org/post
{
  "args": {},
  "data": "{\"text\": \"John\"}",
  "files": {},
  "form": {},
  "headers": {
    "Accept": "*/*",
    "Content-Length": "16",
    "Content-Type": "application/json",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.54.0"
  },
  "json": {
    "text": "John"
  },
  "origin": "xxx.xxx.xxx.xxx, xxx.xxx.xxx.xxx",
  "url": "https://httpbin.org/post"
}

ところが, 当初は, 直接, httpbin.org を叩くことを考えましたが, なぜか, LocalStack を介して起動する Lambda 環境 (Docker コンテナ) で名前解決が出来ないという壁にぶち当たり, 泣く泣く httpbin.org の Docker イメージを利用することにしました.

ちなみに, Lambda 環境にて名前が解決出来ない状態では, 以下のようなエラーが発生します.

2019-12-29T01:26:54.390Z        1929ac8c-970e-1e14-8ed4-9ca0b1b93aca    processing cloudwatch notification
2019-12-29T01:27:04.427Z        1929ac8c-970e-1e14-8ed4-9ca0b1b93aca    Error: getaddrinfo EAI_AGAIN httpbin.org:443

docker-compose.yml 内で dns: キーでリゾルバを設定してみたりしましたが, 結局解決せずでした. 残念. この諦めのおかげで, lambda-cloudwatch-slack のコードを少し修正する必要が出てきました...

lambda-cloudwatch-slack を泣く泣く改修

これは, やって良い対応なのか最後まで悩みましたが, テストの時だけ, https モジュールではなく, http モジュールを利用するような処理を追加しました...ダサい.

if (process.env.ENVIRONMENT == 'test') {
  var https = require('http');
} else {
  var https = require('https');
}

環境変数 ENVIRONMENTtest が指定されている場合のみ, http モジュールを利用します. 今回, lambda-cloudwatch-slack で明示的に https モジュールを利用しているのは, 以下のように Slack へのリクエストのみだったので, このトリッキーな対応で対処出来ました.

var postMessage = function(message, callback) {
  var body = JSON.stringify(message);
  var options = url.parse(hookUrl);
  options.method = 'POST';
  options.headers = {
    'Content-Type': 'application/json',
    'Content-Length': Buffer.byteLength(body),
  };

  var postReq = https.request(options, function(res) {
    var chunks = [];
    res.setEncoding('utf8');
    res.on('data', function(chunk) {
      return chunks.push(chunk);
    });
    res.on('end', function() {
      var body = chunks.join('');
      if (callback) {
        callback({
          body: body,
          statusCode: res.statusCode,
          statusMessage: res.statusMessage
        });
      }
    });
    return res;
  });

  postReq.write(body);
  postReq.end();
};

CircleCI で動かす!

やっぱり, テストといえば, 継続的にテストしていきたいので, CircleCI 上で転がすようにしました. .circleci/config.yml は以下の通り.

version: 2.1

executors:
  default:
    docker:
      - image: circleci/node:10.9.0

commands:
  npm_install:


... 略 ...

jobs:
  build:
    executor:
      name: default
    steps:
      - checkout
      - setup_remote_docker
      - npm_install
      - run:
          name: Install docker-compose
          command: |
            curl -L https://github.com/docker/compose/releases/download/1.25.0/docker-compose-`uname -s`-`uname -m` > ~/docker-compose
            chmod +x ~/docker-compose
            sudo mv ~/docker-compose /usr/local/bin/docker-compose
      - run: 
          name: Build Test Environment
          command: |
            docker-compose build
      - run: 
          name: Run Test Environment
          command: |
            docker-compose up -d
      - run:
          name: Setup Lambda Function
          command: |
            docker-compose exec deploy_lambda sh -c './node_modules/node-lambda/bin/node-lambda deploy --functionName=lambda-cloudwatch-slack --endpoint=http://192.168.1.10:4574 --configFile=deploy.env.test'
      - run:
          name: Run Test
          command: |
            docker-compose exec bats bats /work/test.bats

シンプルはワークフローです. docker-compose が割と普通に動いてくれるので, オール docker-compose で動かすことが出来ました.

動いている様子

ローカル環境

自分の手元の端末では, docker-compose.local.yml を使って動作確認環境を起動します. (これも妥協点の一つ)

$ docker-compose -f docker-compose.local.yml up -d

まずは, Node.js 8.10 で Lambda をデプロイしてテストを実行してみます.

$ env AWS_RUNTIME=nodejs8.10 env AWS_REGION=us-east-1 ./node_modules/node-lambda/bin/node-lambda deploy --functionName=lambda-cloudwatch-slack --endpoint=http://localhost:4574 --configFile=deploy.env.test
$ docker-compose exec bats bats /work/test.bats

引き続き, Node.js 10.x で Lambda をデプロイしてテストを実行してみます.

$ env AWS_RUNTIME=nodejs10.x env AWS_REGION=us-east-1 ./node_modules/node-lambda/bin/node-lambda deploy --functionName=lambda-cloudwatch-slack --endpoint=http://localhost:4574 --configFile=deploy.env.test
$ docker-compose exec bats bats /work/test.bats

CircleCI 環境

おっと, なんか Fail しているようです.

f:id:inokara:20191229113028p:plain

これは, lambda-cloudwatch-slack ではデフォルトの挙動のようで, Lambda に渡すイベントが {} のように空の状態であると, 関数は実行されるものの, 処理するメッセージ (Slack に送信するメッセージ) が存在しないため, Lambda 側でエラーになってしまいます. 以下は Lambda 側のエラー出力です.

2019-12-29T02:32:15.072Z        5a893f2a-a740-1037-19d5-4a77cb46a080    INFO    sns received:{}
2019-12-29T02:32:15.074Z        5a893f2a-a740-1037-19d5-4a77cb46a080    ERROR   Invoke Error    {"errorType":"TypeError","errorMessage":"Cannot read property '0' of undefined","stack":["TypeError: Cannot read property '0' of undefined","    at processEvent (/var/task/index.js:365:43)","    at Runtime.exports.handler (/var/task/index.js:430:5)","    at Runtime.handleOnce (/var/runtime/Runtime.js:66:25)"]}

せっかくなので, このエラーを回避する実装を追加してみたいと思います. 実装と言っても大げさなものではなく, 以下のように追記するだけです.

diff --git a/index.js b/index.js
index 2c9ac11..b8c8841 100644
--- a/index.js
+++ b/index.js
@@ -418,10 +418,10 @@ var processEvent = function(event, context) {

 exports.handler = function(event, context) {

+  if (! event.Records) {
+    context.succeed('Records key has not been set.');
+    return;
+  }

   if (hookUrl) {
     processEvent(event, context);

改めて, CircleCI でテストを実行してみましょう.

f:id:inokara:20191229113919p:plain

いい感じです. 見事にテストをパスしました.

さらに, Node.js 8.10 と Node.js 10.x で連続してテストを流してみましょう. 両方ともにテストが通れば, 今回の目的だったランタイムのバージョンアップによる関数の動作確認がそこそこちゃんと行えることになります.

f:id:inokara:20191229115140p:plain

いい感じです.

ということで

初めて LocalStack を使ってみましたが, 手元でシュッと擬似的な AWS 環境が動くことにとても感動しました. しかし, それなりに使おうとすると, きちんとドキュメントを読んで, 適切な設定を行う必要があることを痛感しました. ということで, 一応, Node.js 8.10 でも Node.js 10.x でも lambda-cloudwatch-slack が動くことがわかったので, 年明けに安心してランタイムのバージョンアップにのぞめそうです.

お疲れ様でした.