S3とLambdaとCircleCIでほぼ無料の社内向けデザインテンプレートサイトを構築する

##はじめに
前回iOSでCircleCIを使って面白かった（[大晦日〜正月にiOSでCircleCIを試したので振り返ってみた](http://qiita.com/saku/items/9c093535967e4452a8d0)）ので今度は新しくCircleCIとAWS Lambdaを使った記事を書いてみました。

今回の内容は会社でデザインテンプレートを作っていて、社内のみんなで共有したいと考えている方や、Lambdaを使って面白いことしてみたい！と考えている人に向けたものになります。

また今回のLambda functionのサンプルコードをGithubにあげていますので興味のある方はforkして煮るなり焼くなり好きにしてくださいm(_ _)m
https://github.com/saku/lambdaUnzipper

あと背景が長いので本題から入りたい方は[こちらからジャンプ](#%E5%AE%9F%E6%96%BD%E6%89%8B%E9%A0%86)してください。

##背景
###ことのはじまり
Rettyでも１年前くらいからCSSの管理を意識しはじめデザインテンプレートを作りました。
この時はデザイナーさん達が自分たちの環境でsassコンパイルしたりKSSのコマンド実行を変更のたびに行ってました。
とても手が掛かりそうなのでfirst stepとしてgulpを導入し、scssとhtmlの変更をwatchして自動的にリロードがかかるようにしました。

デザイナーさんのボトルネックを解消すると、今度はデザイナーさんがGithubにあげた後にいちいちエンジニアが手元にpullしてビルドして確認するといったことが必要になり面倒になりました。
まずは社内にあったMac miniにJenkinsを立てて適当に5分に一回のペースでGithubのmasterの最新をとってきてビルドして問題は解消しました。

###CircleCIを導入
Jenkinsを導入してからしばらくすると、短いサイクルでJobを回しすぎたのでディスク溢れを起こしたり、hubotとの連携もできなかったり、そもそも社内のサーバ自体を減らしたいといった問題を感じるようになってMac miniのJenkinsからCircleCIに乗り換えました。

それ自体はそんなに難しいことじゃなく、メリットもありました。例えばGithubのhookがとれているので必要最小限のビルド回数で済むとか、Artifact機能によりビルドした結果の内容を保管しておけるとか。
ただデメリットもあり、テンプレートを見るためにCircleCIの登録と対象のGithubリポジトリへの権限を持つ必要があったり、テンプレートを見るためのURLを固定にできなくなるという問題がありました。

最初のうちはそもそもデザインテンプレートを見る人も少なかったので許容できていたものの、徐々に使う人が増え始めてこれでは困るようになりました。

###最終的にS3&Lambdaな環境に移行
そこで上記要件を満たしつつもう少しイケてる環境をつくれないかということで今回のネタの内容にいきつきました。

思惑としてはS3に成果物をzipで放り投げて、それを解凍したものをS3のstatic web hostingの機能を使って社内に公開すれば良いという感じです。

##実施手順
今回のやることを簡単に表現すると「S3に成果物をzipで放り投げて、それを解凍する」ということなので以下の３ステップで実現できそうです。
1. CircleCIからS3に成果物をzipでアップロード
2. S3からzipのアップロードを検知してLambdaを起動
3. 対象のzipをLambdaで出力用のbucketに解凍

LambdaはS3のイベントをListenして起動させることができるのでLambdaにzipの解凍させれば目的は達成です。
今回は説明のため、上記の順番とは逆順に作業を進めていきます。

###STEP1：対象のzipをLambdaで出力用のbucketに解凍
最後のステップであるLambdaのzip解凍部分を先に作ります。
####regionを決める
まずはS3とLambda functionを作るregionを決めます。
今回は後でS3を作るときのregionと同じにしておく必要あるのとTokyo RegionでLambdaがない（2015/06/12現在）ので、地図的に日本に近そうなオレゴンを選択しました。

####IAMのRoleを作る
Lambda functionを作るにあたってS3の操作を行うのでLambda用のIAM Roleを作る必要があります。
zipアップロード用とHTML表示用の２つのS3 bucketを操作できるようにすればよいので下記のようなポリシーを持つRoleを作成します

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:*"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": [
        "arn:aws:s3:::design-template-zip/*",
        "arn:aws:s3:::design-template-html/*"
      ]
    }
  ]
}
```

S3 bucketは１つでもできますが、S3のevent通知はS3のフォルダ単位にかけられないため、アップロード先と解凍先を同じにすると解凍したときにもLambdaへのイベント通知がきて起動してしまいます。

このあたりテストしてたときに大量にLambda functionが起動してビビりました（^^;

####Lambda functionを作る
IAM roleを作成したら、それをもとにfunctionを作ります。
AWS Lambda自体でもいくつかのテンプレートを用意してくれているので、S3のものをベースに作ります。

参考サイトを見ながら最終的には下記のようなunzip用のスクリプトを作りました。
今回はデザインテンプレート用途なのでS3にファイルを解凍するときに全てのファイルにキャッシュ無効化のための工夫（CacheControlとExpiresの指定）をしてあります。

```js
var aws = require('aws-sdk');
var s3 = new aws.S3();
var mime = require('mime-types');
var async = require('async');
var JSZip = require('jszip');

var unzipBucket = 'design-template-html';

exports.handler = function(event, context) {
    console.log('Received event:', JSON.stringify(event, null, 2));

    // Get the object from the event and show its content type
    var bucket = event.Records[0].s3.bucket.name;
    var key = event.Records[0].s3.object.key;
    s3.getObject({Bucket:bucket, Key:key},
        function(err,data) {
            if (err) {
                console.log("Error getting object " + key + " from bucket " + bucket +
                    ". Make sure they exist and your bucket is in the same region as this function.");
                context.fail('Error', "Error getting file: " + err);
                return;
            }

            if (data.ContentType != 'application/zip') {
                console.log('not zip!:' + data.ContentType);
                context.succeed();
                return;
            }

            console.log('yeah!! zip!');
            var zip = new JSZip(data.Body);

            async.forEach(zip.files, function (zippedFile) {
                var f = zippedFile;
                console.log("filename:" + f.name);
                var mimetype = mime.lookup(f.name);

                if (mimetype == false) {
                    mimetype = 'application/octet-stream';
                }
                console.log("mimetype:" + mimetype);

                s3.putObject({
                        Bucket: unzipBucket,
                        Key: f.name,
                        Body: new Buffer(f.asBinary(), "binary"),
                        ContentType: mimetype,
                        CacheControl: 'no-cache',
                        Expires: 0
                    }, function(err, data) {
                        if (err) {
                            context.fail(err, "unzip error");
                        }
                        console.log("success to unzip:" + f.name);
                    }
                );
            }, function (err) {
                if (err) {
                    context.fail(err, "async forEach error");
                }
                console.log('all finish!');
                context.succeed();
            });
        }
    );
};
```

利用したライブラリは以下のとおりです。

```json
{
  "name": "lambda-unzipper-set",
  "version": "1.0.0",
  "description": "unzip Design Template zip file, from CircleCI",
  "author": "saku",
  "license": "MIT",
  "devDependencies": {
    "async":      "1.2.0",
    "jszip":      "2.5.0",
    "mime-types": "2.0.13"
  }
}
```

これで解凍するロジック部分は完成です。

###STEP2：S3からzipのアップロードを検知してLambdaを起動
さきほど説明したとおりzip用のbucketとHTML用のbucketの２つ作ります。
２つともLambdaから触るためRegionはオレゴンで作ります。

####zipアップロード用のS3を作成する
Lambdaの連携のため、ファイルアップロード時のイベント設定を行います。
このような感じでイベントを設定します、とても簡単にできてしまってビックリですがこれだけでファイルがあがるとLambdaが起動してくれます！

![スクリーンショット 2015-06-11 20.25.17.png](https://qiita-image-store.s3.amazonaws.com/0/34824/462120a1-9a23-e5ff-03a4-c4e9f264cf33.png)

####HTML表示用のS3を作成する
実際にデザインテンプレートを解凍して配置するbucketです。
社内公開だけにするのでstatic web hostingとbucket policyの設定を行います。

static web hostingは図のようにオンにするだけでOK。

![スクリーンショット 2015-06-11 20.28.08.png](https://qiita-image-store.s3.amazonaws.com/0/34824/ea37c1b5-80a6-ea1a-f8c5-96d041418b33.png)

bucket policyは下記のように設定します。これでIP1, IP2には社内で使っているIPを設定すればOKです。
一般公開しても問題なければpolicyの設定自体不要です。

![スクリーンショット 2015-06-11 20.28.37.png](https://qiita-image-store.s3.amazonaws.com/0/34824/56107bd9-3af7-18a3-6fd9-ac10baf2b56d.png)


```json
{
  "Version": "2008-10-17",
  "Statement": [
    {
      "Sid": "AllowPaticularRead",
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::design-template-html/*",
      "Condition": {
        "IpAddress": {
          "aws:SourceIp": ["IP1", "IP2", "..."]
        }
      }
    }
  ]
}
```

###STEP3：CircleCIからS3に成果物をzipでアップロード
さて、ようやく最後に一番最初に動かす部分にとりかかります。
circle.yml の内容を書き換えていくわけですが、masterにマージされたときだけみんなが見るテンプレートを更新したいのでdeploymentの項に記載していきます。
一部抜粋して、下記のような部分をかきました。

```yml
（...中略）
deployment:
  production:
    branch: master
    commands:
      - sh zipAndUploadArtifacts.sh
      - gulp slack
```

shellの中ではzipの作成とrubyのライブラリでS3へのファイルアップロードを行っています。

```sh
#!/bin/sh

zip -r uploadFiles.zip index.html assets compiled page-sample styleguide
ruby uploadZipS3.rb
```

```ruby
require 'bundler/setup'
require 'aws-sdk-v1'

s3 = AWS::S3.new()

filename = "uploadFiles.zip"
bucket = s3.buckets['design-template-zip']
target = bucket.objects[filename]
target.write(file: filename, content_type: "application/zip")

```

それからS3にファイルをアップロードするために認証を通す必要があるのでIAMでS3アップロード用のユーザを作ります。

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject"
      ],
      "Resource": [
        "arn:aws:s3:::design-template-zip/*"
      ]
    }
  ]
}
```

また、CircleCIではAWSの認証情報は設定画面で書いておくとビルドのステップで`~/.aws/config`と`~/.aws/credentials`を勝手に作ってくれるのでファイルに認証情報を書かなくて良くていいです。

![スクリーンショット 2015-06-11 20.39.00.png](https://qiita-image-store.s3.amazonaws.com/0/34824/655be098-f682-c83e-18fb-850a8e4a6a85.png)

これで全てが整いました。

##いざ、実行！
全ての準備が整ったらCircleCIでビルドを走らせます。
ファイルのアップロードと同時にfunctionが実行されてログが生成されるのをLambdaのログ画面で確認しましょう！

特に問題がなければ無事design-template-htmlのbucketにファイルが解凍されてブラウザアクセスできるようになります。

##まとめ
CircleCI, AmazonS3, Amazon Lambdaを使って静的URLで社内環境からのみアクセスできるデザインテンプレートのページを作りました。
GithubのPRやマージと同時に実行されるのでSlackの通知も作ればタイムリーにデザイナーや開発者に対してデザインテンプレートの更新を通知して確認できる環境ができました。

どれも無料枠のあるサービスなのでうまくすればほぼ無料にて外部サーバでこういった環境を作ることができます。

###今回使った技術
* AWS Lambda
  * IAM Role
  * function
      * unzip
* Circle CI
  * deployment setting
      * zip
      * S3 upload
  * IAM User
      * S3 upload authorization
* S3
  * static web hosting
      * bucket policy
  * event setting
      * Lambda

###参考サイト
* [初めてのJavaScript、初めてのAWS Lambda](http://dev.classmethod.jp/cloud/aws/1st-javascript-1st-lambda/)
* [S3にアップロードされたファイルがzipならlambdaで自動解凍する](http://qiita.com/bump_of_kiharu/items/644d8614b99e86c4b188)
* [Amazon S3のStatic Web Hostingにアクセス制限をかけて使ってみる](http://kray.jp/blog/amazon_s3_static_web_hosting/)
* [AWS S3 document](http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html)
* [AWS JavaScript SDK document](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#putObject-property)

##おわりに
AWS Lambda初めて触ってみたけどとてもおもしろかったです。
イベントをトリガにして自分の作った処理でAWS上のリソースをごにょごにょできるということで夢も広がりそうですね。

それともくもくiOS勉強会というのも隔週で定期的にやっていますので、もし詳しい話聞きたい！実際に動いてる所みたい！ということがあれば是非遊びにきてくださいませー。

もくもくiOS勉強会@Retty
http://connpass.com/series/748/