AWS LambdaのBlueprintを使ってCloudWatchの通知をSlackに送る このエントリをはてなブックマークに登録

2016年03月23日

ダニーダニー / ,

lambda1

はじめに

弊社でもプロジェクトごとにSlackを使っていて、GitHubの通知やテスト結果の通知などの開発関係の通知をSlackのチャンネルに流すということをやっています。

CloudWatch の通知も Slack のチャンネルに流せればいいと思っていたのですが、CloudWatch 単体ではその機能が無かったため、以前は amazon-cloudwatch-to-slackを使って Slack に流していました。

amazon-cloudwatch-to-slack は、CloudWatch からの通知を Slack に転送するためのものです。

それで運用してみた結果

  • サーバーを立てないといけないのが手間
  • 常時サーバーを動かす必要があるでその分のコストが掛かる

という問題がありました。

Lambdaを使うことで、サーバーを立てる必要がなくなったのと、Lambdaはリクエスト回数と処理時間に対して課金なのでコストが抑えられるようになりました。

動かす手順

  1. KMSで鍵を作成
  2. KMSでSlackのをWebhook URLを暗号化する
  3. Lambda 関数のロールを作成
  4. Lambda 関数を作成
  5. SNSのtopicにLambda 関数を追加
  6. CloudWatchの通知にtopicを追加

Blueprintのcloudwatch-to-slackはSlackのWebhook URLをKMSで暗号化するので。KMSの設定もする必要が出ます。

手動でセットアップするのが手間なので、Lambda 関数の作成処理をスクリプト化しました。

セットアップスクリプト

lambda_policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

kms_policy.json.erb

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Stmt1443036478000",
      "Effect": "Allow",
      "Action": [
        "kms:Decrypt"
      ],
      "Resource": [
        "<%= arn %>"
      ]
    }
  ]
}

setup.rb

require 'rubygems'
require 'aws-sdk'
require 'erb'
require 'zip'
require 'dotenv'

def kms_policy(arn)
  ERB.new(File.read('./kms_policy.json.erb')).result(binding)
end

def create_role(role_name, arn)
  client = Aws::IAM::Client.new
  response = client.create_role({
    role_name: role_name,
    assume_role_policy_document: File.read('./lambda_policy.json'),
  })
  client.attach_role_policy({
    role_name: role_name,
    policy_arn: 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole',
  })
  client.put_role_policy({
    role_name: role_name,
    policy_name: 'kms',
    policy_document: kms_policy(arn),
  })
  response
end

def create_function(function_name, role, zip_file)
  client = Aws::Lambda::Client.new
  options = {
    function_name: function_name,
    runtime: 'nodejs',
    role: role,
    handler: 'index.handler',
    code: {
      zip_file: zip_file
    },
    timeout: 3,
    memory_size: 128,
    publish: true,
  }
  client.create_function(options)
end

def create_zip_file(kms_encypted_hook_url, slack_channel)
  source = ERB.new(File.read('./index.js.erb')).result(binding)
  Zip::OutputStream.write_buffer do |f|
    f.put_next_entry('index.js')
    f.write(source)
  end.string
end

def create_kms_key(alias_name)
  client = Aws::KMS::Client.new
  response = client.create_key
  client.create_alias({
    alias_name: "alias/#{alias_name}",
    target_key_id: response.key_metadata.key_id
  })
  response
end

def kms_encrypt(key_id, plaintext)
  ciphertext_blob = Aws::KMS::Client.new.encrypt(key_id: key_id, plaintext: plaintext).ciphertext_blob
  Base64::strict_encode64(ciphertext_blob)
end

def main(name, web_hook_url, slack_channel)
  kms = create_kms_key(name)

  iam = create_role(name, kms.key_metadata.arn)
  kms_encypted_hook_url = kms_encrypt(kms.key_metadata.key_id, web_hook_url)

  zip_file = create_zip_file(kms_encypted_hook_url, slack_channel)

  sleep(25)

  create_function(name, iam.role.arn, zip_file)
end

Dotenv.load

main(ARGV[0], ARGV[1], ARGV[2])

mainの中でsleepしてるのは、Lambda 関数を作成する処理で、引数で渡したロールがまだ作成できなくてエラーになっていたからです。

おそらく成功したレスポンスで返って来てても、内部的には即時反映されていなくてタイムラグが出てるんじゃないかと思います。

AWS SDK for Ruby V2には処理が終わるまで待って、処理が終わってから次の処理が実行できるwaitersという仕組みがあります。
今回の場合は該当するものがなくて使えなかったです。

Aws::IAM::Client.new.waiter_names
=> [:instance_profile_exists, :user_exists]

アラート表示の修正

Blueprintのcloudwatch-to-slackのままだと通知がシンプルなので

lambda-slack

amazon-cloudwatch-to-slackの通知の仕方を参考に、通知を見やすくなるように

Blueprintのcloudwatch-to-slackのコードを修正しました。

var processEvent = function(event, context) {
    var message = JSON.parse(event.Records[0].Sns.Message);
    var subject = ' ' + event.Records[0].Sns.Subject;
    var attachments = [
        {
            'fallback': subject,
            'text' : subject,
            'color': message.NewStateValue == 'OK' ? 'good' : 'danger',
            'fields': [
                {
                    'title': 'Alarm',
                    'value': message.AlarmName,
                    'short': true
                },
                {
                    'title': 'Status',
                    'value': message.NewStateValue,
                    'short': true
                },
                {
                    'title': 'Reason',
                    'value': message.NewStateReason,
                    'short': false
                }
            ]
        }
    ];
    var slackMessage = {
        channel: slackChannel,
        text: subject,
        attachments: attachments
    };
    postMessage(slackMessage, function(response) {
        if (response.statusCode < 400) {
            console.info('Message posted successfully');
            context.succeed();
        } else if (response.statusCode < 500) {
            console.error("Error posting message to Slack API: " + response.statusCode + " - " + response.statusMessage);
            context.succeed();  // Don't retry because the error is due to a problem with the request
        } else {
            // Let Lambda retry
            context.fail("Server error when processing message: " + response.statusCode + " - " + response.statusMessage);
        }
    });
};

セットアップスクリプトを実行する

IAMユーザに権限を付ける

IAMでAPIを実行できるユーザーを作成して、そのユーザーのアクセスキーを作成します。
今回のスクリプトで使ってる、LambdaとKMSとIAMの実行できる権限を付けます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1443036478000",
            "Effect": "Allow",
            "Action": [
                "kms:CreateKey",
                "kms:CreateAlias",
                "kms:Encrypt",
                "iam:CreateRole",
                "iam:PassRole",
                "iam:AttachRolePolicy",
                "iam:PutRolePolicy",
                "lambda:CreateFunction"
            ],
            "Resource": "*"
        }
    ]
}

実行する

IAMでAPIを実行できるユーザーのアクセスキーを設定します。

$ aws configre

awsコマンドはインストールする必要があります。

実行例

$ bundle exec ruby setup.rb 'cloudwatch-to-slack' 'hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX' '#alert'

セットアップが終わったら、先ほど追加したポリシーとアクセスキーを削除します。

SNSの設定を追加

まずtopicを作成します。

93504bff-c7f8-4326-a6a1-dde04cb2a5cc

とのtopicに対して

b27ad6ae-1a36-4f2d-83b6-5d4f3fac7c8a

Lambda 関数をサブスクリプションに追加します。

CloudWatchのアラーム作成

通知の送信先を先ほど作成したtopicを指定します。

61e92b5d-9a5d-4659-8f1d-da91ae77e282

これでCloudWatchからSlackに通知されるようになります。

リポジトリ

https://github.com/f96q/cloudwatch-to-slack

最後に

Lambdaを使うことで、今までサーバーを立ててやっていたことが、サーバーを立てなくても済むようになることが便利でした。

宣伝

053c751a-9a40-43e2-ad6e-b75e78367451

DocBaseとは

情報共有を活発にし、チームを育てるをコンセプトにした情報共有サービスです。
柔軟な権限設定と使いやすさ、社内の人も社外の人も全員を招待できるから、さまざまな人やツールに散らばっていた情報を一元化できます。
積極的な情報共有と業務の効率化を実現し、チームの成長を促します。

詳しくはこちらから。
https://docbase.io

  1. メモからはじめる情報共有 DocBase 無料トライアルを開始
  2. DocBase 資料をダウンロード

「いいね!」で応援よろしくお願いします!

このエントリーに対するコメント

コメントはまだありません。

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)


トラックバック

we use!!Ruby on RailsAmazon Web Services

このページの先頭へ