Node.js
AWS
lambda
Slack
serverless

Container Linuxのセキュリティ修正をSlackに通知するCronJobをAWS Lambdaに移行した


はじめに

Container Linuxのセキュリティ修正をSlackに通知する - FeedlyもSlackもJSON購読できないから自作してみた -で紹介したKubernetes CronJobをAWS Lambdaに移行しました。その際にやったことを紹介します。


Serverless Framework 導入

本件でLambdaを使うのと、別件でAPI Gateway + Lambdaを使うときに色々調べているとServerless Frameworkが便利そうだと思いました。実際に導入してみると、手軽にデプロイできるので気に入りました。


Cronによる定期実行

Container Linuxのセキュリティリリースの有無を毎朝確認したく、KubernetesのCronJobを利用していました。Lambdaでも定期実行できるのは知っていましたが、このあたりの設定もServerless Frameworkでできました。具体的にはLambdaでサポートしているRate または Cron を使用したスケジュール式scheduleに指定します。


serverless.yml

functions:

run:
handler: handler.run
events:
- schedule: cron(15 1 * * ? *)


CloudWatchへのログ出力

Lambdaに移行するにあたり、正常に動作しているか確認するためログを出力する箇所を増やしました。しかし、CloudWatchには開始と終了のログぐらいしかなく、Lambda実行時のロールに適切な権限が付与されていないようでした。こちらもServerless Frameworkで設定しました。


serverless.yml

provider:

name: aws
runtime: nodejs8.10
iamRoleStatements:
- Effect: "Allow"
Action:
- "logs:CreateLogGroup"
- "logs:CreateLogStream"
- "logs:PutLogEvents"
Resource: "arn:aws:logs:*:*:*"

その結果、ロールに紐づくポリシーは以下のようになりました。

{

"Version": "2012-10-17",
"Statement": [
{
"Action": [
"logs:CreateLogStream"
],
"Resource": [
"arn:aws:logs:ap-northeast-1:313471890455:log-group:/aws/lambda/container-linux-release-feed-dev-run:*"
],
"Effect": "Allow"
},
{
"Action": [
"logs:PutLogEvents"
],
"Resource": [
"arn:aws:logs:ap-northeast-1:313471890455:log-group:/aws/lambda/container-linux-release-feed-dev-run:*:*"
],
"Effect": "Allow"
},
{
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*",
"Effect": "Allow"
}
]
}

もともとlogs:CreateLogStreamlogs:PutLogEventsは付与されているようですので、単にconsole.logまで到達していなかっただけかもしれません(?)


async/await 導入

単純に元のスクリプトをhandlerに突っ込み、サンプルの通りにreturn { message: 'executed successfully!', event };としたところ正常終了したと表示されるもののSlackに通知がきません、ログもまったく出ませんでした。Node.jsの非同期処理が走っている間にreturnしてしまったようです。

リリースフィードを取得するところ、Slackに通知するところで非同期処理を行っていたため、その部分をasync/awaitに対応しました。また、前者ではaxiosを導入してソースコードの見通しをよくしました。

async/awaitはまともに使ったことがなかったので試行錯誤してログが出た、出ないを繰り返しました。その結果、なんとか意図通りに動作するようにできました。


handler.js

'use strict';

module.exports.run = async (event, context) => {
const { IncomingWebhook } = require('@slack/client');
const axios = require('axios');
const feed = require('./src/feed');
const rd = require('./src/release-date');
const rn = require('./src/release-note');

const channel = process.env.CHANNEL || 'stable';
if (! feed.isValidChannel(channel)) {
throw new Error('Invalid release channel');
}

console.info(`start fetching ${channel} channel feed.`);

const url = `https://coreos.com/releases/releases-${channel}.json`;
const getFeed = async url => {
try {
const response = await axios.get(url);
return response.data;
} catch (error) {
throw new Error(error);
}
};

const releases = await getFeed(url);
const latest = feed.getLatestVersion(releases);
console.info(`successfully fetched the latest version.\n${JSON.stringify(releases[latest], undefined, 2)}`);

const postMessageToSlack = (version, releaseNotes) => {
const webhook = new IncomingWebhook(process.env.SLACK_WEBHOOK_URL);
const securityFix = rn.replaceLinkFormat(rn.extractSecurityFixes(releaseNotes));
const message = `Container Linux ${version} has security fixes.\n${securityFix}`;

console.info(`start posting notice to slack.\n${message}`);
return webhook.send(message).then((res) => {
console.info(res);
}).catch((error) => {
throw new Error(error);
});
};

if (rd.isIn24Hours(releases[latest]['release_date'])
&& rn.hasSecurityFixes(releases[latest]['release_notes'])) {
await postMessageToSlack(latest, releases[latest]['release_notes']);
} else {
const message = `${channel} channel has no security fixes since ${latest}`;
console.info(message);
return {message, event, context};
}

return { message: 'executed successfully!', event, context };
};



まとめ

コスト面を気にしてKubernetes CronJobをAWS Lambdaに移行しました。使い慣れない技術も多く試行錯誤の連続でしたが、無事に移行することができました。Serverless Frameworkは非常に便利で、LambdaやAPI Gatewayを扱うときはMUSTのツールと言ってもよいでしょう。Node.jsによるfunctionの実装面ではasync/awaitの使いこなしがポイントと感じました。