ランサーズ等のサービスを開発・運用する中で得た知識やノウハウを紹介しています。

Labels:  AWS, 社内サーバー 投稿者:adachin

8年間お疲れ様でした!社内サーバーをAWSに移行したお話

みなさんお久しぶりです。SREのadachin0817です。
今回は歴史あるランサーズの社内サーバーをAWSに2ヶ月ですべて移行することができました。

どのような取り組みとどのように運用しているのか、
サーバー触っているエンジニアさんは気になるところだと思うので公開します!


ランサーズ開発環境の歴史

2011年から開発環境がラックサーバーで動いており、VMware ESXiで仮想サーバー構築して検証していたようです。
歴史を感じる…

そこから数年経って、Dockerとなり、現在の開発環境になるわけですが、
この数年で、色んなエンジニアが稼働しているVMにバッチや、
その他ツール等運用していくことで誰が管理しているか分からず、技術的負債や属人化が見えるようになっていきました。


クラウドに移行するメリットとデメリット

以下、移行することのメリットとデメリットを記載してみました。

  • 年に一回ゴールデンウィーク中に実施される法定停電時のサーバー停止、および起動作業が要らなくなる
  • 技術的負債をなくせる/整理できる
  • 運用コスト削減/サーバー電気代0円へ
  • SREチームが管理することで属人化がなくなる
  • AWSのコストが上がる

では現時点で動いているもので必要なサーバーを洗い出してみました。

  • バッチ5つ(シェルスクリプト/PHP)
  • 社内DNSサーバー
  • dev X 10台(新機能確認用コンテナ)
  • 弥生会計
  • 社内ツール 1つ(Rails)

バッチ

バッチは本番で運用しているサーバーがあるので、5つを移行するだけでしたが、
シェルスクリプト内でC言語でコンパイルしているものがあったりしてなかなか手強かったのを思い出します。


社内DNSサーバー

こちらはRoute53にすべて移行しました。登録するレコードが多く、
aws-cliで一気に登録すると楽なので、以下にサンプルを貼りますので参考にしてみてください。

  • 53.json
{
  "Comment" : "",
  "Changes" : [
    {"Action":"CREATE","ResourceRecordSet":{"Name":"dev0.xxxxxx.jp","Type":"A","AliasTarget":{"HostedZoneId":"xxxxxxxxxxxx","DNSName":"dualstack.rproxy-xxxxxxxx.ap-northeast-1.elb.amazonaws.com","EvaluateTargetHealth":false}}},
    {"Action":"CREATE","ResourceRecordSet":{"Name":"dev1.xxxxxx.jp","Type":"A","AliasTarget":{"HostedZoneId":"xxxxxxxxxxxx","DNSName":"dualstack.rproxy-xxxxxxxx.ap-northeast-1.elb.amazonaws.com","EvaluateTargetHealth":false}}},
    {"Action":"CREATE","ResourceRecordSet":{"Name":"dev2.xxxxxx.jp","Type":"A","AliasTarget":{"HostedZoneId":"xxxxxxxxxxxx","DNSName":"dualstack.rproxy-xxxxxxxx.ap-northeast-1.elb.amazonaws.com","EvaluateTargetHealth":false}}},
    {"Action":"CREATE","ResourceRecordSet":{"Name":"dev3.xxxxxx.jp","Type":"A","AliasTarget":{"HostedZoneId":"xxxxxxxxxxxx","DNSName":"dualstack.rproxy-xxxxxxxx.ap-northeast-1.elb.amazonaws.com","EvaluateTargetHealth":false}}},
    {"Action":"CREATE","ResourceRecordSet":{"Name":"dev4.xxxxxx.jp","Type":"A","AliasTarget":{"HostedZoneId":"xxxxxxxxxxxx","DNSName":"dualstack.rproxy-xxxxxxxx.ap-northeast-1.elb.amazonaws.com","EvaluateTargetHealth":false}}},
    {"Action":"CREATE","ResourceRecordSet":{"Name":"dev5.xxxxxx.jp","Type":"A","AliasTarget":{"HostedZoneId":"xxxxxxxxxxxx","DNSName":"dualstack.rproxy-xxxxxxxx.ap-northeast-1.elb.amazonaws.com","EvaluateTargetHealth":false}}},
    {"Action":"CREATE","ResourceRecordSet":{"Name":"dev6.xxxxxx.jp","Type":"A","AliasTarget":{"HostedZoneId":"xxxxxxxxxxxx","DNSName":"dualstack.rproxy-xxxxxxxx.ap-northeast-1.elb.amazonaws.com","EvaluateTargetHealth":false}}},
 
    {"Action":"CREATE","ResourceRecordSet":{"Name":"dev0-hoge.xxxxxx.jp","Type":"A","AliasTarget":{"HostedZoneId":"xxxxxxxxxxxx","DNSName":"dualstack.rproxy-xxxxxxxx.ap-northeast-1.elb.amazonaws.com","EvaluateTargetHealth":false}}},
    {"Action":"CREATE","ResourceRecordSet":{"Name":"dev1-hoge.xxxxxx.jp","Type":"A","AliasTarget":{"HostedZoneId":"xxxxxxxxxxxx","DNSName":"dualstack.rproxy-xxxxxxxx.ap-northeast-1.elb.amazonaws.com","EvaluateTargetHealth":false}}},
    {"Action":"CREATE","ResourceRecordSet":{"Name":"dev2-hoge.xxxxxx.jp","Type":"A","AliasTarget":{"HostedZoneId":"xxxxxxxxxxxx","DNSName":"dualstack.rproxy-xxxxxxxx.ap-northeast-1.elb.amazonaws.com","EvaluateTargetHealth":false}}},
    {"Action":"CREATE","ResourceRecordSet":{"Name":"dev3-hoge.xxxxxx.jp","Type":"A","AliasTarget":{"HostedZoneId":"xxxxxxxxxxxx","DNSName":"dualstack.rproxy-xxxxxxxx.ap-northeast-1.elb.amazonaws.com","EvaluateTargetHealth":false}}},
    {"Action":"CREATE","ResourceRecordSet":{"Name":"dev4-hoge.xxxxxx.jp","Type":"A","AliasTarget":{"HostedZoneId":"xxxxxxxxxxxx","DNSName":"dualstack.rproxy-xxxxxxxx.ap-northeast-1.elb.amazonaws.com","EvaluateTargetHealth":false}}},
    {"Action":"CREATE","ResourceRecordSet":{"Name":"dev5-hoge.xxxxxx.jp","Type":"A","AliasTarget":{"HostedZoneId":"xxxxxxxxxxxx","DNSName":"dualstack.rproxy-xxxxxxxx.ap-northeast-1.elb.amazonaws.com","EvaluateTargetHealth":false}}},
    {"Action":"CREATE","ResourceRecordSet":{"Name":"dev6-hoge.xxxxxx.jp","Type":"A","AliasTarget":{"HostedZoneId":"xxxxxxxxxxxx","DNSName":"dualstack.rproxy-xxxxxxxx.ap-northeast-1.elb.amazonaws.com","EvaluateTargetHealth":false}}},
   ]
}
  • run
$ jq . 53.json
$ aws route53 change-resource-record-sets --hosted-zone-id xxxxxxxx --change-batch file://53.json --profile aws
{
    "ChangeInfo": {
        "Id": "/change/xxxxxxxxxxxx",
        "Status": "PENDING",
        "SubmittedAt": "2019-05-07xxxxxxxxxxxxxxxx",
        "Comment": ""
    }
}

dev X (10台)/弥生会計

dev X(10台)に関しては、社内でエンジニア以外でも確認できるランサーズの検証用環境となります。
ローカルの開発環境との差異をなくすため、EC2上にdockerをインストールして構築しました。
ALBでは各ドメインごとにリスナールールを使用してリクエストをターゲットにルーティングしています。

また、インスタンスが立ち上がっているとコストがかかるため、CloudWatch Eventsで深夜に止めて、
Slack上で個人で使いたいときに予約して起動するようAPI Gateway、Lambda、Slack commandを利用して工夫してみました。
LambdaはNode.jsで書いたので参考にしてみてください。(以下画像より)

弥生会計はEC2上にWindows Server 2019をインストールしてリモートデスクトップを利用して運用しています。
バージョン16から19へのバージョンアップにつまずきましたが、
サポートに問い合わせして、無事移行することができました。
その他社内ツールで利用しているサーバーはAnsible化していなかったので一から作りました。

  • 確認
    /ec2 dev1 status
  • 起動
    /ec2 dev1 start
  • 停止
    /ec2 dev1 stop

  • index.js
'use strict';

const AWS = require('aws-sdk');
var targetId = '';

// EC2 インスタンスのステータスを確認
function statusEC2Instance(region, instanceId) {
    const ec2 = new AWS.EC2({ region: region });
    const params = {
        InstanceIds: [instanceId],
        DryRun: false
    };
    return new Promise((resolve, reject) => {
        ec2.describeInstances(params, (err, data) => {
            if (err) reject(err);
            else    resolve(data.Reservations[0].Instances[0].State.Name);
        }); 
    });
}

// EC2 インスタンスを起動
function startEC2Instance(region, instanceId) {
    const ec2 = new AWS.EC2({ region: region });
    const params = {
        InstanceIds: [instanceId],
        DryRun: false,
    };
    return new Promise((resolve, reject) => {
        ec2.startInstances(params, (err, data) => {
            if (err) reject(err);
            else     resolve(data.StartingInstances[0].CurrentState.Name);
        }); 
    });
}


// EC2 インスタンスを停止
function stopEC2Instance(region, instanceId) {
    const ec2 = new AWS.EC2({ region: region });
    const params = {
        InstanceIds: [instanceId],
        DryRun: false,
    };
    return new Promise((resolve, reject) => {
        ec2.stopInstances(params, (err, data) => {
            if (err) reject(err);
            else     resolve(data.StoppingInstances[0].CurrentState.Name);
        }); 
    });
}

// 関数指定してインスタンスを制御
function executeControl(ec2Function) {
    var result = '';
    const a = ec2Function(process.env.EC2_REGION, targetId)
        .then(data => {
            result = data;
        }).catch(err => {
            result = { result: 'hoge', data: err };
        });
    return Promise.all([a]).then(() => result );
}

function getSuccessfulResponse(message, result) {
    return {
        "attachments": [
            {
                "color": "#32cd32",
                "title": 'Success',
                "text": message,
            },
            {
                "title": 'Result',
                "text": '```' + result + '```',
            },
        ],
    };
}

function getErrorResponse(message) {
    return {
        "response_type": "ephemeral",
        "attachments": [
            {
                "color": "#ff0000",
                "title": 'Error',
                "text": message,
            },
        ],
    };
}

# インスタンスIDの指定
exports.handler = (event, context, callback) => {
    if (!event.token || event.token !== process.env.SLASH_COMMAND_TOKEN)
        callback(null, getErrorResponse('Invalid token'));
    if (!event.text)
        callback(null, getErrorResponse('Parameter missing'));

    var target = event.text.split(' ')[0];
    switch (target) {
        case 'dev1':
            targetId = 'i-xxxxxxxxxxxx';
            break;
        case 'dev2':
            targetId = 'i-xxxxxxxxxxxx';
            break;
    }
    
    if (event.text.match(/start/)) {
        executeControl(startEC2Instance).then(result => { callback(null, getSuccessfulResponse(target + ' Starting...', result)); });
    } else if (event.text.match(/stop/)) {
        executeControl(stopEC2Instance).then(result => { callback(null, getSuccessfulResponse(target + ' Stopping...', result)); });
    } else if (event.text.match(/status/)) {
        executeControl(statusEC2Instance).then(result => { callback(null, getSuccessfulResponse(target + ' Get status...', result)); });
    } else {
        callback(null, getErrorResponse('Unknown parameters'));
    }
};

まとめ

 

8年も動いていた社内サーバーの電源を無事にシャットダウンすることができました。
このラックサーバーのおかげでランサーズの一部を担っているかと思うとなかなか感動的です。

社内でサーバーを運用することはコスト削減になりますが、メンテナンスが大変なのと、
オンプレであればハードウェア障害は避けられず、運用に伴う専門性が問われるため 、
クラウドに移行して誰でも運用できるような検証環境を実現できたので、
なかなか達成感がありました!

読んでいただきありがとうございました。
また会いましょう!! See you next time!!

ランサーズではサービスを成長させてくれるエンジニア、デザイナーを募集しています!
ご興味がある方は、以下URLよりご応募ください。


【中途採用】
サービスリードエンジニア
テックリード(アーキテクト)
フロントエンドエンジニア
サーバーサイドエンジニア
業務エンジニア(社内システム基盤・基幹システム)

【インターン・学生バイト】
19新卒対象サマーインターン
エンジニアインターン

その他採用情報

関連記事

Amazon ConnectとLambdaとSendGridで作る電話確認システム

こんにちは、SREチームの稲村です。 最近Oculus Questを購入して、日々の運動不足解消に役立てています。 VRの世界は本当に凄いですね。 電話確認システムとは さて、ランサーズでは、クライアント様とランサー様それぞれの存在証明の一つとして電話確認の実 …

AWS
AWS セキュリティ対策 〜 操作履歴を CloudTrail で残してみた

こんにちわ。こじまです。 ランサーズでは,AWS を利用してシステムを運用しています。 コンプライアンスの監査,トラブルシューティング,そして,セキュリティ分析を実施するために AWS の操作履歴を残すことは必須です。   今日は,AWS 操作履歴を …

AWS
RDS for MySQLからAuroraへ移行しました

インフラエンジニアの金澤です。 ランサーズでは、今月、DBMSをRDS for MySQLからAuroraに移行しました。 その内容について、2016/1/28に行われました「ヒカラボ MySQL勉強会」 で発表させていただきました。 【ヒカラボ】RDS fo …