AWS
S3
route53
lambda

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

More than 1 year has passed since last update.

はじめに

みなさま、フカセ暗号ジェネレータをご存知でしょうか?
http://fukase-no-owari.net/

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

Gigazine とか ねとらぼ とか exciteニュース とかにも取り上げられたし、SEKAI NO OWARIの深瀬さんご本人にも認識していただいたんですね。

テレビにも紹介されたりしました。


当時はさくらのレンタルサーバ1台で全てサービスを運用していたのですが、バズってアクセスがとんでもなく跳ね上がってしまい、とてもレンタルサーバ1台では賄えなくなったので月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という暗号画像アップロード用アカウントが「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上に用意します。
このへんを見ながら設定すれば簡単にできました。

Route53でお名前.comのドメインをS3に向ける

こちらのサイトを参考にさせて頂きました。
http://www30304u.sakura.ne.jp/blog/?p=3154

まとめ

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