CloudWatchの料金が高い

同僚に「CloudWatchの料金高くね?」と指摘される
Slack料金通知画面
image.png

なんか高い!
Billingを確認。
image.png

どうやら詳細モニタリングを1分間にしていたからっぽい

EC2画面でとりあえず全て無効化する。5分間隔だと無料なんですね。
http://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/using-cloudwatch-new.html

image.png

あとは見守るのみ

続きを読む

DockerでRocket.chatを構築し、hubotも連携させる

備忘録のために載せておきます。
※Dockerやhubotは他に詳しい記事があるので触れません。

Rocket.chatって?

Slackライクなチャットツール。
https://rocket.chat/

一年ほど前、コミュニケーションツールとしてチャットをプロジェクトで導入する際に
サーバインストール型のチャットツールを探してこれに行きつきました。
周りで誰も使ったことがありませんでしたが、とりあえずお試しで入れたのが経緯。

なんだかんだで1年半ほどプロジェクト内で運用しています。

Dockerレシピ

rocketchat:
  image: rocketchat/rocket.chat:latest
  environment:
    - MONGO_URL=mongodb://mongodb/rocketchat
    - ROOT_URL=http://localhost:80
  links:
    - mongodb
  ports:
    - 80:3000

hubot:
  image: rocketchat/hubot-rocketchat
  environment:
    - PORT=5080
    - ROCKETCHAT_URL=[任意のドメイン]:80
    - ROCKETCHAT_ROOM=
    - LISTEN_ON_ALL_PUBLIC=true
    - ROCKETCHAT_USER=[hubot用ユーザID]
    - ROCKETCHAT_PASSWORD=[hubot用パスワード]
    - BOT_NAME=[任意のhubot名]
    - EXTERNAL_SCRIPTS=hubot-help,hubot-seen,hubot-links,hubot-diagnostics,hubot-reddit,hubot-bofh,hubot-bookmark,hubot-shipit,hubot-maps,hubot-cron,hubot-jenkins-notifier
    - HUBOT_JENKINS_URL=[連携するjenkinsのURL]
    - HUBOT_JENKINS_AUTH=[jenkinsのアカウント:パスワード]
  volumes:
    - /usr/local/share/hubot/scripts:/home/hubot/scripts
    - /etc/localtime:/etc/localtime:ro
  links:
    - rocketchat:rocketchat
  ports:
    - 3001:5080

mongodb:
   image: mongo
   ports:
     - 27017
   volumes:
     - /srv/docker/mongodb/db:/data/db

一部解説

下記はJenkinsのジョブをhubot内から実行するための設定です。

    - EXTERNAL_SCRIPTS=hubot-help,hubot-seen,hubot-links,hubot-diagnostics,hubot-reddit,hubot-bofh,hubot-bookmark,hubot-shipit,hubot-maps,hubot-cron,hubot-jenkins-notifier
    - HUBOT_JENKINS_URL=[連携するjenkinsのURL]
    - HUBOT_JENKINS_AUTH=[jenkinsのアカウント:パスワード]

下記はRocket.chatのログはmongoDB内に格納されるため
コンテナを消してもログを残すためにマウントの設定を入れています。
ホスト側のパスはもちろん任意です。

   volumes:
     - /srv/docker/mongodb/db:/data/db

運用する中で起きた問題

ID管理

開発環境には他サービスも並行して動いており
導入したチャットでも新たにID管理するのは面倒だから何とかならない?
といったことがありました。

対処として、Rocket.Chatには特定サービスとのoAuth認証機能が備わっていましたので
今回のケースではバージョン管理として既に使用していたGitlabに集約することに。

キャプチャ.PNG

hubotコンテナが落ちる

hubotには導入したプラグイン次第で様々なコマンドを使用させることができます。
メンバーの利用状況を見ていると、通知機能(タスク登録)がよくつかわれていたようです。
Cron形式で登録することができ、お昼や定例などのアラーム代わりに使っているのが見受けられました。

ある日突然、hubotが反応しなくなりdocker ps -a で状態を見ると、hubotコンテナが落ちていました。
docker logs にてログを出力したところ、チャットの全ルームの過去ログが流れ出しました。

原因はおそらく、割り当てたメモリ枯渇ではないかなと推測しています。
ちなみにDockerはAWSのEC2上で起動しております。

対処として、docker rm [コンテナ名] でコンテナ削除を行い、docker-compuse up -d
で起動しました。
hubotが持っているログは不要なのでばっさり切り捨て。

ただこの方法だと、上記にありました通知機能で登録したタスクがすべてなくなってしまいますので
コアな運用に用いている場合はhubotコンテナもホストにマウントするなど、一工夫必要かなと思います。

続きを読む

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

はじめに

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

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

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

旧運用フロー

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

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

でした。

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

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

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

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

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

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

以上!

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

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

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

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

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

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

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

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

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

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

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

システム構成図ver2.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

公式サイト

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

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

Web hook

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

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

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

公式サイト

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

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

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

AWS Simple Notification Service (SNS)

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

公式サイト

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

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

AWS Lambda

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

公式サイト

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

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

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

index.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へのマージかどうかをチェックしています。

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し、マークダウンファイルを取得します。

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に変換します。

const marked = require('marked');

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

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

send.js

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

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のメール配信サービスです。

公式サイト

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

終わりに

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

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

続きを読む

AWSの前日コストをSlackに通知してみた。概算でなくて正確な値で。

概要

弊社ではSmoozというブラウザアプリを作っていて、一部機能をEC2のスポットインスタンスで運用しています。
ですが、ユーザー数の変動などでインスタンス数がかなり上下し、コスト変動が激しいです(うれしい悲鳴?)。
結構お金がかかるので気にはなるが、毎日コストエクスプローラーで確認するのが面倒。
どうにかしてみました。

問題点

ぐぐってみると、

同じニーズはたくさんあるようで、Lambdaを使ってCloudWatchのBillingを取得してSlackに通知というのが王道のようです。

ということで真似てやってみたのですが、、、

あれ、実際の額と結構違わない?

ClougWatchのBillingはあくまでも概算なので、正確な値と異なります。
これだとあんま役に立たないよ、ということでコストエクスプローラーからスクレイピングでコストを取得するようにしました。

ざっくり説明

Seleniumでコストエクスプローラーにアクセスして、
前日のコストを取得してそれをSlackに流すようなスクリプトをEC2上で毎日17時に動かしています。

EC2上でHeadless Chromeを動かす

Xvfbを使っても良かったのですが、せっかくHeadless Chromeが出たということで
試してみました。環境構築に関する記事はネット上にたくさんあるのですが、↓が一番サクッと導入できました。

headless chromeをPythonのseleniumから動かして引数を考えた (Ubuntu 16.04)

ちなみにAmazon Linx上でHeadless Chromeを動かす記事もいくつかあったので試したのですが、
うまくいかずUbuntuで動かしてます。
コスト的にAmazon Linux上で動かしたい、、、誰かサクッとできた人いれば教えてください。

Pythonでのスクレイピング部分

サービスとそこまで関係ないので適当に書いてます笑
スクレイピングに慣れ親しんだ方なら瞬殺かと。

#!/usr/bin/python3

import os
import datetime
import slackweb
from time import sleep
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

# headless chrome
CHROME_BIN = "/usr/bin/chromium-browser"
CHROME_DRIVER = os.path.expanduser('/usr/bin/chromedriver')

options = Options()
options.binary_location = CHROME_BIN
options.add_argument('--headless')
options.add_argument('--window-size=1280,3000')

driver = webdriver.Chrome(CHROME_DRIVER, chrome_options=options)

# ログイン
driver.get("https://{ACCOUNT_ID}.signin.aws.amazon.com/console")
driver.find_element_by_id('username').send_keys({USENAME})
driver.find_element_by_id('password').send_keys({PASSWORD})
driver.find_element_by_id('signin_button').click()
sleep(3)

driver.get("https://console.aws.amazon.com/cost-reports/home?region=ap-northeast-1#/savedReports")
sleep(3)

# デイリーレポートを選択
driver.find_element_by_partial_link_text('Daily').click()
sleep(3)

# slackにpostするときに使う
current_url = driver.current_url

# 期間を昨日から昨日、つまり昨日だけにする
driver.find_element_by_xpath('//*[@class="picker-dropdown"]').click()

# 前日の日付
yesterday = datetime.date.today() -datetime.timedelta(1)
yesterday_str = yesterday.strftime('%m/%d/%Y')

# from
elem = driver.find_element_by_xpath('//label[text()="From"]/following::input')
elem.clear()
elem.send_keys(yesterday_str)
sleep(1)

# to
elem = driver.find_element_by_xpath('//label[text()="To"]/following::input')
elem.clear()
elem.send_keys(yesterday_str)
sleep(1)

# 適用
elem = driver.find_element_by_xpath('//div[text()="Apply"]').click()
sleep(1)

# コスト取得
cost = driver.find_element_by_xpath('//*[@id="tr-total"]/td[2]').text

# slackに投稿
slack = slackweb.Slack(url={Webhook_URL})
yesterday_str2 = yesterday.strftime('%Y/%m/%d')
url = 'https://console.aws.amazon.com/cost-reports/home?region=ap-northeast-1'
text = u"{yesterday_str2}のコストは${cost}です。/ {current_url}".format(**locals())

slack.notify(text=text)
driver.quit()

Slackにはこんな感じでPostされます

image.png

(おまけ)コストエクスプローラーはいつ更新されるのか?

コストエクスプローラーに馴染みががある人はわかると思いますが、
一体いつ更新されるのか謎ですよね。

AWSのサポートに問い合わせたところ、
24時間ごとに少なくとも一度はデータを更新する。
ただし、正確な更新時間は非公開とのこと。

それじゃあ困る、、、
とうことでcronを1時間おきに設定して、更新時間を追ってみました。
朝と夕方、翌日に更新されてるみたい。

コストエクスプローラーの日付はUTCでして、
JST 9:00をすぎて昨日のコストを見に行くと、実際に請求される額の半分程度。
その日の16時ごろにまた更新されて実際に請求される額の9割程度。

そしてまた翌日にも更新されて実際の額になっているようです。

続きを読む

フロントエンド環境+CircleCI+AWS環境構築メモ

0.やること

  • WebpackベースでReact&Redux環境を構築する
  • GitHubにプッシュしたらCircleCIが自動テストを行い、S3に用意した検証環境にデプロイ
  • テスト結果をSlackに通知する

準備物

  • AWSのIAMユーザー情報
  • S3バケット
  • GitHubアカウント(BitBacketでもOK)

1.React&Redux&Webpack

ベースとなる環境はreact-redux-starter-kitを利用しました。公式が提供しているcreate-react-appと迷いましたが、Reduxとreact-routerが設定済みということもありreact-redux-starter-kitを選択しました。

react-redux-starter-kitはreact-routerのバージョンが3系なので、開発の様子を見てアップデートしていく必要がありそうです。

Node.jsは推奨版が6系でしたが、8系でも特に問題なく動作したのとNode.jsはあっという間ににバージョンアップされるのでNodeは最新版を選択しました。

$ node -v
v8.4.0
$ npm -v
5.4.2

react-redux-starter-kitでプロジェクト作成
$ git clone https://github.com/davezuko/react-redux-starter-kit.git <PROJECT_NAME>
$ cd <PROJECT_NAME>
$ npm start

http://localhost:3000/ にアクセスしてアヒルの画像が表示されたら環境構築成功です。

webpack-dev-serverが起動しているので、ソースの変更、保存を検知して自動でリロードしてくれます。ディレクトリの構成はAtomicDesignで構成したいので実際の開発ではかなり触ります。

[参考資料]

React + ReduxのプロジェクトにAtomic DesignとImmutable.jsを使ったらいい感じになった話

アメブロ2016 ~ React/ReduxでつくるIsomorphic web app ~

2.GitHubへのプッシュを検知してCircleCIが自動テストと検証環境へのデプロイを行う

CrcleCIとGitHubを接続

CircleCIとGitHubを連携します。CircleCIの公式から「Start Building Free」→「Start With GitHub」の順に進みます。GitHubのアカウントでログインした後「Project」ページの「Add Project」をクリックします。GitHubでログインしている場合はのリポジトリをが自動的に読み込んでくれるので、利用するリポジトリを選択します。Setup Projectの設定はそのままで「StartBuilding」をクリックして完了です。

CircleCIとAWSを接続

ソースコードをAWSのS3にデプロイするので、CircleCIとAWSを連携させます。「Project」→「Setting」→「AWS Permissions」に進んでAccessKeyIdとSecretAccessKeyを入力して完了です。

circle.ymlを作成

CircleCIで行いたいことはcircle.ymlに書きます。今回はESLintの構文チェック結果とKarmaのテスト結果をTestSummaryにレポートを表示させます。

circle.yml
machine:
  node:
    version: 8.4.0
  post:
    - npm install -g npm@5
  timezone: Asia/Tokyo

dependencies:
  pre:
    - sudo pip install awscli

test:
  override:
    - ./node_modules/.bin/eslint --format=junit -o $CIRCLE_TEST_REPORTS/eslint/report.xml .
    - cross-env NODE_ENV=test karma start build/karma.config --reporters junit

deployment:
  branch: master
  commands:
    - npm run build
    - cp -Rf dist/. ./
    - aws s3 cp dist s3://<S3_BACKET_NAME>/ --recursive --acl public-read

circle.ymlの記述方法

machine:

仮想マシンの設定を記述します。デフォルトでもNodeをインストールしてくれますが、バージョンが0系だったのでフロントの環境に合わせて8系をインストールするように設定しました。postは指定したコマンドの後に実行されます。この例ではNodeをインストールした後にnpmのバージョンを上げています。逆に指定したコマンドの前に行いたい処理はpreに記述します。

dependencies:

プロジェクト固有の依存関係をインストールします。今回はaws-cliを利用するのでここでインストールしました。

test:

ここで実際のテストが走ります。package.jsonのtestを自動的に実行してくれるのですが、今回はjunitのxmlを出力してCircleCI上でレポートが見たいのでoverrideを記述してpackage.jsonのtestを上書きました。

deployment:

Webサーバにコードを展開します。今回はシングルページアプリケーションで、ビルドされたソースは静的なので、AWSのS3を選択しました。ここでリポジトリ上のソースをそのまま展開できればいいのですが、webpackでReactをビルドする必要があるので、package.jsonにか書かれているnpm run buildを走らせます。その後生成されたdistフォルダをコンテナのルートに移動させてからaws-cliのコマンドでS3にアップロードしています。branch: masterはmasterブランチにプッシュされた場合のみ実行する記述です。

[参考資料]
Configuring CircleCI

AWS S3にデプロイ

作成したバケットにStatic website hostingを有効します。
※検証環境で利用するのにBasic認証が必要でした。S3にBasic認証をかけるのには一手間必要なので今後CodeDeployを利用してEC2にデプロイしたいと思っています。

Slackと接続

通知したいSlackのWorkspaceのURLを用意します。「Project」→「Setting」から「ChatNotification」に進み、SlackのURLを入力して「Save」します。

これで準備はOKです。

3.動作チェック

適当にソースを変更してmasterブランチにプッシュします。今はmaterに直接プッシュしていますが、開発ではタスクごとのブランチにプッシュしてテストが通ったらmasterやreleaseブランチにプルリクエストを飛ばしてmaterブランチにマージ、プッシュされたらS3に展開という運用を考えています。今回はテストが成功しようが失敗しようが問答無用にS3に展開されます。

circle.ymlに書かれたプロセスが順に実行されています

task1.png

4.結果

TestSummaryに出力

task3.png

Reactがビルドされてサーバに展開

スクリーンショット 2017-09-29 22.15.29.png

Slackへの通知

スクリーンショット 2017-09-29 22.16.36.png

感想

  • CIツールそのものの環境構築を意識せず導入できるので取っ付き易い
  • 基本的な設定はpackage.jsonのscriptとcircle.ymlの記述だけで済むので設定が簡単
  • 黒画面でできる事は大体実現できる

CircleCIは出来ることが多いので、この環境をベースに試行錯誤しながらベストプラクティスを見つけたい思います:v_tone2:

続きを読む

HTTPSの静的コンテンツをホストするならs3よりNetlifyが俺の求めていたものだった

事の発端

社内から静的コンテンツをホストしてHTTPS使える環境が欲しいんだよねー。と要望を受けた。
私はAWSが得意なので考えた。

「静的コンテンツならs3か」
「s3でHTTPSならs3の前段にCloudFrontでSSL証明書入れるか(ちょっと面倒だな)」
「CloudFrontにSSL入れるなら無料のACMだな」
「ACM使うならメール認証だしSES要るな(面倒くせぇ)」
「SESでメール受信するならs3・・・・・・・・・・」

ガシャーン!(ちゃぶ台の音)

「俺がしたいのはこれじゃない感」

ということで

何か上の環境を一発で作れるツールあるかなぁ。と調べているとNetlifyというサービスが引っかかった。

Netlifyってなに?

どうも静的コンテンツをホストしてくれて、SSLが使えるとな。Githubと連携して?
ふむふむ。使ってみよう。

ということでGithubにリポジトリを作って5分位でデプロイ出来る環境が出来ました。
SSLとDNSも5分位で設定完了。

「俺が求めていたものはこれだった」

Netlifyの紹介(検索したら色々出てくるけど)

URL

https://www.netlify.com/

ほどんどのことが無料

  • 商用利用可
  • カスタムドメインのHTTP
  • CIによるデプロイ
  • フォーム処理
  • A/Bテスト
  • コミュニティサポート
  • etc…

有料版で出来ること

  • チームや複数レベルの管理機能
  • メールとチャットのサポート
  • SOC2準拠
  • などその他色々

デプロイ

  • Git系リポジトリと簡単に連動しpushするだけでデプロイ可能

DNS

  • 任意のDNSでも、NetlifyのDNSでも可

SSL証明書

  • 無料(Let’s Encrypt)
  • カスタムドメインも1ボタンで発行

パフォーマンス

  • 自動でCDN付き
  • スケーラブルらしい(静的コンテンツだけだもんね)

ABテスト(BETA)

  • ワンクリックでGitのブランチ毎にABテストが出来る

フォーム

  • フォームはHTMLで作れる
  • データベース不要
  • メール、Slack、Webhookへ通知可能

セキュリティ

  • 有料版ではSOC2取ってるくらいだから強そう
  • 基本静的コンテンツだけだからよっぽどじゃないと問題にならない?

ちょっとした機能

  • デプロイ履歴とログが残る

結論:めっちゃ簡単だったので使ってみてください

https://www.netlify.com/

続きを読む

サーバ死活監視→メール送付をLambda、AmazonSNSで実現する

AWSを使ってのサーバの死活監視ってよくやると思うんですが、大体問題があった時の通知の飛ばし先はSlackだったりします。(無料だし)

とはいえ仕事で使おうとすると、Slackは見れない状況もあるかもしれないと思い、メールで通知を飛ばす、という観点でAWSを利用し実現してみました。

最終的に利用できるようにしたものは以下に登録してあります。

https://github.com/kojiisd/aws-server-monitor

サーバ監視からのメール送付までの仕組み

公開されているサービスに対してREST通信を行い、そのステータスコードを見るだけのシンプルな死活監視を行います。

ただし何も考えずに実装すると、エラーが起きてから回復するまで、定期実行のたびに何度もエラーメールを投げてしまう仕組みとなるため、管理する側としてはうっとおしくもあり(何回もメールが来る)コスト的にも嬉しくありません。またエラー発生時だけメールを飛ばす仕組みとすると、復旧したかどうかがわからず、管理者はヒヤヒヤすることになります。

そこで、現在のステータスをDynamoDBで管理するようにし、状態が変更されたタイミングでメールを送る(異常発生/復旧)という仕組みにしてみました。

監視対象のサーバ情報はS3から取得してくるようにします。

まずはサーバ死活監視とメール送付部分の作成

状態の監視は後ほど加えるとして、まずはメインとなるサーバ死活監視とメール送付のLambda実装です。Python3.6で実装しています。

サーバ死活監視処理

S3から監視対象のデータを持ってくるところと、サーバ監視を実施してエラーが発生したサーバの一覧を作成する処理です。Lambda実行の際の権限の割り振り忘れに注意。

def get_target_servers():
    s3 = boto3.resource('s3')
    obj = s3.Object(BUCKET_NAME, OBJECT_NAME)
    response = obj.get()
    body = response['Body'].read()
    return body.decode('utf-8')

def check_target_servers(target_json):
    data = json.loads(target_json)
    servers = data['servers']

    error_servers = []

    for server in servers:
        name = server['name']
        url = server['url']
        try:
            res = requests.get(url)
            if res.status_code != 200:
                error_servers.append(server)
        except Exception:
            error_servers.append(server)

    if len(error_servers) == 0:
        print("Successful finished servers checking")
    else:
        response = send_error(name, url, error_servers)
        print("Error occured:")
        print(response)
        print(error_servers)

S3上には以下のようなjsonデータを配置するようにします。

{
    "servers": [
      { "name": "googlea", "url": "http://www.google.coma" },
      { "name": "googleb", "url": "http://www.google.comb" },
      { "name": "google", "url": "http://www.google.com" }
    ]
}

上記を監視対象とすると、最後以外がアクセス失敗するので、エラーメールが飛ぶことになります。
以下の値は環境変数として定義しており、Lambda実行前に定義が必要です。

変数名 内容
S3_BUCKET_NAME S3の対象バケット名
S3_OBJECT_NAME S3の対象オブジェクト名
SNS_TOPICS_NAME SNSの送付対象トピック名
DDB_TABLE_NAME DynamoDBのテーブル名

メール送付処理

とりあえずこんな感じで書けばSNSは呼び出せるので、これをカスタマイズします。

import json
import boto3

sns = boto3.client('sns')

def lambda_handler(event, context):
    sns_message = "Test email"

    topic = 'arn:aws:sns:us-east-1:<ACCOUNT_ID>:<TOPICS_NAME>'
    subject = 'Test-email'
    response = sns.publish(
        TopicArn=topic,
        Message=sns_message,
        Subject=subject
    )
    return 'Success'

カスタマイズをしてこんな感じでメソッドにして組み込みました。

def send_error(name, url, error_servers):
    sns = boto3.client('sns')
    sns_message = "Error happens:nn" + json.dumps(error_servers, indent=4, separators=(',', ': '))

    subject = '[ServerMonitor] Error happens'
    response = sns.publish(
        TopicArn=SNS_TOPICS_NAME,
        Message=sns_message,
        Subject=subject
    )

    return response

死活監視でエラーが発生すると以下のようなメールを受け取れます。内容は質素ですが、とりあえずこれで良しとします。

Error happens:

[
    {
        "name": "googlea",
        "url": "http://www.google.coma"
    },
    {
        "name": "googleb",
        "url": "http://www.google.comb"
    }
]

DynamoDBを使った状態管理

さて、このままでも監視はできますが、エラー発生と復旧がわかった方が良いので、DynamoDBを使って状態管理をし、変化があった場合にメールを送付するように改修します。

「url」をプライマリキー、「name」をソートキー(レンジキー)として「server-monitor」というテーブルを作成します。
取得と実装はこんな感じになります。これをカスタマイズして今回の処理に適用します。

import boto3
import json
from boto3.dynamodb.conditions import Key, Attr


dynamodb = boto3.resource('dynamodb')
table    = dynamodb.Table('server-monitor')

def lambda_handler(event, context):
    #add_server()
    #get_server()

    return 'Finish operation'

def get_server():

    items = table.get_item(
            Key={
                 "url": "http://www.google.com",
                 "name": "google"
            }
        )

    print(items['Item'])

def add_server():
    table.put_item(
            Item={
                 "url": "http://www.google.com",
                 "name": "google",
                 "status": True
            }
        )    

カスタマイズして組み込んだ結果はこちらになります。

def check_status(url, name):
    status_ok = True
    try:
        items = dynamodb.Table(DDB_TABLE_NAME).get_item(
                Key={
                    "url": url,
                    "name": name
                }
            )
        status_ok = items['Item']['status']
    except:
        status_ok = None
    return status_ok

def add_server(url, name, status):
    dynamodb.Table(DDB_TABLE_NAME).put_item(
        Item={
                "url": url,
                "name": name,
                "status": status
        }
    )

死活監視後のエラー判定部分も、DynamoDBから現在の状況を確認してメール通知をする処理の追加が必要になります。こんな感じです。

def check_target_servers(target_json):
    data = json.loads(target_json)
    servers = data['servers']

    status_changed_servers = []

    for server in servers:
        name = server['name']
        url = server['url']
        status_ok = check_status(url, name)
        try:
            res = requests.get(url)
            if res.status_code != 200:
                if status_ok != False:
                    server['status'] = "Error"
                    status_changed_servers.append(server)
                add_server(url, name, False)
            else:
                if status_ok == False:
                    server['status'] = "Recover"
                    status_changed_servers.append(server)
                add_server(url, name, True)
        except Exception:
            if status_ok != False:
                server['status'] = "Error"
                status_changed_servers.append(server)
            add_server(url, name, False)

    if len(status_changed_servers) == 0:
        print("Successful finished servers checking")
    else:
        response = send_error(name, url, status_changed_servers)
        print("Error occured:")
        print(response)
        print(status_changed_servers)

動作確認

今までの実装でエラーが発生した時には「Error」と、復旧した時には「Recover」という内容のメールが送信されるようになっているはずです。動作確認をしてみます。

以前作成したWebページがS3上にあるので、アクセス権限を変更してテストしてみます。

Three.jsのかっこいいサンプルとAWSを連携させてみた

OKパターンで実行してみる。。。

メールは送信されず、DynamoDBにデータだけ追加されました。

スクリーンショット 2017-09-24 19.17.32.png

さて、アクセス権限を変更して、アクセスできなくしてから再度実行してみます。無事エラーが発生してメールが飛んで来ました。状態は「Error」です。

Server Status Changed happens:

[
    {
        "name": "s3-test",
        "url": "https://s3.amazonaws.com/xxxxxx/xxxx/css3d_periodictable.html",
        "status": "Error"
    }
]

DynamoDBのデータもエラーを表す「false」にstatusカラムが変更されています。

スクリーンショット 2017-09-24 19.21.23.png

もう一度実行しても、エラーメールは飛んでこないようです。無事状態判定をしてくれています。DynamoDBの値も変化なしです。(キャプチャだけだと何もわかりませんがw)

スクリーンショット 2017-09-24 19.23.43.png

それではページを復旧してみます。またアクセス権限を元に戻してアクセス可能にし、サーバ死活監視処理を実行してみます。

Server Status Changed happens:

[
    {
        "name": "s3-test",
        "url": "https://s3.amazonaws.com/xxxxxx/xxxx/css3d_periodictable.html",
        "status": "Recover"
    }
]

無事復旧メールが届きました。これで完成です。

まとめ

サーバの死活監視を、状態管理しながら実施してみました。今回はAWSのサービスを中心に実現しましたが、おかげでトータル数時間レベルで実現できました。かかるコストも抑えつつ、実際に使えそうなものがこれくらいスピーディーに作れてしまうのは、さすがAWSのマネージドサービス、といったところです。

[おまけ]ライブラリの管理

Lambdaでデプロイパッケージを作成する際、普通に実装すると外部ライブラリをプロジェクトのルートディレクトリに置くことになるので、あまり綺麗なパッケージ構成になりません。

そこで今回、こちらのページを参考にさせてもらいながら、別ディレクトリにパッケージを配置してく方法をとりました。

# pip install -U requests -t ./lib

で、Pythonにはこういう処理を追加する。

sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'lib'))

import requests

こうするだけでlib配下のパッケージを見に行ってくれるようになりました。これはディレクトリ管理することを考えるとかなり助かりました。実際serverlessコマンドでデプロイしても、正しく動きました。

続きを読む