AWSでServerlessの環境をCIするための選択肢を調べたメモ

利用するサービスはAWSに限定するとした場合に開発・テスト・デプロイを継続するための選択肢をいくつか調べてみました。

開発したいものは、「API Gateway – Lambda – DynamoDB」で構成されるWebサービスとします。

正直なところ、対象像を少し絞らないと選択肢が多すぎて好みの問題になりそうなので・・・

注意

調査用のメモです。

実際に全ての選択肢で開発をしてみたわけではなく、入門記事やドキュメントを少しみた程度で判断しています。

そのため正確性に欠ける内容となっている可能性が高いことをご了承ください。

共通点

開発ツールで対応していない部分についてのデプロイについては、AWS CLIで対応していくことになるでしょう。

LocalStack

モックサービスの対応数にまず驚いた。

https://github.com/localstack/localstack

LocalStack はローカルでAWSのサービスのモックを動かしてしまうというツールです。
DockerコンテナでAWS各種サービスを一気に立ち上げることができるので、ローカル環境で開発を完結させることが出来ます。

これ自体にはAWSを管理する機能はなく、Lambdaをローカル環境で開発してテストするときに、ローカルで同時にAPI Gateway + DynamoDBを動かしたいという場合に必要となりそうです。
DynamoDB自身はAmazonからDynamoDB Localが提供されているので、どちらを使うかは検証が必要でしょう。

起動コマンドも簡単で、一発で全て立ち上げることが出来ます。

docker-compose up

macの場合は、$TMPDIRにシンボリックリンクがある場合、少しコマンドを変える必要があるようです。

TMPDIR=/private$TMPDIR docker-compose up

DynamoDB Local

ローカル開発用のDynamoDB。
ほとんどのLambda開発ツールがAPI Gatewayもサポートしていることから、DynamoDBを接続するローカル環境ではLocalStackよりはこちらを使用することになるのではないかと。公式提供でもあるし。

ダウンロードとインストールガイドはこちら

aws-sam-local

SAM は Serverless Application Modelのことで、aws-sam-localはAmazon公式がサポートしているローカル開発環境です。今はまだbetaですが、近いうちに良くなりそうです。

https://github.com/awslabs/aws-sam-local

AWS Blogで利用例が紹介されていて、DynamoDB Localを使う場合についても少し触れられています。
https://aws.amazon.com/jp/blogs/news/new-aws-sam-local-beta-build-and-test-serverless-applications-locally/

作成したLambda FunctionをローカルDockerコンテナで実行したり、現時点ではPythonとNode.jsはデバッグ出来るようです。(自分で試したところ、上手くいかなかった)

また、Lambda関数を単純に実行するだけではなく、ローカルのAPI Gatewayを通して実行を確認できます。

PostManで簡単にAPIの動作検証が行えたり、実際のHTTPアクセスの形でLambda関数の検証がローカルで行えます。
また、実行時間やメモリ消費量が表示されるため、AWSにデプロイする前に関数の効率化が出来ます。

Serverless Framework

現状で一番正解に近いと思います。

https://github.com/serverless/serverless

こちらのQitta記事こっちのQiita記事が参考になりそうです。
DynamoDB Localと連携するためのプラグインが存在し、serverless.ymlファイルにAWS上での構成を記載していき、そのままAWSにデプロイ出来るようです。

Apex

Go言語でLambdaが書ける!!!

純粋にLambdaの開発だけで見ればとても良さそうです。
ただし、ローカル実行はサポートしておらず、AWSリソースの操作もLambdaに限定されています。
その辺りは、Terraformで補う考えのようです。

公式

https://github.com/apex/apex

chalice

Python用のマイクロサービスフレームワークです。

https://github.com/aws/chalice

次の様なコードで、APIを定義できます。

chalice
@app.route('/resource/{value}', methods=['PUT'])
def put_test(value):
    return {"value": value}

デプロイでAPI Gateway, Lambdaに反映されるため、コードの見通しが良くなりそうです。
IAM Rolesなどは自動生成される様なので、とにかく簡単にコードを書いて、すぐデプロイということが出来そうです。

インストールからコード記述、デプロイまでの操作がHelloworldならこんなに簡単に出来るようです。

$ pip install chalice
$ chalice new-project helloworld && cd helloworld
$ cat app.py

from chalice import Chalice

app = Chalice(app_name="helloworld")

@app.route("/")
def index():
    return {"hello": "world"}

$ chalice deploy
...
https://endpoint/dev

$ curl https://endpoint/api
{"hello": "world"}

Zappa

こちらも、とにかく簡単にPythonで作成した関数をAWSにデプロイ出来ることがウリのようです。

https://github.com/Miserlou/Zappa

gifアニメーションで実演が付いています。(README.mdから引用しました)
README.md

Zappa Demo Gif

REST APIを作成する以外にも、AWS Eventsに対応するものや、Asynchronous Task Executionといって、別のLamdba関数を非同期に呼び出すことが出来る機能を持っているようです。
というか、chaliceに比べると非常に多彩。

また、ZappaはWSGI(Web Server Gateway Interface)をサポートしているので、他のアプリケーションサーバーにデプロイすることも可能です。

chalice vs Zappa

こちらに比較記事がありました。

続きを読む

CentOS7なインスタンスのルートボリューム拡張

まったく覚えられないからめも。

CentOS7からデフォルトのファイルシステムはxfsになってた

# df -T
ファイルシス   タイプ   1K-ブロック    使用   使用可 使用% マウント位置
/dev/xvda1     xfs         52417516 3562912 48854604    7% /

(省略)

例に漏れずそのままだとルートボリュームは何Gのせても8Gなのである

# df -h
ファイルシス   サイズ  使用  残り 使用% マウント位置
/dev/xvda1       8.0G  3.6G  4.5G   44% /

(省略)

拡張手順

growpartいるかな

# rpm -qa | grep growpart
cloud-utils-growpart-0.29-2.el7.noarch

growpartする

# growpart /dev/xvda 1
CHANGED: partition=1 start=2048 old: size=16775168 end=16777216 new: size=104855519,end=104857567

xfs_growfsする

# xfs_growfs -d /
meta-data=/dev/xvda1             isize=512    agcount=4, agsize=524224 blks
         =                       sectsz=512   attr=2, projid32bit=1
         =                       crc=1        finobt=0 spinodes=0
data     =                       bsize=4096   blocks=2096896, imaxpct=25
         =                       sunit=0      swidth=0 blks
naming   =version 2              bsize=4096   ascii-ci=0 ftype=1
log      =internal               bsize=4096   blocks=2560, version=2
         =                       sectsz=512   sunit=0 blks, lazy-count=1
realtime =none                   extsz=4096   blocks=0, rtextents=0
data blocks changed from 2096896 to 13106939

拡張されました

# df -h
ファイルシス   サイズ  使用  残り 使用% マウント位置
/dev/xvda1        50G  3.6G   47G    8% /

(省略)

ぐぐってたら、いきなりxfs_growfs して「data size unchanged, skipping」って言われる!って
言ってる人がいたけどgrowpartすれば大丈夫。
でももしかしてgrowpartってfdiskとかで終点変えるのと同じなんですかね。

続きを読む

カテゴリー 未分類 | タグ

serverless frameworkを使って本格的なAPIサーバーを構築(テストコード編)

serverless frameworkを使って本格的なAPIサーバーを構築(テストコード編)

LambdaやDynamoDB、APIGatewayなどの構成をコマンド1つですぐにデプロイしてくれる便利ツールの記事の第4弾です。
保守メンテが楽になりつつも、実戦で速攻で構築ができます!!
MVC編 -> テストコード編 に変更になりました。

目次

framework_repo.png

前回まで、serverless frameworkを使い、AWS Lambda上でExpress動かす記事まで書きました。
今回は、serverless frameworkで 「 lambda + APIGateway + DynamoDB 」 の構成で、Expressを動かしテストできる構成にします。

この記事でできるようになること

  • どこでも実行できるテストを書く

    • DynamoDBのテストをStabを使って実現する

DynamoDB stabを使う

  • ローカルでテストを行う場合に毎回AWSリソースにアクセスさせる必要はないため、スタブ化してダミーの値を返しつつ、どういう値で呼ばれたか、テスト内で監視を行うことができる。

    • ローカルやテストコードの実行、外部テスト(CircleCIなどのCIツール)で便利です。

前準備

  • テスト用のnpm moduleをinstallしておきます。
chai lambda-tester mocha sinon aws-sdk-mock supertest

構築する

  • ディレクトリ構成はこんな感じになりました。
$ tree
├── functions
│   ├── api
│   │   ├── api.js
│   └── routes
│       ├── hoge.js
│       └── fuga.js
├── models
│   ├── hoge-users.js
│   └── huga-users.js
├── serverless.yml
├── tests
│   ├── functions
│   │   ├── hoge-test.js
│   │   └── huga-test.js
│   ├── lib
│   │   └── dynamo-stub.js
│   └── models
│       ├── hoge-users-test.js
│       └── fuga-users-test.js

stub化する

  • 以下のようにすることで、dynamodbのgetやputなどはstub化されます。
tests/lib/dynamo-stub.js
'use strict'

const aws = require('aws-sdk-mock');
const path = require('path');
const chai = require('chai');
const should = chai.should();
aws.setSDK(path.resolve('node_modules/aws-sdk'));
process.on('unhandledRejection', console.dir);

function create() {
  aws.mock('DynamoDB.DocumentClient', 'put', (params, callback) => {
    callback(null, 'successfully put item in database');
  });

  aws.mock('DynamoDB.DocumentClient', 'get', (params, callback) => {
    callback(null, { Item: { request_token: 'UTPZgZTuL4cqYIjxvQFHFTdfuaIONLrp' , request_secret: '5RGcFAAAAAAA1x1BAAABXd68Yr0'} });
  });

  aws.mock('DynamoDB', 'describeTable', (params, callback) => {
    const desc = {
      Table: {
        AttributeDefinitions: [ [Object] ],
        TableName: params.TableName,
        ProvisionedThroughput: {
          NumberOfDecreasesToday: 0,
          ReadCapacityUnits: 1,
          WriteCapacityUnits: 1
        },
        TableSizeBytes: 0,
        ItemCount: 0,
        TableArn: 'arn:aws:dynamodb:ap-northeast-1:12345678:table/aaaa'
      }
    };
    callback(null, desc);
  });
}

module.exports = {
  create
};

api.js

  • api.jsはいつもと変わりなくroutesにあるfunctionを、APIのPathに合わせて読んであげます。
  • この場合は、api/v1/に対して2つのfunctionが呼ばれるようになっています。
functions/api/api.js
'use strict';

const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const hoge = require('../routes/hoge');
const fuga = require('../routes/fuga');

app.use(bodyParser.urlencoded({
  extended: true
}));
app.use(bodyParser.json());

app.use('/api/v1', hoge);
app.use('/api/v1', fuga);

exports.handler = require('express-on-serverless')(app);

if (process.env.NODE_ENV === 'test') {
  module.exports = app;
}

function

  • コントローラーに当たるfunctionは任意のPathがきたら実行されるようにしておきます
  • この場合は、api/v1/hogeに該当するAPIがきた場合に実行されます。
  • 戻り値はjsonなので、特にViewは作っていませんので、ここで返します。
functions/routes/hoge.js
const express = require('express');
const HogeUsers = require('../../models/hoge-users');
const router = express.Router();

router.post('/hoge', (req, res) => {
  let user = new HogeUsers();
  user.create(req)
    .then((hash) => {
      if (hash.error) {
        res.status(hash.code).json({
          message: hash.message
        });
      }
      else {
        res.json({
          message: hash.message
        });
      }
    })
    .catch((err) => {
      console.error('Internal Server Error');
      console.error(err);
      res.status(500).json({
        message: 'Internal Server Error'
      });
    });
});

module.exports = router;

モデル

  • Modelでは、DynamoDBをconstructorで自分で持っておき、データに対する操作を一任します。
  • またバリデーションなどのチェックも行います。
models/hoge-users.js
'use strict';

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

class HogeUsers {
  constructor() {
    this.tableName = `hoge-users-${process.env.STAGE}`;
    this.dynamo = new aws.DynamoDB.DocumentClient();
  }

  create(userParams) {
    return new Promise((resolve) => {

      let permittedParams = /* いろいろチェックする(省略) */

      const params = {
        TableName: this.tableName,
        Item: permittedParams,
        ConditionExpression: 'attribute_not_exists(xxxxxx)'
      };

      this.dynamo.put(params, (err) => {
        if (err) {
          console.error('dynamodb put error');
          console.error(err.message);
          resolve(this._throw(err));
        } else {
          resolve({
            error: null,
            message: null,
            code: 200
          });
        }
      });
    });
  }
}

module.exports = HogeUsers;

functionのテスト

  • ここでは、/api/v1/hogeに対してのテストを書きます。
  • この場合は、400が帰ってくることをテストします。
tests/functions/hoge-test.js
'use strict';

const LambdaTester = require('lambda-tester');
const api = require('../../functions/api/api');
const request = require('supertest');
const dynamoStub = require('../lib/dynamo-stub');

describe('functions hoge Tests', function () {
  this.timeout(0);
  beforeEach(function () {
    dynamoStub.create();
  });

  // /api/v1/hoge
  it('hoge fail post test', (done) => {
    request(api)
      .post('/api/v1/hoge')
      .send({deviceType: 'ios'})
      .expect(400, done);
  });
});

モデルのテスト

  • モデルのテストでは、モデルクラスのテストを書きます。
  • この場合は、バリデーションがうまくいっているか検証をしています(省略)
tests/models/hoge-users-test.js
'use strict';

const enc = require('../../lib/encrypt');
const stub = require('../lib/dynamo-stub');
let hogeUsers;
const HogeUsers = require('../../models/hoge-users');

describe('hoge users Tests', function () {
  this.timeout(0);
  beforeEach(function () {
    stub.create();
    hogeUsers = new HogeUsers();
  });


  it('hoge users validate HOGE', (done) => {
    /* いろいろチェックする(省略) <- のテストとか */
    done();
  });


テストを実行する

  • 最後に、package.jsonにテストを走らせるスクリプトを記述します。
package.json
"scripts": {
    "test": "export NODE_PATH=`npm root -g` && NODE_ENV=test mocha -t 100000 tests/**/*-test.js"
  }
  • 実行してみましょう
$ yarn test
  • テストが成功するとこんな感じになるはずです。

  hoge users Tests
    ✓ hoge name
    ✓ hoge users describeTable
    ✓ hoge users xxxxx HOGE
    ✓ hoge users xxxxx HOGE
    ✓ hoge users XXXXX check yyyy
    ✓ hoge users validate HOGE
    ✓ hoge users validate check yyyy

  fuga users Tests
    ✓ fuga name
    ✓ fuga users XXXXXX
    ✓ fuga users yyyy
    ✓ fuga users validate


  27 passing (1s)

✨  Done in 4.19s.

まとめ

  • aws-mockを使い、DynamoDBのstub化ができました。
  • modelクラスにDynamoDBを持つことで、functionのコード量が減りました。

さいごに

  • serverless frameworkの記事とても楽しかったです。
  • また便利なのがあれば紹介したいと思います。

続きを読む

フロントエンド環境+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/

続きを読む

Webアプリのスマホテストを自動化する

Webブラウザアプリケーションのスマホテストを外部サービスを使い自動化します。
AWS Device FarmRemote TestKit等のサービスが対象になりますが、今回はAWS Device Farmを利用します。
なお、CIツールはプラグインが存在するJenkinsを用います。

必要なもの

Jenkinsのインストール

terraformを用いてEC2上に、Jenkinsをセットアップします。
スクリプトでは、ユーザデータを用いて、Jenkinsのセットアップを実施しています。

参考レポジトリ

bash
$ git clone https://github.com/Thirosue/devicefarm-sample.git
$ cd devicefarm-sample/provisioning/
$ terraform apply -var-file=~/.aws/terraform.tfvars -var 'key_name=[keypair]'
ec2_jenkins.tf
variable "key_name" {}

provider "aws" {
  region = "ap-northeast-1"
}

data "aws_ami" "amazon_linux" {
  most_recent = true
  owners = ["amazon"]

  filter {
    name   = "architecture"
    values = ["x86_64"]
  }

  filter {
    name   = "root-device-type"
    values = ["ebs"]
  }

  filter {
    name   = "name"
    values = ["amzn-ami-hvm-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  filter {
    name   = "block-device-mapping.volume-type"
    values = ["gp2"]
  }
}

resource "aws_instance" "jenkins" {
  ami           = "${data.aws_ami.amazon_linux.id}"
  instance_type = "t2.micro"
  key_name      = "${var.key_name}"
  user_data = <<EOF
IyEvYmluL2Jhc2gKCndnZXQgLU8gL2V0Yy95dW0ucmVwb3MuZC9qZW5raW5zLnJl
cG8gaHR0cDovL3BrZy5qZW5raW5zLWNpLm9yZy9yZWRoYXQvamVua2lucy5yZXBv
CnJwbSAtLWltcG9ydCBodHRwOi8vcGtnLmplbmtpbnMtY2kub3JnL3JlZGhhdC9q
ZW5raW5zLWNpLm9yZy5rZXkKCnl1bSBpbnN0YWxsIC15IGdpdCBqZW5raW5zIGph
dmEtMS44LjAtb3BlbmpkawphbHRlcm5hdGl2ZXMgLS1zZXQgamF2YSAvdXNyL2xp
Yi9qdm0vanJlLTEuOC4wLW9wZW5qZGsueDg2XzY0L2Jpbi9qYXZhCgpjaGtjb25m
aWcgamVua2lucyBvbgovZXRjL2luaXQuZC9qZW5raW5zIHN0YXJ0CgpleGl0IDA=
EOF
}
userdata.sh
#!/bin/bash

wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat/jenkins.repo
rpm --import http://pkg.jenkins-ci.org/redhat/jenkins-ci.org.key

yum install -y git jenkins java-1.8.0-openjdk
alternatives --set java /usr/lib/jvm/jre-1.8.0-openjdk.x86_64/bin/java

chkconfig jenkins on
/etc/init.d/jenkins start

exit 0

Jenkinsの管理コンソールセットアップ

EC2のPublicIPを確認し、以下にアクセス

http://[IPv4 Public IP]:8080

アクセス後、Jenkinsがロックされているので、指示通り/var/lib/jenkins/secrets/initialAdminPasswordを確認し入力します。

Unlock Jenkins

Device Farm Plugin インストール

Jenkins-プラグインマネージャよりaws-device-farm-pluginをインストール。

DeviceFarmPlugin

DeviceFarm AccessKey/SecretKey設定

Jenkinsの管理画面に用意したIAMユーザのAccessKey/SecretKeyを設定

DeviceFarmIAMSetting

テストコード作成

以下を参考にテストコードを作成する。
参考レポジトリ はAppium+JUnitをgradleでbuildしている。

SampleTest.java
import com.codeborne.selenide.Configuration;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.Platform;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;

import java.io.File;
import java.net.URL;
import java.util.concurrent.TimeUnit;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class SampleTest {

    private RemoteWebDriver driver;

    @Before
    public void setUp() throws Exception {
        DesiredCapabilities capabilities = new DesiredCapabilities();
        capabilities.setPlatform(Platform.IOS);
        capabilities.setBrowserName("safari");
        driver = new RemoteWebDriver(new URL("http://127.0.0.1:4723/wd/hub"),
                capabilities);
        driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS);
    }

    @After
    public void tearDown() throws Exception {
        driver.quit();
    }

    public boolean takeScreenshot(final String name) {
        String screenshotDirectory = System.getProperty("appium.screenshots.dir", System.getProperty("java.io.tmpdir", ""));
        System.out.println(screenshotDirectory);
        File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
        return screenshot.renameTo(new File(screenshotDirectory, String.format("%s.png", name)));
    }

    @Test
    public void runTest() throws Exception {
        driver.get("https://www.google.co.jp/");
        Thread.sleep(1000);

        System.out.println(driver.getTitle());
        System.out.println(driver.getPageSource());

        driver.findElement(By.id("lst-ib")).sendKeys("AWS DeviceFarm");
        driver.findElement(By.id("tsbb")).click();
        assertTrue(takeScreenshot("index"));

        assertEquals("AWS DeviceFarm", driver.findElement(By.id("lst-ib")).getAttribute("value"));
        assertTrue(takeScreenshot("result"));
    }
}

build設定

参考レポジトリ はカスタムタスクのinstallZipでDeviceFarmへのuploadファイルを生成する。

スクリーンショット 2017-09-14 時刻 20.28.51.png

task installZip(dependsOn: ["clean", "packageTests", "installDist"]) << {
    new File("build/work").mkdir()
    new File('build/zip.sh') << 'cd build/work; zip -r zip-with-dependencies.zip .'
    copy{
        from "build/install/test/lib/"
        into "build/work/dependency-jars/"
    }
    copy{
        from "build/libs/test-1.0-SNAPSHOT-tests.jar"
        into "build/work/"
    }
    "chmod 755 build/zip.sh".execute().waitFor()
    "build/zip.sh".execute().waitFor()
    copy{
        from "build/work/zip-with-dependencies.zip"
        into "build/"
    }
}

DeviceFarmテスト設定

作成済みのDeviceFarm Project及びDeviceFarm Device Poolを選択して、buildで固めたzipファイルを指定する。

スクリーンショット 2017-09-14 時刻 20.34.19.png

テスト実行

Jenkinsでテスト実行後、DeviceFarmマネジメントコンソールへのレポートリンク等がJenkinsのテスト結果画面に表示されます。

スクリーンショット 2017-09-14 時刻 20.39.01.png

その他(開発中のテストについて)

54.244.50.32~54.244.50.63. の IP範囲をホワイトリストに登録 すれば、開発中資源もテストできそうです。

終わりに

利用事例が少なそうですが、かなり使えそうなサービスなので、今後積極的に利用していきたいと思います。

続きを読む

PackerでLaunch Configuration作るPlugin書いた

私はTerraformとPackerをよく使います。
しかしAutoScalingとLaunch Configurationの関係でいつも困ります。
卵が先か鶏が先かみたいな話で依存関係があります。
私はTerraformでLaunch Configurationを管理するのに疲れました。

そして別のアプローチ方法を取ることにしました。
それが今回作った、 packer-post-processor-amazon-launchconfiguration になります。
このプラグインはPackerのpost-processorの部分でLaunch Configurationを作成することができるプラグインです。
そしてオプションとしてAuto Scaling GroupのLaunch Configurationの値を更新することができます。
AMI作成 -> Launch Configuration作成 -> Auto Scaling Groupの更新が一つのJSONファイルで管理できます。
CIとも相性が良いように今後改善していきたいです。

https://github.com/a4t/packer-post-processor-amazon-launchconfiguration

Packerで管理することで変わること

TerraformとPackerを使った構成でよくある課題

  • Packerで作成したAMIをtfファイルに反映させる方法
  • UserData変わった際にLaunch Configurationが再作成されるが、Auto Scaling Groupとの依存関係

AMIは頻繁に更新した方がいいです。
OpenSSLの脆弱性とかあった際にはインスタンスの再起動がない場合、アップデートが反映されません。
気軽にLaunch Configurationは更新できるようにしておきたいです。
毎デプロイごとにAuto Scaling Groupを更新してもいいかもしれません。
Terraformで管理する場合はAMI作成 -> tfファイルにAMI反映という流れでしたが、その手間がなくなります。

使い方

こちらは基本的なAMI作成時のPackerのJSONです。

{
  "builders": [{
    "type": "amazon-ebs",
    "vpc_id": "vpc-xxxxxxxxxx",
    "subnet_id": "subnet-yyyyyyyyyy",
    "region": "ap-northeast-1",
    "source_ami": "ami-4af5022c",
    "instance_type": "t2.micro",
    "ssh_username": "ec2-user",
    "ami_name": "my-service-{{timestamp}}"
  }]
}

これに+αでこのような値を追加します。

{
  "builders": [{
  .
  .
  .
  }],
  "post-processors":[{
    "type": "amazon-launchconfiguration",
    "config_name_prefix": "my-service-",
    "instance_type": "c4.large",
    "keep_releases": 3
  }]

こちらの設定を簡単に説明すると

  • type

    • plugin名
  • config_name_prefix
    • Launch ConfigurationのPrefix、Prefixの後ろに作成時日付が入る(例: my-service-20170911075353)
  • instance_type
    • 説明不要
  • keep_releases
    • Launch Configurationのrotateの設定、config_name_prefixの後ろを見て古い方から削除していく

Auto Scaling Groupの更新

Auto Scaling Groupの更新は追加でオプションをつけます。

  "post-processors":[{
    "type": "amazon-launchconfiguration",
    "config_name_prefix": "my-service-",
    "instance_type": "c4.large",
    "keep_releases": 3,
    "auto_scaling_group_names": [
      "my-autoscaling-1",
      "my-autoscaling-2"
    ]
  }]

Pluginの導入方法

ここから自分にあった環境を拾ってきてください
https://github.com/a4t/packer-post-processor-amazon-launchconfiguration/releases

そしてPATHの通ったところに packer-post-processor-amazon-launchconfiguration を配置してください
~/.packer.d/plugins が最善かと思います。

おまけ

AMIもRotateしてどんどん削除した方がいいと思うので、こちらも使うといいかもしれません。
http://tech.sideci.com/entry/2016/08/18/130609

packer-post-processor-amazon-launchconfiguration
https://github.com/a4t/packer-post-processor-amazon-launchconfiguration

続きを読む

ECS運用のノウハウ

概要

ECSで本番運用を始めて早半年。ノウハウが溜まってきたので公開していきます。

設計

基本方針

基盤を設計する上で次のキーワードを意識した。

Immutable infrastructure

  • 一度構築したサーバは設定の変更を行わない
  • デプロイの度に新しいインフラを構築し、既存のインフラは都度破棄する

Infrastructure as Code (IaC)

  • インフラの構成をコードで管理
  • オーケストレーションツールの利用

Serverless architecture

  • 非常中型プロセスはイベントごとにコンテナを作成
  • 冗長化の設計が不要

アプリケーションレイヤに関して言えば、Twelve Factor Appも参考になる。コンテナ技術とも親和性が高い。

ECSとBeanstalk Multi-container Dockerの違い

以前に記事を書いたので、詳しくは下記参照。

Beanstalk Multi-container Dockerは、ECSを抽象化してRDSやログ管理の機能を合わせて提供してくれる。ボタンを何度か押すだけでRubyやNode.jsのアプリケーションが起動してしまう。
一見楽に見えるが、ブラックボックスな部分もありトラブルシュートでハマりやすいので、素直にECSを使った方が良いと思う。

ALBを使う

ECSでロードバランサを利用する場合、CLB(Classic Load Balancer)かALB(Application Load Balancer)を選択できるが、特別な理由がない限りALBを利用するべきである。
ALBはURLベースのルーティングやHTTP/2のサポート、パフォーマンスの向上など様々なメリットが挙げられるが、ECSを使う上での最大のメリットは動的ポートマッピングがサポートされたことである。
動的ポートマッピングを使うことで、1ホストに対し複数のタスク(例えば複数のNginx)を稼働させることが可能となり、ECSクラスタのリソースを有効活用することが可能となる。

※1: ALBの監視方式はHTTP/HTTPSのため、TCPポートが必要となるミドルウェアは現状ALBを利用できない。

アプリケーションの設定は環境変数で管理

Twelve Factor Appでも述べられてるが、アプリケーションの設定は環境変数で管理している。
ECSのタスク定義パラメータとして環境変数を定義し、パスワードやシークレットキーなど、秘匿化が必要な値に関してはKMSで暗号化。CIによってECSにデプロイが走るタイミングで復号化を行っている。

ログドライバ

ECSにおいてコンテナはデプロイの度に破棄・生成されるため、アプリケーションを始めとする各種ログはコンテナの内部に置くことはできない。ログはイベントストリームとして扱い、コンテナとは別のストレージで保管する必要がある。

今回はログの永続化・可視化を考慮した上で、AWSが提供するElasticsearch Service(Kibana)を採用することにした。
ECSは標準でCloudWatch Logsをサポートしているため、当初は素直にawslogsドライバを利用していた。CloudWatchに転送してしまえば、Elasticsearch Serviceへのストリーミングも容易だったからである。

Network (3).png

しかし、Railsで開発したアプリケーションは例外をスタックトレースで出力し、改行単位でストリームに流されるためログの閲覧やエラー検知が非常に不便なものだった。
Multiline codec plugin等のプラグインを使えば複数行で構成されるメッセージを1行に集約できるが、AWS(Elasticsearch Service)ではプラグインのインストールがサポートされていない。
EC2にElasticsearchを構築することも一瞬考えたが、Elasticsearchへの依存度が高く、将来的にログドライバを変更する際の弊害になると考えて止めた。
その後考案したのがFluentd経由でログをElasticsearch Serviceに流す方法。この手法であればFluentdでメッセージの集約や通知もできるし、将来的にログドライバを変更することも比較的容易となる。

Network (4).png

ジョブスケジューリング

アプリケーションをコンテナで運用する際、スケジュールで定期実行したい処理はどのように実現するべきか。
いくつか方法はあるが、1つの手段としてLambdaのスケジュールイベントからタスクを叩く方法がある(Run task)。この方法でも問題はないが、最近(2017年6月)になってECSにScheduled Taskという機能が追加されており、Lambdaに置き換えて利用可能となった。Cron形式もサポートしているので非常に使いやすい。

運用

ECSで設定可能なパラメータ

ECSコンテナインスタンスにはコンテナエージェントが常駐しており、パラメータを変更することでECSの動作を調整できる。設定ファイルの場所は /etc/ecs/ecs.config
変更する可能性が高いパラメータは下表の通り。他にも様々なパラメータが存在する。

パラメータ名 説明 デフォルト値
ECS_LOGLEVEL ECSが出力するログのレベル info
ECS_AVAILABLE_LOGGING_DRIVERS 有効なログドライバの一覧 [“json-file”,”awslogs”]
ECS_ENGINE_TASK_CLEANUP_WAIT_DURATION タスクが停止してからコンテナが削除されるまでの待機時間 3h
ECS_IMAGE_CLEANUP_INTERVAL イメージ自動クリーンアップの間隔 30m
ECS_IMAGE_MINIMUM_CLEANUP_AGE イメージ取得から自動クリーンアップが始まるまでの間隔 1h

パラメータ変更後はエージェントの再起動が必要。

$ sudo stop ecs
$ sudo start ecs

クラスタのスケールアウトを考慮し、ecs.configはUserDataに定義しておくと良い。
以下はfluentdを有効にしたUserDataの記述例。

#!/bin/bash
echo ECS_CLUSTER=sandbox >> /etc/ecs/ecs.config
echo ECS_AVAILABLE_LOGGING_DRIVERS=["fluentd"] >> /etc/ecs/ecs.config

CPUリソースの制限

現状ECSにおいてCPUリソースの制限を設定することはできない(docker runの--cpu-quotaオプションがサポートされていない)。
タスク定義パラメータcpuは、docker runの--cpu-sharesにマッピングされるもので、CPUの優先度を決定するオプションである。従って、あるコンテナがCPUを食いつぶしてしまうと、他のコンテナにも影響が出てしまう。
尚、Docker 1.13からは直感的にCPUリソースを制限ができる--cpusオプションが追加されている。是非ECSにも取り入れて欲しい。

ユーティリティ

実際に利用しているツールを紹介。

graph.png

ルートボリューム・Dockerボリュームのディスク拡張

ECSコンテナインスタンスは自動で2つのボリュームを作成する。1つはOS領域(/dev/xvda 8GB)、もう1つがDocker領域(/dev/xvdcz 22GB)である。
クラスタ作成時にDocker領域のサイズを変更することはできるが、OS領域は項目が見当たらず変更が出来ないように見える。

Screen_Shot_2017-08-31_at_11_00_03.png

どこから設定するかというと、一度空のクラスタを作成し、EC2マネージメントコンソールからインスタンスを作成する必要がある。

また、既存ECSコンテナインスタンスのOS領域を拡張したい場合は、EC2マネージメントコンソールのEBS項目から変更可能。スケールアウトを考慮し、Auto scallingのLaunch Configurationも忘れずに更新しておく必要がある。

補足となるが、Docker領域はOS上にマウントされていないため、ECSコンテナインスタンス上からdf等のコマンドで領域を確認することはできない。

デプロイ

ECSのデプロイツールは色々ある(ecs_deployerは自分が公開している)。

社内で運用する際はECSでCIを回せるよう、ecs_deployerをコアライブラリとしたCIサーバを構築した。

デプロイ方式

  • コマンド実行形式のデプロイ
  • GitHubのPushを検知した自動デプロイ
  • Slackを利用したインタラクティブデプロイ

Screen_Shot_2017-08-31_at_11_31_15.png

デプロイフロー

ECSへのデプロイフローは次の通り。

  1. リポジトリ・タスクの取得
  2. イメージのビルド
    • タグにGitHubのコミットID、デプロイ日時を追加
  3. ECRへのプッシュ
  4. タスクの更新
  5. 不要なイメージの削除
    • ECRは1リポジトリ辺り最大1,000のイメージを保管できる
  6. サービスの更新
  7. タスクの入れ替えを監視
    • コンテナの異常終了も検知
  8. Slackにデプロイ完了通知を送信

現在のところローリングデプロイを採用しているが、デプロイの実行から完了までにおよそ5〜10分程度の時間を要している。デプロイのパフォーマンスに関してはまだあまり調査していない。

ログの分類

ECSのログを分類してみた。

ログの種別 ログの場所 備考
サービス AWS ECSコンソール サービス一覧ページのEventタブ APIで取得可能
タスク AWS ECSコンソール クラスタページのTasksタブから”Desired task status”が”Stopped”のタスクを選択。タスク名のリンクから停止した理由を確認できる APIで取得可能
Docker daemon ECSコンテナインスタンス /var/log/docker ※1
ecs-init upstart ジョブ ECSコンテナインスタンス /var/log/ecs/ecs-init.log ※1
ECSコンテナエージェント ECSコンテナインスタンス /var/log/ecs/ecs-agent.log ※1
IAMロール ECSコンテナインスタンス /var/log/ecs/audit.log タスクに認証情報のIAM使用時のみ
アプリケーション コンテナ /var/lib/docker/containers ログドライバで変更可能

※1: ECSコンテナインスタンス上の各種ログは、CloudWatch Logs Agentを使うことでCloudWatch Logsに転送することが可能(現状の運用ではログをFluentdサーバに集約させているので、ECSコンテナインスタンスにはFluentdクライアントを構築している)。

サーバレス化

ECSから少し話が逸れるが、インフラの運用・保守コストを下げるため、Lambda(Node.js)による監視の自動化を進めている。各種バックアップからシステムの異常検知・通知までをすべてコード化することで、サービスのスケールアウトに耐えうる構成が容易に構築できるようになる。
ECS+Lambdaを使ったコンテナ運用に切り替えてから、EC2の構築が必要となるのは踏み台くらいだった。

トラブルシュート

ログドライバにfluentdを使うとログの欠損が起きる

ログドライバの項に書いた通り、アプリケーションログはFluentd経由でElasticsearchに流していたが、一部のログが転送されないことに気付いた。
構成的にはアプリケーションクラスタからログクラスタ(CLB)を経由してログを流していたが、どうもCLBのアイドルタイムアウト経過後の最初のログ数件でロストが生じている。試しにCLBを外してみるとロストは起きない。

Network (1).png

ログクラスタ(ECSコンテナインスタンスの/var/log/docker)には次のようなログが残っていた。

time="2017-08-24T11:23:55.152541218Z" level=error msg="Failed to log msg "..." for logger fluentd: write tcp *.*.*.*:36756->*.*.*.*:24224: write: broken pipe"
3) time="2017-08-24T11:23:57.172518425Z" level=error msg="Failed to log msg "..." for logger fluentd: fluent#send: can't send logs, client is reconnecting"

同様の問題をIssueで見つけたが、どうも現状のECSログドライバはKeepAliveの仕組みが無いため、アイドルタイムアウトの期間中にログの送信が無いとELBが切断してしまうらしい(AWSサポートにも問い合わせた)。

という訳でログクラスタにはCLBを使わず、Route53のWeighted Routingでリクエストを分散することにした。

Network (2).png

尚、この方式ではログクラスタのスケールイン・アウトに合わせてRoute 53のレコードを更新する必要がある。
ここではオートスケールの更新をSNS経由でLambdaに検知させ、適宜レコードを更新する仕組みを取った。

コンテナの起動が失敗し続け、ディスクフルが発生する

ECSはタスクの起動が失敗すると数十秒間隔でリトライを実施する。この時コンテナがDockerボリュームを使用していると、ECSコンテナエージェントによるクリーンアップが間に合わず、ディスクフルが発生することがあった(ECSコンテナインスタンスの/var/lib/docker/volumesにボリュームが残り続けてしまう)。
この問題を回避するには、ECSコンテナインスタンスのOS領域(※1)を拡張するか、コンテナクリーンアップの間隔を調整する必要がある。
コンテナを削除する間隔はECS_ENGINE_TASK_CLEANUP_WAIT_DURATIONパラメータを使うと良い。

※1: DockerボリュームはDocker領域ではなく、OS領域に保存される。OS領域の容量はデフォルトで8GBなので注意が必要。

また、どういう訳か稀に古いボリュームが削除されず残り続けてしまうことがあった。そんな時は次のコマンドでボリュームを削除しておく。

# コンテナから参照されていないボリュームの確認
docker volume ls -f dangling=true

# 未参照ボリュームの削除
docker volume rm $(docker volume ls -q -f dangling=true)

ECSがELBに紐付くタイミング

DockerfileのCMDでスクリプトを実行するケースは多々あると思うが、コンテナはCMDが実行された直後にELBに紐付いてしまうので注意が必要となる。

bundle exec rake assets:precompile

このようなコマンドをスクリプトで実行する場合、アセットがコンパイル中であろうがお構いなしにELBに紐付いてしまう。
時間のかかる処理は素直にDockerfile内で実行した方が良い。

続きを読む