GitHubとAWSを利用して自動メルマガ送信システムを作ってみた

# はじめに

以前からメルマガを隔週で書いて送信していたのですが、これがまた運用フローがとてもめんどくさい。

プログラマー三大美徳である怠慢を思い出し、自動メルマガ送信システムを作ってみました。

いろいろ学びがあったのでQiitaにまとめていきたいと思います。

# 旧運用フロー

そもそもどういう流れでメルマガを書き、送信をしていたかと言うと

1. 送信者はメールクライアント（OutLookやGmailやThunderbirdとか）でHTMLメールを記述し、メルマガメンバーに向けてレビュー依頼メールを送信
1. メルマガメンバーは送信されたメールを確認し指摘点を返信やチャットなどで伝える
1. 送信者は指摘点を修正し再度メルマガメンバーに向けてレビュー依頼メールを送信
1. 修正、送信とレビューを繰り返す
1. レビューが完了したらメルマガ用のメーリングリストに向けてメルマガを送信

でした。

この運用フローの何が辛い、問題かというと下記の点です。

- メールクライアントのHTMLメールの記述が大変
    - 謎の空白やフォントが変わったりなどなど・・・
- レビュー指摘点を修正、再度送信がめんどくさい
- メルマガ用のメーリングリストへのメルマガ送信が手動のため事故が起きる可能性がある

これらの解決を目指し、自動メルマガ送信システムを作りました。

# 自動メルマガ送信システムの運用フロー

1. GitHubリポジトリでmasterからブランチを切る
2. 作成したブランチでマークダウンでメルマガを書き、pushする
3. masterに向けてプルリクエストを作り、レビューしてもらう
4. 指摘点はプルリクエスト上で行い、修正しpushする
5. 最後に承認をもらったらmasterにマージ

以上！

いつもの開発の流れでメルマガが送信されます！

上記フローで何を解決したかというと

### メールクライアントのHTMLメールの記述が大変

これは、マークダウンでメルマガを記述することになったため簡単になりました。

### レビュー指摘点を修正、再度送信がめんどくさい

レビューは全てプルリクエスト上で行うため、簡単になりました。

### メルマガ用のメーリングリストへのメルマガ送信が手動のため事故が起きる可能性がある

自動でメルマガが送信されるため、事故は起きなくなりました。

※システムがバグっていたら事故はもちろん起きる

# 自動メルマガ送信システムの概要

自動メルマガ送信システムは下記のような構成になっています。

![システム構成図ver2.png](https://qiita-image-store.s3.amazonaws.com/0/81308/237b0b67-0b97-edba-7dc0-71afa1cd9f67.png)

GitHubからWeb hookを利用しSNSで通知を受け取り、LambdaがSESを利用しメルマガを送信します。

# 利用技術

- マークダウン
- GitHub Private Repository
- Web hook
- AWS Simple Notification Service (SNS)
- AWS Lambda
- AWS Simple Email Service (SES)



## マークダウン

今までメルマガはメールクライアントでHTMLメールとして作成をしていましたが、作成はとても大変でボトルネックになってました。

そこでエンジニアに馴染みがありコンパイルするとHTMLになるマークダウンに着目し、メルマガの原稿をマークダウンで記述すことにしました。


## GitHub Private

[公式サイト](https://github.com/)

ソースコードをGitで管理するサービスです。

メルマガの原稿はGitHubのリポジトリで管理しています。

## Web hook

Web hookはアプリケーションの更新情報を他のアプリケーションへリアルタイム提供する仕組みです。

例えばサーバーでエラーが発生したらメールが送信されたり、pushしたらSlackに通知が来たりなどなど。

GitHubにもWeb hookの仕組みがあります。

[公式サイト](https://developer.github.com/webhooks/)

GitHub上のいろいろなイベント（push、プルリクエスト作成など）に対して、通知を受け取ることができます。

AWPでは、pushに対してAWSのSNSというサービスを用いて通知を受け取っています。

※本当はマージに対してWeb hookをしたかったのですが見つからなかったのでpushを利用しています

## AWS Simple Notification Service (SNS)

SNSはAWSの通知を受け取るサービスです。

[公式サイト](https://aws.amazon.com/jp/sns/)

GitHubのWeb hookを受け取り、AWSのLambdaサービスを実行します。

SNSは他にも通知を受け取ったら、通知内容をメールで送信したり他のAWSのサービスを実行したりなどできます。

## AWS Lambda

LambdaはAWSのコード実行サービスです。

[公式サイト](https://aws.amazon.com/jp/lambda/)

利用できる言語はJava,JS,Python,C#です。

今回はJSを採用しました。

一部抜粋をして、ソースコードの解説をしていきます。

### index.js

```js
const getFile = require('./src/getFile');
const convert = require('./src/convert');
const send = require('./src/send');
const check = require('./src/check');

exports.handler = (event, context) => {
  const filePath = check(event, context);
  if (filePath !== '') {
    getFile(filePath).then(convert).then(send);
  }
};

```

これが、Lambdaで実行されるルートファイルです。

check.js、getFile.js、convert.js、send.jsを読み込み実行しています。

### check.js

Web hookがpushしか受け取れなかったため、リリースブランチへのpushでもWeb hookが発生します。

そのため、check.jsでは通知がmasterへのマージかどうかをチェックしています。


```js
module.exports = (event, context) => {
  const msg = JSON.parse(event.Records[0].Sns.Message);
  const branchName = msg.ref;
  let filePath = '';

  if (branchName === 'refs/heads/master' && msg.head_commit.added.length > 0) {
    filePath = msg.head_commit.added;
  } else {
    context.done();
  }
  return filePath;
};
```

eventオブジェクトにWeb hookで通知された情報が格納されています。

eventオブジェクトからブランチ名を取得し、masterブランチかどうかチェックをしています。

### getFile.js

getFile.jsでは、GitHubからメルマガのリポジトリをcloneし、マークダウンファイルを取得します。

```js
require('lambda-git')();

const id = 'hoge';
const pass = 'fuga';
const repository = 'piyo';
const repositoryUrl = `https://${id}:${pass}@${repository}`;
const localDir = '/tmp/';

const fs = require('fs');
const simpleGit = require('simple-git');

module.exports = (filePath) => {
  const date = new Date();
  const cloneDir = `${localDir}${date.getTime()}/`;

  return new Promise((resolve) => {
    simpleGit().clone(repositoryUrl, cloneDir, '', () => {
      fs.readFile(cloneDir + filePath, 'utf-8', (err, data) => {
        resolve(data);
      });
    });
  });
};
```

lambda-gitとsimple-gitというライブラリを利用してcloneをします。

clone後、fsというライブラリを利用してマークダウンファイルを読み込みます。

### convert.js

convert.jsでは、マークダウンのテキストを受け取り、HTMLに変換します。

```js
const marked = require('marked');

module.exports = (text) => {
  return {
    text: marked(text),
  };
};
```

markedというライブラリを利用して、マークダウンをHTMLに変換します。

### send.js

send.jsでは、メルマガの原稿（HTML）を受け取り、メールを送信します。

```js
const aws = require('aws-sdk');

const to = ['hoge@hoge.com'];
const from = 'fuga@fuga.com';
const ses = new aws.SES({
  apiVersion: '2010-12-01',
  region: 'us-west-2',
});


module.exports = (sendData) => {
  const eParams = {
    Destination: {
      ToAddresses: to,
    },
    Message: {
      Body: {
        Text: {
          Data: sendData.text,
          Charset: 'utf-8',
        },
        Html: {
          Data: sendData.text,
          Charset: 'utf-8',
        },
      },
      Subject: {
        Data: sendData.title,
        Charset: 'utf-8',
      },
    },
    Source: from,
  };

  ses.sendEmail(eParams, (err, data) => {
    if (err) {
      console.log(err);
    } else {
      console.log(data);
    }
  });
};
```

aws-sdkというライブラリを読み込み、SESというAWSのメール配信サービスを利用してメールを送信します。

## AWS Simple Email Service (SES)

SESはAWSのメール配信サービスです。

[公式サイト](https://aws.amazon.com/jp/ses/)

メルマガはSESを利用して配信されます。


# 終わりに

以上が自動メルマガ送信システムの全容です。


運用フローのボトルネックを見つけ、それをシステムで解決するというシステムエンジニアっぽいことをするのは楽しかったです(｀･ω･´)ゞ