Chromeの設定+α

2017年12月03日
区分
web
報告者:
morishita

この記事は actindi Advent Calendar 2017 の12月3日の記事です

Web系のエンジニア、デザイナーにとって最も利用する仕事道具といえば、Webブラウザだと思います。 その中でもGoogle Chromeを使うことが多いのではないでしょうか。

今回はChromeの便利な設定などについて紹介します。

Chromeの環境を複数持ちたい

私はChromeに拡張を結構インストールしています。便利なんですが、時にそれらが全くないクリーンなChromeを使いたいことがあります。拡張の影響を受けたくないだけなら、シークレットモードを使、うのが、最も手っ取り早いです。 が、すべての拡張をOFFにするのは困る場合、シークレットモードではなく別の環境を持ちたくなることがあります。
また、開発中には複数のアカウントで同時にログインして動作の違いなどを確認したいこともあるのではないでしょうか。

そういう場合には次の方法があります。

Chrome canaryをインストールする

手っ取り早い方法のひとつとして Chrome canary版をインストールすることです。

これはChromeの開発者向けバージョンで、まだ、通常のChromeに入っていない新機能が含まれているものです。 通常版のChromeとは全く独立で動作するので、普段使いのChromeにインストールしている拡張や設定の影響を受けません。 通常リリース版より不安定な可能性はあるのですが、使っている感想としては困ったことはありません。 ちなみに、canary版にはAndroid版もあります。 新しいJavaScriptやCSSの機能をいち早く試せたりするのでインストールしておいて損はないと思います。

別ユーザ(プロファイル)を使う

Chromeには複数のユーザ(プロフィール)を管理する機能が備わっています。 ユーザごとにChromeのバージョン以外は拡張・設定が独立しています。 Chromeの設定(chrome://settings/)を開いて、「他のユーザを管理」をクリックすると 次のような画面が表示されます。

ユーザ管理

この画面の右下「ユーザを追加」をクリックすると、次のようなユーザ名とアイコンを入力する画面が開くので 適当に入力します。

ユーザ登録

入力したら「追加」ボタンをクリックすると新しいChromeのウィンドウが開きます。 それが新たに追加したユーザで起動したChromeです。 ウィンドウの右肩に利用しているユーザの名前が表示されるので区別できます。 これまで使ってきたChromeとは別にブックマークと拡張が管理されますで、 用途別にユーザを作ってそれに応じた拡張をインストールしたりすると便利です。 あるいはまっさらな状態のままにしておいてもいいでしょう。

また、ChromeはGoogleアカウントへのログインを求めてきますが、 ユーザごとに別のアカウントが使えるので複数のアカウントを同時に利用したい場合にも便利です。 別Chromeユーザならば、Googleにかぎらず他のWebサービスでも同時に複数アカウントでログインすることが可能です。

登録したユーザを切り替えるにはウィンドウの右肩をクリックすると一覧が表示されるのでそこで選択できます。

ユーザ登録

Dev Tool

Web系のエンジニア・デザイナーにとって、Chrome Dev Toolもよく使うツールだと思います。 Dev Toolでの便利な設定や機能を紹介します。

コマンドメニューを開く

Dev Toolを表示します。そこで、macならば command+shift+p、Windowsならばctrl+shift+p(win)を押します。 するとatomエディタやsublimeを使っている人にはおなじみのコマンドメニューが開きます。 ここにChrome Dev Toolの機能・設定のすべてがリストアップされます。 あちこち探し回るよりも効率よく機能を呼び出したり設定を変更できます。 コマンドの一部を入力すると、候補を絞り込んでくれるので、うろ覚えの機能でも探しやすいです。

コマンドメニュー

以下にコマンドメニューを眺めていて、こんな機能があったのか! と思った機能を幾つか紹介します。

サードパーティコンテンツにマークを付ける

ネットワーク広告やその他計測ツールなど、他社のJavaScript、CSS等を自社サイトで読み込んでいる場合も多いかと思います。 Dev ToolのNetworkタブではそれらも一緒に表示されるので、どれがどこのものなのかわかりにくいです。 そんなときにはコマンドメニューでshow third party badges を選択すれば、サードパーティのコンテンツには 次のようにマークが表示されます。

Third party badges

マウスオーバーするとサードパーティのサービス名も表示されます。 同様の表示はNetworkタブの他、Consoleでも表示されます。

カバレッジ

開発を続けていると、使われなくなったJavaScriptやCSSが出てきます。変更がはっきりしていると その時に削除するのですが、いつの間にか使わなくなってしまったというものもあるのではないでしょうか。 害がなければ放置されることも多いのではないかと思うのですが、ムダも積み重ねると開発効率や 表示速度の低下の元になったりします。 使われていないJavascriptやCSSを特定するのは時に困難です。そんな時役立つのがカバレッジ機能です。 表示するにはコマンドメニューで show coverage を選択します。

coverage

上のキャプチャで示した円を描いた矢印のボタンをクリックするとページをリロードして計測を開始します。 結果は画面の下にそのページで実行されているJavaScriptやCSSがリストアップされ、 それぞれコードの何%が実行されているかが表示されます。 そのリストから1つを選択すると、画面上部にコードが表示されます。 実行されている部分は緑、実行されていない部分は赤く表示されます。

Railsだとアセットパイプラインで複数のファイルがまとめられます。 したがって該当のページだけではすべてのコードを実行しない場合もあります。 計測は止めるまで継続されるのでこの状態から続けて操作を行えば実行されたコードは緑に変わっていきます。 ページ遷移など操作を続ければ使われていないコードを絞り込んでいくことができます。
利用頻度の低い機能で実は使っていたというリクスがあるので、これだけで削除を判断するのは早計かもしれませんが、 削減候補を探すときには役立ちます。

時々、この機能を使って、使われてなさそうなコードをチェックしてはどうでしょうか。

レイヤー

Firefoxの開発ツールにもあったのですが、Quantumになってからなくなってしまったので残念に思っている方もいるのではないでしょうか。 Chromeにも実は同様の機能があります(内容がペイントされないのでFirefoxのものより見劣りはしますが)。 コマンドメニューから show layers を選択すると次のように表示されます。

layer

キャプチャは私が個人的に公開している防災に関わる「言い伝え」MAP のものですが、パンとローテートで表示を調整するとWebページのレイヤーの重なり具合を確認できます。 レイヤをクリックして選択すると、サイズやメモリ消費量などが表示されます。 これを使えば何故か表示されない要素の重なり順を確認したり、隠れていて見えない無駄要素を見つけることができるでしょう。

最後に

Chrome を活用していこーよを改善していきたいエンジニアを募集していますので、よろしくお願いします。

Github + Lambda + CodeBuild で自動テスト

2017年10月23日
区分
aws
報告者:
morishita

morishitaです。

いこレポの開発環境でプルリクエストに push したら Lambda と CodeBuild を使って Rspecを実行する仕組みを作ったので、ご紹介します。

どんなの?

Githubでプルリクエストを作ったり、プルリクエストにPushすると、こうなって

Github実行中

テストがすべてパスすると、こうなる仕組みを作りました。

GithubOK

Slackにも通知されます。

slack通知

なぜつくったの?

このユースケース自体は CircleCI等を使えば、わりと簡単にできてしまいます。 ではなぜ、作ったのかというと、次の通りです。

分単位課金で何並行でも使える

CircleCIでフリーだと同時に実行できるテストは1で、同時4テストを実行できるプランだと $150/月かかります。 ところが、CodeBuildだと$0.02/分で8vCPU、15GBのインスタンスが使えます。同時に何並行でも実行でき単純に時間課金のみです。現状、いこレポだと一回のテストに約5分、$0.1しかかかりません。 月に$150分使おうとすると、20営業日/月として1日75回もテストできてしまいます。 実際にはそんなに実行されないので、コスト的にも有利です。

AWSの他サービスと連携しやすい

CodeBuild は CodePipeline や、Lambda、CloudWatchと連携しやすく、今後インテグレーション していきやすそうです。

Serverless Framework を使ってみたかった

これまで、Slack に通知などのちょっとしたLambda関数は作っていて、AWSコンソールで直接実装していました。今回はもう少しコード量が増えそうだったので、コンソールではちょっとつらいなぁと思いServerless Framework を使うことにしました。 Serverless Framework と AWSの各種サービスを使ってどんなことができそうか試してみたかったというのもCIサービスを使わなかった理由です。

利用したサービス・ツール

全体の流れ

全体の流れを表したシーケンスは次のとおりです。 シーケンス

2つの処理から構成されています。ひとつはGithubからSNSへのイベント通知をトリガーにLambdaがCodeBuildを起動して、それをGithub・Slackに通知する処理。そしてCodeBuildの終了をCloudWatch Eventが受けてLambdaがGithub・Slackに通知する処理です。

図が煩雑になるのでシーケンス図上は Lambda は一つしか描いていませんが、 それぞれの処理に対応するLambda関数を一つづつ実装しました。

CodeBuild での Rspec 実行

この仕組のメインは CodeBuild で Rspecを実行しているところです。 RailsのRspec と MySQL を動かすコンテナを docker-compose を使って、実行しています。

Railsコンテナでは test-queue を使ってRspecを分散実行しており、 SimpleCov でテスト結果のコードカバレッジも記録します。

ハマったところ

test-queue 導入に起因したもの

実はテスト実行順のランダム化も設定忘れで実施できていなかったので テスト順が変わってFixtureのロードが漏れてテスト失敗が多発しました。 フィクスチャを用意していたのは都道府県や地方といったマスター系のデータだったのですが、 これは rails_helper に config.global_fixturesを設定することにより解消しました。

また、画像アップロードのテストが時々failすることがありました。 test-queue はフォークして複数プロセスでテストを実行するので、それぞれのプロセスには独立したリソースを用意する必要があります。Paperclipのアップロードされたファイルの保存場所が分離されておらず、たまたまテストタイミングが重なるとエラーとなっていたようでした。test-queueの実行スクリプトの after_fork でプロセスごとの保存場所を設定することで解決しました。

ネームスペースが異なる同名のコントローラでfail

たとえは記事ページのコントローラは AtriclesController、記事管理のコントローラはAdmin::AtriclesController というクラス名で実装しているのですが、何故かこれをテスト中取り違えられてテストがfailするということがありました。 これは不本意ながら、require 'admin/articles_controller' のように、ターゲットクラスをspecファイルで読み込むことにより解決しました1。もっとスッキリした解決法はないものかと 思っています。

decker-compose が終了しない

Rspecを実行するためには MysqlとRails2つのコンテナを動かすのですが、Specの実行が終わっても、MySQLが残るので、docker-composeが終わらなくて困りました。 なにか解はないものかとドキュメントを読み返していると--abort-on-container-exit オプションで解決できそうだということがわかりました。これを利用するとコンテナが一つでも終わると、docker-compose自体が終了します。abort と言いながらも、exit 0 で終了するので、CodeBuild的にもFAILにならなず解決できました。

CodeBuildがS3にアップする処理結果は暗号化されている

GithubやSlackに通知した処理結果には、詳細な結果情報を得るためのリンクを付けています。 このリンクのURLはS3の署名付きURLと言うもので、期間限定でS3のファイルをダウンロードできるものです。 SDKを利用すれば、getSignedUrl で簡単にこのURLが生成できると思ったのですが…、 CodeBuildはS3にアップロードするデータは暗号化してしまい、暗号化されたファイルを取得するための署名付きURLは 署名バージョン 4 署名プロセスで署名する必要があったのです。しかしgetSignedUrlはこれに対応していない。 今回は aws-signature-v4を利用してURLを生成して解決しました。 しかし、Javascriptの AWS SDKのs3::getSignedUrlはデフォルトではこれに対応していません。 s3のインスタンスを作る時に次のようにsignatureVersionv4に指定することでs3::getSignedUrlが出力するURLが署名バージョン 4 署名プロセスで署名されたものに変わります。(2017/11/06 修正)

const s3 = new AWS.S3({ signatureVersion: 'v4' });

やってみてどうだったか?

これまで、いこレポではプロダクト自体のコードがそれほど多くなく、自動テストについては サボっていてローカルでRspecを実行していたのですが、pushしたらテストが実行されるのはやはり楽です。

実装に関しては、 Serverless Frameworkの導入はやって良かったです。 Webpackと組み合わせて使えるので、設定すれば babel で ES2015 相当のJavascriptの仕様が使えます。特にasync/awaitが使えるのは大きいです。デプロイもワン・コマンドで済んでしまうのでとても楽できました。ちょっとしたものにも積極的に使っていくべきだと思いました。 また、初期案ではJenkinsを導入しようとしていたので、そのサーバ運用の手間も省けたのも大きいです。 AWS SDKを使ってサービス間をLambdaで繋いでいくことで、サーバレスでの自動化を進めて行けそうな 手応えを得られました。もう少し複雑なことをしたくなったら、Step Functions も使ってみようかなと思っています。

最後に

サーバーレスで、いろいろやってみたいエンジニアを募集していますので、よろしくお願いします。

CloudWatch alertをLambdaでSlackに通知する

2017年09月12日
区分
aws
報告者:
morishita

morishitaです。 アクトインディではAWS上でサーバを運用しており、監視には CloudWatch も活用しています。

CloudWatchではAmazon SNSを通じて メールやSMS(Simple Message Service)にアラートの通知を 送信することができます。 それはそれで便利なのですが、やはりみんな大好きSlackにも通知したいところです。

標準ではAmazon SNSの通知先としてSlackは用意されていないのですが、Amazon Lambdaを利用すると可能になります。
今回はその方法について紹介したいと思います。

全体像はこんな感じです。 全体像

Slackの設定

Incoming Webhooksの設定をします。

通知したいworkspaceのCustom Integrations設定画面(/apps/manage/custom-integrations)より設定します。

既存の webhook を利用しても良いのですが、新たに追加したほうが他と干渉を気にせず作業できます。
また、webhookでは通知先のチャネルを選択しますが、Lambda関数が完成するまでは、他の人に見えないチャネルに設定したほうが良いです。大勢の人が参加するチャネルにテストで通知を流すと鬱陶しいですし、驚かせてしまうかもしれません。
作成したwebhookのURLは後で使うのでメモしておきます。

Amazon SNSの設定

次節で作成するLambda関数のトリガーとなるAmazon SNSのトピックを作っておきます。

AWSコンソールで「Simple Notification Service」を選択します。SNSに移動したら、「Create Topic」を選択して、Topic nameDisplay name を適当に入力してトピックを作成します。
ここで登録した Topic name はLambda関数作成時に使うので覚えておきます。

Lambda関数の作成

実はLambdaには関数の雛形が設計図1として多数用意されています。その中に、Slackへの通知もあるので、雛形として利用しない手はありません。以下の説明はこの雛形ベースで進めます。

AWSコンソールで Lambdaのページに移動したら、「関数の作成」をクリックします。
「設計図の選択」というステップに進むので、slack で検索します。検索結果の中からnodejs版の cloudwatch-alarm-to-slack を選択します。

続いて、「トリガーの設定」ステップに進みます。ここでは先に登録したSNSのトピックを選択します。
次のステップでLambda関数のコードを作成します。

Lambda関数のコード

cloudwatch-alarm-to-slackを選択しているので最初から次のコードが入力されています。 ここでランタイムを変更してしまうとコードが消えるので、Node 6.10にしたくなるところですが、Node 4.3のままにしておきます。 今回利用する機能の範囲ではどちらのバージョンでも大差ありません。

'use strict';

const AWS = require('aws-sdk');
const url = require('url');
const https = require('https');

// The base-64 encoded, encrypted key (CiphertextBlob) stored in the kmsEncryptedHookUrl environment variable
const kmsEncryptedHookUrl = process.env.kmsEncryptedHookUrl;
// The Slack channel to send a message to stored in the slackChannel environment variable
const slackChannel = process.env.slackChannel;
let hookUrl;

function postMessage(message, callback) {
    const body = JSON.stringify(message);
    const options = url.parse(hookUrl);
    options.method = 'POST';
    options.headers = {
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(body),
    };
    const postReq = https.request(options, (res) => {
        const chunks = [];
        res.setEncoding('utf8');
        res.on('data', (chunk) => chunks.push(chunk));
        res.on('end', () => {
            if (callback) {
                callback({
                    body: chunks.join(''),
                    statusCode: res.statusCode,
                    statusMessage: res.statusMessage,
                });
            }
        });
        return res;
    });
    postReq.write(body);
    postReq.end();
}

function processEvent(event, callback) {
    const message = JSON.parse(event.Records[0].Sns.Message);
    const alarmName = message.AlarmName;
    //var oldState = message.OldStateValue;
    const newState = message.NewStateValue;
    const reason = message.NewStateReason;
    const slackMessage = {
        channel: slackChannel,
        text: `${alarmName} state is now ${newState}: ${reason}`,
    };

    postMessage(slackMessage, (response) => {
        if (response.statusCode < 400) {
            console.info('Message posted successfully');
            callback(null);
        } else if (response.statusCode < 500) {
            console.error(`Error posting message to Slack API: ${response.statusCode} - ${response.statusMessage}`);
            callback(null);  // Don't retry because the error is due to a problem with the request
        } else {
            // Let Lambda retry
            callback(`Server error when processing message: ${response.statusCode} - ${response.statusMessage}`);
        }
    });
}

exports.handler = (event, context, callback) => {
    if (hookUrl) {
        // Container reuse, simply process the event with the key in memory
        processEvent(event, callback);
    } else if (kmsEncryptedHookUrl && kmsEncryptedHookUrl !== '<kmsEncryptedHookUrl>') {
        const encryptedBuf = new Buffer(kmsEncryptedHookUrl, 'base64');
        const cipherText = { CiphertextBlob: encryptedBuf };

        const kms = new AWS.KMS();
        kms.decrypt(cipherText, (err, data) => {
            if (err) {
                console.log('Decrypt error:', err);
                return callback(err);
            }
            hookUrl = `https://${data.Plaintext.toString('ascii')}`;
            processEvent(event, callback);
        });
    } else {
        callback('Hook URL has not been set.');
    }
};

ざっくり、やっていることを説明すると、このLambda関数は次の3つのパートに分かれています。

  • exports.handler
  • processEvent
  • postMessage

exports.handlerはLambda関数の実行環境に直接呼び出される関数です。kmsEncryptedHookUrl を復号化して、processEvent を読んでいるだけなので、そのまま利用します。
postMessage processEvent は Slackに送るメッセージを作って、それをpostMessageに渡しています。 postMessageはSlackのwebhookにリクエストを送信するだけなので、これもそのまま利用します。

ということで、書き換える必要があるのはprocessEventのメッセージを作っているところです。 上記実装だと、アラートの名前と状態が変わったくらいしかわからないので、 もう少し、内容を増やしたいところです。 exports.handlerには3つの引数 (event, context, callback) が実行環境から渡されます。

このうち event は次のようなオブジェクトが渡されます。 この例は、CPUUtilizationが閾値を超えた場合のアラートの内容です。(テストのために閾値は低めに設定しています。)

{
    "Records": [
        {
            "EventSource": "aws:sns",
            "EventVersion": "1.0",
            "EventSubscriptionArn": "arn:aws:sns:ap-northeast-1:<略>",
            "Sns": {
                "Type": "Notification",
                "MessageId": "<略>",
                "TopicArn": "arn:aws:sns:ap-northeast-1:<略>",
                "Subject": "ALARM: \"cpuutil-alert\" in Asia Pacific - Tokyo",
                "Message": "<後述>",
                "Timestamp": "2017-09-11T10:16:26.865Z",
                "SignatureVersion": "1",
                "Signature": "<略>",
                "SigningCertUrl": "<略>",
                "UnsubscribeUrl": "<略>",
                "MessageAttributes": {}
            }
        }
    ]
}

ここで重要なのはevent.Records[0].Sns.Messageです。この中に文字列化されたJSON形式で、CloudWatchのアラートの情報が格納されています。 パースして見やすくすると次のような感じです。

{
 AlarmName: 'cloudwatch-alert-name',
 AlarmDescription: 'CPU負荷アラート',
 AWSAccountId: '<略>',
 NewStateValue: 'OK',
 NewStateReason: 'Threshold Crossed: 1 datapoint [37.4 (11/09/17 10:33:00)] was not greater than or equal to the threshold (20.0).',
 StateChangeTime: '2017-09-11T10:38:59.203+0000',
 Region: 'Asia Pacific - Tokyo',
 OldStateValue: 'ALARM',
 Trigger:
  { MetricName: 'CPUUtilization',
    Namespace: 'AWS/EC2',
    StatisticType: 'Statistic',
    Statistic: 'AVERAGE',
    Unit: null,
    Dimensions: [ [Object] ],
    Period: 300,
    EvaluationPeriods: 1,
    ComparisonOperator: 'GreaterThanOrEqualToThreshold',
    Threshold: 20,
    TreatMissingData: '',
    EvaluateLowSampleCountPercentile: '' }
}

この情報を利用してい次の様にメッセージを組み立てみました。

const message = JSON.parse(event.Records[0].Sns.Message);

const alarmName = message.AlarmName;
const alarmDesc = message.AlarmDescription;
const oldState = message.OldStateValue;
const newState = message.NewStateValue;
const reason = message.NewStateReason;
const threshold = message.Trigger.Threshold;

let color = 'warning';
let icon = ':warning:';
let userName = 'サーバの負荷が高まっています'
let text = `CPU利用率が${threshold}%を超えました`
if (newState == "OK") {
    color = "good";
    icon = ":oknya:";
    userName = 'サーバの負荷が下がってきました'
    text = `CPU利用率が${threshold}%を下回りました`
}

let slackMessage = {
    channel: slackChannel,
    username: `${userName}`,
    text: `*[${newState}] - ${text}*`,
    icon_emoji: icon
};
const slackAttachment = {
    color: color,
    icon: icon,
    text: `${alarmName}(${alarmDesc})\n詳しくは <[CloudWatchダッシュボードのURL]|ダッシュボード> で確認ください。`,
    footer: "sent by <[Lambda関数のコンソールURL]|Lambda Func (send-alert-to-slack)>"
}

slackMessage.attachments = [slackAttachment];

Slackの message-attachmentsを利用して、次のようにアラートの種類により色が変わるようにしています。

スラックへの通知

あまり、大きなメッセージを送ると邪魔なので、詳細はCloudWatchのダッシュボードへのURLを示してそちらで確認してもらうようにしています。 また、時間が経つと、「この通知どこで出してるんだったっけ?」となりがちなので、Lambda関数へのURLを付けています。

弊社ではこれを見ながら負荷が高そうなときには負荷がかかりそうな作業は待ってもらうというルールになっています。

暗号化ヘルパー

Lambdaでは実行時に環境変数を与えることができます。
同じコードでも環境変数で動作を変更したりできて便利なのですが、他のサービスのアカウント情報等のひみつ情報を設定すると、Lambdaの設定権限を持つユーザ全てにその値が見えてしまいます。それでは困るという場合には暗号化する暗号化ヘルパーを使うことができます。選択した雛形 cloudwatch-alarm-to-slack では kmsEncryptedHookUrlに暗号化したSlackのwebhook URLを設定するように実装されています。

暗号化ヘルパーを利用するには、コードの下にある、「暗号化ヘルパーを有効にする」にチェックを入れ、暗号化キーを選択します。デフォルトで登録されている「aws/lambda」というキーを利用してもいいですが、筆者の環境ではうまく動作しなかったので、別途登録しました。
暗号化キーは IAMのKMSで登録します。一点、ハマリポイントとしてはIAM自体はグローバルスコープなサービスですが、暗号化キーはリージョン毎に管理されます。なのでLambda関数を作成しているリージョンと同じリージョンでキーを登録しないと使えません。

リージョン選択

環境変数kmsEncryptedHookUrl には webhook URLのhooks.slack.com/services/〜を入力してください。スキーム(https://) は不要です。 入力したら、「暗号化」ボタンをクリックします。

Lambda 関数ハンドラおよびロールの設定

Lambda関数の設定として、次の設定を行います。

  • ハンドラ
  • ロール
  • ロール名
  • ポリシーテンプレート


ハンドラはデフォルトのままにします。
ロールは実行時のサービスロールです。「テンプレートから新しいロールを作成」を選択すると適切な権限を持つロールを作成してくれます。
ポリシーテンプレートは次の2つを選択します。

  • 基本的な エッジ Lambda のアクセス権限
  • KMS の復号化アクセス権限

詳細設定

詳細設定では次を設定します。

  • メモリ
  • タイムアウト
  • KMSキー
  • etc


タイムアウトは少し延ばしたほうが良いでしょう。
KMSキーでは暗号化に利用したキーを選択しないと復号に失敗します。
他の項目はデフォルトのままで良いかまいません。

CloudWatch Alertの設定

作成したLambda関数を動かすには先に作成したSNSをCloudwatch Alertの通知先に追加すれば、アラートの発報時にSlackにも通知されるようになります。
実装中は閾値を下げたり、上げたりしながらアラートを発報させ試すと良いと思います。ただし、大勢のメンバーがいるSlackチャネルに通知したり、他の通知手段も設定されているアラートでやると現場を騒然とさせてしまうので注意してください。

Lambdaは、このようにサービス間のグルーコードを作ったり、ちょっとした処理を自動化するのに便利です。AWSのリソースにも実行ロールに権限を与えれば容易にアクセスできますし、CloudWacthイベントと組み合わせれば定期実行も可能です。
しかも、これらがサーバレスで実現できるのが素晴らしい!

アクトインディでは、AWSやLambdaの可能性を追求したいエンジニアを募集中です。

脚注

  1. 英語ではBluePrint。下手に日本語にするよりBluePrintの方がわかりやすいと思う。 

Railsのログを awslogs で Cloudwatch Logs に出力する

2017年08月28日
区分
Rails
報告者:
morishita

morishita です。 今回はいこレポでのログ出力について紹介します。

いこレポの動作環境

いこレポは ElasticBeanstalk を利用してアプリケーションサーバを稼働させています。 ElasticBeanstalk ではプラットフォームを選択できますが、 Multi Container Docker を利用しています。 この場合、実際にRailsが稼働するのは ECS上に起動された Docker コンテナとなります。

Dockerではコンテナ内に永続化するデータを保存しないことが基本ですが、ログも コンテナ削除時に消えていい情報ではないので何らか外に出す必要があります。

その方法としては次の方法があるかと思います。

案1. ホスト側のディレクトリをマウントして、そこにログを保存する
案2. Fluentd でログサーバに転送する
案3. Cloudwatch Logsに転送する

いこレポではECSインスタンスも基本イミュータブルに運用しており、デプロイ毎に新たなインスタンスを起動して、古いのは捨てているのでホスト外に出す仕組みがもう一段必要になります。よって案1は却下しました。

Fluentd を使うと柔軟なログ転送が可能でElasticSearchに送ったりもやりやすいのですが、S3に格納するにしてもログを一旦受けるログサーバを立てる必要があります。ログサーバを立てて運用する手間を惜しんで案2も今回は見送りました。

いこレポで採用したのは案3のClowdwatch logsに出力するという案3の方法。 それについて以下に述べます。

Dockerの設定

Dockerには ロギングドライバという仕組みがあり、標準出力に吐いたログを設定されたロギングドライバ経由でハンドリングすることができます。

ロギングドライバには fluentd 1syslog がありますが、Clowdwatch Logsに出すには awslogs を使います。 このログドライバは当然、ElasticBeanstalk (具体的にはECS)でも利用できます。

ElasticBeanstalk ではコンテナの定義を Dockerfilecocker-compose.yml ではなく、Dockerrun.aws.json というAWSの独自フォーマットのファイルに記述します。

次のように設定してやれば、awslogsロギングドライバを利用できます。
logConfiguration がロギングドライバの設定部分です。

{
  "AWSEBDockerrunVersion": 2,
  "containerDefinitions": [
    {
      "name": "rails",
      "image": "<DockerイメージのURL>",
      "essential": true,
      "memory": 1024,
      "entryPoint": ["<略>"],
      "logConfiguration": {
          "logDriver": "awslogs",
          "options": {
            "awslogs-group": "<ロググループ名>",
            "awslogs-region": "ap-northeast-1",
            "awslogs-stream-prefix": "production"
          }
      }
    }
  ]
}

logDriverawslogsを指定し、options でログの格納先となる Clowdwatch Logsのロググループを指定します。

ログを出力するためには設定以外に次の2点が必要となるので注意です。

  • 出力先のロググループ は予め作っておくこと
  • ElasticBeanstalk でインスタンスの稼働に利用されるロールがそのロググループへのアクセス権を持っていること

Railsの設定

さて、Docker側の設定ができれば、ログの出力先を標準出力にするだけで Clowdwatch Logs にログが保存されるようになります。

Rails5.1では production.rb にもともと次のように定義されており、RAILS_LOG_TO_STDOUT という環境変数があれば、標準出力にログが出力されるようになっています。

  if ENV["RAILS_LOG_TO_STDOUT"].present?
    logger           = ActiveSupport::Logger.new(STDOUT)
    logger.formatter = config.log_formatter
    config.logger    = ActiveSupport::TaggedLogging.new(logger)
  end

ElasticBeanstalk では管理コンソールから環境変数を設定できるのでそれを利用すれば、設定ファイルを変更することなくログを標準出力に切り替えられます。

ログ出力のフォーマット変更

Railsのログは 1リクエストに対して複数行で出力されます。 例えば次のように。

Started GET "/" for 172.18.0.4 at 2017-08-16 02:10:36 +0000
Processing by TopController#index as HTML
  Rendering top/index.html.slim within layouts/application
  Rendered top/index.html.slim within layouts/application (***.*ms)
  Rendered layouts/_header.html.slim (**.*ms)
  Rendered layouts/_footer.html.slim (**.*ms)
Completed 200 OK in ***ms (Views: ***.*ms | ActiveRecord: **.*ms)

Clowdwatch logsでは1行が1レコードとなってしまうので、どのレコードからどのレコードまでが一連のログなのか追う必要があり、非常に見づらくなります。 1行のJSONで出力すると1レコードとして一連のログを記録できる上に、パースして見やすくしてくれるので、JSONで出力するようにログフォーマットを定義するようにしました。

いこレポではログフォーマットに roidrage/lograge: An attempt to tame Rails’ default policy to log everything. を利用しています。

まず、production.rb に次の設定を追加します。

  config.lograge.enabled = true
  config.lograge.formatter = Lograge::Formatters::Json.new
  config.lograge.custom_payload do |controller|
    {
      host: controller.request.host,
      remote_ip: controller.request.remote_ip,
    }
  end
  config.lograge.custom_options = lambda do |event|
    exceptions = %w(controller action format id)
    {
      time: event.time,
      host: event.payload[:host],
      remote_ip: event.payload[:remote_ip],
      params: event.payload[:params].except(*exceptions),
      exception_object: event.payload[:exception_object],
      exception: event.payload[:exception],
      backtrace: event.payload[:exception_object].try(:backtrace),
    }
  end

config.lograge.formatter = Lograge::Formatters::Json.new でJSONフォーマッターを使うこと指定して、 config.lograge.custom_payload では 標準で出力されている項目以外を追加したい場合に 各アプリケーションの要求仕様に沿ってそれを追加するように実装します。

lograge.custom_options ではどの項目をどういうキーでJSONに出力するのかを実装します。ここで定義した、ハッシュがそのまま、JSONとしてログ出力されます。

rescue_from で404や例外をキャッチして、レスポンスをレンダリングしている場合、 これだけでは 例外情報がログに入りません。 そこでapplication_controller.rbappend_info_to_payload をオーバーライドします。こうしておけばバックトレースもログに出力されるようになります。

  def append_info_to_payload(payload)
    super
    if @exception.present?
      payload[:exception_object] ||= @exception
      payload[:exception] ||= [@exception.class, @exception.message]
    end
  end

config.lograge.custom_payload を実装せずに、append_info_to_payload のオーバーライドでやってしまってもいいかもしれない。

以上を実施して出力されるログの表示はこんな感じです。 ClowdwatchLog

とても見やすくなります。

最後に

おでかけ先探しに悩むパパ・ママを助けてくれるエンジニアを募集していますので、よろしくお願いします。

脚注

  1. じゃあ、 fluentd のドライバを使えば、ログサーバなしで fluentd で運用できたんじゃない?と思われるかもしれませんが、ロギングドライバがやってくれるのはプロトコルのペイロードに載せて転送してくれるところ。例えばS3に保存するにはどこかで一旦受けて、fluentdの S3アウトプットプラグインでS3に送ってやる必要があるのでログサーバの運用が必要となります。 

いこレポ はじめました。

2017年08月14日
区分
ikorepo
報告者:
morishita

はじめまして。morishita です。 4月に入社以来、このブログに投稿する機会をうかがっていましたが、ついに初めての投稿です。

すでにサービスインから2週間ほど経っているのですが、いこレポ といういこーよの姉妹サイトをローンチさせました。
ざっくりどのようなサイトかというと、次のようなニュースサイトです。 パパ・ママエンジニアの皆さん、お子さんとのお出かけの参考にご活用ください。

ikorepo

【いこレポとは?】
お出かけの達人「お出かけコンシェルジュ」が、旅行で人気の観光名所、子連れで楽しめる遊び場・イベント、地元の日帰り穴場を掘り下げ、見どころやおすすめプランを提案します。

【いこレポの特徴】
お出かけコンシェルジュが、完全オリジナルの”子どもが喜ぶ遊び場・お出かけまとめ”記事を執筆します。お台場、伊豆などの人気エリアのまとめ記事を読んで週末のお出かけや旅行計画を立てたり、最新記事からお出かけのトレンド情報を探ったり、人気テーマ別記事から新しいお出かけ体験を見つけることができます。
(ニュースリリースより転載)

さて、このブログはTechブログなので、裏側の仕組みについて少しご紹介したいと思います。

私が入社したタイミングでサービスを始めることは決まっていましたが、設計はこれからという状況でした。自社サービスをやっている会社だと、新規機能開発は度々あっても全くの新規プロダクトの開発に最初から入れる機会は少なく、それだけでもラッキーなのに、どのように作るかについてもほとんど決めて良さそうだったので、やってみたいと思っていたことはほぼ思い通りつぎ込みました。

ソフトウェア構成

弊社でサーバサイドの開発というとRailsでというのが既定路線となっています。他の言語やフレームワークを採用する理由も特になかったですし、開発期間がそう長いわけではなかったので、今回もそのまま踏襲しました。

ということで、開発時の最新バージョンということで次の様な構成です。

  • Ruby 2.4
  • Rails 5.1
  • nginx
  • Vue.js


弊社的に新たな試みとしては、Vue.js の導入です。
いこレポに掲載する記事は自前のCMS機能で執筆されていますが、その部分をほぼ、Vue.js で書きました。React + Redux という選択肢も一応は考えたのですが、ちょっとしたアプリを書いてみて、いまいち私の肌に合わなかったので、ほぼ独断でVue.jsを採用しました。結果的には採用して良かったです。
webpacker によるビルドも、デフォルトで組み込まれている設定がよくできており、ハマる部分はありませんでした。

インフラ構成

ソフトウェア構成をオーソドックスに済ませた分、インフラでは色々、当社としては初めての構成を試みています。

まず最初に、サーバはDockerコンテナで動かそうと決めていました。
Dockerで動かす理由は、仕組みとして Dockerfile にサーバの構築手順がコード化されること、将来的にサーバの台数が増えても OS や基本パッケージの更新がアプリケーションのデプロイと同時にできてしまうので手間が少ないだろうということ、いまどき直接あれこれインストールしてサーバを作るのなんてしないよねいう思い込みです。
開発環境も docker-compose up だけで同じ構成のサーバがローカルで起動できるようにしています(流石にDBはMySQLですが)。

そして、AWSの便利サービスはロックインを恐れず積極的に活用することも最初に方針として決めました。 結果的に現時点で使っているAWSのサービスは次のとおりです。

  • ElasticBeanstalk (Multi Container Docker)
  • RDS Aurora
  • ClowdWatch Logs
  • SES
  • S3
  • ClowdFront
  • Route53
  • ECR (EC2 Container Registry)
  • CodeBuild
  • CodePipeline

AWS 上で Docker を動かす仕組みは何種類かあるのですが、ElasticBeanstalk を選択しました。理由は AWS 側でやってくれることが最も多いからです。 環境を作ると、アプリケーションを実際に動かす ECSインスタンスはもちろん、ALB+ターゲットグループ、オートスケーリング、デプロイとひと通りの仕組みを用意してくれます。 これに、CodePipeline と CodeBuild を組み合わせることで、 Github 上で PR をマージすると自動的にデプロイされるように構成しています。

また、これらのインフラ構築を出来る限りコード化することも目指しました。VPC のネットワークは Ansible で構築しています。アプリケーションサーバの構築については CodeBuild で Docker イメージのビルドを行うので、Dockerfile と CodeBuild のビルド定義ファイルである buildspec.yml でコード化ししており、それを稼働させる ElasticBeanstalk 環境も設定ファイルを用いて、eb コマンドで構築できるように定義しています。
部分部分でコード化の仕組みがバラバラで統一感がないのとCI環境の構築についてはコード化できていないのは今後の課題と思っています。

今後について

いこレポがネット上での存在感を高め、編集チームの皆さんが一生懸命書いた記事をできるだけ多くのパパ・ママに届けるために技術的にできることはどんどんやっていきたいと思っています。
今はまだ、記事数がそれほど多くないですが、増えてくると探せない問題が発生すると思うので、検索、レコメンデーションなども検討していきたいと思います。
また、本番を運用する環境についてはできていますが、開発バックエンドであるステージング環境、開発環境についてはいこーよに遅れを取っているのでこの改善も課題です。

アクトインディでは、いこレポを一緒にグロースさせていくエンジニアを待っています

技師部隊からの
お知らせ

【求人】エンジニア募集しています。

本頁の来客数
八十七万千百七十六名以上(計測停止中)

メンバー一覧

アクトインディ技師部隊員名簿

アクトインディ技師部元隊員

アクトインディへ

カテゴリー

アクトインディ

aaaa