@cosmeの裏側! バックエンドAPIをオンプレミス環境からAWS ECS Fargateに移行した話

はじめに

このブログで書くのは初めてとなります。大体@cosme Webサイトの運用・開発・改善その他何でもエンジニアをやっているttと申します。

アドベンドカレンダーのトップを急遽任されました。この記事はアイスタイル Advent Calendar 2019 1日目の記事です!

この記事が投稿された明後日からは年に一度のコスメ祭りと称しての一大イベント@cosme Beauty Dayが始まります。

こちらもガッツリ携わっているということでいろいろと語る話はあるのですが、今回はタイトルの通り、@cosme WebサイトのバックエンドAPIについて話します。

@cosmeはアイスタイルが運営する日本最大のコスメ・化粧品の口コミ・ランキングサイトです!

概要

この@cosmeのバックエンドAPIですが、主に@cosmeの主軸であるクチコミ・商品ページのデータを返却しているRest APIとなっています。

@cosmeでは商品のページは30万商品以上、商品のクチコミは1500万件以上存在するため、かなり存在感の大きいAPIです。約1年前くらいに新しく作り直され、その後運用や改善を行っていました。

元々オンプレミス環境で元気に稼働していたのですが、@cosmeサイトの一番コアな部分ということで、「可用性や信頼性を高め、高負荷でスケール出来るようにAWSに移行したい」という話が出てきました。

そこで、自分が主導してコスト見積もりやアーキテクチャの見直しをしつつAWSのクラウド環境に移行することになりました。

移行前のアーキテクチャ

移行前はこのような形の構成でした。

  • ネットワーク: オンプレミス環境(自社データセンター)
  • ロードバランシング: オンプレ用のLB
  • OS: CentOS 7
  • 言語: Golang
  • フレームワーク: Goa 1
  • ミドルウェア: nginx / supervisord
  • ログの転送: td-agent -> ElasticSearch (オンプレ内)

Go buildで生成したシングルバイナリをWebサーバアプリケーションとして動作させ、nginxやsupervisord等を使って管理を行う、一般的なサーバの構成です。

Goaに関しては、弊社が採用しているGolang用のWebフレームワークとなっています。
ちょうどこの時期にGoa 1から最新バージョンのGoa 3が出たため、今回の移行ではGoa3へのバージョンアップも考えることになりました。

また、その他解析用としてtd-agentでログをElasticsearchに転送しており、このアーキテクチャも見直す必要があります。

コンテナ技術であるAWS ECS Fargateを採用した理由

元々の構成自体がコンテナ技術を使用していない、VM上でLinuxが動作する形でした。これをそのままAWSに持ってくるとすればEC2とALB、AutoScaring Groupを使用する方法が一般的です。

ここからECS Fargateを採用した理由としては、

  • 高速にスケールし、高負荷にも耐えることが出来るアーキテクチャである
  • Fargateを使用することでEC2等を使うよりも管理・運用コストが減り、構成をシンプルにできる
  • 元々シングルバイナリ・シングルプロセスで動作するコードだったため、コンテナ技術と相性が良い(少し手を加えれば適用できる)
  • 元々の作りがモダンなサービスデザインである、The Twelve-Factor Appの要件を満たしている

ということでイケそうと考え、自社で初のコンテナ本番環境での採用事例としてチャレンジしたという形です。

移行方法

移行方法ですが、このAPIを落としてしまうと@cosmeのクチコミ画面・商品画面全てがダウンしてしまいます。そのため、ダウンタイムも一切ないパターンでの移行となりました。

ダウンタイムを生じさせない方法として、旧環境と新環境を両方稼働した状態でバックエンドAPIのエンドポイントを変更する形での移行を行います。

また、オンプレミス環境からAWSクラウド環境に移行する際の懸念点としては、Direct Connectを用意してオンプレミス環境と接続した環境になっていたため、テストしたところ問題はないことを確認しました。

この状態を維持し、アクセスが旧環境から新環境に全て切り替われば問題なく移行完了、旧環境を破棄するという形です。

移行後のアーキテクチャ

移行するに従い、コンテナ運用のデザインパターンに合わせるため、アーキテクチャを少し変える必要がありました。最終的に、このようなアーキテクチャとなりました。

  • ネットワーク: AWS(オンプレへの接続はDirect Connectを経由する)
  • ロードバランシング: AWS ALB
  • OS: Alpine Linux(コンテナ内)
  • 言語: Golang
  • フレームワーク: Goa 3
  • ログの転送: AWS CloudWatch Logs -> AWS Lambda -> ElasticSearch(オンプレ内)

Alpine Linux

OSに関してはCentOS 7からコンテナ用の軽量Linuxディストリビューションである、Alpine Linuxに変更しました。

変更した理由としては、コンテナサイズの肥大化に対応するためです。

CentOS 7のイメージはベースイメージで80MB程度あるのに対し、Alpine Linuxは現在の最新版であれば3MB程度。軽量を謳っているだけあって非常に軽いです。

Amazon ECS / Fargateを使用する場合、コンテナの起動時間はイメージのサイズが影響します。重いほど起動に時間が掛かるため、オートスケールを導入する場合は極力軽いコンテナが良い、ということでAlpine Linuxに変更しました。

ログの転送

td-agentをそのままコンテナとして建てるにはtd-agent自体のコンテナの起動コストが当然かかります。

そのあたりを考慮し、CloudWatch Logs サブスクリプションのフィルタを作成して、Lambdaを経由してElasticsearchにログデータを挿入するスクリプトを実行する形式に変更しました。

本来であれば間にKinesis Firehoseをデータの転送として使用した方が信頼性が高く、この構成だと公式ドキュメントにも処理できないほどのデータが飛んできた場合は処理できない可能性があると書いてあります。

しかし、今回はエラーログ等の解析用途でかつ一日に100個ほどのレベルでかつ、Elasticsearchにデータが挿入されないとしてもCloudwatch Logsには正常に蓄積されるため、失敗を容認するということで使用しませんでした。

CloudWatch Logs サブスクリプションフィルタの使用 – Amazon CloudWatch Logs

Goaのバージョンアップ

今回の移行とは直接的な関係はありませんが、時期があったためWebフレームワークのバージョンアップも同時に行いました。

なおGoaのバージョンアップはかなりの破壊的変更が加えられていて、別の記事で語れるほどのレベルで非常に大変な作業となっていますが、今回は割愛します。

また、Goa 3になるに従いGoのパッケージ管理をGlideからGo modulesに変更も行いました。

Go modulesはGo1.11よりデファクトスタンダードになったGolangのパッケージ管理です。こちらの対応でGo build時に自動で依存パッケージを取得するため、元々のDockerfileの行数が少し短くシンプルになりました。

Dockerfile

コンテナでの動作はDockerfileを使用して、コンテナイメージを生成する必要があります。

Golangを使用したアプリケーションの場合、最終的に単一のバイナリファイルを生成できるため、コンテナ自体に入れる必要がある依存パッケージは非常に少なくなり、相性が良いです。

マルチステージビルドを使用する

この単一バイナリファイルという利点を生かしてイメージサイズを最適化するために、マルチステージビルドを使用してのイメージ生成が必要です。

マルチステージビルドは、Docker17.05以上で使用可能な機能です。

Use multi-stage builds | Docker Documentation

Golangはビルドする必要があるためGCCやGitなど、ビルドのための依存パッケージを要求します。

しかしビルド後は当然必要なくなるため、そのままビルド用のデータを残しておくことはイメージサイズの肥大化につながります。

対策として、マルチステージビルドの機能を利用することで、ビルド後の必要なファイルだけをコンテナに含めてイメージとして生成することが可能です。

実際のコンテナはもっと複雑ですが、簡単に説明するとこのような形となっています。

FROM golang:1.12.9-alpine3.10 AS build-env

ENV GO111MODULE=on

RUN apk --no-cache add git make build-base

WORKDIR /go/src/app

COPY . .

RUN mkdir -p /build
RUN go build -a -tags "netgo" -installsuffix netgo -ldflags="-s -w -extldflags \"-static\"" -o=/build/app main.go

FROM alpine:3.10.2

COPY --from=build-env /build/app /build/app
RUN chmod u+x /build/app

ENTRYPOINT ["/build/app"]

こうすることで、golang:1.12.9-alpine3.10イメージでGolangのビルドを行い、ビルドしたバイナリファイルだけを何もバンドルされていないAlpine Linuxイメージに持ってくることが出来ます。

このような最適化を行った結果、最終的なイメージサイズは15MB程度まで低下させることが出来ました。たった15MBで裏側を支えていると考えていると、結構すごいと感じます。

実際の運用について

現在は運用を続けて大体2,3か月ほどですが、問題なく安定稼働しています。

運用前に聞いていた懸念点として、Fargateはコンテナの立ち上げが遅いという話を聞くことがあります。

現在はコンテナサイズの最適化なども行った結果か、コンテナを立ち上げてALBに接続されるまでほぼ1分程度となっています。

この設定で安定稼働しており障害も起きておらず、オートヒーリング機能・オートスケール機能等を活用し運用コストも下げられているので今のところは全く問題ないです。

またAWS Fargateを使用した場合、リザーブドインスタンス等の長期利用割引の課金体系がないことがありEC2と比べてコストが高くなるといった問題もありましたが、これに関しては先月Savings PlansというFargateに適用可能な課金体系も出てきたので、これも是非適用したいと考えています。

移行してよかった点

一番は、監視対象のアプリケーションが減って運用や監視コストが減ったことと思います。

VMでの運用は、Webサーバ以外にもtd-agentやnginx、Zabbix Agentなどが乗っており、そちらの死活監視や異常検知も必要となっていました。

今回のアーキテクチャの変更で、nginxで担っていた部分はALBで行い、td-agentはCloudWatch LogsとLambdaで行い、Zabbixでの監視はCloudWatchに変更というアプローチをとったことで、監視コストや障害リスクをかなり減らすことが出来たと思います。

また、移行したことでオンプレミス環境では実現が難しい、オートスケールやオートヒーリング機能に関しても非常に助かっています。

@cosmeサイトでは負荷はピーク時と通常時で比べるとかなり差があり、ソーシャル流入などによる局所的な大量のアクセスもよくあります。

これに対して、自動的にコンテナ台数を増やして対応することでコスト削減しつつアクセス増に対しても安定稼働を提供できており、より強固な基盤を提供できていると感じています。

おわりに

当然、コンテナ技術自体は銀の弾丸ではありません。

通常のEC2等VMを使うよりも当然制約はあり、ECS Fargateの仕様やデプロイ方法等も考え、場合によっては大幅なアーキテクチャの変更が必要になる場合もあります。

また、新しい技術というのはそれだけで情報が少なかったり、詰まった時の解消が難しい傾向はやはりあります。

そのため、開発環境でコンテナ技術を取り入れた開発を行っていても、本番環境までコンテナで動かしているという事例は結構少ないと感じます。

しかし、これからもサービスの安定した提供というエンジニア永遠のミッションと共に、最適な技術等も駆使してサービスを行っていきます。

弊社ではDockerなどのコンテナ技術も含め、BtoCだけではなく機械学習からBtoBサービスまで、幅広い分野で仲間を募集しています!

詳細はこちら

@cosme ダジャレ担当