RailsアプリをECS FargateへデプロイするCI/CD pipelineを構築する

この記事は[Fusic Advent Calendar 2019](https://qiita.com/advent-calendar/2019/fusic)の20日目の記事です。

昨日は@Kta-Mの「[究極のCloudFormationをたずねて三千里](https://qiita.com/Kta-M/items/53bfd638eae21b76250f)」でした。
CloudFormationでこのネタをやるとは、、、斬新なアイディアですね。

----
さて、12月2日〜6日にアメリカのラスベガスで[AWS re:Invent 2019](http://reinvent.awseventsjapan.com/)が開催されました。
私自身も参加して多数のWorkshopを体験することができました。

今回は、その中で最も実用的と感じたWorkshopの内容を、一部アレンジしてre:Playします。

## 作るもの

ECS Fargate上にアプリケーションをデプロイするCI/CD pipelineを構築します。
ECSは2つのサービスを起動し、Blue/Greenデプロイできるようにします。
![Untitled_New_Diagram_-_Cacoo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/cd703593-cacb-c1b2-e820-54967a557d4d.png)

### アレンジポイント
**re:Invent 2019のWorkshopでは「簡易的なPHPのWebページ」をデプロイしていましたが、
今回は「Ruby on Railsのアプリケーション」をデプロイします。**

デプロイするのは下の画像のような、シンプルなScaffoldアプリです。
![EcsRailsApp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/6f958d4c-2f54-1fc3-7908-b25311c4a867.png)


## 準備
### AWS CLIのインストール
以下URLを参考にインストールしてください。バージョン2が評価リリースされていますが、この記事ではバージョン1を使います。
https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/install-cliv1.html
### GithubのAccess Tokenを作る

以下URLの(c)および(d)の手順を実施してください。

https://cicd-with-aws-fargate-lambda-bg.workshop.aws/en/prereq.html

### CloudFormationテンプレートをCloneする

AWSリソース一式を構築するCloudFormationテンプレートが用意されているので、これをCloneします。
また、branchを `fargate` に切り替えておきます。

https://github.com/awslabs/ecs-blue-green-deployment

```bash
$ git clone https://github.com/awslabs/ecs-blue-green-deployment
$ cd ecs-blue-green-deployment
$ git checkout fargate
```

----
Workshopのときとはリージョンや起動するアプリが異なるので、いくつか修正を施しました。

取得するGithubリポジトリをWorkshopで使用したものから、今回私が作成したRailsアプリケーションのリポジトリに変更します。

```diff:ecs-blue-green-deployment.yaml
Parameters
   GitHubRepo:
     Type: String
-    Default: ecs-demo-php-simple-app
+    Default: ecs-rails-app
     Description: The repo name of the sample service.
     AllowedPattern: "[A-Za-z0-9_.-]*"
     MaxLength: 50
```

Railsアプリケーションの場合、コンテナが起動してから `assets:precompile` を実行するため、HelthCheckが通るようになるまで時間がかかります。
このため、HelthCheckの猶予時間を360秒に設定します。

```diff:templates/service.yaml
       LaunchType: FARGATE
       TaskDefinition: !Ref TaskDefinition
       LoadBalancers:
-        - ContainerName: simple-app
+        - ContainerName: rails-app
           ContainerPort: 80
           TargetGroupArn: !Ref TargetGroup
+      HealthCheckGracePeriodSeconds: 360
       NetworkConfiguration:
         AwsvpcConfiguration:
           AssignPublicIp: ENABLED #MENTION DISABLED if in private subnet with NAT gateway
```

Logを閲覧できるようにしないとデバッグがしづらいのでCloudWatch Logsへ書き出すように設定を加えています。
コンテナのWorkDirやEntrypoint、環境変数の定義を追加しています。
※master.keyはこの後の手順で作成します。

```diff:templates/service.yaml
        - FARGATE
       ExecutionRoleArn: !GetAtt TaskIamRole.Arn
       ContainerDefinitions:
-        - Name: simple-app
+        - Name: rails-app
           Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${Repository}:${Tag}
+          LogConfiguration:
+            LogDriver: awslogs
+            Options:
+              awslogs-group: /ecs/logs/ecs-rails-app-group
+              awslogs-region: !Ref "AWS::Region"
+              awslogs-stream-prefix: rails-app
+          WorkingDirectory: /app
           EntryPoint:
-            - /usr/sbin/apache2
-            - -D
-            - FOREGROUND
+            - /bin/sh
+            - -c
+            - /app/script/entrypoint_production.sh
           Essential: true
           Memory: 512
           PortMappings:
             - ContainerPort: 80
           Environment:
             - Name: Tag
-              Value: !Ref Tag
\ No newline at end of file
+              Value: !Ref Tag
+            - Name: RAILS_ENV
+              Value: production
+            - Name: RAILS_SERVE_STATIC_FILES
+              Value: true
+            - Name: RAILS_LOG_TO_STDOUT
+              Value: true
+            - Name: RAILS_MASTER_KEY
+              Value: <master.keyの内容を設定>
```


東京リージョンではap-northeast-1bが選択できないため、ap-northeast-1cを選択させます。

```diff:templates/vpc.yaml
     Type: AWS::EC2::Subnet
     Properties:
       VpcId: !Ref VPC
-      AvailabilityZone: !Select [ 1, !GetAZs ]
+      AvailabilityZone: !Select [ 2, !GetAZs ]
       MapPublicIpOnLaunch: true
       CidrBlock: !Ref Subnet2CIDR
       Tags:
```

※参考までにこちらでも変更を確認できるようにしています。
https://github.com/yuuu/ecs-blue-green-deployment/commit/4294e098993511de134221758e834d5b9ecc674b

### デプロイ対象のリポジトリを準備する

上記で入力した名前のリポジトリを、自分のGithubアカウント配下に置く必要があります。
今回は私が用意したリポジトリをforkしてお使いください。
https://github.com/yuuu/ecs-rails-app
<img width="998" alt="yuuu_ecs-rails-app.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/a60ee989-be64-bc33-bfd8-a0d9f22a6ddf.png">

config/master.keyを生成するために、以下コマンドを入力してください。

```bash
$ rm config/credentials.yml.enc
$ EDITOR=vi bundle exec rails credentials:edit
(エディタが起動したら :wq を入力)
$ git commit -m 'update credentials'
$ git push origin master
```

無事に生成できたら前述の `templates/service.yaml` へ反映しておきましょう。

### CloudFormation用のS3バケットを作成する

CloudFormationで使うS3バケットを作成します。S3バケットは他人と重複しない名前を適当に設定してください。

```
$ aws s3 mb s3://<unique-bucket-name>
```

## デプロイする
### 環境構築＆デプロイ
準備が完了しましたのでいよいよデプロイします。

先ほどCloneした `ecs-blue-green-deployment` のディレクトリへ移動して以下コマンドを実行します。
実行すると対話形式でパラメータの入力を求められるので、下記に従って入力してください。

```bash
$ bin/deploy

+ echo -n 'Enter S3 Bucket to host the templates and scripts > '
Enter S3 Bucket to host the templates and scripts > + read bucket
(作成したバケット名を入力)
+ echo -n 'Enter stackname to create or update the stack > '
Enter stackname to create or update the stack > + read stackname
(任意のstacknameを入力)
+ echo -n 'Enter GitHub User > '
Enter GitHub User > + read GitHubUser
(Github ユーザ名を入力)
+ echo -n 'Enter GitHubToken > '
Enter GitHubToken > + read GitHubToken
(作成したGithub Access Tokenを入力)
```

CloudFormationが動き出し、諸々のリソースが作成されます。
![CloudFormation_-_スタック_ecs-rails-app1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/eea94539-274a-4f13-ee22-ba5075ac9a93.png)

しばらくするとCodePipelineが動き出します。
Githubリポジトリからソースコードを取得してビルドしている様子が確認できます。
![CodePipeline_-_AWS_Developer_Tools.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/a678251f-d3e4-851c-ad11-dd382e4ee2fd.png)

### 動作確認
デプロイが終わったらEC2のコンソールを開きます。(ECSではないので注意)
ロードバランサーの画面へ移動して、Stack名に設定した名前のロードバランサーを探します。
無事に見つかったらDNS名をコピーしましょう。
![ロードバランサー___EC2_Management_Console.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/ee608cd7-7e89-a404-8835-214b9d707bae.png)

`http://(ロードバランサーのDNS名)/posts` へアクセスすると、アプリケーションが動いていることを確認できます。
![EcsRailsApp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/6f958d4c-2f54-1fc3-7908-b25311c4a867.png)

動作確認ができたらCodePipeline上でapproveをしておきましょう。
![CodePipeline_-_AWS_Developer_Tools.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/3ad80412-872c-7cab-22d5-26e8e96b13a6.png)

## Blue/Greenデプロイする
これから、Railsアプリケーションに変更を加えて、デプロイをします。

デプロイ後、プレ環境で変更が正しく動作することを確認できます。
CodePipelineの承認ボタンをクリックすると、ALBのリスナーとターゲットが入れ替わり
本番環境のURLで新しいバージョンのアプリケーションが閲覧できるようになります。

![Untitled_mockmock_-_Cacoo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/0ec8dbe5-dbb2-d482-f91c-d996f56a207b.png)

![Untitled_mockmock_-_Cacoo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/280aac77-94a3-9f63-6b4a-77aed0456d9b.png)

### アプリケーションのコードを変更しデプロイする
今回は例として以下の通り変更します。

```diff:app/views/posts/index.html.erb
 <p id="notice"><%= notice %></p>
 
-<h1>Posts</h1>
+<h1>Posts!!!</h1>
 
 <table>
   <thead>
```

このソースコードをpushしましょう。
まもなくデプロイが始まるので、デプロイ完了となるまで待ちます。

```bash
$ git add .
$ git commit -m 'change h1 text'
$ git push origin master
```

### 動作確認
次に本番環境へアクセスします。

`http://(ロードバランサーのDNS名)/posts`
![EcsRailsApp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/0861be40-a9d0-5baf-96cd-469ab25e825a.png)

変更が反映される前の状態が残っています。

では次に、プレ環境を確認します。
このシステムではロードバランサーの8080ポートへアクセスするとスペア環境へ接続できるようになっています。
`http://(ロードバランサーのDNS名):8080/posts`
![EcsRailsApp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/0c22e4f2-f1c5-ca23-4825-48b3038151c4.png)

変更が反映されていますね。

----

変更を承認すると、Lambda FunctionによってALBのリスナーとターゲットの関係がスワップされます。
本番環境が新しいバージョン、プレ環境が古いバージョンとなることが確認できました。

`http://(ロードバランサーのDNS名)/posts`
![EcsRailsApp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/0c22e4f2-f1c5-ca23-4825-48b3038151c4.png)

`http://(ロードバランサーのDNS名):8080/posts`
![EcsRailsApp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/0861be40-a9d0-5baf-96cd-469ab25e825a.png)

## 今後の課題
Workshopの後で知ったのですが、 **ECS自体にBlue/Greenデプロイ機能が存在している** そうです。
今回はLambdaを使ってALBのリスナーとターゲットを入れ替えていますが、ECSの機能を使った方がよりスマートにBlue/Greenデプロイできるはずです。

現状、RDBはsqlite3を使っていますが、実際の本番環境でsqlite3を使うことはほぼ無いので、RDSインスタンスへの接続も一度試してみたいところです。

また、CodeBuildでのビルド時間、CodeDeployでのデプロイ時間が **合計10分以上かかっている** ので、もう少し短縮する必要がありますね。

## まとめ
re:Invent 2019のWorkshopの内容をアレンジすることで、RailsアプリをECS FargateへデプロイするCI/CD pipelineを構築することができました。

こういったWorkshopに多数参加できるre:Invent 2019はとても有意義だったなと改めて実感しました。
この経験を今後の開発に活かせるよう、精進します。

## 参考
https://cicd-with-aws-fargate-lambda-bg.workshop.aws/en/intro.html
https://www.enisias.cloud/docker/152/