はじめに
去年からエンジニアの友人とTeXで書けるQ&Aサービスというものを開発しており、今回無事にリリースまで到達できました。
フレームワークはフロントはVue.js、バックエンドはRuby on Railsを使用しました。
友人はフロントエンド、自分はバックエンドがそれぞれ得意領域だったので、なるべくお互いの領域に集中できるようにフロントエンドとバックエンドはリポジトリごと完全に分離しました。(どうしてもフロントの方が書くべきコード量が多くなるので、最終的には自分もフロントを手伝うことになりました。。。)
思いの外快適に開発を進められたので、もし他にも友人と楽しく個人開発したい!という方の参考にでもなればと、開発体制やサービスのアーキテクチャなどを軽く共有してみようと思います。
サービスの概要
TeXで書けることが売りのQ&Aサイトです。
ELPOT TeXで書ける理系向けQ&Aサイト
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
を使用しています。
開発体制
開発環境
backendは完全にDockernizeしています。
本番用のimageはなるべく軽くしたいのと、開発用は開発用でvimとかのデバッグに使うpackageいれときたいのもあって、Multi-stage Build
でいい感じにしています。
なお、--target=production
でbuildした時余計なimageであるdevelopmentまでbuildされないように、BuildKit
を有効にしておくと良いと思います。
ご参考までに今回使ってるDockerfile + entrypoint.sh
とdocker-compose.yml
載せときます。
需要あったら暇な時詳細まとめます。
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
#!/bin/sh
set -e
rm -f /var/src/my_app/tmp/pids/server.pid
exec "$@"
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に書いてあることを期待してます。
スキーマ駆動開発
スキーマ駆動開発とは、最初に利用者(今回はフロント開発者)と提供者(今回はバックエンド開発者)の間でAPIのスキーマをYAMLやJSONなどで固めておき、それを基にお互いが同時並行で開発を進めていく方式です。
フロントエンドとバックエンドが分離された開発体制における大変なこととして、
- API仕様に関する互いの認識齟齬があり、結果実装後の手戻りが発生してしまう
- バックエンドの実装が完了するまで、フロントエンドの実装が開始できずタイムロスが生じる
などがあると思います。
スキーマ駆動開発を取り入れることで、YAMLやJSON文書として仕様に関するお互いの認識を明確に擦り合わせられ、また周辺ツールの力を借りることでモックサーバの自動生成もできるようになるので、上記の問題点が解決し、非常にスムーズな同時並行開発ができるようになります。
今回はREST APIを採用していたため、OpenAPI3.0に沿ってYAMLでAPI仕様を記述しました。
OpenAPIは周辺ツールが豊富で、VSCodeにもプレビュー用のプラグインが存在しますし、(OpenAPI Preview)上のdocker-compose.ymlを見てもらうと分かるのですが、誰でも手軽にプレビューの確認・HTTPリクエストの実行ができるようにコンテナを立てるのも容易にできます。
この手法を採用したおかげで、コミュニケーションもスムーズになり、開発効率は格段に上がりました。
ブランチ戦略
Github Flow
を少し変形させた感じで運用しています。
produtionへのデプロイはタグ付け方式で行っています。
概要
- 使うブランチは master, develop, feature/*, fix/*
- master ブランチから developブランチを切る
- develop ブランチは staging 環境と同期しており、検証用に使う
- doc の更新系は master に直 push で OK
Flow
- master ブランチから develop ブランチを切る
- develop ブランチから feature/*ブランチを切る
- feature/*ブランチで開発を行う
- feature/*ブランチから develop ブランチに PR を出し、merge
- staging 環境でテストを行い、NG なら 新たに fix/*ブランチで修正を行った後 develop に再度 PR
- キリのいいタイミングで、staging 環境でのテスト結果が OK なら、develop からmasterへPRを出しmerge
- /v.*/に match する tag を切り、productionへデプロイ
タスク管理・コミュニケーションツール
タスク管理はTrello
、コミュニケーションツールはSlack
です。
友人同士なんだからLINEでいいんじゃないの?って思うかもしれませんが、やはり日々の雑談と開発に関わる話が交じってしまうのでNGです。
Slackだと#general
では雑談、#develop
では開発に関わる話と明確に分けられるので便利です。
それに、やっぱりスタンプを独自に作れるので楽しいです笑
タスク管理がTrelloなのは無料だし感覚軽いからです。
2人程度の趣味開発にJiraやRedmineはオーバーです。
また、SlackはCircleCI, GitHub, trello, Qase と連携させて通知チェックもまとめて行えるようにしています。
CI/CDパイプライン
概観としてはこんな感じです。
CircleCIでテストもデプロイもやってます。
CircleCIは本当に便利で、docker-compose.ymlファイルをほぼそのまま生かしたテスト実行ができるのでかなり楽に設定ファイルが書けます。
現時点での最新版のmachineを使用すればDockerのMulti-stage buildも、BuildKitも利用できます。
以下ご参考までに設定ファイルを載せておきました。
これも需要あるかわかりませんがまた暇な時詳細解説します。
backend
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: /.*/
#!/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
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
直感的なUIで使いやすく、エクセルとかスプレッドシートを使ってるときのようないかにもな仕事やってる感を消せるのでおすすめです(休日の楽しい個人開発の時まで仕事気分を味わいたくはありません)
最後に
今回総論として、Vue.js+Railsでスムーズな少人数開発を行うための1例を紹介させてもらいました。
Dockerfileの構成はじめ、各論については軽くしか触れてないので、また時間があったら記事に起こしてみようと思います。
サービスは一応リリースできましたが、エディタの強化、監視/ログ収集基盤の構築、パフォーマンス改善などやることはまだまだ無限にあるので、今後も楽しみながら開発を続けていこうと思います。