💨

【Nuxt x Rails】サンプルTODOアプリ - CI/CD編

2022/04/11に公開

今回検証目的でフロントに Nuxt、バックエンドに Rails、インフラに AWS を使って以下のような TODO アプリを作りました。
image
この記事では GitHub Actions に関する解説を行います 🙋‍♂️

  • 以下の記事で全体の解説を行っています。

https://zenn.dev/tokku5552/articles/nuxt-rails-sample

  • 全ソースコードはこちら

https://github.com/tokku5552/nuxt-rails-sample

環境

  • インフラ構成図

image

GitHub Actions のワークフロー

今回は job をdeploy_cdk,deploy_rails,deploy_frontの 3 つに分けて、直列に実行させています。

全コード
.github/workflows/workflow.yml
name: deploy prd

on:
  push:
    branches:
      - main

jobs:
  deploy_cdk:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Setup Node
        uses: actions/setup-node@v1
        with:
          node-version: "16.13.2"
      - name: cache
        uses: actions/cache@v2
        with:
          path: ./cdk/node_modules
          key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.OS }}-node-
            ${{ runner.OS }}-
      - name: npm install
        working-directory: ./cdk
        run: npm install
      - name: cdk deploy
        working-directory: ./cdk
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: ap-northeast-1
          AWS_DEFAULT_OUTPUT: json
          CDK_ENV: ${{ secrets.CDK_ENV }}
        run: |
          echo "$CDK_ENV" > .env
          npx cdk deploy --all
  deploy_rails:
    runs-on: ubuntu-latest
    needs: deploy_cdk
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: cache vendor
        id: cache
        uses: actions/cache@v2
        with:
          path: api/vendor/bundle
          key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
          restore-keys: |
            ${{ runner.os }}-gems-
      # Ruby をインストールする
      - name: Set up Ruby 2.6.6
        uses: ruby/setup-ruby@8f312efe1262fb463d906e9bf040319394c18d3e # v1.92
        with:
          ruby-version: 2.6.6
      # バンドラーをインストールし、初期化する
      - name: Bundle install
        working-directory: ./api
        run: |
          gem install bundler
          bundle config path vendor/bundle
          bundle install --jobs 4 --retry 3
      # awscliのインストール
      - name: install awscli
        working-directory: ./api
        run: |
          curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
          unzip awscliv2.zip
          sudo ./aws/install --update
          aws --version
          curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" -o "session-manager-plugin.deb"
          sudo dpkg -i session-manager-plugin.deb
      - name: setup ssh
        working-directory: ./api
        run: |
          # sshキーをコピー
          mkdir -p /home/runner/.ssh
          touch /home/runner/.ssh/MyKeypair.pem
          echo "${{ secrets.SSH_DEPLOY_KEY }}" > /home/runner/.ssh/MyKeypair.pem
          chmod 600 /home/runner/.ssh/MyKeypair.pem
      - name: deploy to EC2
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: ap-northeast-1
          AWS_DEFAULT_OUTPUT: json
          RAILS_ENV: ${{ secrets.RAILS_ENV }}
        working-directory: ./api
        run: |
          echo "$RAILS_ENV" > .env
          source .env
          TARGET_INSTANCE_ID=$TARGET_INSTANCE_ID bundle exec cap production deploy
  deploy_front:
    runs-on: ubuntu-latest
    needs: deploy_rails
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Setup Node
        uses: actions/setup-node@v1
        with:
          node-version: "16.13.2"
      - name: cache
        uses: actions/cache@v2
        with:
          path: ./front/node_modules
          key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.OS }}-node-
            ${{ runner.OS }}-
      - name: npm install
        working-directory: ./front
        run: |
          npm install -g yarn
          yarn install
      - name: front deploy
        working-directory: ./front
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: ap-northeast-1
          AWS_DEFAULT_OUTPUT: json
          FRONT_ENV: ${{ secrets.FRONT_ENV }}
        run: |
          echo "$FRONT_ENV" > .env
          source .env
          yarn generate
          aws s3 sync dist s3://nuxt.s3bucket/ --include "*"
          aws cloudfront create-invalidation --distribution-id ${{ secrets.DISTRIBUTIN_ID }} --paths "/*"

冒頭と全体の流れ

冒頭部分ですが名前つけとトリガーの設定を行っています。
main ブランチが更新されたときにキックされるようになっています。

.github/workflows/workflow.yml
name: deploy prd

on:
  push:
    branches:
      - main

jobs:
  deploy_cdk:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
...

テストはなしで、ビルドもジョブを分けていないですが、より実践的にはビルドを並列で実行してデプロイは選択式にするとより良いと思います。
参考:ワークフローの手動実行 - GitHub Docs

deploy_cdk

AWS CDKv2 によるインフラ部分です。
node を入れて node_modules をキャッシュしています。
AWSCLI は入れていないですが、AWSCLI が使えるような状態を環境変数で設定してやる必要があります。AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY

.github/workflows/workflow.yml
jobs:
  deploy_cdk:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Setup Node
        uses: actions/setup-node@v1
        with:
          node-version: "16.13.2"
      - name: cache
        uses: actions/cache@v2
        with:
          path: ./cdk/node_modules
          key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.OS }}-node-
            ${{ runner.OS }}-
      - name: npm install
        working-directory: ./cdk
        run: npm install
      - name: cdk deploy
        working-directory: ./cdk
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: ap-northeast-1
          AWS_DEFAULT_OUTPUT: json
          CDK_ENV: ${{ secrets.CDK_ENV }}
        run: |
          echo "$CDK_ENV" > .env
          npx cdk deploy --all

その他、ホストゾーン ID など、今回の構成で必要な情報を GitHub のシークレットに保存しておき、cdk deploy 実行前に.envに書き出しています。
cdk 側でdotenvによる環境変数の自動読み込みを設定しているんので、.envに書き出すだけで OK です。
スタックを分けているので--allのオプションが必要です。

CDK の詳細に関しては以下を御覧ください。

https://zenn.dev/tokku5552/articles/nuxt-rails-cdk

deploy_rails

Rails 側ですが、Ruby をバージョン指定でインストールし、bundle install を行います。
capistrano でデプロイを行いますが、そのために AWSCLI のインストールと SSH の鍵の配置を行っています。

.github/workflows/workflow.yml
  deploy_rails:
    runs-on: ubuntu-latest
    needs: deploy_cdk
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: cache vendor
        id: cache
        uses: actions/cache@v2
        with:
          path: api/vendor/bundle
          key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
          restore-keys: |
            ${{ runner.os }}-gems-
      # Ruby をインストールする
      - name: Set up Ruby 2.6.6
        uses: ruby/setup-ruby@8f312efe1262fb463d906e9bf040319394c18d3e # v1.92
        with:
          ruby-version: 2.6.6
      # バンドラーをインストールし、初期化する
      - name: Bundle install
        working-directory: ./api
        run: |
          gem install bundler
          bundle config path vendor/bundle
          bundle install --jobs 4 --retry 3
      # awscliのインストール
      - name: install awscli
        working-directory: ./api
        run: |
          curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
          unzip awscliv2.zip
          sudo ./aws/install --update
          aws --version
          curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" -o "session-manager-plugin.deb"
          sudo dpkg -i session-manager-plugin.deb
      - name: setup ssh
        working-directory: ./api
        run: |
          # sshキーをコピー
          mkdir -p /home/runner/.ssh
          touch /home/runner/.ssh/MyKeypair.pem
          echo "${{ secrets.SSH_DEPLOY_KEY }}" > /home/runner/.ssh/MyKeypair.pem
          chmod 600 /home/runner/.ssh/MyKeypair.pem
      - name: deploy to EC2
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: ap-northeast-1
          AWS_DEFAULT_OUTPUT: json
          RAILS_ENV: ${{ secrets.RAILS_ENV }}
        working-directory: ./api
        run: |
          echo "$RAILS_ENV" > .env
          source .env
          TARGET_INSTANCE_ID=$TARGET_INSTANCE_ID bundle exec cap production deploy

注意すべき点なのは、今回の構成では EC2 へは SSM 経由で SSH 接続したいので、AWSCLI を通常通りインストールしたあと、session-manager-pluginを別途インストールする必要があります。
SSH の鍵はシークレットから生成しておいて、接続の設定(プロキシコマンドなど)は capistrano 側に記載してあるので、ここではセットする必要はありません。
例のごとく RAILS で必要な環境変数は.envに書き出して読み込んでいます。

capistrano の設定に関しては以下を御覧ください。

https://zenn.dev/tokku5552/articles/nuxt-rails-backend

deploy_front

最後にフロント側のデプロイです。
フロント側では yarn を使っているので、yarn install したあとにデプロイ作業を行っています。

.github/workflows/workflow.yml
  deploy_front:
    runs-on: ubuntu-latest
    needs: deploy_rails
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Setup Node
        uses: actions/setup-node@v1
        with:
          node-version: "16.13.2"
      - name: cache
        uses: actions/cache@v2
        with:
          path: ./front/node_modules
          key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.OS }}-node-
            ${{ runner.OS }}-
      - name: npm install
        working-directory: ./front
        run: |
          npm install -g yarn
          yarn install
      - name: front deploy
        working-directory: ./front
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: ap-northeast-1
          AWS_DEFAULT_OUTPUT: json
          FRONT_ENV: ${{ secrets.FRONT_ENV }}
        run: |
          echo "$FRONT_ENV" > .env
          source .env
          yarn generate
          aws s3 sync dist s3://nuxt.s3bucket/ --include "*"
          aws cloudfront create-invalidation --distribution-id ${{ secrets.DISTRIBUTIN_ID }} --paths "/*"

デプロイは awscli で S3 バケットへのアップロードと CloudFront のキャッシュ削除を行っています。
yarn generatenuxt generateが走り、SPA モードでdistにビルドされた結果が出力されます。
それをオリジンに指定している S3 バケットにアップロードして、その後全てのパスのキャッシュを削除しているという流れです。

Nuxt 編はこちら

https://zenn.dev/tokku5552/articles/nuxt-rails-frontend

まとめ

今回の構成では事前に AWS CDK でインフラを構築したあと、EC2 の設定を行って、更に必要な情報(EC2 のインスタンス ID、CroudFront の DISTRIBUTION ID、S3 のバケット名)を取得しておく必要があります。(つまり CDK 側の変更箇所によっては EC2 が再作成されて、環境変数を再設定しないと CI/CD が動かなくなる...)
本来は EC2 側は手動で設定済みの EC2 から AMI を作成しておいて、それを使ってインスタンスを生成し上記の情報は CDK からパラメータストアかどこかに保持しておいて、コードのデプロイ時にはそれを都度参照したほうがより良いと思います。

Discussion