古い社内サービスをクラウド化した話

こんにちは花田です。
今回はオンプレミス環境からクラウド環境に移行した話を紹介させていただきます。

はじめに

対象システムはかなり古いシステムなので以下のような問題があります。

  • 当時の仕様を知っている人がいない
  • 様々なメンバーが代々受け継いで開発しているのでコードの書き方がバラバラ
  • 条件分岐が複雑
  • 1メソッドの行数が数百行 などなど

オンプレミス環境

言語、フレームワーク等は以下のようになっています。

  • Perl
  • Catalyst
  • Nginx
  • Starman

サーバー構成図

「WEB+DB(REPLICA)サーバー」1台と「DB(SOURCE)サーバー」1台の合計2台構成です。

クラウド化の目的

当初はオンプレミス環境の冗長化が目的だったのですが
「せっかくならAWSを触ってみたい!」と思い、AWSでクラウド化できないか?POやマネージャーに打診したところ快く承諾してくれました。
(こういう「やってみたい」「学んでみたい」という意見を尊重してくれるのでいい会社です)
そのためAWSを用いた冗長化が目的となります。

クラウド化するにあたって

古いシステムをクラウド化するので以下のことを重視しました。

  1. 古いシステムなので極力既存コードは修正しない(すごく重要)
  2. せっかくならサーバーレス化(ECS Fargate)
  3. Perl / MySQL / ライブラリのバージョンアップ

実はオンプレミス環境で冗長化を目指していた時期があり、その時バージョンアップ調査を行っていました。
そのおかげでクラウド化のタイミングで一気にバージョンアップすることができました。

AWS構成図

上記がAWS構成図の全貌です。こう見るといろんなサービスを利用しているのがわかると思います。
この中でもためになりそうな箇所をピックアップして紹介したいと思います。

ネットワーク構築

サブネット

社内システムのため以下のように作成しています。

  • NAT用にパブリックサブネットを2個作成
  • APP/DB用にプライベートサブネットを2個ずつ作成

VPCエンドポイント

NAT経由だとコストが増えるので、VPCエンドポイントを作成することでコスト削減を行っています。

ロードバランサー

ロードバランサー設定は少し悩みました。
最初はALBで設定をしていたのですが、ALBだとグローバルIPの固定化ができないためNLBに変更しました。
NLBだと固定IPを付与できるのですがセキュリティ面の問題が出てきます。
ECS FargateでNLBを利用する場合ターゲットグループのタイプが「IPタイプ」になり、Nginxのセキュリティグループから「NLB privateIP」はわかるが「クライアントIP(外部サービスIP/自社サービスIP)」がわからない状態になります。
そのためNginxのセキュリティグループでクライアントIPのみを許可することができませんでした。
構築/運用コストを抑えたいと思っていたので最終的にはAGA (AWS Global Accelerator)+ALB構成で設定しています。
GA+ALB構成は料金が高いというデメリットがありますが、月20万程度のアクセス(ほぼ他システムからのアクセス)とギガを超えるようなデータ量もないので採用しました。
※ちなみにリリース後GAの請求額を確認したところ月$18でした。

グローバルIPの固定化のコストパターン表は、過去記事にわかりやすくまとめられています。 blog.engineer.adways.net

WEB構築

ECS Fargate + ECR構成で行いました。

コンテナ構成

コンテナ構成は以下の通りです。

  • Nginx
  • APP(Catalyst+Starman)
  • BATCH(Catalyst+Starman)

APPとBATCHの中身はほぼ同じです。
コンテナ起動時にStarmanで起動するかcrondで起動するかの違いぐらいです。

コンテナ間通信

「Nginx → APP」への通信はECSのService Discoveryを用いています。
こうすることでAPPコンテナが止まったとしてもNginx側でメンテナンス画面にできるので便利です。

Catalyst + Starman側の設定

  • Catalyst + StarmanのService設定内の「サービスの検出」で設定を行います。

  • Service設定を終えるとRoute53で以下のようなレコードが作成されます。

Nginx側の設定

nginx.confに先程作成されたRoute53の「レコード名」を指定することで通信が行えるようになります。

location / {
     ・
     ・
    set $starman "catalyst-starman-service.ecs_internal:5000";
    proxy_pass http://$starman$request_uri;
    ・
    ・
}

BATCHスケジュール

BATCHコンテナの実装方法を考えた際にECS Scheduled Taskを利用する方法もありましたが、以下の理由でやめました。

  • Dockerイメージの容量が大きい
  • 2分に1度動くcronが複数あり起動頻度が多いので料金が高い
  • 重いイメージを短い期間で起動するためECS Scheduled Taskが動かないときがある

Dockerイメージを作成する

ECS Fargateを利用するのでDockerイメージを作る必要がありますが、以下のような問題がありました。

alpineが使用できない問題

Dockerイメージは軽くするのが鉄則だと思いますが、古い社内システムなのでalpaineだと困ることが出てきました。
「Catalystで古いライブラリを使用してる」 + 「ライブラリ数が多い」ためalpaineにPerlを入れただけだとまともに動かず、
結局依存ライブラリを手動で入れることになりAPP、BATCHでのalpineの使用は諦め、公式のPerlイメージで作成しました。
Nginxはライブラリも少ないのでalpineでイメージを作成しています。

BATCHコンテナが環境変数を読み込まない問題

ECSのタスク定義に環境変数を設定してもcronからだと読み込まれません。
crontabに直接環境変数を設定すれば動くのですが、Terraformでタスク定義の環境変数を管理しており、二重管理を避けたかったため別の方法で解決しました。
少しゴリ押し気味ですが、以下のshellを作成してコンテナ起動時に実行するようにしています。

#!/bin/bash

touch ~/.bash_profile

# NOTE: perl実行用pathを追加
echo "export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" >> ~/.bash_profile

envs=(`printenv`)
for env in "${envs[@]}"
 do
 echo "export ${env}" >> ~/.bash_profile
done

コンテナ開発の小ネタ

Dockerイメージを作るときはtagありで作成すること

初歩的なことですが、tagなしでイメージを作ると毎回0からイメージを作成するので時間がかかります。
Dockerイメージ量が大きいほど時間の無駄になるので、tagを付けてからイメージを作成したほうがいいです。

Docker内でサーバーを立てるとき

こちらも初歩的なことですが、Docker内でStarmanを起動する際に127.0.0.1だとエラーになります。
そのため0.0.0.0:5000で起動する必要があります。

コンテナにログインする方法

ECS Execを用いてコンテナにログインする際、毎回以下のようなコマンドで実行していました。

$ aws ecs execute-command \
    --cluster [cluster名] \
    --task [taskID] \
    --container [コンテナ名] \
    --interactive \
    --command "/bin/bash"

この方法だとコンテナが起動停止するたびにtaskIDが変わるので修正して実行する必要があります。
上記を解決するためにはecskコマンドがおすすめです。

$ ecsk exec -i -- /bin/bash

taskIDが変更されていても上記コマンドでログインできます。
また「コンテナ → ローカル」「ローカル → コンテナ」のようにコンテナとローカル間のコピーもできるのでとても便利です。

github.com

AWSリソースをIaC化

AWS環境を作成する際に多くのリソースを作ることになるのでTerraformでIaC化しています。

Terraform管轄外のAWSサービス

TerraformがあることでAWS環境を作るのが楽になるのですが、以下のサービスはTerraform管轄外にしています。

  • SSM
  • ACM
  • Route53

SSMは機密情報が含まれるのでTerraform管轄外にしており、
ACMとRoute53は1度作ったら終わりなので手動で作成するようにしています。

Terraform構文チェック

Terraform開発を楽にするため以下のツールを利用しています。

ツール名 内容
terraform fmt 標準装備されている機能で、Terraformの標準の形式やスタイルへ自動で整えてくれる
terraform validate 標準装備されている機能で、HCLの記述が正しいかどうか構文チェックを行う
tflint 非推奨の構文や未使用の宣言を記述している場合に警告を出してくれる
tfsec Terraformのコードからセキュリティ問題を検知してくれる

これらを毎回実行するのは面倒なので、開発時/デプロイ時のTerraform実行前に以下のshellで実行しています。

#!/usr/bin/env sh

echo "[terraform fmt START!]"
terraform fmt --diff

echo "[teraform validate START!]"
terraform validate

echo "[tflint START!]"
tflint --init
tflint

echo "[tfsec START!]"
tfsec . --tfvars-file staging.tfvars

デプロイ

デプロイはCodeシリーズを利用しています。

デプロイフロー

GitHubでAPP用とTerraform用のリポジトリを分けているため、デプロイフローも少し変えています。
Terraformをデプロイする際はチーム内でterraform planを確認してからリリースしたいので承認フローを挟んでいます。

APP側のデプロイ構成図

Terraform側のデプロイ構成図

Blue/Greenデプロイ

APP側にはBlue/Greenデプロイを設定しています。
そのおかげでデプロイを行っても利用者には影響なくリリースすることができます。
しかしBlue/GreenデプロイにはALBが必要になります。
利用者側のALBはNginxにしか向いておらず、APPとBATCHコンテナに対してdeployできなかったため内部ALBを置くことで対応しました。

最後に

オンプレミスからクラウドへ移行したことを書き出してみましたが、いかがでしたでしょうか?
AWS開発を行うことでサービスの堅牢性を高めることができ、ECS、IaC化、CodeDeployと技術的な挑戦もできたのでチームのレベルアップにも繋がりました。
古いシステムをクラウド化するのは大変だと思いますが、得れることも大きいので挑戦してみても良いと思います。
同じような境遇の方のご参考になれば幸いです。