読者です 読者をやめる 読者になる 読者になる

Lambdar

主に技術やソフトウェアについて

AWS SNS トピック通知を Slack に流す Terraform モジュールを作った

可用性が重要な全ての Web インフラストラクチャにおいて,アラート通知は欠かせない. 幸い,AWS では CloudWatch によってモニタリングが,そして Simple Notification Service (SNS) によってそのアラート通知が簡単に行える. Amazon SNS では,Eメール通知だけでなく AWS Lambda を使った通知も可能であるため,ほとんどどのようなメッセージプラットフォーム宛でも通知を流せる.

勿論,Slack も例外ではない.

既に SNS トピック通知を Slack に流すライブラリは存在する. しかし,それを Terraform で管理したいときには少々の苦労を伴う.インフラストラクチャを Terraform で構築しているなら,SNS から Slack に通知を流すのに必要なリソースも全て Terraform の支配下に置きたくなるだろう. そこで,そのライブラリに少しばかり手を加え,必要なことを行ってくれる Terraform モジュールを作った.

本稿では,その aws-sns-slack-terraform モジュールについて説明する.

github.com

TL;DR

あなたの Terraform スクリプトに,以下のようなコードを追加すれば良い.

module "sns_to_slack" {
  source = "github.com/builtinnya/aws-sns-slack-terraform/module"

  slack_webhook_url = "hooks.slack.com/services/XXX/XXX/XXX"
  slack_channel_map = "{ \"topic-name\": \"#slack-channel\" }"
}

resource "aws_sns_topic" "test_topic" {
  name = "topic-name"
}

resource "aws_lambda_permission" "allow_lambda_sns_to_slack" {
  statement_id = "AllowSNSToSlackExecutionFromSNS"
  action = "lambda:invokeFunction"
  function_name = "${module.sns_to_slack.lambda_function_arn}"
  principal = "sns.amazonaws.com"
  source_arn = "${aws_sns_topic.test_topic.arn}"
}

resource "aws_sns_topic_subscription" "lambda_sns_to_slack" {
  topic_arn = "${aws_sns_topic.test_topic.arn}"
  protocol = "lambda"
  endpoint = "${module.sns_to_slack.lambda_function_arn}"
}

変更すべき箇所を以下に挙げる.

  • 通知したいチームの Webhook URL にする.
slack_webhook_url = "hooks.slack.com/services/XXX/XXX/XXX"
  • 通知させたいトピック名,通知を流したいチャンネル名に変更する.文字列の内容は JSON オブジェクト形式であり,複数のトピック-チャンネルマッピングを指定できる.
slack_channel_map = "{ \"topic-name\": \"#slack-channel\" }"
  • SNS トピックのリソース名,トピック名をまともにする.変更した場合は,source_arntopic_arn での指定も忘れずに変更すること.
resource "aws_sns_topic" "test_topic" {
  name = "topic-name"
}

以上を適用すれば設定は完了だ.詳細は GitHub リポジトリを参照してほしい.

動いているところ

f:id:builtinnya:20170518005219p:plain

上の画像は,GitHub リポジトリ に用意したサンプル(minimal)を使って CloudWatch アラーム通知を実際に行っている場面のスクリーンショットだ. CloudWatch アラームについての通知だけでなく,AutoScaling イベントなども整形・表示される.サポートされていないものについてはメッセージのみが表示される.

仕組み

f:id:builtinnya:20170518015757p:plain

上図は,通知システム全体の構成を示すものだ(図は draw.io で描いた).図で示される通り,以下の流れで通知が行われる.

  1. イベントソース(e.g. CloudWatch Alarms)から SNS トピックに通知が行われる
  2. SNS は,そのトピックの購読者である AWS Lambda のハンドラにイベントを渡す
  3. イベントを受け取った Lambda ハンドラ(robbwagoner/aws-lambda-sns-to-slack を少し変えたもの)は,イベントの内容を見てどのサービスのものかを特定し,サポートしているものであればメッセージを綺麗に整形して Slack に投稿する.そうでなければ単にトピックから通知されたメッセージをそのまま流す.

実際に Slack に投稿するプログラムは robbwagoner/aws-lambda-sns-to-slack をほとんどそのまま使っている.ただし,Terraform へのつなぎこみやカスタマイズ性を向上させるため,以下の変更を行った.

  • Webhook URL,Slack 投稿時に表示されるデフォルトのユーザ名,デフォルトのチャンネルを環境変数で指定できるようにした.
  • トピック-チャンネルのマッピングを環境変数で指定できるようにした.マッピングは JSON オブジェクト形式の文字列だ.AWS Lambda では環境変数にカンマを含む文字列を指定するとエラーが出る問題があるため,Terraform の base64encode 関数を使って Base64 形式にエンコードしておき,Lambda ハンドラ側でデコードすることでこの問題を回避している.Base64 はバイナリデータを ASCII 文字列で表すエンコーディングだが,今回のように使用できない文字列の制限を回避するためにも使われる.Eメールでの利用や,BASIC 認証を実現する Authorization ヘッダでの利用はよく知られている.

おわりに

本稿では,SNS トピック通知を Slack に投稿するシステムを簡単に構築できる Terraform モジュールについて述べた. ソースコードは GitHub 上で公開されている.Issue や Pull Request を歓迎する.

CircleCI 2.0 Beta における Docker イメージのビルド

先日,CircleCI 2.0 がオープンベータになった

CircleCI 1.0 ではベースコンテナとして LXC,ファイルシステムとして Btrfs を採用しているため,バージョン 1.11 以降の Docker を利用することができなかった. CircleCI 2.0 ではそのような制限は存在しない.Docker をネイティブサポートしているからだ.

ユーザは任意の Docker イメージを自由に組み合わせて望みの CI コンテナ環境を作ることができる. プロジェクト毎の設定で「Ubuntu 12.04 (Precise)」か「Ubuntu 14.04 (Trusty) 」の二択から選んでいたことを考えると,レゴブロックを買い与えられた子供のように喜ぶべきだろう.

しかし,CircleCI 1.0 時代においても我々はアプリケーションを Docker イメージとしてビルドし,プッシュし,デプロイしていたはずである. CircleCI 2.0 では Docker コンテナの中で Docker を使って Docker イメージをビルドするだって?そんなのやったことないよ!


幸い,CircleCI 2.0 はこの Docker-in-Docker 問題 についても解答を与えてくれている. 本稿では,CircleCI 2.0 における Docker イメージのビルドについて,イメージレイヤーのキャッシュも含めてその方法を簡単に述べる.

TL;DR

プロジェクトルートに .circleci ディレクトリを作成し,そこに以下のような config.yml を置けば良い.

version: 2
jobs:
  build:
    working_directory: /app
    docker:
      - image: docker:17.05.0-ce-git
    steps:
      - checkout
      - setup_remote_docker
      - run:
          name: Install dependencies
          command: |
            apk add --no-cache \
              py-pip=9.0.0-r1
            pip install \
              docker-compose==1.12.0 \
              awscli==1.11.76
      - restore_cache:
          keys:
            - v1-{{ .Branch }}
          paths:
            - /caches/app.tar
      - run:
          name: Load Docker image layer cache
          command: |
            set +o pipefail
            docker load -i /caches/app.tar | true
      - run:
          name: Build application Docker image
          command: |
            docker build --cache-from=app -t app .
      - run:
          name: Save Docker image layer cache
          command: |
            mkdir -p /caches
            docker save -o /caches/app.tar app
      - save_cache:
          key: v1-{{ .Branch }}-{{ epoch }}
          paths:
            - /caches/app.tar
      - run:
          name: Run tests
          command: |
            docker-compose -f ./docker-compose.test.yml up
      - deploy:
          name: Push application Docker image
          command: |
            if [ "${CIRCLE_BRANCH}" == "master" ]; then
              login="$(aws ecr get-login)"
              ${login}
              docker tag app "${ECR_ENDPOINT}/app:${CIRCLE_SHA1}"
              docker push "${ECR_ENDPOINT}/app:${CIRCLE_SHA1}"
            fi

細部はプロジェクトによって当然異なる.このファイルが有効なサンプルプロジェクトを GitHub 上に用意した.

github.com

このサンプルプロジェクトには Express を使った「Hello World!」だけを返す Node.js アプリケーションと,Jestsupertest を使ったささやかなテストが含まれている. また,Amazon EC2 Container Registry (以下 ECR と略す)に Docker イメージをプッシュするために,ECR リポジトリの作成を行う単純な Terraform スクリプトもある. ECR を選んだ理由は,.circleci/config.yml を解説用にほんの少しばかり複雑にするためである.

以降では,この .circleci/config.yml の内容について説明を加える.

.circleci/config.yml

全体の構造

version: 2
jobs:
  build:
    working_directory: ...
    docker: ...
    steps: ...

詳細は公式ドキュメントに譲るが,特筆すべき点を以下に挙げておく.

  • CircleCI 1.0 では固定されていた実行セクションを,ジョブという単位でユーザが自由に定義できるようになった(build という名前のジョブは CircleCI が始めに実行する唯一のジョブである)
  • 同様に,各ジョブのステップをユーザが自由に定義できるようになった(リポジトリのチェックアウトやキャッシュ関係処理を含む)

つまり,CI コンテナ環境だけでなくステップについても,我々はより大きな自由を手に入れたことになる.

docker executor

docker:
  - image: docker:17.05.0-ce-git

この部分では,冒頭で述べた CI 環境を定義している.ここで定義した環境上でステップが実行される.

我々が欲しいのは,Docker がインストールされた Docker イメージである. さらに,ソースコードをチェックアウトするために Git もインストールされていると良い. それを満たすイメージが docker:17.05.0-ce-git である.このイメージは公式のものであり,-git が接尾辞としてついているイメージには Git がインストールされている.

このように,最新バージョンの Docker クライアントを使うことができる.

checkout

- checkout

最初のステップである checkout は,名前が示す通りソースコードをチェックアウトする特別なステップである. ソースコードは working_directory で指定したディレクトリに展開される.

setup_remote_docker

- setup_remote_docker

この呪文を唱えると, Docker-in-Docker 問題が回避できる. 実際には,CI コンテナ(プライマリコンテナ)から孤立した環境を立ち上げて,そのリモートホストの Docker Engine を利用している

必要なライブラリのインストール

- run:
    name: Install dependencies
    command: |
      apk add --no-cache \
        py-pip=9.0.0-r1
      pip install \
        docker-compose==1.12.0 \
        awscli==1.11.76

Python および Pip をインストールしてから,Docker Compose と AWS CLI をインストールしている. Docker Compose はテストの実行に用いる.AWS CLI は,Docker イメージを ECR リポジトリにプッシュする際に用いる.

実際のプロジェクトでは,これらの依存ライブラリも含めたイメージをあらかじめ作成しておくと良い.

Docker イメージのビルドとキャッシュ

- restore_cache:
    keys:
      - v1-{{ .Branch }}
    paths:
      - /caches/app.tar
- run:
    name: Load Docker image layer cache
    command: |
      set +o pipefail
      docker load -i /caches/app.tar | true
- run:
    name: Build application Docker image
    command: |
      docker build --cache-from=app -t app .
- run:
    name: Save Docker image layer cache
    command: |
      mkdir -p /caches
      docker save -o /caches/app.tar app
- save_cache:
    key: v1-{{ .Branch }}-{{ epoch }}
    paths:
      - /caches/app.tar

ここが本稿の肝である.基本的には,以下のことを順番に行っている.

  1. v1-{{ ブランチ名 }} を接頭辞とするキーを持つキャッシュが存在する場合,/caches/app.tar にリストアする.app.tar は,(あれば)前回ビルドした Docker イメージファイルである.
  2. /caches/app.tar が存在する場合,それを Docker にロードする.これにより,前回ビルドした Docker イメージレイヤーが再利用できる.
  3. Docker イメージをビルドする.--cache-from=イメージ名 を指定すること.
  4. ビルドした Docker イメージを /caches/app.tar に保存する
  5. 次回のビルドで利用できるように,/caches/app.tar をキャッシュする.キャッシュキーは v1-{{ ブランチ名 }}-{{ Unix エポックタイム }} としている.

このような回りくどいことをしているのは,デフォルトではリモート Docker Engine におけるレイヤーキャッシングが行われないからである. 実は,このレイヤーキャッシングを行う機能が存在するが,正式リリース版では有料機能になる可能性がある.また,Beta 版においてもサポートに問い合わせて有効にしてもらう必要があるらしい.

イメージレイヤーをキャッシュすることで実際にどの程度の速度改善が得られるかは,サンプルプロジェクトの CircleCI ビルド履歴 を参照してほしい.例えば, build #12 はキャッシュあり,build #13 はキャッシュなし(without cache)でビルドしたものである. 残念ながら,この例では 22 秒程度の改善しか見られなかった.

テスト

- run:
    name: Run tests
    command: |
      docker-compose -f ./docker-compose.test.yml up

Docker Compose でテストを実行している. 今回のサンプルプロジェクトではアプリケーションコンテナのみでテストが完結するが,データベースコンテナなどが必要になった場合,Docker Compose を用いるとセットアップが容易である.

プッシュ

- deploy:
    name: Push application Docker image
    command: |
      if [ "${CIRCLE_BRANCH}" == "master" ]; then
        login="$(aws ecr get-login)"
        ${login}
        docker tag app "${ECR_ENDPOINT}/app:${CIRCLE_SHA1}"
        docker push "${ECR_ENDPOINT}/app:${CIRCLE_SHA1}"
      fi

ブランチが master であるときだけ,ビルドした Docker イメージを ECR リポジトリにプッシュする.ログイン情報を取得するために,先にインストールした AWS CLI を用いる. 今回は行っていないが,実際のプロジェクトではイメージをプッシュした後にデプロイを実行することが多い(例えば,Amazon EC2 Container Service を用いる場合,ecs-deploy などを使う).

おわりに

CircleCI 2.0 においてもリモート Docker Engine によって Docker イメージのビルドが問題なく行えることが確認できた. また,効果は限定的だが,Docker イメージレイヤーのキャッシングも行えることを見た.

問題にぶつかったら,公式ドキュメントはもちろん,コミュニティフォーラムが参考になる.