Vue.jsとRails APIモードでストレスフリーに少人数開発できた話

# はじめに
去年からエンジニアの友人と**TeXで書けるQ&Aサービス**というものを開発しており、今回無事にリリースまで到達できました。

フレームワークはフロントはVue.js、バックエンドはRuby on Railsを使用しました。

友人はフロントエンド、自分はバックエンドがそれぞれ得意領域だったので、なるべくお互いの領域に集中できるようにフロントエンドとバックエンドはリポジトリごと完全に分離しました。（どうしてもフロントの方が書くべきコード量が多くなるので、最終的には自分もフロントを手伝うことになりました。。。)

思いの外快適に開発を進められたので、もし他にも友人と楽しく個人開発したい！という方の参考にでもなればと、開発体制やサービスのアーキテクチャなどを軽く共有してみようと思います。


# サービスの概要

TeXで書けることが売りのQ&Aサイトです。
[ELPOT TeXで書ける理系向けQ&Aサイト](https://el-pot.com)

TeXというのは1978年に*Donald E. Knuth*が開発した、比較的歴史のある"組版システム"です。
大学で理系学部に所属していた人なら一度は使ったことがあるかと思います。
詳しく説明すると面倒くさいので端折ると、用者側からは**数式などを綺麗に描画するためのマークアップ言語**と思ってもらえればいいかと思います。

開発の動機としては、数式を気軽に扱えるQ&Aプラットフォームって、(少なくとも日本では)あまり聞いたことないなーと。
某Q&Aサイトなどで無理やりテキストで数式を表現しているような投稿がよく見られますが、あれでは投稿する側も面倒くさいし読む側も読みにくいし、しんどみが深いです。
数式が誰でも書きやすく、読みやすいQ&Aサイトを作ることによって、オンライン上での数学や物理談義をもっと盛り上げられたらなと思い、今回開発に至りました。

現状の投稿エディタでは、TeXとMarkdownで記述することができます。
今後はより数式を扱う敷居を下げるために、エディタを拡張しボタン一つで数式を埋め込めるようなUI/UXにしていこうと思っています。

# アーキテクチャ
上述したとおりバックエンドはrailsをAPIモードで動かし、フロントエンドはVue.jsでSPAを構築しています。

バックエンドのデプロイ先は`Heroku`で、Dockernizeしているため`Container Registry`にpushしたimageを元にコンテナを動かしています。
フロントエンドのデプロイ先は`S3 + CloudFront`という定番構成です。

`Route53`はドメインレジストラとしても使っているため、フロントエンド関連の請求書は完全にAWSにまとめられ管理が楽です。
メール認証などで使うsmtpサーバーとしては`SendGrid`を使用しています。


システム構成図としてはだいたいこんな感じです。
![el_pot_diargram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319032/4c79c070-35f4-ee30-567a-4c1a465d9012.png)


# 開発体制

## 開発環境
backendは完全にDockernizeしています。
本番用のimageはなるべく軽くしたいのと、開発用は開発用でvimとかのデバッグに使うpackageいれときたいのもあって、`Multi-stage Build`でいい感じにしています。
なお、`--target=production`でbuildした時余計なimageであるdevelopmentまでbuildされないように、`BuildKit`を有効にしておくと良いと思います。
ご参考までに今回使ってる`Dockerfile + entrypoint.sh`と`docker-compose.yml`載せときます。
需要あったら暇な時詳細まとめます。

```:Dockerfile
FROM ruby:2.6.5-alpine AS base
ENV LANG=ja_JP.UTF-8 \
    TZ=Asia/Tokyo

ARG APP_HOME="/var/src/my_app"
WORKDIR $APP_HOME

RUN apk add --update --no-cache \
      postgresql \
      tzdata && \
    apk add --virtual build-packs --update --no-cache \
      build-base \
      curl-dev \
      postgresql-dev && \
    gem install bundler

COPY scripts/entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]

COPY . .

EXPOSE 3000
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]

# 開発用の追加設定
FROM base AS development

RUN apk add --update --no-cache \
      less \
      vim
RUN bundle install -j4

# 本番用の追加設定
FROM base AS production

RUN bundle config set frozen 'true' && \
    bundle config set without 'development test' && \
    bundle install -j4 && \
    apk del build-packs
```

```entrypoint.sh
#!/bin/sh
set -e
rm -f /var/src/my_app/tmp/pids/server.pid
exec "$@"
```

```docker-compose.yml
version: "3.7"
volumes:
  data_volume:
services:
  db:
    image: postgres:11.6-alpine
    ports:
      - "5432:5432"
    restart: always
    volumes:
      - data_volume:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: pass
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=C"
  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: development
    volumes:
      - .:/var/src/my_app
    ports:
      - "3000:3000"
    environment:
      DATABASE_HOST: db
    links:
      - db
      - redis
    cap_add:
      - ALL
    tty: true
    stdin_open: true
    privileged: true
    logging:
      driver: "json-file"
      options:
        max-size: "100k"
  swagger:
    image: swaggerapi/swagger-ui
    container_name: el_pot_swagger
    ports:
      - "10000:8080"
    volumes:
      - ./openapi.yml:/usr/share/nginx/html/api/openapi.yml
    environment:
      API_URL: http://localhost:10000/api/openapi.yml
  smtp:
    image: schickling/mailcatcher
    container_name: el_pot_smtp
    ports:
      - "1080:1080"
      - "1025:1025"

```


frontendは今回SSRなどはせずシンプルなSPAなので、普通にマシンにNode.jsを入れてもらって開発してます。
pre-commitとかの詳しい設定などは友人が書いた記事[《FE編》数式を使って質問できるQ&Aサービス「el-pot」をローンチしました - Qiita](https://qiita.com/katuo0011/items/7057af57ed195c359d66)に書いてあることを期待してます。

## スキーマ駆動開発
スキーマ駆動開発とは、**最初に利用者(今回はフロント開発者)と提供者(今回はバックエンド開発者)の間でAPIのスキーマをYAMLやJSONなどで固めておき、それを基にお互いが同時並行で開発を進めていく方式**です。

フロントエンドとバックエンドが分離された開発体制における大変なこととして、

- API仕様に関する互いの認識齟齬があり、結果実装後の手戻りが発生してしまう
- バックエンドの実装が完了するまで、フロントエンドの実装が開始できずタイムロスが生じる

などがあると思います。
スキーマ駆動開発を取り入れることで、YAMLやJSON文書として仕様に関するお互いの認識を明確に擦り合わせられ、また周辺ツールの力を借りることでモックサーバの自動生成もできるようになるので、上記の問題点が解決し、非常にスムーズな同時並行開発ができるようになります。

今回はREST APIを採用していたため、OpenAPI3.0に沿ってYAMLでAPI仕様を記述しました。
OpenAPIは周辺ツールが豊富で、VSCodeにもプレビュー用のプラグインが存在しますし、([OpenAPI Preview](https://marketplace.visualstudio.com/items?itemName=zoellner.openapi-preview))上のdocker-compose.ymlを見てもらうと分かるのですが、誰でも手軽にプレビューの確認・HTTPリクエストの実行ができるようにコンテナを立てるのも容易にできます。

この手法を採用したおかげで、コミュニケーションもスムーズになり、開発効率は格段に上がりました。

## ブランチ戦略
`Github Flow`を少し変形させた感じで運用しています。
produtionへのデプロイはタグ付け方式で行っています。

### 概要
- 使うブランチは master, develop, feature/\*, fix/\*
- master ブランチから developブランチを切る
- develop ブランチは staging 環境と同期しており、検証用に使う
- doc の更新系は master に直 push で OK

### Flow
1. master ブランチから develop ブランチを切る
2. develop ブランチから feature/\*ブランチを切る
3. feature/\*ブランチで開発を行う
4. feature/\*ブランチから develop ブランチに PR を出し、merge
5. staging 環境でテストを行い、NG なら 新たに fix/\*ブランチで修正を行った後 develop に再度 PR
6. キリのいいタイミングで、staging 環境でのテスト結果が OK なら、develop からmasterへPRを出しmerge
7. /v.*/に match する tag を切り、productionへデプロイ


## タスク管理・コミュニケーションツール
タスク管理は`Trello`、コミュニケーションツールは`Slack`です。

友人同士なんだからLINEでいいんじゃないの？って思うかもしれませんが、やはり日々の雑談と開発に関わる話が交じってしまうのでNGです。
Slackだと`#general`では雑談、`#develop`では開発に関わる話と明確に分けられるので便利です。
それに、やっぱりスタンプを独自に作れるので楽しいです笑

タスク管理がTrelloなのは無料だし感覚軽いからです。
2人程度の趣味開発にJiraやRedmineはオーバーです。

また、SlackはCircleCI, GitHub, trello, Qase と連携させて通知チェックもまとめて行えるようにしています。

## CI/CDパイプライン
概観としてはこんな感じです。

![ci_cd_pipeline.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319032/d574f320-35d8-286f-094f-63550a2ff2b6.png)


CircleCIでテストもデプロイもやってます。
CircleCIは本当に便利で、docker-compose.ymlファイルをほぼそのまま生かしたテスト実行ができるのでかなり楽に設定ファイルが書けます。
現時点での最新版のmachineを使用すればDockerのMulti-stage buildも、BuildKitも利用できます。

以下ご参考までに設定ファイルを載せておきました。
これも需要あるかわかりませんがまた暇な時詳細解説します。

**backend**

```config.yml
version: 2.1
executors:
  executor:
    machine:
      image: ubuntu-1604:201903-01
    environment:
      - COMPOSE_FILE: docker-compose.ci.yml
jobs:
  test:
    executor: executor
    steps:
      - checkout
      - run:
          name: docker-compose build
          command: docker-compose build
      - run:
          name: docker-compose up
          command: docker-compose up -d
      - run:
          name: setup db
          command: docker-compose run --rm app bundle exec rails db:create db:migrate
      - run:
          name: rubocop
          command: docker-compose run --rm app bundle exec rubocop
      - run:
          name: brakeman
          command: docker-compose run -T --rm app bundle exec brakeman -A -w1 -z
      - run:
          name: rspec
          command: docker-compose run --rm app bundle exec rspec
      - run:
          name: docker-compose down
          command: docker-compose down
  staging-deploy:
    executor: executor
    steps:
      - checkout
      - run:
          name: build docker image
          command: docker build --target=production --rm=false -t registry.heroku.com/${STAGING_HEROKU_APP_NAME}/web .
      - run:
          name: setup heroku command
          command: .circleci/setup_heroku.sh
      - run:
          name: heroku maintenance on
          command: heroku maintenance:on --app ${STAGING_HEROKU_APP_NAME}
      - run:
          name: push container to registry.heroku.com
          command: |
            docker login --username=_ --password=$HEROKU_AUTH_TOKEN registry.heroku.com
            docker push registry.heroku.com/${STAGING_HEROKU_APP_NAME}/web
      - run:
          name: release the latest version
          command: heroku container:release web --app ${STAGING_HEROKU_APP_NAME}
      - run:
          name: heroku db migrate
          command: heroku run bundle exec rails db:migrate --app ${STAGING_HEROKU_APP_NAME}
      - run:
          name: heroku maintenance off
          command: heroku maintenance:off --app ${STAGING_HEROKU_APP_NAME}
  production-deploy:
    executor: executor
    steps:
      - checkout
      - run:
          name: build docker image
          command: docker build --target=production --rm=false -t registry.heroku.com/${PRODUCTION_HEROKU_APP_NAME}/web .
      - run:
          name: setup heroku command
          command: .circleci/setup_heroku.sh
      - run:
          name: heroku maintenance on
          command: heroku maintenance:on --app ${PRODUCTION_HEROKU_APP_NAME}
      - run:
          name: push container to registry.heroku.com
          command: |
            docker login --username=_ --password=$HEROKU_AUTH_TOKEN registry.heroku.com
            docker push registry.heroku.com/${PRODUCTION_HEROKU_APP_NAME}/web
      - run:
          name: release the latest version
          command: heroku container:release web --app ${PRODUCTION_HEROKU_APP_NAME}
      - run:
          name: heroku db migrate
          command: heroku run bundle exec rails db:migrate --app ${PRODUCTION_HEROKU_APP_NAME}
      - run:
          name: heroku maintenance off
          command: heroku maintenance:off --app ${PRODUCTION_HEROKU_APP_NAME}
workflows:
  version: 2
  test_and_deploy:
    jobs:
      - test:
          filters:
            tags:
              only: /v.*/
            branches:
              ignore: master
      - staging-deploy:
          requires:
            - test
          filters:
            branches:
              only: develop
      - production-deploy:
          requires:
            - test
          filters:
            tags:
              only: /v.*/
            branches:
              ignore: /.*/

```

```setup_heroku.sh
#!/bin/bash
wget https://cli-assets.heroku.com/branches/stable/heroku-linux-amd64.tar.gz
sudo mkdir -p /usr/local/lib /usr/local/bin
sudo tar -xvzf heroku-linux-amd64.tar.gz -C /usr/local/lib
sudo ln -s /usr/local/lib/heroku/bin/heroku /usr/local/bin/heroku

cat > ~/.netrc << EOF
machine api.heroku.com
  login $HEROKU_LOGIN
  password $HEROKU_API_KEY
EOF

# Add heroku.com to the list of known hosts
ssh-keyscan -H heroku.com >> ~/.ssh/known_hosts

```

**frontend**

```config.yml
version: 2.1

orbs:
  aws-s3: circleci/aws-s3@1.0.11

executors:
  default:
    docker:
      - image: circleci/node
      - image: circleci/python:2.7

commands:
  yarn_install:
    steps:
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "yarn.lock" }}
            - v1-dependencies-
      - run:
          name: Install dependencies
          command: yarn install
      - save_cache:
          paths:
            - node_modules
          key: v1-dependencies-{{ checksum "yarn.lock" }}

jobs:
  test:
    executor: default
    working_directory: ~/repo
    steps:
      - checkout
      - yarn_install
      - run:
          name: ESlint
          command: yarn run lint
      - run:
          name: UnitTest
          command: yarn run test
      - run:
          name: Build
          command: yarn build:prd
      - run:
          name: Check dist
          command: ls -la dist

  staging-deploy:
    executor: default
    working_directory: ~/repo
    steps:
      - checkout
      - yarn_install
      - run:
          name: build
          command: yarn run build:stg
      - aws-s3/sync:
          from: dist
          to: s3://staging.el-pot.com
          overwrite: true

  production-deploy:
    executor: default
    working_directory: ~/repo
    steps:
      - checkout
      - yarn_install
      - run:
          name: build
          command: yarn run build:prd
      - aws-s3/sync:
          from: dist
          to: s3://el-pot
          overwrite: true
      - run:
          name: Cache clear
          command: aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_DISTRIBUTION_ID --paths '/*'

workflows:
  version: 2
  test_and_deploy:
    jobs:
      - test:
          filters:
            tags:
              only: /v.*/
            branches:
              ignore: master
      - staging-deploy:
          requires:
            - test
          filters:
            branches:
              only: develop
      - production-deploy:
          requires:
            - test
          filters:
            tags:
              only: /v.*/
            branches:
              ignore: /.*/
```


## テスト
テストケースの作成・管理・実行はすべてこちらを使いました。
[Qase](https://app.qase.io/)

直感的なUIで使いやすく、エクセルとかスプレッドシートを使ってるときのようないかにもな**仕事やってる感**を消せるのでおすすめです（休日の楽しい個人開発の時まで仕事気分を味わいたくはありません）


# 最後に
今回総論として、Vue.js+Railsでスムーズな少人数開発を行うための1例を紹介させてもらいました。
Dockerfileの構成はじめ、各論については軽くしか触れてないので、また時間があったら記事に起こしてみようと思います。

サービスは一応リリースできましたが、エディタの強化、監視/ログ収集基盤の構築、パフォーマンス改善などやることはまだまだ無限にあるので、今後も楽しみながら開発を続けていこうと思います。