AWS Lambdaをいい感じに管理する

## はじめに

本記事は、[Supership株式会社 Advent Calendar 2018 - Qiita](https://qiita.com/advent-calendar/2018/supership)の12日目の記事になります。

### AWS Lambdaとは

言わずもがな、AWS Lambdaとはサーバーを管理せずコードを実行できるサービスです。
最近の話題だとRubyサポートが始まりましたね。 [re:Invent](https://aws.amazon.com/jp/blogs/compute/announcing-ruby-support-for-aws-lambda/)
今後、ますます気軽に利用する人が増えるのではないでしょうか。

### 管理方法

手軽に利用する反面、ソースなどの管理が煩雑になってしまうのが問題です。
そこで、現在のプロジェクトでの管理方法(node)をご紹介したいと思います！

といいつやっていることは簡単で

- Gitで管理
- Zipに固める(gulp)
- awsにzipでuploadする

上記三点を実現させています。

### 実践編
今回は例としてRSSを読み込んでSlackに通知するサンプルコードを用いてご説明いたします

### 準備
#### AWScli環境を用意（mac）

準備としてローカルからawsにアクセスできるようcliをインストールします。

1. awscliをbrewでインストール

```bash
$ brew install awscli
```

2. 認証情報を設定

```bash
$ aws configure
AWS Access Key ID [******************]: # アクセスキー
AWS Secret Access Key [*******************]: # アクセスシークレット
Default region name [ap-northeast-1]: # デフォルトのリージョン
Default output format [None]: # awscliコマンドの実行結果の出力形式
```

#### lambda関数を作成しておく
![1.png](https://qiita-image-store.s3.amazonaws.com/0/14450/6dd9182a-2cd2-f20e-88f1-7e0e0f85efa7.png)
![2.png](https://qiita-image-store.s3.amazonaws.com/0/14450/ca1ea2fe-8a05-db22-dcde-6f27f80500fb.png)


#### Gitで管理
こちらは改めて説明するまでもないのですが、普段使い慣れているサービスでok.
今回はgithubを使用します。
ディレクトリは下記のような構成となります。

```bash
.
├── README.md
├── circle.yml
├── encrypted-file.txt
├── gulpfile.js # gulpでzip化するのでそのファイル
├── package.json
├── plaintext-file.txt
├── src # Lambdaソースコード
│   └── advent-clendar-notifier
│       └── index.js
└── test-data # Lambdaソースコード内で使用するenvデータを格納
    └── advent-clendar-notifier.js
```

ソースはこんな感じのrssをslackに流すだけのコードを作成

```js
/*
 * RSS Notifier to Slack
 * https://github.com/blueimp/aws-lambda
 *
 * Required environment variables:
 * - webhook:     AWS KMS encrypted Slack WebHook URL.
 *
 * Optional environment variables:
 * - channel:     Slack channel to send the messages to.
 * - username:    Bot username used for the slack messages.
 * - icon_emoji:  Bot icon emoji used for the slack messages.
 * - icon_url:    Bot icon url used for the slack messages.
 *
 * Copyright 2018, Hiroki Hatsushika
 *
 * Licensed under the MIT license:
 * https://opensource.org/licenses/MIT
 */

'use strict';

const ENV = process.env;
if (!ENV.webhook) throw new Error('Missing environment variable: webhook');

let webhook;

const AWS = require('aws-sdk');
const url = require('url');
const https = require('https');
let Parser = require('rss-parser');
let parser = new Parser();

function handleResponse (response, callback) {
  const statusCode = response.statusCode;
  console.log('Status code:', statusCode);
  let responseBody = '';
  response
    .on('data', chunk => { responseBody += chunk; })
    .on('end', chunk => {
      console.log('Response:', responseBody);
      if (statusCode >= 200 && statusCode < 300) {
        callback(null, 'Request completed successfully.');
      } else {
        callback(new Error(`Request failed with status code ${statusCode}.`));
      }
    });
}

function post (requestURL, data, callback) {
  const body = JSON.stringify(data);
  const options = url.parse(requestURL);
  options.method = 'POST';
  options.headers = {
    'Content-Type': 'application/json',
    'Content-Length': Buffer.byteLength(body)
  };
  console.log('Request options:', JSON.stringify(options));
  console.log('Request body:', body);
  https.request(
    options,
    response => { handleResponse(response, callback); }
  ).on('error', err => { callback(err); }).end(body);
}

function buildSlackMessage (data) {
  return {
    channel: ENV.channel,
    username: ENV.username,
    icon_emoji: ENV.icon_emoji,
    icon_url: ENV.icon_url,
    text: data.link,
    attachments: [
      {
        fallback: data.title,
        title: data.title,
        title_link: data.link,
        text: data.link
      }
    ]
  };
};

function fetchRssContents (url) {
  return new Promise((resolve, reject) => {
    parser.parseURL(url).then(function (feed) {
      if (!feed) {
        reject(new Error('Feed Read Error'));
      }
      const item = feed.items[0];
      resolve({
        title: item.title,
        link: item.link
      });
    });
  });
}

function processEvent (event, context, callback) {
  console.log('Event:', JSON.stringify(event));
  fetchRssContents(ENV.feed).then((rssData) => {
    const postData = buildSlackMessage(rssData);
    post(webhook, postData, callback);
  }).catch((error) => {
    console.log(error);
  });
}

function decryptAndProcess (event, context, callback) {
  const kms = new AWS.KMS();
  const enc = {CiphertextBlob: Buffer.from(ENV.webhook, 'base64')};
  kms.decrypt(enc, (err, data) => {
    if (err) return callback(err);
    webhook = data.Plaintext.toString('ascii');
    processEvent(event, context, callback);
  });
}

exports.handler = (event, context, callback) => {
  if (webhook) {
    processEvent(event, context, callback);
  } else {
    decryptAndProcess(event, context, callback);
  }
};
```

#### Zipに固める

lambdaのソースができたら、次にGulpを使いZipに固める
`npm run dist`に `gulp　dist`を仕込んでおいて実行！
`/dist`フォルダにzipファイルが格納される

実行例

```bash
⋊> aws-lambda-ac-2018 on develop ⨯ npm run dist                                                               21:32:24

> advent-clendar-notifier-lambda@1.0.0 dist ~/Documents/myGithub/aws-lambda-ac-2018
> gulp dist

[21:32:33] Using gulpfile ~/Documents/myGithub/aws-lambda-ac-2018/gulpfile.js
[21:32:33] Starting 'dist'...
[21:32:33] Starting 'copy'...
[21:32:33] Finished 'copy' after 56 ms
[21:32:33] Starting 'install'...
[21:32:33] Finished 'install' after 15 ms
[21:32:33] Starting '<anonymous>'...
[21:32:33] Finished '<anonymous>' after 24 ms
[21:32:33] Starting '<anonymous>'...
[21:32:33] Finished '<anonymous>' after 1.02 ms
[21:32:33] Finished 'dist' after 101 ms
Build tag: d526e370-fd40-11e8-965c-b9f3f9ff9183
```

最後にあらかじめ作っておいたlambda関数にuploadして反映

```bash
$ ⋊> aws-lambda-ac-2018 on master ◦ aws --region ap-northeast-1 lambda update-function-code --function-name advendcalendar-rss-to-slack --zip-file fileb://./dist/advent-clendar-notifier-7c2de9a0-fd43-11e8-8915-a941d229fe31.zip --publish
{
    "FunctionName": "advendcalendar-rss-to-slack",
    "FunctionArn": "arn:aws:lambda:ap-northeast-1:XXXXXXXX:function:advendcalendar-rss-to-slack:2",
    "Runtime": "nodejs6.10",
    "Role": "arn:aws:iam::XXXXXXXX:role/service-role/elastic-beanstalk-events-to-slack",
    "Handler": "index.handler",
    "CodeSize": 1384,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2018-12-10T13:10:46.583+0000",
    "CodeSha256": "XXXXXXXXXXXXXXXX",
    "Version": "2",
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "XXXXXXXX-610f-4a54-a8e7-d8ed1210a026"
}
```

しばらく経つと反映されます
![3.png](https://qiita-image-store.s3.amazonaws.com/0/14450/440900b2-59ac-161c-7b9d-8ab97092ec39.png)


### まとめ

いかがだったでしょうか！？ちょっと長ったらしく書いてしまいましたが、
やってることは簡単なので、仕組み化してlambdaを楽しみましょう！！
他にも、Encryptionの設定であったり色々あるのですが今回はここまで〜

※ 今回のソースは　[こちら](https://github.com/tetuo41/aws-lambda-ac-2018)です。

## 一緒に働きませんか？　働きましょうよ
私の所属するチームでは、人材を募集しています。 
バックエンドからフロントエンドまで幅広い技術を触ってみたい方、
広告配信に興味の方どんどんご応募ください！お待ちしています！

[Supership株式会社 採用サイト](https://rec-log.jp/site/jobVw.aspx?ON2kgQumITWpPVUsEYsvO12xg4uAI7WDa9oGCcQJOf2LgiuOIlWRanoUCqQWOt2Zgwu2IyW5aBo8CEQaOH2dgKugIMWjaPomCSQoOV2rgXuuL0WxJ33zb6pC)