【後編】「よし、じゃあこのRailsアプリ、自動デプロイされるようにして」「へ、へぃ...。」って人のためのAWS環境構築

# AWSでGitHubと連携した自動デプロイの方法

## はじめに

前回の記事では、EC2インスタンスをオートスケーリングに連携するところまで解説しました。本記事では、動作しているアプリケーションをAWS CodeDeployとCodePipelineを利用して自動デプロイするところまでを解説します。具体的には、neon-cyber-webインスタンス（ロードバランサーあり）とneon-cyber-workerインスタンス（ロードバランサーなし）のデプロイを自動化します。

## 前提条件

- AWSアカウント
- GitHubリポジトリ
- 前回の記事で設定したEC2インスタンス（neon-cyber-web、neon-cyber-worker）

https://qiita.com/a16111k/items/03c1ac8780188c62da94

## 用語の説明

### AWS CodeDeploy

AWS CodeDeployは、サーバー、Lambda関数、ECSサービスなどにアプリケーションのデプロイを自動化するサービス

### AWS CodePipeline

AWS CodePipelineは、ビルド、テスト、デプロイの各ステージを自動化する継続的デリバリーサービス

### IAM>ロール

AWS IAMロールは、特定のAWSサービスに対して一時的な権限を付与するためのもの

## ステップ1：必要なロールの設定

### CodeDeploy用のサービスロール

1. AWS管理コンソールでIAMを開きます
2. 左側のメニューから「ロール」を選択し、CodeDeployのサービスロール`AWSCodeDeployRole`を探してクリックします
3. 名前は`neon-cyber-code-deploy-role`とでもしておきます

### EC2インスタンス用のロール

1. EC2インスタンスにアタッチされているロールを確認します
![ec2にアタッチされているロールの確認-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/a25b2029-e22e-6005-5ecc-c7f12663ccba.png)

1. CodeDeployを実行するために`AWSCodeDeployRole`をアタッチします
![ec2のロールにcodedeployアタッチ-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/f9e98962-6888-17a6-3f9b-ed6f5a206ede.png)

1. s3へのアクセス権限を付与するため`AmazonS3FullAccess`をアタッチします
![ec2にアタッチされているロール-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/de8c698f-596b-7e8a-cd1f-8c0c86e25dca.png)
CodePipelineはデプロイのログをs3に書き込みます。なのでs3に書き込みの権限がないとデプロイ中にAccess Deniedというエラーがでてしまいます。

https://qiita.com/wMETAw/items/d91f4706be775e1c320c

![codepipeleがs3にログを書き込もうとするが、権限がなくエラー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/9654a941-70e6-b085-1ce8-89dcec0addb1.png)




## ステップ2：EC2インスタンスにCodeDeployエージェントをインストール

### neon-cyber-webとneon-cyber-workerインスタンスにエージェントをインストール

1. EC2インスタンスにSSHで接続します
2. 以下のコマンドを実行してCodeDeployエージェントをインストールします

    ```bash
    sudo apt update
    sudo apt install ruby-full -y
    sudo apt install wget -y
    wget https://aws-codedeploy-us-east-1.s3.us-east-1.amazonaws.com/latest/install
    chmod +x ./install
    sudo ./install auto
    ```

3. エージェントのステータスを確認します

    ```bash
    systemctl status codedeploy-agent
    ```

   `Active: active (running)` と表示されれば、エージェントが正常に動作しています。

4. エージェントが正常に動作していることを確認できたら、AMIのベースとなるインスタンスにも同じ作業を行ない、起動テンプレートを差し替えておきましょう。
オートスケーリングを利用したインスタンスの更新が行われた際にもちゃんとコードデプロイエージェントが利用できるように。

## ステップ3:appspec.ymlの作成
appspec.yml ファイルは、AWS CodeDeployがデプロイを実行するために必要な設定ファイルです。このファイルにより、CodeDeployはデプロイプロセス中に実行するスクリプトや必要な設定を記述します。

### appspec.ymlが行っていること
- ソースコードのどのファイルをどのディレクトリにコピーするかを指定
- CodeDeployのライフサイクルに紐づくコマンドを実行する

### appspec.ymlの作成と配置
Gitで管理しているリポジトリ直下に配置します。
```appspec.yml
version: 0.0
os: linux
files:
  - source: /
    destination: /var/www/rails/neon-cyber #必ずGithubで管理しているディレクトリ直下
permissions:
  - object: /var/www/rails/neon-cyber
    pattern: "**"
    owner: root
    mode: 775
    type:
      - file
      - directory
hooks:
  ApplicationStop:
    - location: deployment_scripts/stop_application.sh
      runas: root
  BeforeInstall:
    - location: deployment_scripts/remove_old_files.sh
      runas: root
  AfterInstall:
    - location: deployment_scripts/get_parameters.sh
      runas: root
    - location: deployment_scripts/install_gems.sh
      runas: root
    - location: deployment_scripts/run_db_migrations.sh
      runas: root
  ApplicationStart:
    - location: deployment_scripts/restart_application.sh
      runas: root

# 本当はタイムアウトの秒数を設定することが多いようなのでひつようであれば適宜調べて追記してくださいm(_ _)M
```

次にそれぞれのライフサイクルで行われる処理の内容を記述します。

### ApplicationStop

```deployment_scripts/stop_application.sh
#!/bin/bash

echo 'stop_application.sh////////////////////////////////////////////////////////////////////////'
sudo su -

sudo systemctl stop rails.service

PROCESS_ID=$(pgrep -f puma)
if [ -n "$PROCESS_ID" ]; then
  echo 'pumaの停止'
  kill -9 $PROCESS_ID
fi

PROCESS_ID=$(pgrep -f sidekiq)
if [ -n "$PROCESS_ID" ]; then
  echo 'sidekiqの停止'
  kill -9 $PROCESS_ID
fi

# プロセスが終了するまで待つ
sleep 10
```

sleepを入れないとすぐ、次の`BeforeInstall`の処理に移行してしまい、下記のエラーが起きたのでこのように記述しています。（もっと最適な方法はいくらでもあると思います）
![アプリケーションサーバーを停止する処理のあと古いファイルを消す処理を行うが、サーバーが停止する前に実行されてエラー-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/3dd3f3e2-95cb-9b4a-9a1b-32af58f0580c.png)


### BeforeInstall

```deployment_scripts/remove_old_files.sh
#!/bin/bash

echo 'remove_old_files.sh////////////////////////////////////////////////////////////////////////'
sudo su -
# ディレクトリの操作権限、ディレクトリの削除、新たにディレクトリの作成までを行う
directory=/var/www/rails/neon-cyber/
if [ -n "$(ls $directory)" ]; then
    chmod -R 777 $directory
    rm -rf $directory
    mkdir $directory
fi
```

### AfterInstall

```deployment_scripts/get_parameters.sh
#!/bin/bash

# ssmからパラメータを取得し.envに書き出す
sudo -i
echo 'get_parameters.sh////////////////////////////////////////////////////////////////////////'
# SystemManagerのパラメータストアから環境変数を取得し、`.env`ファイルを作成

TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") \
&& INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id)
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") \
&& ZONE=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/placement/availability-zone)
REGION=$(echo $ZONE | sed 's/.$//')
FILENAME="/var/www/rails/neon-cyber/backend/.env"
NAME=$(aws --region ${REGION} ec2 describe-instances --instance-ids "${INSTANCE_ID}" --query 'Reservations[0].Instances[0].Tags[?Key==`Name`]|[0].Value')
NAME=$(echo ${NAME} | awk '{print substr($0, 2, length($0)-2)}')

if [ "${NAME}" = "neon-cyber-web" ] || [ "${NAME}" = "neon-cyber-worker" ]; then
  SSM_PARAMS=$(aws --region ${REGION} ssm get-parameters-by-path --path "/neon-cyber-production"  --with-decryption)
  for params in $(echo $SSM_PARAMS | jq -r --raw-output '.Parameters[] | .Name + "='\''" + .Value + "'\''"'); do

      echo ${params#"/neon-cyber-production/"}
  done > ${FILENAME}
fi
```

```deployment_scripts/install_gems.sh
#!/bin/bash

echo 'install_gems.sh////////////////////////////////////////////////////////////////////////'
export INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
NAME=$(aws ec2 describe-instances --instance-ids "${INSTANCE_ID}" --query 'Reservations[0].Instances[0].Tags[?Key==`Name`]|[0].Value')
NAME=$(echo ${NAME} | awk '{print substr($0, 2, length($0)-2)}')

if [ "${NAME}" = "neon-cyber-web" ] || [ "${NAME}" = "neon-cyber-web-test" ]; then
  su -l -c "cd /var/www/rails/neon-cyber/backend && bundle install"
elif [ "${NAME}" = "neon-cyber-worker" ] || [ "${NAME}" = "neon-cyber-worker-test" ]; then
  su -l -c "cd /var/www/rails/neon-cyber/backend && bundle install"
else
  echo "ec2インスタンスに適切なNameタグが付与されていないため、アプリケーションの起動に失敗しました。" 1>&2
fi
```

```deployment_scripts/run_db_migrations.sh
#!/bin/bash

su -l -c 'cd /var/www/rails/neon-cyber/backend && bundle exec dotenv -f ".env" ridgepole -c config/database.yml -E production -f db/Schemafile --apply'
```

### ApplicationStart
```deployment_scripts/restart_application.sh
sudo systemctl restart rails.service
```

ここまでできればCodeDeployが実行できるはずです。

## ステップ4：CodeDeployの設定

### デプロイアプリケーションの作成

1. CodeDeployコンソールを開きます。
2. 「アプリケーションを作成」をクリックし、アプリケーション名（例: `neon-cyber-app`）を入力します。
3. コンピューティングプラットフォームに「EC2/オンプレミス」を選びます。

### デプロイグループの作成

#### neon-cyber-web-code-deploy

- デプロイグループ名（例: `neon-cyber-web-code-deploy`）
- サービスロールに先ほど作成した`neon-cyber-code-deploy-role`を選択します。
- デプロイタイプは「インプレース」を選択します。
- ロードバランシングを有効にし、前回の記事で設定したロードバランサーを選択します。

![デプロイグループの作成-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/f5aad877-e097-5bf9-d4b5-6af0dc76bcbe.png)

### neon-cyber-worker用の設定

1. 上記と同様の手順で、`neon-cyber-app`アプリケーションを選択し、デプロイグループ名`neon-cyber-worker-code-deploy`というグループを作成します。
2. 同様にインプレース方式とし、ロードバランシングは無効にします。

### デプロイの確認 
「デプロイの作成」をクリックしてデプロイが実行されるか試してみたいと思います。
この時初回はロールバックを無効にしておいた方が良いと思います。初めてなのでロールバックのしようがありません。一度デプロイに成功してからは失敗した際にロールバックするようにした方が良いと思います。
![codedeployでデプロイできるか実験-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/49efcd6d-920c-53be-c2f7-9da890b503a7.png)

## ステップ5：CodePipelineの設定

1. CodePipelineコンソールを開きます。
2. 「パイプラインを作成」をクリックします。
![codepipelineのダッシュボード-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/63022411-7f94-1c49-1289-4d0985e0c3af.png)

3. パイプライン名（例: `neon-cyber-pipeline`）を入力します。
4. 新たにサービスロールを作成します。
![codepipelineの作成-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/af4b20fa-ffdb-a160-1c04-dd4d83eaa19b.png)
### ソースステージの設定

1. ソースプロバイダーに「GitHub」を選択し、GitHubアカウントに接続します。
![codepipeline_githubとの初回の連携.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/77815a75-c6b9-aa55-0401-9631925049e2.png)
2. リポジトリとブランチを選択します。
![codepipelineソースステーじ-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/18b6c660-4108-428e-4320-2f7c28e4476d.png)

### ビルドステージの設定
![codepipeline_githubとの初回の連携.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/71a2fd60-bbd1-b85f-0a1c-b2182d85828e.png)

1. このステップでは、簡略化のためビルドステージをスキップします（必要に応じてビルドステージを追加してください）。

### デプロイステージの設定

1. デプロイプロバイダーに「CodeDeploy」を選択します。
2. 先ほど作成した`neon-cyber-web-code-deploy`と`neon-cyber-worker-code-deploy`のデプロイグループをそれぞれ選択します。
![codepipelineデプロイステージ-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/d2fd4245-8b6b-ec68-9bef-502a4b2d7c79.png)

## ステップ6：デプロイの確認

1. GitHubリポジトリに変更をプッシュすると、自動的にCodePipelineがトリガーされ、デプロイが開始されます。
2. CodeDeployコンソールでデプロイの進捗を確認できます。

実際に特定のブランチにコミットがプッシュされた際にcode-pipelineが実行されるか確認してみます。

```bash
  git commit -m "code-pipeline test" --allow-empty
  git push
```

## 【その他】デプロイ中にハマったことハマったときに真っ先に疑うところ

### EC2にアタッチされているロールの確認
### CodeDeployのロールの確認
### CodeDeployエージェントの再起動
EC2インスタンスにSSHで接続し、以下のコマンドを実行してCodeDeployエージェントを再起動してみたらすんなり解決することもありました

```bash
sudo service codedeploy-agent restart
```

### ロールバックを無効にする
初回デプロイ時にロールバックを無効にしておくとよいです。初回は失敗してもロールバック使用がないからです。
![codedeployのエラー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/2296e991-7700-ea8e-824f-a5e9069c5921.png)

### `rm -rf /root/.aws/credentials` とCodeDeployエージェントの再起動
![codedeployのエラー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427079/39f77b64-6ad5-a96b-a26b-ecfb8cf85ba5.png)

```bash
InstanceAgent::Plugins::CodeDeployPlugin::CommandPoller: Error polling for host commands: Aws::CodeDeployCommand::Errors::AccessDeniedException -
```
なぜだかわかりませんが、古い認証情報が原因で問題が発生することがあります。以下のコマンドを実行して認証情報を削除し、CodeDeployエージェントを再起動すると治ることがありました。
https://velog.io/@gingaminga/AwsCodeDeployCommandErrorsAccessDeniedException

```bash
sudo rm -rf /root/.aws/credentials
sudo service codedeploy-agent restart
```

### appspec.ymlでdestinationの設定を間違えたとき
```
InstanceAgent::Plugins::CodeDeployPlugin::CommandPoller: Calling PutHostCommandComplete: "Code Error"
2024-06-05T05:31:45 INFO  [codedeploy-agent(395492)]: Version file found in /opt/codedeploy-agent/.version with agent version OFFICIAL_1.7.0-92_deb.
2024-06-05T05:31:45 INFO  [codedeploy-agent(395492)]: [Aws::CodeDeployCommand::Client 200 0.037543 0 retries] put_host_command_complete(command_status:"Failed",diagnostics:{format:"JSON",payload:"{\"error_code\":5,\"script_name\":\"\",\"message\":\"The CodeDeploy agent did not find an AppSpec file within the unpacked revision directory at revision-relative path \\\"appspec.yml\\\".
```
ファイルのディレクトリが違うとこのようなエラーが出ます。

### codedeploy-agentのログを確認したいとき
`tail -n 100 /var/log/aws/codedeploy-agent/codedeploy-agent.log`
などとすると良いと思います。

## 最後に
一応形になりましたが、最適でない部分も多分にあるかと思われます。おかしなところがあったらご指摘いただけますと幸いです。

以下、大変勉強になった動画です。
<https://www.youtube.com/watch?v=8mPm7jolnVk&t=855s>
<https://www.youtube.com/watch?v=_Yj1VgVkvVw&list=PLjVOh_I4G7Vd2fMtsDMyFWtcjdKD2vZTj>