CircleCIを初めて導入する【CD編：Rails6, Ruby2.7, MySQL8, Capistrano, EC2】

# はじめに

数日前にCircleCIでCI部分(自動ビルト、自動テスト)が実装できたので
次の実装としてCD部分(自動デプロイ)に取り組みました。

なお、CircleCIからcapistranoを介して
EC2に対してデプロイを実行する設定です。

私の場合、AWSセキュリティグループやcapistranoといった
CircleCIではない箇所でエラーに遭遇しました。

同じ状況に陥った方の参考になれば幸いです。



# 前提

・CircleCIのCI部分(自動ビルド、自動テスト)は実装できている
・ローカル上からcapistranoによってEC2にデプロイができている



# 使用技術

・Rails: 6.1.4
・Ruby: 2.7.3
・mysql: 8.0.25
・capistrano: 3.16.0
・EC2: AmazonLinux2



# CircleCIの実装前に

実装目標はCircleCIでの自動デプロイです。

そのため、まずは **`ローカルから`**

**`1. SSH接続が可能か`**
**`2. Capistranoでデプロイが可能か`**

の２点を検証すべきです。

私の場合は、AWSインフラを構築後、数週間の期間が空いておりましたが
ここの確認を飛ばしていましたため、
エラーが発生した際の原因突き止めに時間がかかりました。


# (1) ssh接続の確認

ここでは、sshコマンドを一部省略するために、下記ファイルを確認しています。
（この設定は任意です。通常のsshコマンドでEC2ログインするなら不要です。）

```config:~/.ssh/config

Host 自分のアプリ名
  Hostname 自分のElasticIP
  User ec2-user（AWSデフォルトのユーザーの場合）
  IdentityFile ~/.ssh/アプリ名.pem

```

ローカルのターミナルからコマンドを入力し、
EC2にssh接続できるか検証します。

```zsh:ターミナル

# 省略コマンド

% ssh 自分のアプリ名

# 通常コマンド

% ssh -i ~/.ssh/アプリ名.pem ec2-user@自分のElasticIP



〜〜返答〜〜

       __|  __|_  )
       _|  (     /   Amazon Linux 2 AMI
      ___|\___|___|


↑ ssh接続が成功してる

```

何故かログインできない場合は、EC2のセキュリティグループで
インバウンドルールを疑ってみてください。

接続先をマイIP（ローカルPC）にしている場合は、
wifiの切断などがあると、再びDHCPが働き、
別のグローバルIPが割り振られる可能性があります。

その場合は、
`・再度AWSコンソールでマイIPを選び直す`
`・Anywhere-IPv4を選択する`
で解決します。

![スクリーンショット 2021-09-12 11.52.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/13f6b9e3-4dc0-c2b2-705a-1ce176ed6580.png)



# (2) capistranoデプロイの確認

ローカルからcapistranoのデプロイ実行を起動させます。

```zsh:ターミナル

# Railsアプリのディレクトリで実行

% bundle exec cap production deploy 



〜〜返答〜〜

省略

00:57 puma:restart

↑ puma再起動などエラーなく一連の動作が実行されている。

```

途中でデプロイが止まってしまう場合、Gemfile.lock(gem)が
`・ローカル`
`・本番環境`
で差異がないか疑ってみてください。

このエラーであれば、
**`ローカルでbundle installコマンドを実行した後に`**
**`git pushコマンドでGitHubのmasterブランチに`**
**`コードを反映させる`**
で解決します。

```zsh:ターミナル

### Railsアプリのディレクトリで実行
# gemインストールして、Gemfile.lockに反映
% bundle install

# 変更したファイル(Gemfile.lock)をGitHubのmasterブランチに反映
% git push

# 反映したmasterブランチのコードを元にcapistranoがデプロイ実行
% bundle exec cap production deploy 

```

私の場合は、CircleCIの実装に入ってから
Gemfile差異によるエラーが発生して、capistranoがどこのGitHubコードを
もとに実行しているのか整理がつかなくなり、悩みました。

(capistranoの設定によっては、masterブランチではなく
所属するブランチに変更できるのかもしれません。)


(1),(2)も初期実装から間が空いているほど
エラーになる可能性が高いかと思います。
　-> wifiいじったり、gem追加/削除したりしてる可能性で

なので、CircleCIの自動デプロイ実装前に確認しておくことが
良いと思います。



# では、本題のCircleCI（自動デプロイ設定）

工程としては以下の通りです。

・CircleCIコンソール: ssh秘密鍵を登録
・CircleCI設定ファイル: ssh秘密鍵を読み込ませる
・CircleCI設定ファイル: capistranoのデプロイが起動させる
・CircleCI設定ファイル: masterブランチの時だけデプロイされる様に条件をつける

次の章から実装していきます！



# ssh秘密鍵を登録

今までは、ローカルからEC2へSSH接続しておりましたが、
CircleCIからEC2へSSH接続できる様にSSH秘密鍵を登録します。

ここでは、CircleCIのコンソールを操作します。

まず、対象リポジトリのプロジェクトで画面右上にある
`Project Settingsボタン`をクリックします。

![スクリーンショット 2021-09-12 13.01.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/395cc9ef-935a-b1f8-744b-08c71767bd6b.png)

次に画面が切り替わったら、画面左側にある
`SSH Keysタブ`をクリックします。

![スクリーンショット 2021-09-12 13.06.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/181bc6a3-86c9-9782-0419-3483fcb6a84f.png)

SSH Keyタブに切り替わったら、画面を下にスクロールして
`Add SSH Keyボタン`をクリックします。

![スクリーンショット 2021-09-12 13.07.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/ba546b1f-3e1f-8cef-fc74-3ad20670e687.png)

すると、ポップアップが出現しますので
下記の２つを入力して、Add SSH Keyボタンを押します。


1. ドメイン名かElasticIPを入力
2. ssh接続の認証に必要な鍵のデータを貼り付け

2.は以下の様にターミナルからコマンドでコピーもできます。

```ssh:ターミナル

% pbcopy < ~/.ssh/〇〇.pem
　↑ 裏でクリップボードにコピーされている状態（後は貼り付けるだけ）

```

![スクリーンショット 2021-09-12 13.08.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/38561b4e-1296-2754-457c-0d3206e036f6.png)

Add SSH Keyボタンを押すと、
●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●といった
Fingerprintが生成されます。

これは次章で使いますので、コピーしておきましょう。
以上で、SSH秘密鍵の登録が完了です。



# ssh秘密鍵を読み込ませる

先ほど登録したSSH秘密鍵を
CircleCI設定ファイル(.circleci/config.yml)で読み取らせます。

```yml:.circleci/config.yml

省略

      - add_ssh_keys:
          fingerprints:
            - "●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●"

```

●●部分はCircleCIコンソール画面のfingerprintsに表示されているものを
そのままコピペします。

ここでSSH接続の検証のために、git pushコマンドをしてみましょう。

上手くいけば、Installing additional ssh keysが緑色で表示されて
ssh秘密鍵のインストールが成功していることを確認できます。

![スクリーンショット 2021-09-12 13.46.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/7dfa7354-4823-216f-dbee-f4fdb42e31e9.png)

ここまでの作業により、CircleCIからEC２へのssh接続ができる様になりました。


# capistranoのデプロイが起動させる

ここでも同じくCircleCI設定ファイル(.circleci/config.yml)を
編集していきます。

次はcapistranoの実行コマンドを追記します。


```yml:.circleci/config.yml

省略
      - add_ssh_keys:
          fingerprints:
            - "●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●"

      # capistrano実行コマンドを追記
      - deploy:
          name: Capistrano deploy
          command:
            bundle exec cap production deploy

```

追記できたら、git pushしてみましょう。

これでCircleCIからcapistranoに橋渡しがされて
capistranoがデプロイ実行をしてくれます！



# masterブランチの時だけデプロイされる様に条件をつける

前章までの実装では、git pushした時点で
毎回デプロイが実装されてしまいます。

そこで、masterブランチにマージされた時だけ
デプロイが実行される様に修正していきます。


```yml:.circleci/config.yml

省略
      - add_ssh_keys:
          fingerprints:
            - "●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●"

      # capistrano実行コマンドにif文で条件分岐をさせる
      - deploy:
          name: Capistrano deploy
          command: |
            if [ "${CIRCLE_BRANCH}" != "master" ]; then
              exit 0
            fi
            bundle exec cap production deploy

```

以上で完成です！



# 実装後のconfig.yml

参考までに自動デプロイの実装を終えて完成したconfig.ymlを載せておきます。

ただ、自動ビルド/自動テスト/自動デプロイをまとめて記述しているので
可読性としてどうかな〜って気持ちがあります。

ですので、個人的には各フェーズで３つのjobに分けて
見やすく変更していこうと考えています。

当記事では、自動デプロイの簡易記述として
上記の改善点は考慮せずにこれで完了とします。


```yml:.circleci/config.yml

version: 2.1
jobs:
  build:
    docker:
      - image: circleci/ruby:2.7.3-node-browsers
        environment:
          RAILS_ENV: 'test'
      - image: circleci/mysql:8.0
        command: mysqld --default-authentication-plugin=mysql_native_password
        environment:
          MYSQL_ALLOW_EMPTY_PASSWORD: 'true'
          MYSQL_ROOT_HOST: '%'

    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "Gemfile.lock" }}
            - v1-dependencies-
      - run:
          name: install dependencies
          command: |
            bundle install --jobs=4 --retry=3 --path vendor/bundle
      - save_cache:
          paths:
          - ./vendor/bundle
          key: v1-dependencies-{{ checksum "Gemfile.lock" }}
      - run: yarn add @fortawesome/fontawesome-free
      - run: mv config/database.yml.ci config/database.yml
      - run: bundle exec rake db:create
      - run: bundle exec rake db:schema:load
      - run:
          name: Rubocop
          command: bundle exec rubocop
      - run:
          name: RSpec
          command: |
            mkdir /tmp/test-results
            TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \
              circleci tests split --split-by=timings)"
            bundle exec rspec \
              --format progress --format RspecJunitFormatter \
              --out /tmp/test-results/rspec.xml \
              $TEST_FILES
      - store_test_results:
          path: /tmp/test-results
      - store_artifacts:
          path: /tmp/test-results
          destination: test-results
      - add_ssh_keys:
          fingerprints:
            - "●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●:●●"
      - deploy:
          name: Capistrano deploy
          command: |
            if [ "${CIRCLE_BRANCH}" != "master" ]; then
              exit 0
            fi
            bundle exec cap production deploy

```



# ちょっと疑問に思ってる事

CircleCIの自動デプロイを実装しているQiita記事を見ていると
・RAILS_MASTER_KEY
・PRODUCTION_SSH_KEY
の環境変数をCircleCIコンソールで設定している情報が多かった。

https://qiita.com/YK0214/items/98bbdf7ce25c557c77a3

CircleCIではなくcapistranoが上記を読み込めば上手く動作するのでは…
という仮説のもと、今回の実装としているけど自分の勉強不足かな？

上記2点の設定はしていないために
セキュリティ的に甘くなっている可能性もあるので
今後に不都合が出てきた際には修正しよう！

はい。独り言でした。



# 参考記事

https://qiita.com/spaghetti_man03/items/882b70f663f4a80ae770



# CircleCIの自動ビルド/自動テスト編

CI部分はこちらの記事で紹介しております。

https://qiita.com/Shuhei_Nakada/items/a1cfae77e3463a5b9ee8


# 終わりに

これで、CI/CDどちらも実装はできたものの最小限の実装しか出来ておりません。

CircleCIには、並列作業したり、ワークフローで実行順序を決めたり、
もっと効率化させる実装があるはずなので、
先人たちの実装を参考にしてブラッシュアップをしていこうと思います。

最後までお読み頂き、ありがとうございました。