Docker開発環境の構築～AWSで本番環境の自動リリースまで

エイチーム引越し侍でインフラの改善やNuxt.jsの新規構築/運用を行っている@harekuと申します。

この記事は[エイチームブライズ/エイチームコネクト/エイチーム引越し侍 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/hikkoshi) 1日目の記事です。

本記事は、**「Dockerについては何となく知ってるけど、開発環境の構築や、本番環境の自動リリースまでのフローが分からない」といった方に向けた記事**です。

## この記事を読んで得られること
- Dockerアプリケーションの開発環境を構築できるようになる
- Dockerアプリケーションのインフラを構築できるようになる
- Dockerアプリケーションの自動リリース環境を構築できるようになる

今回使うアプリケーションは「Nuxt.js」というVue.jsのフレームワークです。**本記事のコードは全て「[GitHub - hareku/terraform-nuxt-fargate](https://github.com/hareku/terraform-nuxt-fargate)」で公開しています**。

※インフラの構築には[Terraform](https://www.terraform.io/)という管理ツールを用いています。本記事ではコンソール画面から立ち上げる等の解説は行っておりませんので、また別途他の記事を参照して頂ければと思います。

## Nuxt.jsの開発環境をDockerで起動する
まずはNuxt.jsの開発環境をDockerで構築します。動かすDockerイメージはH2OとNode.jsの2つです。

**H2OはApacheやNginxのようなサーバーソフトウェア**です。H2OではHTTP2に最適化されており、特別なチューニングをせずとも高いパフォーマンスを発揮します。そのため、今回はH2OをRPサーバー（Reverse Proxy Server）として使います。RPサーバーは必ず必要ではありませんが、**静的ファイルをNuxt.jsではなくRPから返すことによってNode.js側の負荷を軽くしたり、静的ファイルの圧縮、アクセス制限などをコントロールしたりすることができます。**

一方の**Node.jsではNuxt.jsを動かします**。開発環境ではこちらのDockerイメージに入って`yarn install`（依存パッケージのインストール）や`yarn run dev`（開発サーバーの起動）などを行います。**Dockerのイメージは開発環境と本番環境で限りなく同じであるため、環境の差異による不具合が起きにくくなります。**この環境差異の吸収は、多くのミドルウェアをサーバー上にインストールする必要性がある言語で、とくに重要性が増します。例えばPHPであればMySQLやOPCacheなどのミドルウェアが必要になる場合が多いため、恩恵を受けやすいです。

さて、これらをdocker-composeを使って立ち上げます。
docker-composeファイル: https://github.com/hareku/terraform-nuxt-fargate/blob/master/nuxt.js/docker-compose.yml

```
# H2OとNode.jsを起動
$ docker-compose up -d

# Node.jsのコンテナに入る
$ docker-compose exec node sh

# 依存npmパッケージをインストール
$ yarn install

# localhost:80で起動
$ yarn run dev
```

[http://localhost:80](http://localhost:80) を見てみれば、Nuxt.jsが起動しているはずです。
これでローカルの環境を汚さずにH2OとNode.jsの環境を用意することができました！

## 本番環境のインフラの全体像を把握する
アプリケーションを開発できれば、次はどのようにデプロイして公開するのかといった段階に入ります。
**まずはインフラストラクチャの全体像を把握**してみましょう。主要なサービスは、以下の4つです。

- **CloudFront**（CDN）
- **Application Load Balancer**、ALB（ロードバランサー ）
- **ECS Fargate**（Dockerの動作環境）
- **CodePipeline**（自動リリース環境）

ユーザーがアプリケーションに到達するおおまかな流れとしては、以下の通りです。

1. ユーザーがCloudFrontにアクセスする
2. CloudFrontがALBにリクエストを流す
3. ALBがFargateで起動しているDockerコンテナ（H2O）にリクエストを流す
4. H2OコンテナからNodeコンテナにリクエストを流す
5. Nodeコンテナで起動しているNuxt.jsがリクエストを処理し、レスポンスを生成する
6. 生成したレスポンスをALB,CloudFrontを介してユーザーに返す

この流れを元に詳しく見ていきましょう。

### CloudFront
**CloudFrontは、AWSが提供しているCDNサービス**です。CloudFrontを使い、jsや画像などの静的ファイルを世界中のエッジサーバーにキャッシュしておくことで、ユーザーへ高速に配信することができます。
CloudFront側でのキャッシュ時間の制御はもちろん、静的ファイルの圧縮やオリジン（今回ではALB）へのパス制御が可能です。またLambda@Edgeというものを使えば、オリジンへのアクセス時やレスポンス時に、PythonやNode.jsによる処理を挟むことができます。

Lambda@Edgeの活用例を挙げるとすれば、画像のリアルタイムリサイズです。クックパッドさんの（[AWS Lambda@Edge で画像をリアルタイムにリサイズ](https://techlife.cookpad.com/entry/2018-05-25-lambda-edge)）という記事でも紹介されており、**私が構築/運用したいくつかのプロジェクトでもLambda@Edgeによる画像最適化を行なっています**。気になる方は「[Lambda@Edgeによる画像リサイズを本番運用した感想 - Qiita](https://qiita.com/hareku/items/3c49e5f60a7cf0989cd0)」という記事も参考にしてください。
またLambda@Edgeによる画像最適化のソースはGitHubで公開しています。（[GitHub - hareku/lambda-edge-image-optimizer-terraform](https://github.com/hareku/lambda-edge-image-optimizer-terraform)）

もちろんLambda@Edgeは応用的な使い方ですので、**単純にキャッシュとしてCloudFrontを使うだけでも十分効果はあります**。オリジンへのアクセス負荷やレスポンス速度向上、DoS対策に有効に働きます。またキャッシュによるネットワーク転送量削減で、費用削減にも期待できる場合があります。

### Application Load Balancer（ロードバランサー ）
**Application Load Balancer（以下、ALB）は、AWSが提供するロードバランサー**です。
ロードバランサーを使うことで、ユーザーからのリクエストを複数のサーバーへ分散することができます。その他にもリクエストのURIによってリダイレクト処理を行ったり向き先のサーバー群を変更したりすることが可能です。

またヘルスチェックという、**アプリケーションが正常に動作しているかをチェックする機能**があります。これはアプリケーションの特定のパスにHTTPリクエストを送り、正常なステータスコードが返ってきているかどうかで判定します。もし非正常なサーバーがあればそちらにリクエストを流すことをやめ、**設定した必要コンテナ数に達していなければ、新しいコンテナを立ち上げる**などの対応を行ってくれます。

### ECS Fargate
さて、次がDockerの動作環境の心臓ともいえる部分であるECSです。

**ECS（Amazon Elastic Container Service）とは、AWSが提供するDockerアプリケーションの管理サービス**のことです。ECSにはクラスターやサービス、タスクなどのいくつかの用語が登場します。

1. クラスターというアプリケーションの枠組みのようなものがあって
2. その中にサービスというALBやネットワーク周りを定義するものがあり
3. そしてサービスの中に、タスクというコンテナの設定が定義されたものがあります

もう少し詳しく見ていきます。

#### Cluster（クラスター）
**Clusterはインフラやサービスの境界線、アプリケーションの抽象的な枠組みのようなもの**です。（難しく考えなくていいです）
例えばサービス名をmy-appとすれば、「my-app-cluster-production」や「my-app-cluster-staging」などがClusterになります。IAM（AWSにおける権限管理）で管理する単位という考え方もできます。

#### Service（サービス）
**Serviceは、ALBとの紐付けや、起動するタスク数などを設定するもの**です。
例えば、起動しているアプリケーションサーバーとキューを処理するサーバーがあるとすれば、それぞれ「my-app-main-service」「my-app-queue-service」がそれぞれServiceになります。（命名は少し微妙な感じですが）

#### Task（タスク）
**Taskは、実行させるコンテナの種類や、コンテナの実行環境について定義するもの**です。使用するCPUやメモリなどのリソースもここで定義します。

クラスター、サービス、タスク定義の概念のさらなる理解については、[Amazon EC2 Container Service(ECS)の概念整理](https://qiita.com/NewGyu/items/9597ed2eda763bd504d7)という記事がおすすめです。またこれらを完全に理解しなくとも先に読み進んでいただいて構いません。

#### 起動タイプ

次にコンテナを立ち上げる環境（起動タイプ）についてです。起動タイプには以下の2つがあります。

- EC2 起動タイプ
- Fargate 起動タイプ（New!）

今まではEC2インスタンスの上にコンテナを起動させ、それらをECSで管理していました。しかし2018年の7月、東京リージョンにFargateという起動タイプが登場し、現在はFargateに人気が集まりつつあります。
**Fargateではコンテナを起動させるEC2インスタンスを管理する必要はなく**、AWS側で提供された抽象化されたコンピューターリソースを使用し、コンテナを立ち上げることができます。FargateによってECS周りはシンプルになり、さらに運用しやすくなったといえます。

### CodePipeline
次はDockerアプリケーションの自動リリースフローについての説明です。まずは継続的インテグレーションと継続的デリバリーについて理解してみましょう。

#### 継続的インテグレーション（Continuous Integration、CI）
継続的インテグレーションは、開発者のコード変更を定期的にビルドしたりテストしたりするというものです。
AWSではCodeBuildというサービスが、このCIに該当します。

#### 継続的デリバリー（Continuous Delivery、CD）
継続的デリバリーは、開発者のコード変更を検知して、自動的にビルドやテスト、および本番へのリリースが実行されるというものです。
AWSではCodePipelineというサービスがCDにあたります。

## CodePipelineを使ったFargateへの自動リリースフロー
次は上記で紹介したAWSのCI/CDサービスを使って、Fargateアプリケーションへの自動リリースフローを説明します。以下は大まかな流れです。

1. CodePipelineが**CodeCommit（AWSが提供するリポジトリサービス）へのプッシュを検知**する（S3やGitHubなども可）
2. CodePipelineがソースコードをS3にアップロードする
3. **CodeBuildがS3からソースコードを取得し、新しいDockerイメージをビルド**する
4. CodeBuildがビルドしたDockerイメージを**ECR（AWSが提供するDockerイメージレジストリ）にプッシュ**する
5. CodePipelineがCodeDeploy（AWSが提供するデプロイサービス）を呼び出し、**タスク定義を更新してアプリケーションをデプロイ**する

このように**CodePipelineは橋渡しのような役割があり、CodePipelineからCodeBuildやCodeDeployを呼び出す流れ**になります。

- ※ CodePipelineがソース元として対応していないGitLabでも、GitLabのミラーリング機能を使ってCodeCommitと同期すればCodePipelineを使えます。
- ※ 現在CodePipelineでECSへデプロイする際に、60分間手動でキャンセルできないという仕様があるため、その対策方法を「[CodePipelineのECSデプロイをキャンセルする方法 - Qiita](https://qiita.com/hareku/items/90ed026256242c65e178)」でまとめています。

### DBのMigrationやSeedingを行う場合
Nuxt.jsの場合はデータベースのMigrationやSeedingが必要ないですが、RailsやLaravelではリリースフローでそれらのコマンドを実行したいはずです。
そのような場合は、**CodeBuild内でMigrate用のタスクを起動させるコマンドをコメントアウトしながら対応**するか、もしくは**CodePipelineのビルドステージとデプロイステージ間に手動承認のステージをはさみ、その待機中に必要であれば手動でタスクを呼び出す**、という対応にすれば良いかもしれません。

「[LaravelをElastic BeanstalkからFargateに移行しました - Qiita](https://qiita.com/hareku/items/73f7730c1adc01bbe5a0#1-db-migration%E3%81%AF%E3%81%A9%E3%81%93%E3%81%A7%E3%81%99%E3%82%8B%E3%81%8B)」という記事では、CodeBuild内で`aws ecs run-task`する対応方法を紹介しています。

### 静的ファイルの安全なデプロイ
ECSのデプロイでは、静的コンテンツが一時的に404エラーになるという問題を抱えています。原因は以下です。

1. ユーザーが`/`にアクセスする
2. ECSのサービスが新しいタスク（以下タスクA）にリクエストを流し、タスクAが`/`のHTMLを返す
3. ユーザーが返ってきたタスクAのHTMLで必要なjsファイル（`/task-a.js`）をリクエストする
4. ECSのサービスが古いタスク（以下タスクB）にリクエストを流すが、タスクBには`/task-a.js`が存在しないため404エラーを返す

このように**新旧のタスクが同時に起動していることにより、一定確率で404エラーが発生**してしまいます。一定確率というのは、2と4で同じタスクを参照しなかった場合です。そのためS3への安全なデプロイを行う必要があります。詳しくは「[ECSのデプロイ時に一定確率で静的ファイルが404になる問題を回避する - Qiita](https://qiita.com/hareku/items/6be1b71e58033b9739fd)」に書いていますので、そちらを参照してください。

## Dockerの開発環境の構築～AWSで本番環境の自動リリースまで
以上がDockerアプリケーションの開発環境の構築～本番環境への自動リリースまでのご紹介です。

いままでDockerの開発からリリースまでの全体像が見えなかった人の参考になれば幸いです。もし何か分からないことがあれば、お気軽にコメントください。

**上記のコードは全てGitHubで公開しています。**[GitHub - hareku/terraform-nuxt-fargate](https://github.com/hareku/terraform-nuxt-fargate)

### お知らせ
エイチームグループでは一緒に活躍してくれる優秀な人材を募集中です。
興味のある方はぜひとも[エイチームグループ採用ページ](http://www.a-tm.co.jp/recruit/)（[Webエンジニア詳細ページ](http://www.a-tm.co.jp/recruit/requirements/career/lifestylesupport-webengineer/)）よりお問い合わせ下さい。

明日はエイチームコネクトの凄腕エンジニアより、機械学習についての投稿があります。お楽しみに！