AWS Copilotを使ってRailsアプリケーションのCI/CDパイプラインを爆速で構築する

# 記事の概要
AWS Copilotを使って作成したECS上のRailsアプリケーションにおいて、CI/CDパイプラインを構築します。
また、その際にハマったポイントを共有します。

↓の記事の続きになります。
[AWS Copilotを使ってRailsコンテナの本番環境を爆速で構築する](https://qiita.com/sshon/items/07efef19325bf9891491)

### AWS Copilot
AWSでコンテナ化されたアプリケーションの開発、リリースを容易に行うためのコマンドラインツールです。
コマンドを叩くとCloudFormationが動き、必要なリソースの作成やデプロイを行うことができる。CI/CDパイプラインもコマンド一つで作成できます。

※Fargate起動タイプのみサポートしています

### Pipeline
Githubへのpushをトリガにしてコンテナデプロイを実行するための仕組みです。
CopilotによってPipelineを作成すると、CloudFormationによってCodeBuildのリソースが作成されます。

# Pipeline作成方法
### 手順概要
- GitHubの個人アクセストークン準備
- master.keyのAmazon SSMへの登録
- Pipelineの定義
- Pipelineの作成
- ビルドプロジェクトのイメージ修正(Rubyのバージョン次第)

### GitHubの個人アクセストークン準備
PipelineからGithubリポジトリを参照するために、GitHubの個人アクセストークンが必要なので事前に準備しておく必要があります。

アクセストークンは`repo`、`admin:repo_hook`のスコープが必要です。
[個人アクセストークンを使用する](https://docs.github.com/ja/github/authenticating-to-github/creating-a-personal-access-token)

### master.keyのAmazon SSMへの登録
Railsのmaster.keyは普通はGit管理から外すためGithub上にありませんが、PipelineはGithubリポジトリを参照してDockerイメージを作るためmaster.keyを何かしらの方法でECSに渡す必要があります。
セキュアに管理するためにAmazon SSMのパラメータストアを利用します。


下記コマンドでmaster.keyの文字列をAmazon SSMのパラメータストアに登録します。
タグの値については適宜自身のものに変更してください。

```
  $ aws ssm put-parameter --name RAILS_MASTER_KEY --value master.keyの文字列 --type SecureString\
  --tags Key=copilot-environment,Value=production Key=copilot-application,Value=copilot-demo
```

登録されました。
<img width="420" alt="スクリーンショット 2020-08-05 22.53.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279908/9f4bef1e-77ee-3606-e5bb-fd83c2977382.png">

続いて、Serviceのmanifest.ymlを変更します。
secretsを宣言することでECSからSSMのパラメータストアを参照できます。
「ECSで参照できる環境変数: SSMに登録したパラメータのキー」のように指定します。

```copilot/api/manifest.yml
  variables:
    RAILS_ENV: development

  secrets:
    RAILS_MASTER_KEY: RAILS_MASTER_KEY 　# ECSで参照できる環境変数: SSMに登録したパラメータのキー
```

### Pipelineの定義
続いてPipelineを定義します。
EnvironmentやServiceを作成した時と同じように対話型で進めていきます。
このタイミングでPipelineが参照するGithubリポジトリや個人アクセストークンを指定します。

```
$ copilot pipeline init

  Would you like to add an environment to your pipeline? [? for help] (y/N) 
  > y

  Which environment would you like to add to your pipeline?  [Use arrows to move, type to filter, ? for more help]
  > production

  Which GitHub repository would you like to use for your service?  [Use arrows to move, type to filter, ? for more help]
  > https://github.com/ssshun/rails_copilot

  Please enter your GitHub Personal Access Token for your repository rails_copilot: [? for help] 
  > XXXXXXXXXXXXXX
```

完了すると以下の2ファイルが作成されます。基本はデフォルトのままで問題ないです。

- buildspec.yml
- pipeline.yml


### Pipelineの作成
以下のコマンドでPipelineを作成

```
  $ git add . && git commit -m "Adding Pipeline Buildspec" && git push
  $ copilot pipeline update
```


しばらく経ってからCodeBuildを確認すると、ビルドプロジェクトとパイプラインが作成されているのがわかります。
<img width="400" alt="スクリーンショット 2020-08-07 18.09.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279908/7c123249-8f26-8543-8a0d-340a5519ac1c.png"> <img width="400" alt="スクリーンショット 2020-08-07 18.09.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279908/6b7af1e1-f5b3-1d17-d409-ec505a50ce82.png">

### ビルドプロジェクトのイメージ修正
アプリケーションのRubyのバージョン次第ではこの手順は不要です。
2020/08/08時点では、アプリケーションがRuby2.7を利用する場合、この作業が必要でした。

buildspec.ymlを修正し、ビルドプロジェクトのrubyランタイムのバージョンを2.7に変更します。

```copilot/buildspec.yml
  version: 0.2
  phases:
    install:
      runtime-versions:
        docker: 18
        ruby: 2.7
```

続いて、AWSコンソールにて、ビルドプロジェクト > ビルド詳細 > 環境 からイメージを修正します。
CopilotでPipelineを作成した場合、イメージが`aws/codebuild/amazonlinux2-aarch64-standard:1.0`になるのですが、Ruby2.7のランタイムが`aws/codebuild/amazonlinux2-aarch64-standard:1.0`に対応していないためです。

Ruby2.7に対応している`aws/codebuild/amazonlinux2-x86_64-standard:3.0`に変更します。
<img width="564" alt="スクリーンショット 2020-08-07 18.26.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279908/dd43012f-ea4f-69b5-be83-e7d11fc4a075.png">

buildspecを修正したので、Pipelineを更新します。

```
  $ git add . && git commit -m "Modifying Pipeline Buildspec" && git push
  $ copilot pipeline update
```

以下のようにPipelineが作成されました。
<img width="735" alt="スクリーンショット 2020-08-06 0.35.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279908/8b10b787-b775-b33a-7c24-d93087ae89a1.png">

# ハマったポイント
### Pipeline名がCloudFormationの命名規則違反
プロジェクトディレクトリにアンダースコアを入れて、そのままPipelineを作成すると以下のようなエラーが発生します。

```
check if pipeline exists: describe stack pipeline-copilot-demo-ssshun-rails_copilot: ValidationError: 1 validation error detected: Value 'pipeline-copilot-demo-ssshun-rails_copilot' at 'stackName' failed to satisfy constraint: Member must satisfy regular expression pattern: [a-zA-Z][-a-zA-Z0-9]*|arn:[-a-zA-Z0-9:/._+]*
```

原因ですが、プロジェクトのディレクトリ名にアンダースコアが含まれると、デフォルトのPipeline名にもアンダースコアが入ります。
そのため、このままだとCloudFormationのスタック名の命名規則に違反してしまいます。

```copilot/pipeline.yml
  # The name of the pipeline
  name: pipeline-copilot-demo-ssshun-rails_copilot # プロジェクトディレクトリがrails_copilotの場合のデフォルト
```

以下のようにアンダースコアが含まれないように名前を修正すれば解消されます。

```copilot/pipeline.yml
  # The name of the pipeline
  name: pipeline-copilot-demo-ssshun-rails-copilot
```

### ビルドプロジェクトのランタイムとイメージの不整合
CodeBuildにてビルドが成功しないのでログを確認したところ以下のエラーが発生していました。
アプリケーションのランタイム(Ruby2.7)とビルドプロジェクトのランタイム(Ruby2.6)の不整合が原因と思われます。

```
  rbenv: version `ruby-2.7.1' is not installed (set by /codebuild/output/src020247698/src/.ruby-version)
```

ビルドプロジェクトのランタイムをRuby2.7に変更したらいけるかと思い、buildspecを修正しましたが、

```copilot/buildspec.yml
  version: 0.2
  phases:
    install:
      runtime-versions:
        docker: 18
        ruby: 2.7
```

再度、別のエラーが発生しました。
Ruby2.7のランタイムがCodeBuildのビルドプロジェクトのイメージ`aws/codebuild/amazonlinux2-aarch64-standard:1.0`に対応していないためでした。

```
  Phase complete: DOWNLOAD_SOURCE State: FAILED
  Phase context status code: YAML_FILE_ERROR Message: Unknown runtime version named '2.7' of ruby. This 
  build image has the following versions: 2.6
```

上の手順で書いた通り、イメージをRuby2.7に対応している`aws/codebuild/amazonlinux2-x86_64-standard:3.0`に変更して解決しました。

### ECSからSSMのパラメータストアが参照不可
master.key参照のためにAmazon SSMのパラメータストアを登録する際に、タグは要らないだろうと思って以下のように登録していました。

```
  $ aws ssm put-parameter --name RAILS_MASTER_KEY --value master.keyの文字列 --type SecureString
```

その状態でPipelineでデプロイしたところ、ECSタスクで以下のエラーが発生しました。
パラメータストアのRAILS_MASTER_KEYの参照権限がないとのことでした。

```
  Fetching secret data from SSM Parameter Store in ap-northeast-1:AccessDeniedException:
  User: arn:aws:sts::XXXXXXXXXXXX:assumed-role/copilot-demo-production-api-ExecutionRole-DYGK7EGS469J/0e93e07304b641d58e5fc147122d2f4c is not authorized to perform: ssm:GetParameters on resource: arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/RAILS_MASTER_KEY
  status code: 400, request id: 8b6bac83-1d83-4abe-8d19-bbaa4a5e4a29
```

調べていくと、`manifest.yml`でsecretsを設定するとECSのタスク実行ロールにSSM参照用のIAMポリシーが作成されることがわかりました。
ポリシーをよくよく見ると、パラメータストアのタグで条件指定して参照権限を付与していました。ここら辺のきめ細やかさはすごいですね。

```json:copilot-demo-production-apiSecretsPolicy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Condition": {
                "StringEquals": {
                    "ssm:ResourceTag/copilot-environment": "production",
                    "ssm:ResourceTag/copilot-application": "copilot-demo"
                }
            },
            "Action": [
                "ssm:GetParameters"
            ],
            "Resource": [
                "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/*"
            ],
            "Effect": "Allow"
        },
        {
            "Condition": {
                "StringEquals": {
                    "secretsmanager:ResourceTag/copilot-application": "copilot-demo",
                    "secretsmanager:ResourceTag/copilot-environment": "production"
                }
            },
            "Action": [
                "secretsmanager:GetSecretValue"
            ],
            "Resource": [
                "arn:aws:secretsmanager:ap-northeast-1:XXXXXXXXXXXX:secret:*"
            ],
            "Effect": "Allow"
        },
        {
            "Action": [
                "kms:Decrypt"
            ],
            "Resource": [
                "arn:aws:kms:ap-northeast-1:XXXXXXXXXXXX:key/*"
            ],
            "Effect": "Allow"
        }
    ]
}
```

パラメータストア登録時には以下のようにしっかりとタグ指定しましょう。

```
  $ aws ssm put-parameter --name RAILS_MASTER_KEY --value master.keyの文字列 --type SecureString\
  --tags Key=copilot-environment,Value=production Key=copilot-application,Value=copilot-demo
```

# まとめ
今回作成したコードは以下です。
https://github.com/ssshun/rails-copilot

AWS Copilotを使って作成したECS上のRailsアプリケーションにおいて、CI/CDパイプラインを構築しました。
AWSコンソールでの操作は最小限で簡単にデプロイフローを作成できました。