AWSでフカセ暗号ジェネレータを完全サーバレス化した話

#はじめに
みなさま、[フカセ暗号ジェネレータ](http://fukase-no-owari.net/)をご存知でしょうか？
http://fukase-no-owari.net/

自分はこれの作者なんですけど、一時期フカセ暗号ジェネレータはツイッターを中心にちょっとだけバズったんですね。

[Gigazine](http://gigazine.net/news/20150811-fukase-no-owari/) とか [ねとらぼ](http://nlab.itmedia.co.jp/nl/articles/1508/10/news125.html) とか [exciteニュース](http://www.excite.co.jp/News/bit/E1439196586330.html) とかにも取り上げられたし、SEKAI NO OWARIの深瀬さんご本人にも認識していただいたんですね。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">なんか凄いの出来てるし！！笑 <a href="http://t.co/QRsVO0BUI9">http://t.co/QRsVO0BUI9</a></p>&mdash; Fukase(SEKAINOOWARI) (@fromsekaowa) <a href="https://twitter.com/fromsekaowa/status/630674144082792448">2015, 8月 10</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

テレビにも紹介されたりしました。
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">今日の18:30から、フジテレビ・みんなのニュースの「ネットNAVI」というコーナーで”フカセ暗号ジェネレーター”が紹介されるので、是非見てみてください (・∪・) <a href="http://t.co/Yb1SyQpKhn">pic.twitter.com/Yb1SyQpKhn</a></p>&mdash; くず (@kzhrknt) <a href="https://twitter.com/kzhrknt/status/630976179034439680">2015, 8月 11</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

当時はさくらのレンタルサーバ１台で全てサービスを運用していたのですが、バズってアクセスがとんでもなく跳ね上がってしまい、とてもレンタルサーバ１台では賄えなくなったので月4000円くらいするさくらVPSを借りて最初のバズ期を乗り越えていました。

あれから半年ほどたった今、アクセスは激減し、負荷の割に月4000円のサーバではスペックが有り余ってしまったのでどうにかしようと思い、もともと興味があったAWSに乗り換えることを決意したのでそのメモとして残そうと思います。

#Lambdaでフカセ暗号を生成
VPSではImageMagickを使って暗号の生成を行っていたのですが、AWSではLambdaを使います。
このLambdaのFunctionがやってることはこんな感じ。

- 暗号化出来ない記号などの文字をバリデート
- ある規則に従って一文字ずつ分割された画像を連結
- 連結させた画像に枠を付けていい感じに余白を作る
- 完成した画像をS3に保存する

コードはこんな感じで作りました。（nodejsあまり書いたこと無くて汚いですが…）

```
'use strict';
var async = require('async');
var japanese = require('hepburn');
var aws = require('aws-sdk');
var s3 = new aws.S3({params: {Bucket: 'fukase-no-owari.net'}});
var gm = require('gm').subClass({ imageMagick: true });
var imageType = 'png';
var w = 900;
var h = 600;

function isForbidden(char) {
    if (char === "0" || char === "1" || char === "2" || char === "3" || char === "4" || char === "5" || char === "6" || char === "7" || char === "8" || char === "9" || char === '.' || char === ',' || char === '/' || char === ':' || char === ';' || char === '-' || char === '=' || char === '"' || char === '\\' || char === '#' || char === '$' || char === '%' || char === '&' || char === '(' || char === ')' || char === '^' || char === '~' || char === '¥' || char === '|' || char === '[' || char === ']' || char === '{' || char === '}' || char === '@' || char === '`' || char === '+' || char === '*' || char === '_' || char === '?' || char === '<' || char === '>') {
           return true;
       }
    return false;
}

function convertChar (string) {
    for (var i = 0; i < string.length; i++) {
        if (string[i] === '?') {
            string[i] = '_';
        } else if (string[i] === ' ') {
            string[i] = '^';
        } else if (isForbidden(string[i])) {
            return false;
        }
    }
    return string;
}

exports.handler = function(event, context) {
    var text = japanese.fromKana(event.text).toLowerCase();
    console.log('text : ', text);
    var list = text.split('');
    list = convertChar(list);
    if (list === false) {
        context.fail("use forbidden char");
    }
    console.log('list : ', list);
    text = list.join('');

    if (list.length > 20) {
        context.fail("error");
    }

    var width;
    var height;
    if (list.length < 10) {
        width = 880;
        height = 380;
    } else {
        width = 880;
        height = 230;
    }

    s3.getObject({
        Bucket: 'fukase-no-owari.net',
        Key: 'resimg/' + text + '.png'
    }, function(error, res) {
        if (!error) {
            context.done(null, text);
        }
    });

    console.log('tobuffer');
    var firstChar = list.shift();
    if (firstChar === '?') {
        firstChar = '_';
    } else if (firstChar === ' ') {
        firstChar = '^';
    }
    gm('./img/' + firstChar + '.png').toBuffer('PNG', function(err, buffer) {
        if (err) {
            context.done(err);
        } else {
            var resultbuff = buffer;
            async.eachSeries(list, function(data, callback) {
                console.log('composite');
                gm(resultbuff).append('./img/' + data + '.png', true).toBuffer('PNG', function(error, res) {
                    resultbuff = res;
                    callback(error);
                });
            }, function(err1) {
                if (err1) {
                    context.done(err1);
                } else {
                    async.waterfall([
                        function resizeImg(callback) {
                            console.log('resize');
                            gm(resultbuff).resize(width, height, '!').toBuffer('PNG', function(e, r) {
                                if (e) {
                                    callback(e);
                                } else {
                                    resultbuff = r;
                                    callback();
                                }
                            });
                        },
                        function addBorderImg(callback) {
                            console.log('addBorder');
                            gm(resultbuff).borderColor('white').border((w - width)/2, (h - height)/2).toBuffer('PNG', function(e, r) {
                                if (e) {
                                    callback(e);
                                } else {
                                    resultbuff = r;
                                    callback();
                                }
                            });
                        },
                        function addText(callback) {
                            gm(resultbuff).fontSize(35).drawText(270, 590, 'Generated by http://fukase-no-owari.net/').toBuffer('PNG', function(e, r) {
                                if (e) {
                                    callback(e);
                                } else {
                                    resultbuff = r;
                                    callback();
                                }
                            });
                        },
                        function putObject(callback) {
                            console.log('put object');
                            s3.putObject({
                                Bucket: 'fukase-no-owari.net',
                                Key: 'resimg/' + text + '.png',
                                Body: resultbuff,
                                ContentType: 'image/png',
                                ACL: 'public-read'
                            }, callback);
                        }
                    ], function (err2, result) {
                        if (err2) {
                            context.done(err2);
                        } else {
                            context.done(null, text);
                        }
                    });
                }
            });
        }
    });
};
```

#Lambdaでフカセ暗号をツイッターに投稿
フカセ暗号ジェネレータには「Twitterでquestionする」ボタンがあって、これを押すと暗号の画像がTwitter上で展開される形で投稿されます。
Twitter上で展開される形で投稿するには、あらかじめその画像がTwitter上にアップロードされている状態でなければいけません。
アップロードするのは誰でもいいので、[@fukaseowari](https://twitter.com/fukaseowari)という暗号画像アップロード用アカウントが「Twitterでquestionする」ボタンを押す度に対象のS3上にある画像をアップロードします。
ということでやってることは単純。

- S3から対象の画像を取得してTwitterに投稿

以上です。
コードはこんな感じ。

```
'use strict';
var async = require('async');
var aws = require('aws-sdk');
var Twitter = require('twitter');
var s3 = new aws.S3({params: {Bucket: 'fukase-no-owari.net'}});

exports.handler = function(event, context) {
    var image = event.image;
    console.log("image : " + image);
    async.waterfall([
        function getImage(callback) {
            s3.getObject({
                Bucket: 'fukase-no-owari.net',
                Key: 'resimg/' + image + '.png'
            }, callback);
        },
        function uploadImage(data, callback) {
            var client = new Twitter({
                consumer_key: '',
                consumer_secret: '',
                access_token_key: '',
                access_token_secret: ''
            });
            var params = {media: data.Body};
            client.post('media/upload', params, function(error, result, response){
                if (!error) {
                    console.log(result);
                    // Lets tweet it
                    var status = {
                        status: "question\nGenerated by http://fukase-no-owari.net/\n#フカセ暗号ジェネレーター",
                        media_ids: result.media_id_string
                    }
                    client.post('statuses/update', status, function(error, tweet, response){
                        if (!error) {
                            console.log(tweet);
                            callback(error, tweet.id_str);
                        } else {
                            callback(error, result);
                        }
                    });
                } else {
                    console.log(error);
                    callback(error);
                }
            });
        }
    ], function (err, result) {
        if (err) {
            context.done(err);
        } else {
            context.done(null, result);
        }
    });

}
```

#S3で静的ウェブサイトホスティング
トップページのindex.htmlやjsとかcssとか画像ファイルをホスティングするためのファイルをS3上に用意します。
[このへん](http://www.awsmicrosite.jp/s3-hosting/)を見ながら設定すれば簡単にできました。

#Route53でお名前.comのドメインをS3に向ける
こちらのサイトを参考にさせて頂きました。
http://www30304u.sakura.ne.jp/blog/?p=3154

#まとめ
ほんとにメモ程度にサラッとしか書いてないですが、これでかなりサーバ代の節約ができたかなと思うので、これからウェブサービス作るときは積極的にAWS使っていきたいと思います。