Quantcast
Channel: メドピア開発者ブログ
Viewing all 210 articles
Browse latest View live

CIで稀にSegmentation faultが起きてRubyが死ぬ問題と対応

$
0
0

CTO室SREの@sinsokuです。

先日、弊社のCIで稀によく Segmentation faultが起きるようになりました。

f:id:sinsoku:20200313181753p:plain

_人人人人人人_
> 突然の死 <
 ̄Y^Y^Y^Y^Y ̄

調べてみた

最初は気づかなかったけど、画像の右端のダウンロードっぽいアイコンをクリックすると、実行結果のログを全文見ることができます。

[BUG] Segmentation fault at 0x000056529cd6d5e0
ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-linux]

-- Control frame information -----------------------------------------------
c:0059 p:---- s:0312 e:000311 CFUNC  :[]
c:0058 p:0016 s:0307 e:000306 METHOD /home/circleci/*******/vendor/bundle/gems/activesupport-5.2.3/lib/active_support/execution_wrapper.rb:105
c:0057 p:0004 s:0303 e:000302 METHOD /home/circleci/*******/vendor/bundle/gems/activesupport-5.2.3/lib/active_support/execution_wrapper.rb:83
c:0056 p:0008 s:0298 e:000297 METHOD /home/circleci/*******/vendor/bundle/gems/activesupport-5.2.3/lib/active_support/reloader.rb:72
c:0055 p:0011 s:0294 e:000293 BLOCK  /home/circleci/*******/vendor/bundle/gems/activejob-5.2.3/lib/active_job/railtie.rb:27 [FINISH]
c:0054 p:---- s:0289 e:000288 CFUNC  :instance_exec
c:0053 p:0145 s:0283 e:000282 BLOCK  /home/circleci/*******/vendor/bundle/gems/activesupport-5.2.3/lib/active_support/callbacks.rb:118
(途中略)
c:0008 p:0022 s:0029 e:000028 BLOCK  /home/circleci/*******/spec/jobs/conference_mail_sending_job_spec.rb:89
c:0007 p:0003 s:0026 e:000025 BLOCK  /home/circleci/*******/spec/support/multithreaded.rb:5
(以下略)

どうやらマルチスレッドに関する何かで問題が起きているっぽい。

再現した

とりあえず、手元で20回くらい実行しても稀に死ぬのは分かった。

$ for n in $(seq 1 20); do \
    bundle exec rspec spec/jobs/conference_mail_sending_job_spec.rb:96; \
  done

調べてみた

  • 100%再現させる方法が分からない
  • エラーの起きるActiveSupport::ExecutionWrapperに怪しいコードはない
  • Thread についてあまり詳しくない

調べたけど、何も分からない...

ruby-jp で相談した

f:id:sinsoku:20200313181953p:plain

相談したら、少し経ってシンプルな再現コードが見つかった。ありがたい。

f:id:sinsoku:20200313182030p:plain

その後、笹田さんが原因と100%再現するコードを投稿。(すごい)

f:id:sinsoku:20200313182043p:plain

そして、いつの間にか笹田さんが問題を解決するパッチをコミットし、Issueの登録も済んでいた。(すごい)

https://bugs.ruby-lang.org/issues/16676

どうやら、 #hashの実行中に他スレッドから同じHashを弄ると問題が起きるようです。

テストコードの修正

すでにRubyのmasterブランチでは修正されていますが、CIで2.8.0-devを使うわけにもいきません。

Rubyの新しいバージョンがリリースされるまでのワークアラウンドとして、Railsにパッチを当てる事で対応します。

まず、以下の内容で lib/patches/fix_execution_wrapper.rbを作成します。

# frozen_string_literal: trueraise('Consider removing this patch') ifRUBY_VERSION != '2.6.5'modulePatches# Rubyが稀にSegmentation faultでエラーになる問題を修正するパッチ。## `Thread.current` の代わりに `Thread.current.object_id` を使うこと# で#hashの実行時に他スレッドがテーブルを弄るのを避けます。## 元の実装は以下を参照してください。# ref: https://github.com/rails/rails/blob/v6.0.2.1/activesupport/lib/active_support/execution_wrapper.rb## ## Issue## `#hash` can change Hash object from ar_table to st_table# ref: https://bugs.ruby-lang.org/issues/16676moduleFixExecutionWrapperdefself.active?@active[Thread.current.object_id]
    enddefrun!self.class.active[Thread.current.object_id] = true
      run_callbacks(:run)
    enddefcomplete!
      run_callbacks(:complete)
    ensureself.class.active.delete Thread.current.object_id
    endendendActiveSupport::ExecutionWrapper.prepend(Patches::FixExecutionWrapper)

このパッチを config/environments/test.rbで読み込む。

Rails.application.configure do# 中略endrequire'patches/fix_execution_wrapper'

これで本当に直るかは自信なかったのですが、1週間くらいCIの様子をみていても Segmentation faultが起きませんでした。
たぶん大丈夫です。

まとめ

自分1人で調べていても全く再現コードを作れなかったし、再現コードを見ても原因に検討もつきませんでした。

  • ruby-jpに感謝
  • 笹田さんすごい
  • ThreadとRubyは難しい

もしスレッド周りで同じ問題を踏んだとき、この記事が参考になれば幸いです。


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html



バックグラウンドで実行するバッチ処理の改善のためSidekiq Enterpriseを導入しました🥳

$
0
0

こんにちは、エンジニアの森田です。 MedPeerでは、バックグラウンドで非同期に処理を実行させる方法としてSidekiqを使っておりましたが、今回Sidekiq Enterprise(Proを含む)を導入しました。

https://sidekiq.org/products/enterprise.html

今回はSidekiq Enterpriseを導入するにあたって解決したかった課題と実際の導入方法、導入後の活用事例をを紹介できればと思います!

Sidekiq Enterpriseとは?

Sidekiq Enterpriseとは、その名の通りエンタープライズ向けの機能拡張が行われた有料版のSidekiqです。(Sidekiq Enterpriseとは別にSidekiq Proもありますが、Sidekiq Enterpriseを導入するとSidekiq Proの機能も使用出来るようになります。)

ProとEnterpriseそれぞれで主に下記のような拡張がされています。

Sidekiq Pro

  • RELIABILITY
  • BATCHES

Sidekiq Enterprise

  • RATE LIMITING
  • PERIODIC JOBS
  • ENCRYPTION

詳しい機能の紹介は公式のWikiが充実しているので、興味の有る方はそちらをご確認いただくのが良いかとおもいます。

Home · mperham/sidekiq Wiki · GitHub

Sidekiq Enterprise導入に至った背景

MedPeerではバックグラウンドで実行するバッチ処理にSidekiqを使用しています。サービスの成長に伴い日々Jobは増えていて、100を超えるJob(2020/03/19現在)が実行されています。

+----------------------+--------+--------+---------+---------+-----+-------+
| Name                 |  Lines |    LOC | Classes | Methods | M/C | LOC/M |
+----------------------+--------+--------+---------+---------+-----+-------+
| Jobs                 |   4482 |   3607 |     106 |     500 |   4 |     5 |
+----------------------+--------+--------+---------+---------+-----+-------+

Jobの中には、利用者へのインセンティブ進呈に関わる等、影響が大きいものもありJobにおける処理の信頼性の担保が今まで以上に求められるようになってきました。

またデプロイによるJobへの影響も当時の問題として発生しておりました。 実行完了までに長時間かかるようなJobがあり、デプロイ時のSidekiqのプロセス再起動により実行が中断されるとJobの処理が中断され再実行しても正常に完了できず、毎日運用担当の方からメール配信のスケジュールを共有いただき、エンジニア側でデプロイタイミングがかぶらないように注意するような運用をしていました。。。

f:id:madogiwa0124:20200325115651p:plain

このような背景から解決方法を探していたところ弊社技術顧問である前島さんからSidekiq Enterpriseを導入してみてはどうかと提案いただき、導入を検討することとなりました。

結果として上記のような問題は、Sidekiq Enterpriseの導入により解決することが出来ました🎉

Sidekiq Enterpriseの導入方法

ではSidekiq Enterpriseの具体的な導入方法について書いていきます✍

Sidekiq Enterprise申し込み

導入にあたってまずは、Sidekiq Enterpriseの下記のページから申し込みを行います。

https://billing.contribsys.com/sent/new.cgi

金額は100スレッドで月額$179(2020/03/19現在)です。スレッド数が増えていく毎に金額があがっていきます💸

You can use Sidekiq Enterprise with any number of apps and processes and machines as long as their total worker thread count in production is <= the licensed amount. Development and staging environments are free and unlimited. https://github.com/mperham/sidekiq/wiki/Commercial-FAQ

この金額に影響を与えるスレッド数は本番環境で実行されているスレッド数となっているようでstagingやdevelopmentといった本番以外の環境は自由に使うことができます。

申し込みを行うとSidekiq Enterpriseのinstall時の認証に必要なキー情報がメールにて送付されます 📩

Sidekiq Enterpriseのインストール

Sidekiq Enterpriseをアプリケーションに導入する場合は下記のようにGemfileに追記します。

source "https://enterprise.contribsys.com/"do
  gem 'sidekiq-ent'
  gem 'sidekiq-pro'end

そして下記のようにbundle configまたは環境変数に申込時に受け取った認証用のキー情報を設定します。

export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=foo:bar
# or
bundle config --local enterprise.contribsys.com foo:bar

上記のGemfileと認証用のキー情報の設定が完了したらbundle installを実行することでSidekiq Enterpriseを導入することができます 🎉

※導入したい機能によっては、initializer/sidekiq.rb等で起動時に有効化する必要があるものもございますので、Wikiを読んで導入方法を確認することをおすすめします。

ActiveJobからSidekiq::Wokerへ書き換え

Sidekiq Enterpriseの全ての機能を利用するためにはActiveJobではなくSidekiq::Wokerを使用する必要があります。そのため弊社ではSidekiq Enterpriseの機能を利用したいJobまたは新規のJobについては、ActiveJobではなくSidekiq::Workerをincludeする形式で実装するようにしています。

# beforeclassMyJob< ApplicationJob
  queue_as :defaultdefperform(*_args)
    # something logicendend# afterclassMyJobincludeSidekiq::Worker
  sidekiq_options queue: :defaultdefperform(*_args)
    # something logicendend

ActiveJobを継承したJobをSidekiq::Workerをincludeする形式に修正するにあたって、引数でActiveRecordのオブジェクトを受けるような形式になっている場合には注意が必要です。

The arguments you pass to perform_async must be composed of simple JSON datatypes: string, integer, float, boolean, null(nil), array and hash. https://github.com/mperham/sidekiq/wiki/Best-Practices#1-make-your-job-parameters-small-and-simple

ActiveJobは良しなに引数のオブジェクトをシリアライズしてくれますが、SidekiqではWikiに記載の通り引数の値をto_sしてRedisにエンキューする都合上、Sidekiq::Workerをincludeしたときのperform_laterに当たるpeform_asyncの引数には単純な値しか渡すことは出来ません。そのため、下記のようにuserではなくuser_idとして取得してUserのオブジェクトを取得するような形で修正が必要となります。

# beforeclassMyJob< ApplicationJob
  queue_as :defaultdefperform(user:)
    # something logicendend# afterclassMyJobincludeSidekiq::Worker
  sidekiq_options queue: :defaultdefperform(user_id:)
    user = User.find(user_id)
    # something logicendend

これでSidekiq Enterpriseの機能を使う準備が整いました🎉

Sidekiq Enterpriseの活用事例

最後に弊社におけるSidekiq Enterpriseの活用事例としてReliabilityを使ったJob実行の信頼性の向上を紹介いたします。

Reliabilityを使ったJob実行の信頼性の向上

Jobにおける処理の信頼性の担保が課題としてあげられていたので、Sidekiq Proの機能であるReliabilityを使って信頼性の向上を図っています。

Reliabilityとは、Sidekiq Proの機能である信頼性向上のための機能です。クライアント側(エンキューする側)とサーバー側(デキューして実行する側)に機能が追加されています。

https://github.com/mperham/sidekiq/wiki/Reliability

弊社でも下記の2つを活用しています。

  • Redisへのenqueueに失敗した場合にメモリ上にenqueueしておいて、接続可能となったタイミングでenqueueできるReliability Client

  • Sidekiqのプロセスが停止した場合にもRedisを総なめして孤立したqueueを実行するsuper_fetch

ネットワーク障害等によりRedisに一時的に接続出来ないために失われていたJobや、実行中にSidekiqのプロセスが停止し孤立してしまったJobの実行を担保出来るようになり、信頼性を向上させることが出来ました🎉

Ent Rolling Restartsを使った安全な再起動

デプロイ時のSidekiqのリスタートによる完了までに長時間掛かるJobの中断を防ぐために、Sidekiq Enterpriseの機能であるEnt Rolling Restartsを使って安全に再起動しています。

通常のSidekiqのリスタートではTSTP+TERMを使ったリスタートを行うと思います。通常であればこの方式で安全に再起動出来るのですが、長時間完了までに掛かるJobの中断を防ぐことは出来ません。

Sidekiq EnterpriseのEnt Rolling Restartsを使うことで、長時間掛かるJobの完了を待って安全に再起動を行うことが出来ます。

There is no limit to the time it can continue running. Upon signalling a rolling restart, a new process will be started to pick up new jobs. https://github.com/mperham/sidekiq/wiki/Ent-Rolling-Restarts

具体的にはRolling Restartsを検知した際に、下記のような形で再起動が行われます。

  • 旧プロセスは新規のJobの実行を停止し、既存のJobの実行が終わったら停止する。
  • 新プロセスが起動して新規のJobの実行を開始する。

弊社でも導入の背景に記載したとおり、長時間完了までに掛かるJobがデプロイ時に中断される問題がありましたが、Ent Rolling Restartsの機能のおかげで現在はデプロイ時にJobの実行有無を確認する必要はなくなりました🎉

おわりに

Sidekiq Enterpriseについて弊社の事例をご紹介しました。もしもSidekiq Enterpriseの導入検討の一助になれば幸いです✨

運用担当とのやりとりやJob実行の信頼性の向上のための内製化コード、なども省けて金額以上のメリットを感じています!

またSidekiq Enterpriseの導入をすすめる中でSidekiqのWikiを読んだのですが、すごく充実した内容になっているので導入に関わらずSidekiqを触っている方は読んで見ると色々と役に立つ内容が多そうでした🙌

それでは👋


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html


ビジュアルリグレッションテストを導入した話

$
0
0
f:id:mpg-kazuhiro-kobayashi:20200408153648p:plain

こんにちは。フロントエンドエンジニアの小林和弘です。

Vue.js + Atomic Designでつくられたプロジェクトにビジュアルリグレッションテストを導入しました。

ビジュアルリグレッションテストでUIの安全性を高める

コンポーネントの改修、新機能の追加、ライブラリのアップデートを行う際、UIに不要な変更が入っていないか不安になることがあると思います。リファクタリングをしようにも、意図しないところでUIが壊れないか心配になります。

画面表示に関わるコードを改修するたびに、ローカル環境やステージング環境で全UIコンポーネントを確認するのは難しいです。

また、ステージング環境と本番環境を並べて変更されたUIを目視で確認するのも非現実的です。

ビジュアルリグレッションテストはその名前の通り、視覚的な回帰テストを指します。改修前後のスクリーンショットの差分を検証するためのテストです。
開発におけるUIの安全性を高め、安心してUI改善を行えるようにビジュアルリグレッションテストの導入を行いました。

使用したツール

  • reg-suit
    • ビジュアルリグレッションテストのためのテスティングツール
    • 差分レポートを作成してくれる
    • GitHubへのPull Request通知機能があり、PR毎にUI差分が見れる
  • Storybook
    • UIコンポーネントのカタログを作成する
    • 実装済のUI、UIパターンをすぐに確認できる
    • 画面上でコンポーネントの挙動把握ができる
    • アドオンが豊富でデバイスサイズ変更時のUI表示確認などができる
  • Storycap
    • Storybookから各コンポーネントのスクリーンショットを作成する
    • reg-suitと同じGitHub Organizationのreg-viz内で管理されている

Storybookの導入

まずは比較画像の元となるStorybookを導入します。
インストール用のnpm@storybook/cliが提供されているのでnpxコマンドでインストールをします。
今回はVue.jsプロジェクトに導入するのでtypeオプションにvueを設定します。

$ npx -p @storybook/cli sb init --type vue

インストール時に行われるのは

  • Storybookと依存モジュールのインストール
  • StorybookのAddonのインストール
  • npm scriptsにStorybookを実行するスクリプト追加
  • storiesディレクトリの追加
  • 設定ファイルを格納する.storybookディレクトリの追加

になります。

storiesディレクトリ内にはサンプルのStoryファイルとVueコンポーネント(Welcome, MyButton)が格納されています。

package.jsonにはstorybook, build-storybookが追加されます。

{"scripts": {"storybook": "start-storybook -p 6006",
    "build-storybook": "build-storybook"
  }}

storybookを実行するとnpm scriptsに設定されたポート番号 6006でStorybookが立ち上がります。

$ yarn storybook

f:id:mpg-kazuhiro-kobayashi:20200407121031p:plain

Storycapの導入

次にStorybookに登録されたStoryのスクリーンショットを作成して、reg-suitで画像の比較ができるようにしていきます。
スクリーンショット作成のためにStorycapをインストールします。

$ yarn add storycap --dev

次に.storybook/config.jsを作成して、StorybookのAddonとしてStorycapを登録します。
今回はVue.jsのプロジェクトなので@storybook/vueからaddDecoratorを呼び出しています。

import { addDecorator } from '@storybook/vue';
import { withScreenshot } from 'storycap';

addDecorator(withScreenshot);

package.jsonにStorycapを実行するnpm scriptsを追加します。

{"scripts": {"screenshot": "storycap --serverCmd \"start-storybook -p 6006\" http://localhost:6006"
  }}

実際にコマンドを実行すると、__screenshots__ディレクトリにスクリーンショットが保存されます。

スクリーンショット画像はバージョン管理に含める必要がないので.gitignoreに追記してGitの管理下から外しておきます。

__screenshots__

reg-suitの導入

まずreg-suitをインストールします。

$ yarn add reg-suit --dev

次にローカルのreg-suit initコマンドを実行します。対話形式でreg-suitの設定ができます。

$ yarn reg-suit init

Plugin(s) to install (bold: recommended)
使用するプラグインを選択します。
今回は下記の3つを選択しています。


Working directory of reg-suit. => .reg
テストの結果を出力するディレクトリを指定します。デフォルトの.regディレクトリのままで問題ありません。


Append ".reg" entry to your .gitignore file. => Yes
reg-suitの出力結果はAWS s3で管理するので、Gitの管理下から外すためYesにします。


Directory contains actual images. => __screenshots__
テストに利用する画像のディレクトリを指定します。
Storycapのデフォルト値の__screenshots__を指定します。


Threshold, ranges from 0 to 1. Smaller value makes the comparison more sensitive. => 0
テストの差分比較の閾値を設定します。
厳密に差分検知をしたい場合は0を指定します。


notify-github plugin requires a client ID of reg-suit GitHub app. Open installation window in your browser => Yes
GitHub Appのreg-suitの登録を行います。
reg-suitの設定ページがブラウザで開くので、ビジュアルリグレッションを導入したいリポジトリを選択します。
Client IDをクリップボードにコピーしておきます。

f:id:mpg-kazuhiro-kobayashi:20200407164809p:plain


This repositoriy's client ID of reg-suit GitHub app => {Client ID}
GitHub AppのページでコピーしたClient IDを設定します。


Create a new S3 bucket => No
AWSにログイン済みでs3の作成権限があればYesにしてbucketの作成を行います。
権限がなかったので今回はNoで回答しました。


Existing bucket name => ***
s3のBucketが作成済みの場合、ここでbucket名を設定します。


Update configuration file => Yes
ここまで回答した設定を、設定ファイルのregconfig.jsonに反映します。


Copy sample images to working dir => No
サンプル画像のコピーは不要なのでNoで回答します。


以上でreg-suitの設定が完了です。

次にpackage.jsonのnpm scriptsにreg-suitのコマンドを追記します。

{"scripts": {"regression": "reg-suit run"
  }}

AWS s3の権限があればローカル実行で動作します。

$ yarn regression

CircleCIの設定

GitHubのリモートブランチへpushした時に自動でテストを実行するために、CIの設定を追記します。

今回使用しているのはCircleCIです。

GitHubアカウントでCircleCIにログインして、Projectsから対象のプロジェクトを選択します。

f:id:mpg-kazuhiro-kobayashi:20200407120526p:plain

プロジェクト選択後、Project Settings > Environment VariablesからAWS s3のAccess Key IDとSecret Access Keyを設定します。

reg-suitコマンドではデフォルトでAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY変数を参照するようになっています。

別の変数名を定義したい場合(今回は仮にREGRESSION_ACCESS_KEY, REGRESSION_SECRET_KEY)、npm scriptsのregressionで変数を代入しておきましょう。

{"scripts": {"regression": "AWS_ACCESS_KEY_ID=$REGRESSION_ACCESS_KEY; AWS_SECRET_ACCESS_KEY=$REGRESSION_SECRET_KEY; reg-suit run"
  }}

次にCircleCIの設定ファイルconfig.ymlにJOBを追加します。

jobs:
  visual_regression:
    steps:
      - checkout
      - restore_cache:
          name: Restore node_modules
          key: yarn-{{ checksum "yarn.lock" }}-{{ .Environment.CACHE_VERSION_NPM }}
      - run:
          name: Install dependencies
          command: yarn install
      - save_cache:
          name: Cache node_modules
          key: yarn-{{ checksum "yarn.lock" }}-{{ .Environment.CACHE_VERSION_NPM }}
          paths:
            - ~/workspace/node_modules
      - run:
          name: install jp fonts
          command: sudo apt-get install fonts-ipafont-gothic fonts-ipafont-mincho
      - run:
          name: screenshots
          command: yarn screenshot
      - run:
          name: regression
          command: yarn regression --quiet

デフォルトでは日本語フォントがTofuになってしまうため、IPAフォントをインストールしています。

実際に使う

実際にGitHub上でPRを作成するとCircleCI上でvisual_regressionが実行され、完了したらキャプチャ画像のような結果レポートのコメントが追加されます。

f:id:mpg-kazuhiro-kobayashi:20200407120737p:plain

this reportのリンクをクリックすると、s3にアップロードされた差分比較ができるページに遷移します。

f:id:mpg-kazuhiro-kobayashi:20200407190805p:plainf:id:mpg-kazuhiro-kobayashi:20200407190820p:plain

差分があった場合、もしくは新規でスクリーンショットが追加された場合、reg-suitのチェックが失敗します。

レビュアーがreg-suitのPRのコメントやレポートを確認して問題がないか確認します。

UI変更に問題があった場合はPR上でUIの修正を進めます。

UIの変更に問題がなければ、Approveをします。Approveするとreg-suitのチェックがパスするようになります。

まとめ

Storybook + reg-suitによるビジュアルリグレッション導入はこれで完了です。

やっていることとしては

  • StorybookでUIのカタログを作成
  • StorycapでStorybookのスクリーンショットを作成
  • reg-suitでGitのブランチ間のスクリーンショットの比較レポートを作成
  • reg-suitのGitHub AppでPR上にコメントを通知

になります。

実際に非レスポンシブなサイトを部分的にレスポンシブ対応する際にビジュアルリグレッションテストでUI破壊をいくつか検知でき、その恩恵を受けることができました。

意図しないUI破壊を防ぐためにビジュアルリグレッションテストを導入しましたが、大元となるのはStorybookに登録されたStoryです。
なのでStorybookのメンテナンスを怠るとテストが形骸化してしまいます。

Storybookはメンテナンスコストが高く、導入が難しいという意見もあります。しかしStorybookによるUIのカタログ化は、メンテナンスコストを差し引いても大きなメリットがあると考えています。
ビジュアルリグレッションテストのスクリーンショットに利用できるだけではなく、実装済みUIの再実装を未然に防いだり、Storybook上でコンポーネントの動作を確認しながらUI開発ができたり、Addonを使ってレスポンシブ表示を確認できたりと様々なメリットがあります。

この記事で皆様の安全なUI管理に少しでも貢献できれば幸いです。


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html


中途で入社したての私からみたメドピア開発環境のいいところ

$
0
0

2020年1月付けで入社した社長室 エンジニアの芝田と申します。 社長室ではkakariという、かかりつけ薬局化を支援するサービスをやっており、そちらでサーバーサイドエンジニアとして働いています。

エンジニアとしてのキャリアはメドピアで2社目で、まだまだ勉強中の身です。 今回はメドピアでの開発を始めて、開発環境のいいところや実装のtipsを一部ご紹介したいと思います。

開発環境のいいところ

CIでRSpecやRubocopをはじめとする複数のLint👮が通っていないと原則マージできない

Rubyは自由な記法ができるメリットの反面で、記法のばらつきが比較的出がちです。そこは、Lintによってある程度カバーすることが可能です。

また、ClassLengthLineLengthAbcSize等によって、ファイルの肥大・コードの複雑度合いを知ることができます。 kakariではClassLength 100行以上はマスターデータではない限り許可していないので、読むのを諦めたくなるファイルは無いです。

Dangerを使い、他のファイルと合わせてこう書いてほしいというレビュー漏れを無くしている

例えば、プルリクエストを出した際に、XXX.created_at.strftime('%Y/%m/%d %H:%M')をチーム内ではl(XXX.created_at, format: :datetime_with_slash)と書いてほしい時に、Dangerで設定しておくと自動で警告を出してくれます。

API~フロントエンド間で開発前にOpenAPIを使って、送るパラメータと期待するレスポンスを決定している

yamlを書くだけでSwagger Editor上やChromeの拡張でリッチなUIのAPI仕様書を自動生成してくれたり、committeeを使って、OpenAPIで定義したレスポンスをRspecで自動チェックしてくれたりします💪

kakariのメンバーが以前作成した資料がありますので、ご興味ある方は併せて確認お願いします。

speakerdeck.com

可能な限りパフォーマンスの良い書き方を求められる。

bulletでテスト時にN+1を検知したり、無駄な繰り返し処理をできるだけ減らす書き方を求められます。

例えば、対象となる患者を探すロジックを書きたい時、account.patients.not_deleted.select do |patient|だと論理削除されていない患者全体を取得し、患者数分繰り返し回してしまい患者数が多いほどパフォーマンスが悪くなります。

この場合、wherefindメソッドを使って一気に対象患者を検索するようにすることが望ましいです。 また、関連付けされた値をキャッシュしたかったり、テーブル同士をjoinやキャッシュする必要がある場合は、eager_loadpreloadメソッドを適切に使うようにしています。

テストをしっかり書いている

入社する前の私はどちらかというとテストは書かず、ブラウザで動作確認をしていました。しかし、テストがない環境では、バージョンアップやリファクタリングが辛かったり、動作が要件通りになっているだけ(行が長かったり引数が多い)のメソッドを書きがちです。

特にレビューや仕様追加によって、コードを変更した際、常に要件通りのチェックを毎回するのか?となり、テストを書かなかった工数分が後で確認工数増加やデグレとして降りかかってきます。

社内全体でテストがしっかり書く習慣となっているため、kakariでは最新のRails6系やRuby 2.7系を使っており、他の依存ライブラリも常にアップデートされています。また、リファクタリング系のissueにはテストが既に書かれているため、書いた本人以外でも着手しやすいようになっています。

実装のtips

プレーンなRubyファイルで書かれたPOROでロジックをこまめに切り出す

私はまとまったロジックが必要になった時、モデル側にインスタンスメソッドやクラスメソッドを書いて実装しがちでした。しかし、ロジックがモデルに集中すると、関心事が入り乱れてテストし辛いコードになりがちです。

kakariではFoo::Updaterのようなクラスを切り、クラスメソッドでcallすることが多いです。-er(~する人)が呼ばれる(call)という名前だと、メソッド名に悩まされにくく、読み手側の頭にも入ってきやすいかと思います。

classFoo::Updaterdefself.call(params:)
    new(params).call
  enddefinitialize(params)
    @params = params
  enddefcall
    update
  endprivateattr_reader:paramsdefupdate# 何かの処理endend

POROに関しては弊社の技術顧問である@willnetさんが書いた記事もありますので、ご興味ある方は併せて確認お願いします。

tech.medpeer.co.jp

繰り返し参照されるメソッドは変数に格納する

2度目も参照されるロジックの場合、結果を変数に格納しています。初回はインスタンス変数がnilになるので、右側の式が実行されます。

2度目以降の実行では変数が使われるので、2度目以降に引数を変えて異なる結果を取得したい場合は意図しない挙動になるので注意してください。

deffoo_object@foo_object ||= Foo::Creator.call
end

列挙型で使いたいカラムはデフォルト値をDB側で定義するのではなく、モデル側で定義する

enumerizeを使う場合、モデルにデフォルト値を持つことが可能です。メリットとして、項目が増えたときにデフォルトの値を変えたい際、migrateファイルを発行せずに済みます。またtextで指定できるので、何の値をデフォルトにしているかが明確です。

classCreateFooBars< ActiveRecord::Migration[6.0]
  defchange
    create_table :foo_barsdo |t|
      t.integer :age_code, default: 4
      t.timestamps
    endendend

ではなく、下記のようにモデル側で定義する。

classFoo< ApplicationRecordextendEnumerize

  enumerize :age_code, in: {
    all: 0, older_forty: 1, older_fifty: 2, older_sixty: 3, older_sixty_five: 4,
    older_seventy: 5, older_seventy_five: 6, older_eighty: 7
  }, default: :older_sixty_fiveend

名前の重複のない関連付けの参照をする

例えばモデルとしてはfoo_answerfoo_questionで示したいが、関連付けする際には、デフォルトでfoo_answer.foo_questionとなってfooが冗長です。 Railsのデフォルトのinverse_ofから外れてfoo_answer.questionfoo_question.answersとしたい時はinverse_ofを明示的に設定します。

classFooQuestion< ApplicationRecord
  has_many :answers, class_name: 'FooAnswer', dependent: :destroy,
                     foreign_key: :question_id, inverse_of: :questionend
classFooAnswer< ApplicationRecord
  belongs_to :question, class_name: 'FooQuestion', inverse_of: :answersend

Viewでしか使わない整形用のメソッドはDecoratorに切り出す

erbやHamlで実装している箇所はDecoratorを使ってviewにロジックを直接書かないようにしています

moduleFooDecoratordeffull_name"#{last_name} #{first_name}"endend

おわりに

私は今までの考え方として、スピードを優先するときはある程度汚い状態のコードがリリースされるのは仕方がないと思っていました。しかし、メドピアではビジネスのスピード感を犠牲にせず、そしてマンパワーにも頼らず、便利なライブラリで賢く仕組み化して、コードの品質を落とさない取り組みを実践しているという部分に触れられたことが、入社して良かった点の1つです。なので、このような開発環境で成長したい人にとっては、メドピアへの入社というのは良い選択肢の一つだと感じました。

今回は他の記事と比べて1つの事項を深掘りした内容ではありませんが、「お、使ってみようかな」・「メドピアの開発環境のことをもっと知りたいな」と思っていただける内容が1つでもあれば幸いです!読んでいただき、ありがとうございました。


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html


最小手数で始めるTailwind CSS

$
0
0

パクチーパクパク小宮山です。
掲題通りTailwind CSSの始め方を最小手数で書いていきます。余談は一切ありません。

tl;dr

CSS管理は諦めてTailwind CSSを使おう。

Get Started

tailwindcss.com

ひたすら公式通りに進めます。例によってフロントエンドプロジェクトの環境構築はひたすら面倒なので、Tailwind CSS以外のツールチェインはなるべく使わない構成を目指します。

installします。

$ yarn init
$ yarn add tailwindcss

セットアップします。

$ yarn tailwindcss init

こういうファイルが作られました。

tailwind.config.js

module.exports = {
  purge: [],
  theme: {
    extend: {},
  },
  variants: {},
  plugins: [],
}

スタイルのエントリーポイントなるCSSファイルを作成します。ファイル名は任意です。このファイルをTailwind CSSが用意しているCLIでビルドすることで、実際にhtmlファイルで読み込むCSSファイルが出力されます。

tailwind.css

@tailwind base;

@tailwind components;

@tailwind utilities;

TODOリスト感のある素朴なHTMLファイルを用意します。この時点ではpublic/style.cssはまだ生成されていません。

見た目だけの実装なのでフォームも飾りです。

public/index.html

<!doctype html><htmllang="en"><head><metacharset="utf-8"><metaname="viewport"content="width=device-width, initial-scale=1"><linkrel="stylesheet"href="style.css"><title></title></head><body><h2>New Todo</h2><form><inputtype="text" /><button>submit</button></form><h2>List Todo</h2><ul><li><p>todo 1</p><p>2020 05/12</p></li><li><p>todo 2</p><p>2020 05/12</p></li><li><p>todo 3</p><p>2020 05/12</p></li></ul></body></html>

public/style.cssを生成するためのscriptを用意しておきます。

package.json

{"name": "minimum-tailwindcss",
  "version": "1.0.0",
  "scripts": {"build:css": "tailwindcss build tailwind.css -o public/style.css"
  },
  "dependencies": {"tailwindcss": "^1.4.6"
  }}

実行します。

$ yarn build:css

お好きなwebサーバーを起動してpublic/index.htmlを開きます。package.jsonを汚したくなかったのでnpxでずるして最小手数の体裁を保ちます。

$ npx http-server ./public

スタイルが何もあたっていない状態のwebサイトが完成しました。Tailwind CSSにはnormalize.cssが含まれている(v1.4.6時点)ので、リセット系CSSを別で用意する必要はありません。

f:id:robokomy:20200518174836p:plain
ネイキッドウェブサイト

準備が整ったので早速Tailwind CSS流にスタイルを当てていきます。

<body><h2class="mb-2 px-2 text-xl">New Todo</h2><formclass="mb-4 px-4"><inputtype="text"class="p-2 border" /><buttonclass="ml-2 p-2 rounded text-white bg-blue-500">submit</button></form><h2class="mb-2 px-2 text-xl">List Todo</h2><ulclass="py-2 px-4"><liclass="p-2 border"><pclass="border-b">todo 1</p><pclass="text-sm">2020 05/12</p></li><liclass="mt-2 p-2 border"><pclass="border-b">todo 2</p><pclass="text-sm">2020 05/12</p></li><liclass="mt-2 p-2 border"><pclass="border-b">todo 3</p><pclass="text-sm">2020 05/12</p></li></ul></body>

大分それっぽくなりました。

f:id:robokomy:20200511191309p:plain
それっぽい見た目

使い方は見た目通りで、classがそれぞれ特定のCSS定義として用意されています。

例えばp-4ならpadding: 1rem;mt-2ならmargin-top: 0.5rem;といった感じです。インラインスタイルを簡略化したような使用感です。

デフォルトスタイル余談

tailwindcss.com

Tailwind CSSを使う上でまず最初に注意したほうがよいことは、line-heightのデフォルト値です。デフォルトでline-height: 1.5;htmlにあたっているので、全ての余白を自力で指定してピクセルパーフェクトを目指す場合は少し厄介です。

htmlに反映されていることもあり、気にせずスタイルを当てていって途中で変更したくなってしまうと相当な被害になることが予想されます(私です)。それっぽいline-heightを全体に当てておくか、パーフェクトを目指して全てを自力で当てるかの方針はなるべく早期フェーズでの選択がおすすめです。

Vendor Prefixes余談

ターゲットとするブラウザ次第では必要になるであろう、みんな大好きVendor Prefixesです。結論を言ってしまうとTailwind CSS自体にはVendor Prefixes的な対応は入っていません。

それを解決するのはもっとうまくやれる他のツールに任せているというのが公式スタンスです。ドキュメントでもAutoprefixerとの併用が紹介されています。

tailwindcss.com

この先はもうPostCSSの話題になってしまうので深入りはしませんが、Tailwind CSSをPostCSSのプラグインとして利用することも可能なので導入もそんなに手間ではありません。

ファイルサイズ問題

Tailwind CSSを活用する上で無視することのできない非常に重要な問題がファイルサイズです。

実際にtailwindcss buildを叩いてみた方なら、このような表示がされて既に嫌な予感を持っていたかもしれません。

🚀 Building... tailwind.css

   ✅ Finished in 1.56 s
   📦 Size: 1.95MB
   💾 Saved to public/style.css

✨  Done in 4.11s.

「📦 Size: 1.95MB」です。これは相当に大容量です。normalize.cssが含まれているとはいえ、scriptもfontも含まれていないただのCSSファイルでこれは流石に無視できるサイズではありません。

ファイルサイズが肥大化する理由は明白で、p-4mt-2といったCSSのプロパティと数値の組み合わせが無数に存在するからです。さらにレスポンシブ対応でsm:p-4なんて指定も用意されているので、それら全てが含まれていると考えれば膨れるのも当然なわけです。

実はあのBootstrapにもこのようなutility的なclass群は存在しています。しかし用意されているものは必要最低限で、Tailwind CSSほどの汎用性も拡張性もありません。

内部事情までは知りませんが、おそらくファイルサイズの肥大化という問題は少なからず意識して絞っているのではないでしょうか。

getbootstrap.com

Bootstrapがおそらく敢えて避けているであろう、ファイルサイズがひたすら肥大化していくutility的なclass群という方向にTailwind CSSは振り切っているわけです。その方向に振り切る以上、便利さと引き換えにファイルサイズは諦めなければならない・・という時代もかつてはあったのかもしれません。しかし今は令和です。あれも欲しい、これも欲しいもっともっと欲しいを実現してくれる強力なツールが存在します。

PurgeCSSです。

PurgeCSS

purgecss.com

まただよ、またフロントエンド開発環境に登場人物が増えたよ即ブラウザバックしかけた方はちょっとだけ待ってください。なんとTailwind CSSは最近のリリースでPurgeCSSも内包するようになったので、設定ファイルを微修正するだけです。Tailwind CSS陣営としても、ファイルサイズ肥大化は重要な問題で、その解決法を明示する必要があると判断したのでしょう。

github.com

デフォルトの設定ファイルから、purge部分を少しだけ変更します。Tailwind CSSのclass表現を使っているファイルが全て含まれるようにパスを指定します。そうすることで、そのファイル内で現れていない不要なclassが全て削除されます。

tailwind.config.js

module.exports = {
  purge: ['./public/**/*.html'],
  theme: {
    extend: {},
  },
  variants: {},
  plugins: [],
}

パージ機能を有効にするにはNODE_ENV=productionの指定が必要です。早速実行してみます。

NODE_ENV=production yarn build:css
🚀 Building... tailwind.css

   ✅ Finished in 1.29 s
   📦 Size: 12.08KB
   💾 Saved to public/style.css

✨  Done in 1.87s.

CSSのファイルサイズ肥大化は、人類にとって最早克服された問題だったのです。PurgeCSSが存在するからこそTailwind CSSのutility-firstという方針が成立すると言っても過言ではないかもしれません。これぞシナジーです。Cookie Clickerをやり込んだ皆さんなら、シナジーが如何に強力かつ重要なのかは身をもって体験しているはずです。

続けて拡張の話題に移りますが、そこでもPurgeCSSという存在が控えていることが非常に重要となります。

PurgeCSS余談: 禁忌事項

PurgeCSSを使う上で、いつか足元を撃ち抜くかもしれない禁忌事項が1つあります。それは、classを必ず完全な形で記述する」ことです。

例えばfont-sizeを動的に指定しようとして、こんな記述をしてしまうかもしれません。

fontSize = 'text-' + size; // size: 4 | 6 | 8

撃ち抜きました。完全に撃ち抜いて水中から氷の天井を見上げています。理由は単純で、PurgeCSSは正規表現によって、使われているclassを探します。つまり動的に生成されたclassは発見のしようがなく、無慈悲にproductionビルド時に削除されます。

多少遠回りになっても、classを完全一致な文字列としてファイル内に記述しなければいけません。例えばこのように。

fontSize = { 4: 'text-4', 6: 'text-6', 8: 'text-8'};

PurgeCSSの要請からこのような完全一致で書く必要があるわけですが、CSSセレクタをこのように完全一致で書くことを習慣つけるのはものすごくおすすめです。以前似たような話題で開発ブログも書きました。

tech.medpeer.co.jp

以前まではgrepがしにくいというやや個人的かもしれない理由だったんですが、今ではPurgeCSSの要請という強力な後ろ盾を得たのでバンバン推していきます。

拡張

デフォルトで用意されているclassでも不便はあまりないんですが、どうしてもそれだけでは足りないシーンというのもあります。

例えばTailwind CSSはremベースの指定が基本となっています。p-4ならpadding: 1rem;'、p-6なら1.5rem`といった具合です。

なんかそれっぽい感あってrem指定いいですよね。しかし残念ながら世の中そんな甘くなく、往々にしてpx単位ベタ打ちのピクセルパーフェクトを求められてしまうことだってあります。pxremに変換してなんとか表現するという努力も悪くないですが、なかなかに不毛な作業です。

そんなときはさくっとTailwind CSSを拡張してしまいましょう。

tailwind.config.js

module.exports = {
  purge: ['./public/**/*.html'],
  theme: {
    extend: {
      spacing: {// px単位
        ...[...Array(120)].reduce((m, _, i) => {
          m[`${i}px`] = `${i}px`
          return m
        }, {}),
      },
    },
  },
  variants: {},
  plugins: [],
}

豪快にp-1pxからp-120pxまで用意してみました。ピクセルパーフェクトし放題です。僅かばかりの良心で120pxにしましたが、必要な分だけ増やしてしまってください。

さて、こんなことをしたらファイルサイズがどんなことになるか想像はつくと思いますが、せっかくなのでそのままビルドしてみます。

🚀 Building... tailwind.css

   ✅ Finished in 4.59 s
   📦 Size: 4.97MB
   💾 Saved to public/style.css

✨  Done in 6.11s.

やばいですねぇ、これはやばい。それではオチも何もなく結果も見えていますがPurgeCSSを通したビルドを行なってみます。

🚀 Building... tailwind.css

   ✅ Finished in 4.42 s
   📦 Size: 11.79KB
   💾 Saved to public/style.css

✨  Done in 4.99s.

そういうことなんですね。Tailwind CSSの拡張は本来ならばファイルサイズ肥大とトレードオフで、神経すり減らしながら必要最小限になるよう調整しなければなりません。しかし今は令和です。我々の後ろにはPurgeCSSという対不要CSS最終防衛兵器が控えています。

ファイルサイズが2倍に膨れるようなこんな拡張を施しても、使わなかった分は全て削除されます。常に必要最小限の拡張が達成可能です。

レンダリング関数との組み合わせ

この頃流行りのライブラリと組み合わせてみます。最小手数と宣言してしまっているのでなるべく最小手数で使えそうなツールを探しました。探す手間は私が負ったので見逃してください。

サンプルコードが何の環境構築もなしに簡単に動いたので今回はPreactでいきたいと思います。まともに使った経験はないのでなんとなくで使っていきます。

preactjs.com

Getting Startedにて紹介されている最小手数っぽい方法でさきほどのTodoページを書き換えてみます。リスト部分のみです。

<bodyclass="p-2"><scripttype="module">import{ h, render } from "https://unpkg.com/preact?module";const li = i => h(
'li',{class: 'mt-2 p-2 border border-red-500'},[          h('p', {class: 'border-b'}, `todo ${i}`),          h('p', {class: 'text-sm'}, '2020 05/12'),])

const app = h('div', null, [        h('ul', {class: 'py-2 px-4 border border-red-500'}, [1, 2, 3].map(i => li(i)))
]);      render(app, document.body);</script></body>

jsxのセットアップをしだすと最小手数をはみ出しそうなのでh関数でゴリゴリと書きます。ReactしかりVueしかりElmしかり大体同じ使い心地です。結局のところTailwindCSSを使うときはclassの当て方にしか関心を持つ必要がないので、どんなツールを使おうが相性が悪くなることはないです。

f:id:robokomy:20200511200704p:plain
こんな感じ

レンダリングをscriptで制御できる利点の1つといえばリストをループでまとめて書けることです。ただループで回す欠点として、当然ですが全ての要素に同じclassが当たります。

'mt-2 p-2 border border-red-500'

そうするとこのように、端っこの要素に付けたくないmarginborderが付いてしまうことがよくあります。

f:id:robokomy:20200511200721p:plain
気になる隙間

まず浮かぶ解決策は普通にclassを付けてcssを当てていく方法です。

.item:first-child{margin-top: 0; }

しかしせっかくTailwindCSS使っているのだから、見通しをよくするためにも独自のclassは極力使いたくないという欲が出てきます。よし分かったcssを使いたくないなら、scriptで制御してしまえばよいではないか方針に切り替えます。

'p-2 border border-red-500' + (i === 0 ? '' : 'mt-2')

確かにこれで解決して世界は平和になったように見えるんですが、我々が求めているのは本当にこれだったのかという疑問が残ります。

そんなところで、TailwindCSSは新たな解を用意してくれています。

これを、

'mt-2 p-2 border border-red-500'

↓こうする。

'mt-2 p-2 border border-red-500 first:mt-0'

first:mt-0'というclassが増えました。見た通りです。これを付けると&:first-child { margin-top: 0; }と同様な効果があり、リストの先頭要素だけmargin-top0にすることができてしまいます。

f:id:robokomy:20200511201224p:plain
しゅっ

後出しですが注意点として、このfirst:mt-0という機能はデフォルト設定のままでは使えません。このようなprefixで制御するスタイル機能は複数あり、すべてを有効にすると相当なファイルサイズになってしまうからです。

tailwindcss.com

有効にするにはvariantsという設定を拡張します。firstlasthoverなど有用なものは揃っているので、気になったものはとりあえず有効にしちゃいましょう。使っていないものはどうせPurgeCSSで削除されます。

追加した設定はデフォルトのものとマージはされず、上書きされるので注意してください。

tailwind.config.js

module.exports = {
  purge: ['./public/**/*.html'],
  theme: {
    extend: {},
  },
  variants: {
    margin: ['responsive', 'first', 'last'],
  },
  plugins: [],
}

first:m-0first:m-1first:mt-0という風に用意されるclassが倍々で増えるので当然ですがcssファイルサイズも相当に膨れます。

🚀 Building... tailwind.css

   ✅ Finished in 1.69 s
   📦 Size: 2.12MB
   💾 Saved to public/style.css

はい、PurgeCSSの出番です。

🚀 Building... tailwind.css

   ✅ Finished in 1.44 s
   📦 Size: 11.79KB
   💾 Saved to public/style.css

レンダリング関数との組み合わせ: Elm余談

メドピアでElmは一切使われていないので完全に余談なんですが、Tailwind CSSはElmプロジェクトにものすごくおすすめです。

自分自身Elmは最近少し触っているくらいでそこまで詳しくはないという前置きをしておいて、script部分に関してはその徹底した関数型言語特性から非常に強力なんですが、スタイルに関しては重要視されていないのかあまりよい戦略が見つかりません。探せばなくもないんですが、模索中な段階だったりやたらとややこしかったりするものが多いです。

ベストプラクティスじゃなくてもいいからとにかく手軽にスタイルを当てたいんだということで、結局素のCSSファイルをindex.htmlで読み込んだり、インラインスタイルを使っていくとう場面が結構あるのではないでしょうか。そして古き良きweb開発におけるスタイル管理苦難の旅路を追体験していくわけです。

そんなあなたにTailwind CSS。

div [ class "p-2 border text-blue-500" ] [ text "hello world!" ]

ただclassを提供するだけで特定フレームワークに依存しないので、もちろんElmとも相性ばっちりです。PurgeCSSは正規表現で使われているclass名を探しているだけなので.htmlでも.jsxでも.elmでもファイル形式は問題になりません。

インラインスタイルを超えて

Tailwind CSSの使い心地はインラインスタイルライクですが、そのポテンシャルはインラインスタイル特有の制約をものともしません。先ほども紹介した、first:mt-0というprefix付きの指定方法(variants)がそれです。

インラインスタイルの泣き所として、first:hover:といったセレクタや、レスポンシブのためのメディアクエリを使うことができません。まともなwebページを作る上でこれらの制約は致命的です。仕方ないから無理な部分だけCSSを別で作って対応したとしても、今度はインラインスタイルとCSSファイル内のスタイルが散らばって管理が面倒になっていきます。

一方でTailwind CSSはインラインスタイルライクではあっても中身はセレクタ指定のCSSなので、このような制約もうまく回避してくれています。

first:hover:は先の例で既に示しました。そしてレスポンシブも同じく、xs:p-1sm:p-2という風にprefixを付けるだけで分岐が可能です。

余談: そうはいっても万能ではなかった

残念ながら万能ではありません。実際に使ってみて、これは辛いなと感じたシーンもちょくちょくあります。

例えば親要素がhoverされたら子要素をdisplay: none;にしたいなんていう場面です。1つ1つのDOMに対してclassを当てていくという使い方になるので、親子であろうとDOMを跨いだスタイル制御をすることは現状だと厳しそうです。敗北した気分でしぶしぶCSSを書きましょう。

Tailwind CSS上級者の方々ならもしかしたら解決策を持っているかもしれません。求む情報発信。

実際に導入したNuxt.jsプロジェクトでのCSS比率余談

Tailwind CSS流のclassだけで実際どこまでスタイルを作れるのかは気になる点だと思います。どうしても素のCSSを書かなければならない場面があったとしても、そういう場面が多すぎたらTailwind CSSの導入はかえってスタイル定義の散逸を招いてしまうからです。

ということでNuxt.js利用の実際のプロダクトで集計してみました。.vueという拡張子のファイル153個に対して、<styleという文字列grepでヒットしたファイルが13個です。

その13個の内容はざっと見た限りこのようなものです。

  • html全体にかかるfont-familyなどの設定
  • <slot />で挿入した要素に対してのスタイル指定
  • <select>へのappearance: none;
  • アニメーション関係(transitionanimation@keyframes
  • DOM跨ぎのhover:制御

少なくないといえば少ないかもしれませんが、前向きに捉えればこれら以外はすべてTailwind CSS流儀でカバーできているわけです。まずまずな結果ではないでしょうか。

ちなみにそのプロダクトというのはこちらです。こっそりデバッグツールで覗いてみてもらうとTailwind CSSの雰囲気が分かるかもしれません。

spot-rmc.medpeer.jp

まとめ

近年のフロントエンド関連技術は激しく進化しまくっているものの、CSS関連の話題はどうしても置き去りにされやすいです。そうはいっても辛さは無視できないので様々な手法も考案されてきてはいますが、フレームワーク依存だったりまた新たな辛さが出てきたりとなかなか明る い未来は見えてきません。

そういう状況の中で、主観ベースですが、Tailwind CSSは過去最高に使い勝手が良かったです。インラインスタイルライクなのに制約が少なく拡張性が高い、そしてCSSファイルを管理する必要がほぼない。この特徴が非常に強力です。

CSS管理は諦めてTailwind CSSを使おう。現時点で私から提示できるCSS戦略のベストプラクティスです。


これは全く余談ではないんですがメドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html


Terraform用のGitHub Actionsをterraform-github-actionsから後継のsetup-terraformに移行する

$
0
0

SREの侘美です。

最近はfirst call for オンライン診療の開発でRailsのコードを書いてました。

hashicorp/terraform-github-actionsから後継である hashicorp/setup-terraformへ移行した際にいくつか設定でハマったので、そのことについて書いていきたいと思います。

背景

メドピアではterraformでAWSのインフラを管理しています。
terraformのリポジトリでは、レビューがスムーズに行えるようにGitHub Actions上で terraform planterraform applyterraform fmt等を実行できる hashicorp/terraform-github-actions を利用し、下の画像のようにplan結果をPRに自動で投稿するようにしていました。

f:id:satoshitakumi:20200520154554p:plain

新サービスをリリースし仕事も一段落した先日、TerraformのGitHub Actionsに関するとあるドキュメントが更新されていることを発見しました。

Teraform GitHub Actions - Terraform by HashiCorp

なんと hashicorp/terraform-github-actions のメンテナンスが終了されていました!

メンテナンスされていないActionsを利用したままでは、最新のterraformのバージョンに対応できない日がやってきそうなので、さっそく後継の setup-terraform でGitHub Actionsの設定を書き換えることにしました。

準備

まずGitHub Actionsの設定を修正する前に、 terraform-github-actions と 後継である setup-terraform の特徴と terraform planの実行例を比較してみました。

terraform-github-actions

リポジトリ

github.com

特徴

  • 各ステップで uses: hashicorp/terraform-github-actions@masterを指定して、 initplanなどのサブコマンドを指定して使う
  • tf_actions_comment: 'true'を指定することで、plan結果をPRに投稿してくれる
  • planの差分が無い場合はPRに投稿はしない

Actions上でterraform planを実行するサンプル

steps:- uses: actions/checkout@v2

  - name: Terraform Init
    uses: hashicorp/terraform-github-actions@master
    with:tf_actions_version: ${{ env.TF_VERSION }}
      tf_actions_subcommand:'init'- name: Terraform plan
    uses: hashicorp/terraform-github-actions@master
    with:tf_actions_version: ${{ env.TF_VERSION }}
      tf_actions_subcommand:'plan'tf_actions_comment:'true' # PRへplan結果を投稿する設定

setup-terraform

リポジトリ

github.com

特徴

  • 文字通りterraformをsetupし、 terraformコマンドが利用できるようにする
  • GitHub Actionsのoutput等に対応するようにscriptでwrapされている
  • plan等は run: terraform planで実行する
  • その他の機能は無く、plan結果のPRへの投稿などは自前で設定する必要がある

Actions上でterraform planを実行するサンプル

steps:- uses: actions/checkout@v2

  - uses: hashicorp/setup-terraform@v1
    with:terraform_version: ${{ env.TF_VERSION }}

  - run: terraform init

  - run: terraform plan -no-color

setup-terraformへの乗り換え

特徴が把握できたところで、setup-terraformを利用してPRにplan結果を投稿する設定をしていきます。 基本的には terraform-github-actions の仕様を再現する形としています。

具体的には下記の3点です。

  • PRへのplan結果の投稿
  • 差分が無い場合は投稿を抑制
  • 差分以外の余計な出力の削除

PRへのplan結果の投稿

setup-terraform の READMEに記載されている設定を参考にします。

下記のように actions/github-scriptを使い createComment関数でコメントを投稿します。
secrets.GITHUB_TOKENはActions上で自動で定義される変数です。

- uses: actions/github-script@v1
  env: # id: planのステップの出力を参照STDOUT:"```terraform\n${{ steps.plan.outputs.stdout }}```"with:github-token: ${{ secrets.GITHUB_TOKEN }}
    script: |
      const output = `<details><summary>tf plan:</summary>\n\n${process.env.STDOUT}\n\n</details>`;

      github.issues.createComment({
        issue_number: context.issue.number,
        owner: context.repo.owner,
        repo: context.repo.repo,
        body: output
      })

差分が無い場合は投稿を抑制

実装方法としては2通りあります。

1つはterraformの -detailed-exitcodeオプションを使うやり方です。
このオプションを付与することで、コマンドのexit codeが下記のようになります。

  • 0: 成功かつ差分なし
  • 1: エラー
  • 2: 成功かつ差分あり

GitHub Actionsではexit codeが0以外の場合はエラーとなりworkflowで利用するには continue-on-error: trueをplanのステップに追加する必要があります。

steps: # 他のstepは省略- name: terraform plan
    id: plan
    run: terraform plan -detailed-exitcode
    continue-on-error:true # 0以外のexit codeでもworkflowを継続する- name: comment on PR
    if: ${{ steps.plan.outputs.exitcode == 2 }}
    # 以下PRにコメントする処理

2つめは単純に出力内容の文字列から取得する方法です。
こちらはあまりロバストではないですが、 continue-on-error: trueを利用しなくてすむため、今回はこの方法を採用しています。

steps: # 他のstepは省略- name: terraform plan
    id: plan
    run: terraform plan
  
  - name: comment on PR
    if: ${{ !contains(steps.plan.outputs.stdout, 'No changes.') }}
    # 以下PRにコメントする処理

差分以外の余計な出力の削除

terraform-github-actions ではterraform planを実行した際に大量に出力される <resource_id>: Refreshing state...のような出力を削除した上でPRにコメントしてくれます。

f:id:satoshitakumi:20200520154554p:plain

terraform-github-actionsの実装を確認してみると、 sedコマンドで ------(略の区切り線を基準に行を削除していました。

terraform-github-actions 同様に sedコマンドで消しても良いのですが、PR投稿のgithub-script内でついでに整形する実装にします。

steps:- uses: actions/github-script@v1
    env:STDOUT:"${{ steps.plan.outputs.stdout }}"with:github-token: ${{ secrets.GITHUB_TOKEN }}
      # NOTE: 区切り文字で囲まれた範囲のみを出力するscript: |
        const lines = process.env.STDOUT.split('\n')
        const separator = '-'.repeat(72)
        let index = lines.indexOf(separator)
        let outputLines = lines.slice(index + 1)
        index = outputLines.indexOf(separator)
        if (index) {
          outputLines = outputLines.slice(0, index)
        }
        const planOutput = '```' + outputLines.join('\n') + '```'
        const output = `<details><summary>plan:</summary>\n\n${planOutput}\n\n</details>`;

        github.issues.createComment({
          issue_number: context.issue.number,
          owner: context.repo.owner,
          repo: context.repo.repo,
          body: output
        })

最終的なGitHub Actionsの設定

最終的な設定は下記のようになりました。

steps:- name: Checkout Repo
    uses: actions/checkout@v2

  - name: setup Terraform
    uses: hashicorp/setup-terraform@v1
    with:terraform_version: ${{ env.TF_VERSION }}

  - name: terraform init
    run: terraform init

  - name: terraform plan
    id: plan
    run: terraform plan -no-color -lock=false- uses: actions/github-script@v1
    if: ${{ !contains(steps.plan.outputs.stdout, 'No changes.') }}
    env:STDOUT:"${{ steps.plan.outputs.stdout }}"with:github-token: ${{ secrets.GITHUB_TOKEN }}
      # NOTE: 区切り文字で囲まれた範囲のみを出力するscript: |
        const lines = process.env.STDOUT.split('\n')
        const separator = '-'.repeat(72)
        let index = lines.indexOf(separator)
        let outputLines = lines.slice(index + 1)
        index = outputLines.indexOf(separator)
        if (index) {
          outputLines = outputLines.slice(0, index)
        }
        const planOutput = '```' + outputLines.join('\n') + '```'
        const output = `<details><summary>tf plan:</summary>\n\n${planOutput}\n\n</details>`;

        github.issues.createComment({
          issue_number: context.issue.number,
          owner: context.repo.owner,
          repo: context.repo.repo,
          body: output
        })

所感

terraform-github-actions から比べるとPRへplan結果を投稿する付近の処理を自前で用意しなければならず、難易度は上昇したように思えました。
ですが、無事に後継の setup-terraform へ移行することができたので、terraformの最新バージョンへの追従する際のActions関連の懸念を減らすことができました。


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

特定保健指導"フィッツプラス"事業を支えるモノリシック Rails + VIPER Swift アーキテクチャ

$
0
0

みなさんこんにちは。フィッツプラス開発エンジニアの福本(@terry_i_)です。
早いもので入社して半年が経ちました。普段はRailsを中心に色々と書いてます。

リモートワークが長く続いていることもあって、最近は自宅の開発環境を(過剰に)整備するのがマイブームです。先日はlogicoolのPCスピーカーを買いました。所得がゴリゴリ削られていってツラい。

さて今回は、これまで忙しくて紹介する機会のなかったフィッツプラスの事業概要や、アーキテクチャおよび使用する技術についてお話しします。

アーキテクチャに悩むエンジニアの方の参考になったり、皆さんのフィッツプラスへの事業理解が深まれば幸いです。


特定保健指導とは?

いきなり技術の話に入る前に、タイトルの”特定保健指導”という事業ドメインについて簡単にご説明します。

www.mhlw.go.jp

この”特定保健指導”という単語で、すぐピンと来るエンジニアの方は多くないでしょう。というのも、特定保健指導は健康保険に加入している40歳以上の方を対象に実施されているためです。

私も例に漏れずピチピチの若エンジニアですので、あまりよく知りませんでした。今は若い方もその内お世話になることと思います。

特定保健指導は、特定健康診査という定期検診で対象となった(要するに”引っかかった”)方の生活習慣病の予防および改善を目的に行われています。

具体的な内容としては、有資格者が対象者と最初に面接をし、その後一定期間継続的にサポートするプログラムです。それを”管理栄養士”という国家資格を有する専門職の方が、食生活を中心としたアドバイスを行って、生活習慣改善のサポートを行っています。

フィッツプラスはその”特定保健指導”を行うtoB向けのWebサービスを中心に、一般の方向けにも食事のアドバイスを行うアプリを開発・運営しています(後述)。つまり、フィッツプラスはメドピア内で”食”の観点から予防医療をケアする立ち位置で事業を推進していることになります。

メドピアグループはヘルステック企業として、幅広い医療領域を技術でサポートしています。中でも「予防領域」は、高齢化社会により高騰した医療費の削減などの社会的背景から、昨今とても重要視されております。

アーキテクチャ

f:id:terryyy:20200620202230p:plain

さて、この章から具体的な技術の話をしていきます。上記の図は、先ほど説明したフィッツプラス事業のサービスの中核であるRailsアプリケーション(dietplus-serverと呼んでいます)と、関連するアプリやサービスとの関係を図にしたためたものです。

「関連する」という表現ですが、この図には記載されていないWebサービスが複数稼働しています。モダンなプロジェクトで言うと、Nuxt.js + Rails6でのSPA構成のサービスを絶賛開発してたりします。完成した暁には、そちらの担当エンジニアが記事を書いてくれると思うのでマァ首を長くして待っていてください。

上記の図をすべて解説すると薄い本が1冊書けてしまうので、中心となるRailsサービス『dietplus-server』と、上部オレンジ色の領域にあるiOSアプリケーション『DietPlus』のふたつに的を絞って今回はお話します。

以降では、まず裏側を支えているdietplus-server(Rails)について、その後にDietPlus(iOS)について解説します。そうすることで、現状のアーキテクチャ全体でのトータルなメリットや課題感をお伝えできればと考えています。

そういった目的上、RailsとSwiftの両方について触れています。「Swiftの話だけ聞きたいんだ俺は」という方は、お手数ですが、”VIPER Swift”の章から読んでいただけると幸いです。

モノリシック Rails

f:id:terryyy:20200620193739p:plain

中核となるRailsですが、先ほどの図では詳細が分かりづらいので、今回お話したいAPIと管理画面に関わるライブラリを記載した図を別途作ってみました。特徴的な部分について説明していきます。

ちなみに、2020年6月1日時点のrails statsでは以下のような結果となりました。Rails のサイズ感が伝われば幸いです。

+----------------------+--------+--------+---------+---------+-----+-------+
| Name                 |  Lines |    LOC | Classes | Methods | M/C | LOC/M |
+----------------------+--------+--------+---------+---------+-----+-------+
| Controllers          |  11144 |   9420 |     223 |     840 |   3 |     9 |
| Helpers              |    212 |    175 |       0 |      27 |   0 |     4 |
| Jobs                 |    447 |    364 |      14 |      18 |   1 |    18 |
| Models               |  13809 |   8639 |     167 |     799 |   4 |     8 |
| Mailers              |    586 |    502 |      26 |      66 |   2 |     5 |
| Channels             |      8 |      8 |       2 |       0 |   0 |     0 |
| JavaScripts          |     67 |     21 |       0 |       4 |   0 |     3 |
| Libraries            |   1019 |    909 |       7 |       9 |   1 |    99 |
| Mailer specs         |      8 |      6 |       1 |       0 |   0 |     0 |
| Decorator specs      |     67 |     60 |       0 |       0 |   0 |     0 |
| Loyalty specs        |    205 |    147 |       0 |       0 |   0 |     0 |
| Model specs          |   6153 |   5438 |       0 |       0 |   0 |     0 |
| Request specs        |  10968 |   9586 |       0 |       0 |   0 |     0 |
| System specs         |   6937 |   5931 |       0 |       0 |   0 |     0 |
| Lib specs            |    659 |    560 |       0 |       0 |   0 |     0 |
| Job specs            |    154 |    123 |       0 |       0 |   0 |     0 |
+----------------------+--------+--------+---------+---------+-----+-------+
| Total                |  52443 |  41889 |     440 |    1763 |   4 |    21 |
+----------------------+--------+--------+---------+---------+-----+-------+
  Code LOC: 20038     Test LOC: 21851     Code to Test Ratio: 1:1.1

ActiveModelSerializers

APIでJSONを返すオブジェクトの作成はActiveModelSerializersで行っています。いちいちviewファイルを作ってレンダリングさせる必要がなく、関連オブジェクトの指定もRailsっぽく書けます。メドピアでは過去に他のチームでも採用情報があり、かつ一般的にもよく使われるgemなので特に違和感なく使えています。

tech.medpeer.co.jp

OpenAPI

各APIの定義はOpenAPI仕様のドキュメントをSwagger Editorで書き、Swagger UIで閲覧しています。

特徴としては、当初のアーキテクチャ図の通りAPIのレスポンスを返す先のアプリケーションが2つ(図のオレンジと緑の領域)ある点です。幸い2つのAPIは互いに独立しているので、各レスポンス先ごとに spec.ymlの参照パスを分けた docker-composeのコマンドを、以下のようにMakefileを作って運用しています(一部改変しています)。

# Makefile
## Swagger-ui
### DietPlus
dietplus/api/docs:
    docker run --rm -p 8591:8080 -v $(CURDIR)/${DIETPLUS_API_SPEC_PATH}:/usr/share/nginx/html/spec.yml -e API_URL=spec.yml swaggerapi/swagger-ui
### DietPlus Pro
dietpluspro/api/docs:
    docker run --rm -p 8591:8080 -v $(CURDIR)/${DIETPLUS_PRO_API_SPEC_PATH}:/usr/share/nginx/html/spec.yml -e API_URL=spec.yml swaggerapi/swagger-ui

OpenAPIについては、私が後からSwaggerを導入したため、モック環境など一部整っていない部分があります。引き続き徐々に環境整備を進めていきたいという気持ちです。気持ちはあります。

Houston(プッシュ通知)

RailsからiOSアプリに対してのプッシュ通知(いわゆるAPNs)は、HoustonというgemをActiveJobと併用して行っています。Houston::Notificationをインスタンス化するだけで、iOSアプリに送る通知のバッヂや音声を簡単に設定し送信できます。

問題としては、執筆時点でmasterがiOSのバージョン13のプッシュ通知に対応していない点が挙げられます。

詳細は以下のIssueに記載されていますが、Apple Developersが要求するheaderの情報をgemで設定できないことが原因です。

github.com

幸いにもこのIssueに対応するPRが上げられているので、現在はGemfileのgitオプションを使用し該当するcommitを取り込む形で対処しています。実際には以下のように記述しています。

# ios push notification# TODO: マージされたら git オプション外す
gem 'houston', git: 'https://github.com/ab320012/houston', ref: 'efbeb6c'

上記の対応には若干懸念が残っていて、オプションでcommit hashを直接指定している関係上、ハッシュ値が変わってしまった場合にbundle installできなくなります。rebaseforce pushなどが行われると、参照しているcommit hashの値が変わってしまう危険性があるようです。*1

Banken(権限管理)

管理画面にログインするユーザー権限の管理手法として、Bankenを採用しています。

github.com

前提として、後述の『DietPlus』を含む複数のスマホアプリを同じ管理画面を用いて管理しています。そして、アプリごとにメニューから画面を切り替えて操作するようになっており、(当然ですが)他のアプリの管理栄養士や管理者がユーザーの個人情報を見られないようにしています。また、同じアプリ内の画面でもセンシティブな情報(例: ユーザーとのチャットのやり取り)が含まれるものがあったりするため、画面ごとの細かい権限の制御が必要です(詳細は後述)。

アプリごとの namespace(実際はmodule)が複数存在し、画面ごとに権限を定義する必要があるため、Controllerベースで権限を付与するBankenは違和感なく使えています。RSpecでテストコードを書く際は、Request Spec内で権限ごとにループでテストを回しています(以下例)。

shared_examples_for "アプリの管理者と開発者のみアクセスできる"do
  [
    { name: 'アプリ管理者', trait: :app_admin },
    { name: '開発者', trait: :developer },
  ].each do |user_value|
    context user_value[:name] do
      let!(:user) { create(:admin_user, user_value[:trait]) }

      it { expect(response.status).to eq 200 }
    endendend

context '各権限でアクセスする'do
  before { get admin_app_index_path }

  it_behaves_like "アプリの管理者と開発者のみアクセスできる"end

VIPER Swift

f:id:terryyy:20200620194007p:plain

ここからは、クライアントサイドであるSwiftコードの設計と使用するライブラリについてお話できればと思います。

今回スポットを当てるアプリ『DietPlus』ですが、食事の写真を投稿すると管理栄養士の方がアドバイスをしてくれるサービスです。2019年10月にiOSアプリをフルリニューアルしてリリースし、その後いくつかの機能追加や改善を行いました。

medpeer.co.jp

こちらもコードのサイズ感をお伝えすると、2020年5月時点でのclocの実行結果は以下のとおりです。

$ cloc --include-lang=Swift,Objective\ C --exclude-dir=Pods,Carthage ./
    6401 text files.
    6269 unique files.
    5721 files ignored.

github.com/AlDanial/cloc v 1.86T=4.17 s (168.1 files/s, 13199.7 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Swift                          69797071142333492
Objective C                      58637363
-------------------------------------------------------------------------------
SUM:                           70297931146033855
-------------------------------------------------------------------------------

DietPlusのSwiftコードにおける特徴は、ピュアなVIPERアーキテクチャで構築されている点でしょう。VIPERについて詳細に書くと薄くない本が数冊書けてしまうので割愛しますが、いわゆるClean Architectureの一種です。

qiita.com

Rubyエンジニア的に言うと、フレームワークの『Hanami』に近いイメージがあります。Actionごとにクラスを切って1画面≒1クラスになる点や、ViewsとTemplateファイルが独立している点などが、VIPERのViewやPresenterの仕組みと似通っていると感じました。*2

Entityがキモ

f:id:terryyy:20200616231704p:plain

さて、VIPERにおいて最も設計が難しい点のひとつ(諸説あり)は、Entityに何を置くかでしょう。Clean ArchitectureではEntityを「アプリケーションに依存しないドメインおよびビジネスロジック(を示すデータの構造やメソッドの集合)」だとしています。*3

このEntityをインターフェースやDBから完全に切り離し、依存の方向を一方向にすることで(図参照)UIなどの変更が多い部分を変更しやすく、そうでない部分に影響を与えないようにします。この設計をいかに維持できるかで、プログラムの変更を容易にできるかが決まります。

DietPlusにおけるEntity

DietPlusにおけるEntityですが、結論から言うと「ユーザーの食事」と「食事の日付」に関わる部分が、最も中心的なドメインロジックとなっています。

人間の食事習慣や日付といった概念は普遍的なものですが、その食事や日付に対して「どうコメントを返信するか」「どういう」は、サービスを提供する私たち側の問題です。これをしっかり分けて考えることで、変更の多い部分をできる限りInteractorPresenterに切り出せています。

具体的にはこんな感じのコードが、ユーザーの食事投稿を表示するPresenterに書かれていて、Entityである食事(Meals)をUIで表現するデータに変換しています。

// MARK: - MealRecordPresenterProtocolfinalclassMealRecordPresenter:MealRecordPresenterProtocol {

    structConstant {
        // 反映可能食事枚数の上限staticletmaxMealPhotoCount:Int=4
    }

    
    structInitialState {
        vardate= Date(second:nil)
        varmemo=""varselectedCategory:MealCategory= .breakfast
        varselectedStyle:MealStyle= .home
        vartags:[MealTag]= []
    }
    
    // 取得可能枚数の上限privatevarmaxAddableCount:Int {
        return Constant.maxMealPhotoCount - photos.count + deletePhotos.count - addedImages.count
    }
    private(set) vardate= Date(second:nil)
    private(set) varphotos= [Photo]()
    private(set) vardeletePhotos= [Photo]()
    private(set) varaddedImages= [UIImage]()
    varmemo:String=""varmealTags= [MealTag]()
    varselecetedCategory:MealCategory= .breakfast
    varselectedStyle:MealStyle= .home
    
    private(set) varmealDetail:MealDetail? // Editの場合に取得private(set) varinitialState:InitialState?
    privatevarcompletionHandler: (() ->Void)?
    
    weak varview:MealRecordViewProtocol!varinteractor:MealRecordInteractorProtocol!varrouter:MealRecordRouterProtocol!init(completionHandler: (() ->Void)?) {
        self.completionHandler = completionHandler
    }
}

一方、食事を表すEntityであるMeal.swiftはシンプルに書かれています。

ここにすべては記載できませんが、ファイル内のSwiftコードはExtensionの拡張を含めて66行でした。主要なEntityとしては薄い部類だと感じます。

structMeal:Codable {
    
    letid:IDlettime:Dateletcategory:MealCategoryletstyle:MealStylevarcontent:String?
    varmemo:String?
    letcreatedAt:DateletupdatedAt:Datevarphotos:[Photo]varmealTags:[MealTag]structID:Identifiable {
        letrawValue:Int
    }
    
    enumCodingKeys:String, CodingKey {
        case id
        case time
        case category ="categoryCode"case style ="styleCode"case content
        case memo
        case createdAt
        case updatedAt
        case photos
        case mealTags
    }
 
}

Embedded Frameworkによるマルチモジュール構成

また、VIPERのレイヤーの分割や依存関係の構造を守るために、UIコンポーネントや拡張メソッドを別のモジュールに切り出して管理しています。具体的には、以下の3つにモジュールが分かれています。

# DietPlus(App)
    - アプリ本体のコード
    - 画面に関するModule(View, Interactor, Presenter, Router)
    - Entityおよびサービスクラス(API、Database, Keychain, UserDefaultsなど)
# UIComponent
    - 各種UIパーツの格納
    - Color Asset, Image Assetも基本的にはこっちで管理
    - UITableViewCell, UICollectionViewCellといったCellクラスもUIComponentに追加
    - アプリ本体のモジュールはImportしない(依存は一方向のみ)
# Common
    - Extensionメソッド(UIは除く)
    - Standard Libraryに関するUtilityクラス
    - UIに限定されない各種定義値

アーキテクチャの設計を遵守できるのはもちろん、依存の方向性をある程度強制できるので「UIComponent →アプリ本体」という依存を作り循環参照が起きてぐちゃぐちゃになるそしてしぬ…ということが防げます。他にもnamespaceをきっちり分けることで、呼び出すモジュールやクラスを明確にできるという利点があります。

qiita.com

まだ実施していませんが、EntityやAPIは他のアプリのコードと比較して変更の頻度が速くないため、これも別モジュールに切り出して良いかもしれません。

現状のメリット/課題

さて、冒頭から偉そうに解説していますが、RailsとSwiftのどちらも私が設計したものではなく、過去に在籍したエンジニアの方が設計したものです(そのため私の解釈がある程度混ざっています)。私はその恩恵に預かっているわけですが、これまで半年間の開発で感じたアーキテクチャの「メリット」と「課題」についてお話します。

メリット

少人数のエンジニアリソースで開発できる

個人的にはこれが最も大きなメリットだと感じるのですが、コードベースの大きさと比較すると、人数の少ないチームで開発を進められます。

著書『人月の神話』の中で、フレッド・ブルックスは基本的な原則を明らかにしました。小さなチームなら、どんな方法論もうまくいくのです。―ケイト・トンプソン 『ZERO BUGS シリコンバレープログラマの教え』*4

実際に2020年5月の執筆時点で、フィッツプラスはサーバサイド4名とアプリエンジニア1名の計5名で開発を行っています。今回ご紹介したサービス以外にもRailsアプリケーションが2つにPHPのサービスやPythonスクリプトなどがあり、それらの存在も考えると少ない人数ではないでしょうか。小さなチームではコミュニケーションコストを低く抑えられ、サービスの前提やコードの変更状況などの共有がとてもラクです。

また、一般的な話をすると、そもそもベンチャー企業では物理的に大量のエンジニアを採用しづらいパターンもあるかと思います。まず最初はサービスをモノリシックに作り、市場に必要とされる機能を開発していくスタイルは、オーソドックスですがひとつの解ではあると思いました。

拡張性が高く複数のアプリケーションを展開しやすい

これはVIPERのくだりでドメインを定めたおかげですが、Entityで閉じ込めたロジックが複数のアプリケーションで共有されやすい状態だと感じます。

社内にはDietPlusに近いドメインを持ったiOSおよびAndroidのアプリ(冒頭アーキテクチャ図参照)が他にも存在していますが、アプリやバックエンドともに既存アプリと同じようにコードを書くことで再現できる部分が多く、後から入った身としては助かります。

また、テストを書く際に、テストケースを豊富に書くべき部分が明確になります。具体的には、日付に関しては境界値テストを必ず書いたり、APIリクエスト時のパスが想定しない日時だった場合の異常系のテストなどを増やしケースを充実させています。一方で、管理画面上(View)では課金などのクリティカルな処理を行っていないので、System Specは薄くて済みます。

実はサーバサイドと連携するアプリを他にも増やす予定があり(まだ喋れないやつ)、現在私が担当者としてモリモリとコードを書いているのですが、こういったことを簡単にできるのはひとつの強みです。

課題

アカウントや権限の管理が複雑になる

Bankenの章でピンと来た方がいるかもしれませんが、複数の権限が必要なアプリが複数存在しているため権限が複雑になってきています。

それぞれユーザーの権限を各Modelのenumで判断しているため、権限の説明やコンテキストをコードで表現・管理するのが難しいです。マイグレーション時にDBにコメントを残すことができますが、そこに盛り込むのに権限の説明は少し長すぎます。Model内に長文でコメントアウトを残すのが妥当なラインでしょうか。

長期的には、太ってきた権限を他のテーブルに分割していく等の改善方法があるかと考えています。権限の説明をコードで把握するのを諦めて、しっかりドキュメントを残すことも大切でしょう(視線を泳がせながら🐟)。

Swiftのファイル数が多くなる

VIPERに限らず、Clean Architectureでは”ファイル数が多くなりがち”です。責務を分割すればひとつのファイル(あるいはレイヤー)あたりのコード数が少なくなるので、その裏返しと考えれば当然です。

ひとつの画面を作るために、VIPERの頭文字(E除く)とStoryboardがひとつ(不要な場合もある)の合計5つのファイルを作成する必要があります。RailsならViewファイルを作成して、Controllerとrouteファイルに追記するくらい(諸説あり)なので、比べるとやはり多いと感じます。

これについては、コードとファイルの自動生成gemのGenerambaを用ることで工数を削減しています(アプリ開発のライブラリにRuby製のgemが使われていると、Rubyistとしては少し嬉しい気持ちになります)。工数の削減以外にも、Module構成やクラス記述などを統一できるメリットもあります。

github.com

他の自動生成ツールとしては、SwiftGenでリソースと型の作成を自動で行ったりしています。

一部のコードがmodule間でDRYにならない(しづらい)

A::UserB::Userといった別moduleの類似クラス(AやBはアプリ名)が数多く存在するのですが、共通化すべきコードとそうでないコードの見極めが難しいと感じます。各アプリでグロース速度が異なるのでなおさらです。普段コードを書いていて「あっ、このscopeってBの方には生えてなかったのか...」ということがよくあります。共通化するにも「3つ以上のmodule間で共通して使われ続けるであろう処理」かどうかの判断は容易ではありません。

個人的には、ヘンに共通化して罠にハマるくらいなら、メンテナンスするコード量が多少増えても、影響範囲をmodule内に閉じ込めておく方が無難なのではないかと考えます。

さいごに

長くなりましたが以上です。最後までお付き合いいただきありがとうございました。アプリケーションのアーキテクチャや使用するgemなど、皆さまになにか得るものがありましたら幸いです。

冒頭で事業について触れましたが、メドピアはメインサービスである「MedPeer」を中心に、さまざまな医療領域をカバーするため新しい事業をつぎつぎと立ち上げています。事業やプロダクトが社内にたくさんある現状から学べること・経験できることはとても多く、エンジニアとしてとても魅力的な環境だと思います。

ステマみたいになりましたしかし、プロダクトをより良くするために、私たちにはエンジニアの力がもっと必要です。というか一生「足りない」って言い続けてる気がしますが、そんな中でも一緒に走りながらお互いを高めあえるエンジニアの方はぜひメドピアへ!

■募集ポジション

medpeer.co.jp

■開発環境

medpeer.co.jp

*1:リポジトリをforkしprotect branchすればケア可能: 参考

*2:実際、HanamiはClean Architectureに影響を受けているそうです: 『Hanamiフレームワークに寄せる私の想い(翻訳)』https://techracho.bpsinc.jp/hachi8833/2018_03_28/54381

*3:『クリーンアーキテクチャ(The Clean Architecture翻訳)』https://blog.tai2.net/the_clean_architecture.html

*4:"37.象の多くの側面"より引用

薬局向けサービス”kakari”にruby-vipsを導入した話

$
0
0

こんにちは。
外出自粛が続き、大胸筋の育成が疎かになっているエンジニアの宮原です。

ruby-vipsという画像処理用のGemを、かかりつけ薬局化支援サービスの「kakari(かかり)」で導入してみました。
今回は、ruby-vipsとkakariに実装した画像処理の内容について紹介させていただきます。

ruby-vipsとは

ruby-vipsは、画像処理ライブラリであるlibvipsのRubyバインディングになります。 こちらのGemを利用することで、Ruby on RailsのWebアプリケーションに画像処理の機能を追加することができます。 実際にruby-vipsの導入方法や、簡単な使い方は下記スライドにて紹介しておりますので、ご参照いただければと思います。

※昨年の11月に、鹿児島Ruby会議01にてruby-vipsの使い方を紹介させていただきました。

どのような機能で利用しているのか

kakariには、「FAX同時受信」機能というものがあります。 患者さんが送信した処方せんを、薬局が「kakari」上で確認できると同時に、薬局内のFAXにも自動送信される機能になります。
こちらの、「薬局内のFAXにも自動送信される」箇所で、ruby-vipsを利用して画像処理を行っています。

処理の流れとしては以下のとおりになります。

  1. S3から患者さんがアップロードした画像をダウンロード
  2. ダウンロードした画像をグレースケール画像に変換
  3. グレースケール画像をモノクロ画像に変換
  4. バイナリ画像を保存
  5. バイナリ画像や患者さんの情報から、FAX送信用のPDFを作成
  6. PDFファイルをFAX送信

上記手順の、2番から4番の箇所でruby-vipsを利用して画像処理を行ってます。

なぜ画像処理する必要があるのか

FAXは、非常に古くから利用されている画像伝送方式で、以下のような課題があります。

  • フルカラー対応の機器が少数
  • 伝送可能容量の限界

それでは1つずつ見ていきましょう。

フルカラー対応の機器が少数

フルカラーで出力できる機器が少なく、ほとんどの機器は白黒の2階調もしくは中間調を含む階調でしか出力できません。 このため、患者さんがアップロードした処方せん画像をそのままFAXで送ってしまうと、読みづらい処方せんが薬局さん側で出力されてしまいます。

f:id:nyagato_00_miya:20200727194500j:plain※ カラー画像をそのままFAX送信した際の例

患者さんがアップロードした画像を、グレースケールかモノクロ画像に変換することが必須であることが分かりました。

伝送可能容量の限界

昨今のスマートフォンでは、いとも簡単に4032 x 3024 pxの高解像度な写真を撮影できます。 しかしながら、この画像をそのままFAXで送信することはできません。
そうです。FAXの伝送規格では、こんなに大きな解像度の画像を送ることは想定していないのです。 なので、適切なサイズにリサイズして上げる必要があります。

これらの理由から、患者さんがアップロードした処方せん画像をFAXで送信できる画像に加工する必要があるのです。

なぜruby-vipsを選んだのか

Rubyの世界で画像処理を行うには、いずれかの選択肢があります。

  • ImageMagickを使う
  • GraphicsMagickを使う
  • OpenCVを使う
  • libvipsを使う

kakariでは、「処理性能の高さ」と「Rails Wayに乗る」という2つの理由から、libvipsを使う方法を選択しました。

libvipsの処理性能

下記のグラフは、画像処理ライブラリ毎の処理速度とメモリ使用量を比較したものになります。 この比較の結果から、libvipsは処理速度・メモリ使用量共に優れていることがわかります。

f:id:nyagato_00_miya:20200721210343p:plain

f:id:nyagato_00_miya:20200721210402p:plain

Rails Wayな画像処理

Active Storageで利用する画像処理用のGemが、MiniMagickからImageProcessingに変更されました。 github.com

ImageProcessingは、ImageMagick/GraphicsMagickまたはlibvipsライブラリのいずれかの方法で、画像を処理する機能を提供するGemになります。 つまり、Rails Wayな画像処理ライブラリにruby-vipsが加わったということです。

ruby-vipsは、高い処理性能を持ったRails Wayな画像処理用のGemということになりますね。

2値化手法の選定

患者さんからアップロードされる画像は、様々な照明条件で撮影されており、非常にバラエティーに富んだものになります。 このため、画像毎に適切に処理を行わないと、きれいなモノクロ画像を作成できません。
下記の表に示した例では、出力画像の約半分が黒つぶれしており読むことができない状態になります。

Input ImageOutput Image
f:id:nyagato_00_miya:20200727190552p:plainf:id:nyagato_00_miya:20200727190644p:plain

※ 処方せんの内容が読み取れないように、画像はサイズを小さくしております。
※ モザイク処理はあとから追加したものです。

特定の閾値を設定する方法や、大津の2値化では、適切にモノクロ画像を作成できないことがわかりました。

適応的閾値処理

大津の2値化でうまく処理できない場合があることが分かったので、適応的閾値処理を試してみることにしました。

OpenCVのドキュメントでは、以下のような説明がされています。

先の例では、ある画像に対して一つの閾値を与えて閾値処理をした。しかし、撮影条件により画像領域で異なる光源環境となるような画像に対しては期待する結果が得られない.そういう状況では「適応的閾値処理」を使うと良い.適応的閾値処理では,画像の小領域ごとに閾値の値を計算する.そのため領域によって光源環境が変わるような画像に対しては,単純な閾値処理より良い結果が得られる.

「光源環境が変わるような画像に対しては,単純な閾値処理より良い結果が得られる」との記載があったので、OpenCVを利用して実験してみます。
下記のサンプルは、入力画像を2つの方法(大津の2値化・適応的閾値処理)を利用してモノクロ画像に変換しています。

// 画像読み込み
cv::Mat image;
image = cv::imread("test.png", 1);

// グレースケール画像へ変換
cv::Mat gray_image;
cv::cvtColor(image, gray_image, CV_RGB2GRAY);

// モノクロ画像へ変換    
cv::Mat otsu_image, adaptive_image;
// 大津の2値化を利用して、モノクロ画像を作成
cv::threshold(grayImg, otsu_image, 0, 255, cv::THRESH_BINARY|cv::THRESH_OTSU);
// 適応的閾値処理を利用して、モノクロ画像を作成
cv::adaptiveThreshold(grayImg, adaptive_image, 255, cv::ADAPTIVE_THRESH_MEAN_C, cv::THRESH_BINARY, 11, 15);

※パラメータ値は適当な値です。
※C++のサンプルコードです。

下記の表に示した画像が、大津の2値化と適応的閾値処理を利用して作成したモノクロ画像になります。
適応的閾値処理を利用することで、きれいなモノクロ画像を作成できることがわかりました。

Input ImageMono Image(Otsu)Mono Image(adaptiveThreshold)
f:id:nyagato_00_miya:20200727190552p:plainf:id:nyagato_00_miya:20200727190644p:plainf:id:nyagato_00_miya:20200727190819p:plain

※ 処方せんの内容が読み取れないように、画像はサイズを小さくしております。
※ モザイク処理はあとから追加したものです。

ruby-vipsで適応的閾値処理を実装する

OpenCVを利用した実験で、適応的閾値処理が有効であることが分かったので、ruby-vipsを使って実装していきます。

  1. 注目画素と周囲の画素の平均値を求める(閾値を求める)
  2. 求めた閾値を利用して、注目画素を2値化する

注目画素と周囲の画素の平均値を求める

入力画像が、以下のような3×3の画像で、注目画素が中央の画素だとします。 f:id:nyagato_00_miya:20200727152745p:plain

例えば、注目画素t(1, 1)に対して平均値を求める場合は、以下のような式で求めることができますね。

\displaystyle{
t(1, 1) = 1/9(110 + 125 + 200 + 100 + 120 + 110 + 255 + 255 + 120)
}

実際には、左の画像(入力画像)に対して、右のフィルタを適応させていきます。 f:id:nyagato_00_miya:20200727155357p:plain ruby-vipsには便利な#convメソッドがあるので、こちらを利用しました。
下記コードは、入力画像に対して平均化フィルタを適応させた例になります。

# 入力画像(二次元配列から作成)
image = Vips::Image.new_from_array [[110, 125, 200], [100, 120, 110], [255, 255, 120]]
# 平均化フィルタ
averaging_filter = Vips::Image.new_from_array [[1/9, 1/9, 1/9], [1/9, 1/9, 1/9], [1/9, 1/9, 1/9]]

# 入力画像に平均化フィルタを適応(畳み込み演算)
# 画素毎に求めた閾値を配列に格納する
thresholds = image.conv(averaging_filter, precision: :float).to_a

求めた閾値を利用して、注目画素を2値化する

前段の処理で作成した閾値配列を利用して、注目画素の2値化を行います。 #new_from_arrayメソッドを利用して、2値画像用の配列からVips::Imageのオブジェクトを作成します。

width, height = input_image.size

(0...height).each do |y|
  (0...width).each do |x|
    mono_pixels[y][x] = input_image[y][x][0] < thresholds[y][x][0] ? 0 : 255
  end
end

# mono_image配列から、Vips::Imageのオブジェクトを作成する
Vips::Image.new_from_array mono_pixels

実装結果

下記の表に、OpenCV・ruby-vipsを利用して作成したモノクロ画像の例を示します。 ruby-vipsで実装した適応的閾値処理は、独自実装ですがOpenCVと遜色ない結果の画像を生成することができました。

Input ImageOutput Image(OpenCV)Output Image(ruby-vips)
f:id:nyagato_00_miya:20200727190552p:plainf:id:nyagato_00_miya:20200727190819p:plainf:id:nyagato_00_miya:20200727190920p:plain

※ 処方せんの内容が読み取れないように、画像はサイズを小さくしております。
※ モザイク処理はあとから追加したものです。

これで、無事薬局さんにきれいなFAXで処方せんを送信できるようになりました!

サーバーサイドに画像処理の機能を組み込んでみて

今回は、ruby-vipsを使った画像処理について紹介させていただきました。

僕がサーバーサイドエンジニアになる前は、前職で生産技術開発の領域で画像検査用のアプリケーション等を作ってました。
画像検査するときは、照明条件や撮影機器(光学フィルタ・レンズ・カメラ)を厳密に定めることができます。
例えば、接着剤の塗布量を検査する時は、光源にUV照明、特定の波長帯のみ通過する光学フィルタを利用するなど、後続の画像処理がやりやすい条件で撮影してました。

しかしながら、今回のFAX同時受信機能では、多種多様な画像が入力されるリアルワールドの画像処理です。
どのような画像が入力されても、適切に処理を行う必要があり、それが難しさでもあり面白さでした。

おわりに

最後までお付き合いいただきありがとうございました。画像処理やruby-vipsの使い方など、皆さまになにか得るものがありましたら幸いです。

今回はkakariにフォーカスした内容で解説させていただきましたが、メドピアはメインサービスである「MedPeer」を中心に、さまざまな医療領域をカバーするため新しい事業をつぎつぎと立ち上げています。

メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html


Rails未経験でRailsエンジニアとして入社して感じたメドピアのエンジニア文化

$
0
0

2020年6月付けで入社したフィッツプラス システム開発部の讃岐と申します。 DietPlus Proというアプリや、特定保健指導の進捗管理用のWebアプリケーションを開発するサーバーサイドエンジニアとして働いています。

特定保健指導については、以前に福本さんが使用している技術も含め書いてくださったのでそちらをご覧下さい。

tech.medpeer.co.jp

エンジニアとしてのキャリアはメドピアで2社目となります。Railsは未経験でしたがRailsエンジニアとしての採用でした。

そんなわけでメドピアでRailsエンジニアとしてのキャリアが始まったわけですが、開発環境については芝田さんが他の記事で紹介してくれているので、今回はメドピアのエンジニア文化を紹介していきたいと思います。

1. 新しいことを学ぶ機会が多い

メドピアには「Rails読書会」というRailsやエンジニアリングに関する書籍をwillnetさんと読み合わせできる会だったり、「PR振り返り会」という共有したいトピックがあるPRについてエンジニア間でそれぞれの意見を交換をする会が毎週あります。

そこで経験豊富なエンジニアの方々がチーム関係無しに参加されて、エンジニアリングについて様々な観点で議論されて、自分では得られなかった情報や、知らなかったトピックがぽんぽん出てくるのでかなり多くのことを学べます。

最近のRails読書会ではSRE サイトリライアビリティエンジニアリングを読み合わせていて、メドピアのSREの方の意見を直で聞くことができています。以前はSREというものはフワッとした理解だったのですが、SREの方の意見を直で聞くことで今SREがどのように事業に貢献していて、自分もその恩恵を受けているかをかなりリアルに感じることができています。

僕がエンジニアとしての経験がかなり浅いので得られるものが多いというのもあると思いますが、経験が浅いエンジニアでもそういった会に参加して、いろんなチームの経験豊富なエンジニアの方の意見を直接聞ける機会がある、というのはかなり嬉しいです。

毎週水曜日の決まった時間に上記の会があるので、頻度もいい感じに参加しやすい形になっていると思います。

2. 技術者支援の制度がちゃんと使用されている

メドピアには年間12万円までAWS・Azure・GCPなどのIaaSの使用料や技術書、資格取得などを補助してくれるテックサポートという制度があります。

前職でも一応資格取得費用などは補助してくれる制度とかはあったのですがだれも使ってない&フローがわかりにくかったので利用していませんでした。

メドピアはそういった制度が形骸化してだれにも使われない、ということはなく実際に使用されています。入社後にきちんと制度を利用する際のフローや参考ドキュメントなどを教えてもらえるので入社した方がスムーズに使えるからだと思います。

みなさんかなり有効活用されていて、各種有料IDEやHHKBなどのキーボード、各種書籍を買われている方が多い印象です。

おそらく社内で一番有効活用している先輩の利用額はこんな感じです。笑

f:id:sanuki_tech:20200908134314p:plain

この制度のおかげで高い技術書なんかも気兼ねなく買って勉強できるのでエンジニアが成長しやすい環境だと思います。僕も何冊か買って勉強しています。

また、メドピアで働くエンジニアは裁量労働制なので時間の都合が付けやすく、勉強会のためにすこし早く抜ける、ということなんかもできるのでかなり働きやすい環境でもあります。

こういった制度がちゃんと有効活用されていて学びやすさ、働きやすさとして会社から還元されているのはとても良い文化だと思っています。

テックサポートについては別の記事でも紹介されているので是非読んでみてください。

tech.medpeer.co.jp

3. 技術顧問がwillnetさん、Matzさん

「1. 知見を得られる機会が多い」でもお話させて頂きましたがwillnetさん、そしてMatzさんが技術顧問としてメドピアをサポートしてくれています。

willnetさんは毎週の勉強会やSlackのtimesチャンネルで参加してくださっていて、その知見で様々な疑問などを解決してくださっています。以下のような感じでtimesチャンネルに質問され、それに回答を頂けます。

f:id:sanuki_tech:20200908134339p:plain

そしてMatzさんは月一でWeb会議を開いて頂いているので、そこで毎月疑問などをぶつけられます。Matzさんに質問したい内容がまとめられているスプレッドシートがあってそこにずらっと質問が並べられています。

f:id:sanuki_tech:20200907194621p:plain

言語開発者に直で意見をぶつけられたりする機会なんて滅多にあるものではないと思っているので、この環境を作って下さった弊社CTOの福村さんには「Matz」の文字列をみる度に感謝しております。

(弊社CTOの福村さんとMatzさんがこの記事でMatzさんが技術顧問になる経緯などを対談しているので是非みてみてください。)

www.wantedly.com

こう言った著名な技術顧問の方がいるおかげで会社全体の技術力の底上げになっていると思います。 Railsの実装やRuby内部のことが気になった際にすぐに質問できるという環境はエンジニアにとってものすごく良い環境だと思っています。

4. コミュニティを大事にしている

RubyKaigiは毎年エンジニアの方が殆ど総出で行く恒例行事だそうです。 (残念ながら今年はオンライン開催なので現地には行けないですね。)

他にもいろんなコミュ二ティ、カンファレンスのスポンサーになっていたりもします。

以下はRubyKaigiのスポンサーになった記事です。 tech.medpeer.co.jp

以下は前述した弊社CTOの福村さんのRuby,Railsコミュニティとの向き合い方についてのスライドです。 speakerdeck.com

僕はこういったカンファレンスなんかにあまり参加する機会がなかった(単にぼっち参加にビビってた)のですが、メドピアで働くうちに自然と興味も湧いてきて今年はいろいろ参加してみる予定です。

他にも先輩がGotanda.rbのOrganizerだったり、銀座Railsに登壇してたり、社内でコミュニティを盛り上げようという雰囲気があるのでかなりコミュニティに貢献したいモチベーションが上がります。

かなり直近ですがRubyKaigi Takeout 2020のスポンサーにもなってました!

f:id:sanuki_tech:20200907195302p:plain

おわりに

メドピアでRailsエンジニアとしてのキャリアが始まったわけですがいままで紹介してきたメドピアのエンジニア文化のおかげで毎日様々なことを快適に学べています。

こういった文化を作ってきたメドピアのエンジニアの方に感謝しながら僕もよい文化作りに貢献していきたいと思います。


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html`

API認証基盤の改善について

$
0
0

今月の一日でメドピアに入社してちょうど1年になったCTO室の内藤(@naitoh) です。

主にやっていることはAPI認証基盤の改善です。 この1年でやってきた事を技術ブログで紹介させて頂きます。

背景

メドピア で採用されているバックエンドの言語(フレームワーク)は本 blog のタイトルにもあるように PHP から Rails に移行が行われているのですが、 実は上記以外に Golang(以下 Go) も使用しています。

このあたりの当時の開発背景は下記の記事に書かれておりますのでご参考にして頂ければと思います。

tech.medpeer.co.jp

私が昨年入社した時点でこのAPI認証基盤(API ゲートウェイ)の保守が難しく、Go のわかる開発者がほぼいなかったため機能追加が行えない状態になっていました。 このAPI認証基盤へのユーザーログイン処理時のアクセス負荷が朝方に集中し、メドピアのサービスに影響が出ないか懸念されていたため、まず安心して保守できる状態に持っていく事から取り組み始めました。

安心して保守できる状態に持っていくためにやった事

  • 開発環境でコンパイルできるようにする。
  • CIが停止していたので、動くようにする。
  • テストコードを追加する。
  • テストコードを使って動作を理解する。エラーメッセージを修正する。
  • 未使用のライブラリを削除する。
  • 社内の非公開リポジトリ(ライブラリ)の2重管理を排除する。

開発環境でコンパイルできるようにする。

2015年の開発当時は Go 1.5 を使っていたため、入社時点で開発環境として支給された macOS Mojave ではコンパイルしたバイナリを実行すると実行バイナリの応答が無い状態になり、30分程待っても何も起らず正しくコンパイルできてない事が判明しました。(本番への deploy 環境は Linux なので問題なし。)

Go のコンパイラのバージョンを変えいくつか試したところ macOS 10.12 Sierra 以降では go 1.7以上必須である事がわかったため、Go のバージョンを当時の最新の 1.13 まで上げ、かつ、struct 周りの記述ミスでコンパイルエラーが発生していたのを修正する事で開発環境のmacOS Mojaveで無事動作するようになりました。

CIが停止していたので、動くようにする。

2015年の開発当時は社内の Circle CI 1.0 (Enterprise) で CIが動作していたようなのですが、社内開発環境のクラウド移行時に、メンテナー不在の影響でクラウド環境のCircle CI 2.0 移行が行われずCIが停止していました。 Circle CI 1.0 当時の設定が残っていたので、Circle CI 2.0 で動作するように対応を行い、カバレッジ情報の出力を追加するなどCI環境を整備しました。

テストコードを追加する。

ある程度のテストコードは書かれていたのですが、メインロジック部分のテストコードが一部しか存在しなかったため、処理を理解するためテストコードのカバレッジを上げる事に取り組みました。 ただ、期待値となるドキュメントの場所がわからず、社内メンバから教えてもらった内容も記述が2015年のまま古い箇所が散見されたため、本当にこれが当時の情報なの?と恐る恐るテストコードを書きながら正常系のテストコードを書き上げました。(結果的に正しい情報でした。)

テストコードを使って動作を理解する。エラーメッセージを修正する。

エラー発生時のエラーコードが毎回同じ値を返したり、エラーメッセージが同じ箇所が複数あったり詳細エラーメッセージが出力されないバグがあった事が原因で、どこでエラーが発生したかソースコードを見ても判断ができず、クライアント側も最初の処理からリトライせざるを得ないなど利用者にわかりづらいAPIとなっていました。

そのため、異常系のテストコードを追加し、詳細エラーメッセージが正しく出力されるように修正、エラー時の挙動を理解することで、本来意図している内容にエラーメッセージの修正を行い合わせてエラーコードの整備など使い勝手の改善を行いました。

未使用のライブラリを削除する。

2015年の開発当時に組み込まれた未使用のライブラリがそのままになっており、(vendor 配下のリポジトリにはあったのですが)元のリポジトリが残っていないものもありました。 削除してテストが通る事を確認し、メンテナンス対象を整理しました。

社内の非公開リポジトリ(ライブラリ)の2重管理を排除する。

上記 vendor 配下の整理を行なったのですが、社内開発ライブラリ(非公開リポジトリで別管理)もvendor配下に登録されていたため2重管理になっており、両方のソースコードに修正が入るような状態になっていました。(glide で 管理は行われていたのですが、Go 1.5 で vendoring が始まったばかりで当時はこのように管理するようにしたようです。)

社内ライブラリは vendoring 不要なため vendor配下での 2重管理を廃止し、Circle CIで glide がうまく動作しなかったため(Go 1.13 では正式版ではなかったため Go Module への移行は見送り) dep に移行し、コンパイル時に dep ensureで社内ライブラリをvendor配下に展開する対処を行いました。

安心して保守できる状態に持っていくためにやった事のまとめ

これらの対処により約4ヶ月ほどでメンテナンスが可能な状態に改善、無事リリースする事ができました。 懸念となっていた朝のログイン時の負荷問題は不要なToken再発行処理である事が判明し、別途行われたクライアント側の改修作業で無事問題は解消されました。

現在は次のステップとしてAPI認証基盤がもっと活用されるようにする取り組みを行なっています。

現在の取り組み

上記対処を実施するまでAPI認証基盤の機能追加ができなかったためか、メドピアの各サービスはサービス単位に機能を作成する事が多く、各サービス間の連携が弱いという課題がありました。

また、メンテナンスが行える状態になったのですが、利用を広げるにあたり下記の課題がありました。

  • API開発をGoで行う必要があり、社内の Rails 開発者にとって敷居が高い。
  • JSON RPC API ゲートウェイ として実装されていたため一般的な REST APIではなく利用がしづらい。

これを解決するために、APIゲートウェイのバックエンドを Go ではなく Rails での実装を追加、新規にREST API のサポートを行なう事でこの課題を解消し、各サービス間の連携を強化する新APIの追加を行なっています。

f:id:ju-na:20201109163946p:plain
API Gateway

(API ゲートウェイそのものは負荷が集中する部分で、機能追加が少ない部分のため、Go での実装を継続しています。)

おわりに

以上のような施策を実施し、社内サービスのボトルネックを一つずつ改善していく取り組みを行なっています。

前職は組み込み系のテストプログラム開発を行なっていたため Go は触った事がなかったのですが、Go は言語仕様が比較的小さいため2週間程の勉強は必要でしたが無事改善が回せております。 開発分野が前職とはだいぶ異なるため新たに学ぶ事が多いですが、(Goに限らず)日々新しい事に取り組みつつ楽しく開発をしております。


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

メドピアのECSデプロイ方法の変遷

$
0
0

f:id:satoshitakumi:20201118153720p:plain

CTO室SREの侘美です。好きなLinuxディストリビューションはLinux Mintです。

メドピアでは現在多数のサービスを運用しており、そのほとんどがAmazon ECSを構成の中核として利用しています。

ECSに対してデプロイを行う方法としては、CodeDeploy、CodePipeline、Copilot(ecs-cli)等があり、CloudFormationやTerraform等のIaCツールで何をどこまで管理するかも合わせて検討する必要があります。 どの方法にもメリット・デメリットがあり、Twitterや技術ブログを観測している範囲ではデファクトスタンダードと呼べる方法は未だに無いように思われます。

メドピアで最初にECSを利用し始めたのは2018年ころであり、これまで試行錯誤しながらECSのデプロイ方法とタスク定義の管理方法を模索してきました。 今回はメドピア社内で試してきた、ECSへのデプロイ方法とその課題や解決方法をご紹介したいと思います。

前提

各デプロイ方法に共通する運用は次の通りです。

  • インフラはTerraformでコード化
  • RailsとTerraformでGitリポジトリは別
  • Railsはサービス担当のエンジニアが実装
    • SREが実装を変更する場合もある
  • Terraformは横断して複数のサービスを担当するSREが実装

1. ecs-cli

社内でECSを採用し始めた当初はecs-cliを利用していました。 現在は後継のCopilotというツールがリリースされています。

ecs-cliの公式ドキュメントでは次のように紹介されており、簡単にECSを構築することが売りのツールです。

ローカルの開発環境からクラスタやタスクの作成、更新、監視を簡素化するための高レベルのコマンドを提供します。

メドピアでは、ecs-cliを使い次のようなデプロイパイプラインを構築していました。

f:id:satoshitakumi:20201117180938p:plain
ecs-cliによるデプロイ

  • CodePipelineでデプロイパイプラインを構築
  • リリース対象ブランチの場合CircleCIからCodePipelineをスタート
  • CodeBuild上でCapistranoでイメージビルド
  • CodeBuild上でCapistrano + ecs-cliでデプロイ
  • 上記Capistranoのタスクやecs-cliで利用するタスク定義用のファイルはRails側リポジトリで管理

ecs-cliによるデプロイの課題

この構成でいくつかサービスを運用していくうちに以下の課題があがってきました。

  • ecs-cliの仕様で一部ECSの機能を利用できない。
    • ECSサービスを複数のTargetGroupに所属させることができない。(現在は複数のTargetGroupに対応しています)
  • タスク定義の管理がRails側リポジトリで行われている。
    • タスクのパラメータ変更等をSREが実装する際に、Rails側リポジトリにPRを作成しRails側リポジトリのリリースフローに則る必要がある。
    • →RailsエンジニアとSREの責任の境界が曖昧になってしまっていた。
  • ALBやECSタスクに付与するIAMロール名やSSMパラメータストアのパラメータ名など、Terraform側リポジトリで管理されているリソースのARN等をRails側リポジトリにハードコードしなければならない。

そこで、これらの課題を解決する構成として、次のCodePipelineによるECSへのデプロイへと移行することにしました。

2. CodePipeline

ecs-cliの利点はあくまでも「簡単にECSを管理できる」というものであり、Terraformを利用して細部まで管理を実施したい場合には不向きと判断しました。

ECSに関する設定の大半をRails側リポジトリで管理していた従来の構成を見直し、それぞれのリポジトリで管理する対象を次のように変更しました。

  • Rails側リポジトリ
    • Dockerfile
  • Terraform側リポジトリ
    • デプロイパイプラインと各CodeBuildで実行するスクリプト
    • ECSタスク定義

CodePipelineは通常CodeBuildやCodeDeployと組み合わせてデプロイパイプラインを構築するサービスですが、CodePipeline自体もECSへデプロイする機能を持っています。(よくCodeDeployを利用していると勘違いされがちです)

CodePipelineによるECSへのデプロイの特徴としては、CodeDeployよりも簡単な代わりに、機能はCodeDeployよりも少ないといった感じです。
参考:https://docs.aws.amazon.com/codepipeline/latest/userguide/action-reference-ECS.html

terraformの実装例

resource "aws_codepipeline" "deploy" {

  # 他の設定項目は省略

  stage {
    name = "Build"

    action {
      name             = "BuildRails"
      category         = "Build"
      owner            = "AWS"
      provider         = "CodeBuild"
      input_artifacts  = ["input"]
      version          = "1"
      output_artifacts = ["imagedefinitions"]

      configuration = {
        ProjectName = aws_codebuild_project.rails.name
      }
    }
  }

  stage {
    name = "Deploy"

    action {
      name            = "DeployApp"
      category        = "Deploy"
      owner           = "AWS"
      provider        = "ECS"
      input_artifacts = ["imagedefinitions"]
      version         = "1"

      configuration = {
        ClusterName = aws_ecs_cluster.app.name
        ServiceName = aws_ecs_service.app.name
        FileName    = "imagedefinitions.json"
      }
    }
  }
}

ビルドステージでは、イメージビルドを行いECRにイメージをpushし、 imagedefinitions.jsonというファイルを作成しデプロイパイプラインのアーティファクトに登録します。

デプロイステージでは上記の imagedefinitions.jsonを指定することで、既存タスク定義を流用しイメージのみを新しいイメージに変更したタスク定義のリビジョンが作成され、デプロイされます。

ecs-cliでのデプロイと同様の図で表すと以下のようになります。

f:id:satoshitakumi:20201117180949p:plain
CodePipelineによるデプロイ

このような実装にすることでシンプルにECSへデプロイを行うパイプラインを構築できます。

テクニック:TerraformとCodePipelineの両方からタスク定義を更新可能にする

CodePipelineでECSへのデプロイを行うと、ECSタスク定義に新しいリビジョンが追加されます。 このリビジョンは1つ前のリビジョンのタスク定義を、CodePipelineで指定した imagedefinitions.jsonに記載されたイメージで置き換えた設定となります。

# デプロイ前のタスク定義のリビジョン
{
  "family": "prd-rails",
  "cpu": "1024",
  "memory": "2048",
  "containerDefinitions": [
    {
      "name": "web",
      "image": "元のイメージのURL",
      "environments": [...略]
    }
  ]
}

# これに↓のimagedefitions.jsonを使ってCodePipelineでデプロイする

# imagedefinitions.json
{
  "name": "web",
  "imageUrl": "新しいイメージのURL"
}

# すると、imageUrl以外が同じである次のようなタスク定義のリビジョンが追加される

# デプロイ時に追加されるタスク定義のリビジョン
{
  "family": "prd-rails",
  "cpu": "1024",
  "memory": "2048",
  "containerDefinitions": [
    {
      "name": "web",
      "image": "新しいイメージのURL",
      "environments": [...略]
    }
  ]
}

このように、イメージ以外はデプロイ時に変更することができません。 そのため、CPUやメモリ、各コンテナの環境変数等はterraformで変更する必要があります。

terraformはtfstateに現在のリソースのARN等を記録し、コードとリソースの関係を保持しています。 そのため普通に実装してしまうと「デプロイするたびに新しいタスク定義のリビジョンが作成され、tfstateとAWS上のリソースに乖離が発生する」という問題を引き起こしてしまいます。

これを解決する実装方法が、terraformの公式ドキュメント中にかかれています。
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ecs_task_definition

resource "aws_ecs_service" "mongo" {
  name          = "mongo"
  cluster       = aws_ecs_cluster.foo.id
  desired_count = 2

  # Track the latest ACTIVE revision
  task_definition = "${aws_ecs_task_definition.mongo.family}:${max(aws_ecs_task_definition.mongo.revision, data.aws_ecs_task_definition.mongo.revision)}"
}

ECSサービスに指定するタスク定義を dataを用いて最新のタスク定義を参照することで、CodePipelineでデプロイ時に作成された最新のタスク定義を参照することができます。

また、terraform側でタスク定義のメモリ等の設定を変更した際は、terraform apply時に新たにタスク定義のリビジョンが作成されます。 dataで取得した存在する最新のリビジョンと、メモリの変更でこれから作成されるリビジョンをmax関数にわたすことで、以下を実現できます。

  • terraform側のタスク定義に変更なし
    • CodePipelineがデプロイしたリビジョンが最新なので、apply時はそのまま最新のリビジョンが使われる
  • terraform側でタスク定義の属性を変更
    • terraformがこれから作成するリビジョンが最新なので、作成されたリビジョンで置き換えられる

この実装を利用することで、メモリを変更し terraform applyを実行すれば、新しいタスク定義のリビジョンが作成され、それがECSサービスに指定されることで新しいタスクに入れ替わります。

CodePipelineによるデプロイの課題

ecs-cliを利用していた際の課題はだいぶ解決できたのですが、CodePipelineを利用したECSへのデプロイにも課題がありました。

CodePipelineによるECSへのデプロイはローリングアップデートで行われます。 Rails等のassetsファイルをハッシュ付きで生成し配信するWebアプリケーションの場合、ローリングアップデートを行うと、アップデート時に404エラーが確立で発生してしまいます。

f:id:satoshitakumi:20201117183217p:plain
ローリングアップデート時に404エラーが発生する仕組み

メドピア AWS勉強会 ECS編より

この課題を解決する方法はいくつかありますが、ロールバック方法まで含めて検討した結果、次のCodeDeployによるデプロイを採用することとなりました。(CodeDeploy以外の案に興味がある方は上記勉強会資料をご参照ください)

3. CodeDeploy

CodeDeployの ECSAllAtOnceデプロイ設定を利用することで、ローリングアップデートではなく、Blue/Greenデプロイメント方式でECSタスクを更新することができます。
参考:https://docs.aws.amazon.com/codedeploy/latest/userguide/deployment-configurations.html

また、CodeDeployのロールバック機能を利用すればシンプルな操作でECSタスクのロールバックを実施することも可能です。

CodeDeployを利用した場合のデプロイパイプラインは次のようになります。

f:id:satoshitakumi:20201117183711p:plain
CodeDeployによるデプロイ

ちなみに、CodeDeployによるECSへのデプロイの場合はデプロイ毎に完全なタスク定義の設定をCodeDeployにわたすので、前述したようなテクニックを利用してTerraformとデプロイによるタスク定義更新を考慮する必要はなくなります。
タスク定義はTerraformの aws_s3_bucket_objectリソースを使いTerraform側のリポジトリでテンプレートファイルとして管理しています。ビルドステージのCodeBuild上でS3からタスク定義のテンプレートを取得し、ビルドしたDockerイメージのURLを埋め込み、アーティファクトとして次のステージのCodeDeployへ伝える実装としています。

いろいろとデプロイ方法、タスク定義の管理方法を試してきましたが、結局はスタンダードなCodeDeployの利用に落ち着きました。

今後の改善予定

現状のデプロイにもいくつか課題があるため、まずは下記の点を改善していこうと計画しています。

SREとRailsエンジニアの責任境界の明確化

現状ではDockerfileをSREが用意し、CodeBuild上で実行するイメージビルドコマンドもSREがメンテしています。 DockerfileとDockerイメージビルドはRailsエンジニア管理に変更し、SREとRailsエンジニアの責任の境界を下記のように変更したいと考えています。

  • Railsエンジニア:Dockerイメージをビルドし、pushするまで
  • SRE:Docker Registryにpushされたイメージをデプロイする

PRのCIでイメージビルドを行いデプロイパイプラインを短縮

現在イメージビルドに約10分程度時間がかかっています。 デプロイパイプラインをさらに短縮するため、リリースPRの段階でイメージビルドをCI上で実行できないか検討しています。

これらの改善がある程度進んだ後には、また本ブログで内容を公開したいと思います。


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

フロントエンドのコードを書いている時に考えていること - まず状態から始めよ編

$
0
0

椅子に甘えないと心に決めて最近はスタンディングメインで仕事してます小宮山です。

実は私はペアプロ・モブプロ好き人間です。なぜ好きかというと、単にワイワイコードを書けるというのもありますが、何よりもそのときに考えていることをリアルタイムに共有できるからです。

メドピアのCTO室フロントエンドグループ(最近正式にグループ化されました)は、CTO室という何やら凄そうな名前の部署に所属している通り、メドピア社内のフロントエンド開発を幅広く支援するという役割を持っています。その一環としてペアプロ歓迎ムードを漂わせているわけです。

そして先日久しぶりにペアプロに誘われたのでほいほい承って参戦してみて、やはりペアプロという場はいいなと感じてこんな記事を書いています。

で、何をテーマにするかというとタイトルの通りです。おそらく近頃のフロントエンド開発に慣れた方なら特に意識しなくともそういう考えをしているのではと思うので、それほど目新しく斬新な考え方というわけではありません。

ただ世の中の開発者全員が全員、近頃のフロントエンド開発に慣れているわけでもないはずで、特に普段はバックエンドメインで片手間にフロントエンドも触るけどよく分からんという方もいるのではと思います。私は近頃のフロントエンド開発に慣れた側に立てているだろうということを最上の謙虚な心を持って認めると、残念ながら慣れていない方が感じている、何が分からないか分からないという気持ちを汲むのはなかなか難しかったりします。

なので役に立つのかは分からないけれども、もしかしたら誰かの役に立つかもしれないということで、どんなことを考えながらコードを書いているを紹介してみようというのが今回の趣旨です。

実装タスク

概要

  • 既にテーブル形式でデータ一覧を表示する機能がある
  • そのテーブルにて、行を選択できるようにしたい
  • 複数行選択を可能にしたい
  • 表示されている行全ての選択を切り替える全選択機能も欲しい

要するにこれをこうしたいというタスクです。選択して何をしたいんだということは気にせずいきます。

f:id:robokomy:20201008124711p:plain
Before 👉 After

1stステップ - 機能の境界を意識する

選択して何をしたいんだということは気にせずいきます。

さらっと書きましたが実はこれも考え方として重要かもしれないということで1stステップです。例えば大元の要件が「まとめて選択して削除したい」という場合、「まとめて選択して削除する」機能と考えるのではなく、「まとめて選択する」機能と「削除する」機能を分けて考えた方がいいケースがほとんどです。

せっかくなのでなぜかを説明すると、今後新たに「まとめて選択して移動したい」という要望が来ても対応しやすいからです。そしてフロントエンドという領域にDBはないので、「削除する」「移動する」という機能はそれぞれAPIに処理を委ねることになります。ではフロントエンド側に何が残るのかというと、「まとめて選択する」という機能と、「選択したものをAPIに投げる」という機能です。APIに投げるのは多少のインタフェース調整が必要かもしれませんが、実質ただの関数実行です。つまり「まとめて選択する」という機能さえ作れてしまえば、「まとめて選択して○○したい」という要望の大半は叶ったようなものです。

以上を踏まえて、選択して何をしたいんだということは気にせず、「まとめて選択する」という機能をこれから実装しようと一目散に考えます。

2ndステップ - 状態から考え始める

選択して何をしたいかは気にしませんが、「選択して何かをする」が控えていることを忘れてはいけません。もし本当に「まとめて選択する」という機能だけが欲しいのであれば、テーブルに<input type="checkbox />"をまぶした時点でもう実装は完了ということになります。

ここでふと思ったのですが、もしかしたらjQuery時代であればこれは正しかったのかもしれません。なぜなら「選択する」という機能はチェックボックスを設置するだけで実際に満たされるからです。そしてチェックボックスのDOMをそれぞれ取得して選択状態を調べてその後の「何かをする」に引き渡せば終了です。「全選択」機能はもう少し追加の実装が必要になるものの、まぁ適当にイベントハンドラを設定して適当な処理を適当に書けばおそらくなんとかなるでしょう。

当然ですが現在は(少なくとも観測範囲内では)jQuery時代ではないのでこういう考え方はしません。

「選択して何かをする」が控えている

スタート地点はここです。処理的にはゴール地点ですが設計的にはスタート地点です。選択した後に、その選択したという状態を必要とする後続処理が控えています。つまり、「選択したという状態」が欲しいわけです。

「選択したという状態」がある。ここが全ての基点となります。

何度でも言いますが「選択したという状態」が基点です。「選択する」という機能は二の次です。とりあえず機能を作り出すのではなく、真っ先に状態を定義します。

3rdステップ - 状態を形にする

基点となる状態が見えてきたのでそろそろ手を動かします。先に状態以外も含めた全体像を設計しきってしまうというのもありですが、ペアプロ想定ということで手を動かして抽象度を下げていきます。

「選択したという状態」を表現します。今回選択対象となるデータはそれぞれユニークキーidを持っていると想定します。特段難しく考えるまでもなく、選択したデータのidArrayObjectで持てば良さそうです。ぱっと見シンプルな気がするのでArrayでいきましょう。どちらでも大した違いはないのでお好みで。

let selectedIds:number[]=[]

主役が完成しました。結局のところ、後続の処理に回すために興味がある情報はこれだけです。

型が付いている方が視認性が良いと思うのでTSで書きます。JS原理主義者のみなさんごめんなさい、私はTSに屈しました。

ちなみですがパフォーマンスをシビアに求めるならObject方式がオススメです。

let selectedIds:{[key: string]: boolean}={}

そこまでのシビアさが求められるケースはあまりないのでお好みでどうぞ。あるいはAPIがどちらを採用しているかで判断するのがいいかもしれません。

4thステップ - 状態を変化させる

4rdと書きたい気持ちを抑えて4thステップです。

主役となる状態が作れたので、次はその状態に対してどんな操作をしたいか考えます。今回の操作はシンプルで、「選択する」と「選択を外す」です。2種類の操作ということですが、どちらの操作を行いたいかは状況によって変わります。状況とは、上で定義した状態のことです。

改めて説明するまでもなく、「選択済」なら「選択を外す」、そうでなければ「選択する」が実現したいことです。

function toggleSelected(selectedIds:number[], id: number): number[]{if(TODO 選択済){return selectedIds.filter(_id => _id !== id)}else{return[...selectedIds, id]}}

特定フレームワークに依存しない考え方がテーマなので、なるべく簡素に書いていきます。

「選択済」かを判定して「選択を切り替える」関数を作りました。ただどうやら関数を完成させるには、「選択済」かの判定が必要なようです。ではそれはどうすれば得られるのか。もちろん、主役である「選択したという状態」から求めることができます。

function isSelected(selectedIds:number[], id: number): boolean{return selectedIds.includes(id)}

「選択を切り替える」関数も完成させます。

function toggleSelected(selectedIds:number[], id: number): number[]{if(isSelected(selectedIds, id)){return selectedIds.filter(_id => _id !== id)}else{return[...selectedIds, id]}}

この時点で、「選択されたという状態」、「選択済」かの判定、「選択を切り替える」という操作が揃いました。なんだかもう選択機能が作れたような気がしてきませんか?気のせいではないです。個別の選択機能はもうこれで完成です。まだUIがないだけです。

5thステップ - 状態をUIと繋げる

選択するための状態とその状態を切り替えるための関数は既に作成しました。あとはそられをUIと連携させれば完成なのですが、この連携というのがなかなか厄介です。というのはかつての話で、今はそれほど厄介ではありません。

なぜ厄介ではなくなったかというと、それは昨今のフロントエンドに関わる方なら耳にタコができるくらい聞かされたであろう宣言的なUI構築を売りにした各種フレームワークのおかげです。宣言的というのが重要です。だからこそ先ほども状態ありきで処理を作っていました。そして状態に関する処理を既に作ってしまったので、あとはフレームワークのお作法に沿ってUIを組み立てていくだけです。細かく気を使う箇所は都度あるにせよ、状態さえ定まってしまえば、宣言的なUIの構築は最早シンプルな作業です。

というわけでここから先は単に今まで作ったものをフレームワークと合わせて組み立てていくだけなので、特筆すべきことはあまりありません。

というわけで終わりますと投げっ放しにする度胸もないので続きます。せっかくなので今回はみなさん大好きSvelteでいきます。

svelte.dev

先に完成形を張ってしまいます。公式サイトにさくっと触れる砂場を用意してくれているので、そこでも遊んでみてください。

<script>const dataList = [{ id: 1, name: 'メドピア1号'},{ id: 2, name: 'メドピア2号'},]let selectedIds = []  $: isAllSelected = selectedIds.length === dataList.lengthfunction toggleAllSelected(){if(isAllSelected){      selectedIds = []}else{      selectedIds = dataList.map(data => data.id)
}}</script><p>選択中: {selectedIds.join(', ')}
</p><table><thead><tr><th><inputtype="checkbox"checked={isAllSelected} on:change={toggleAllSelected} /></th><th>ID</th><th>Name</th></tr></thead><tbody>
    {#each dataList as { id, name }}
      <tr><td><inputtype="checkbox" bind:group={selectedIds}value={id} /></td><td>{id}</td><td>{name}</td></tr>
    {/each}
  </tbody></table>

悲しいお知らせがあります。まずはパッと見てもらえれば分かるように、さすがに砂場だとTypeScriptは非対応でした。心の目で型の補完をお願いいたします。

さらに悲しいお知らせが続きまして、なんと先ほど意気揚々と作成したisSelectedtoggleSelectedという関数は出番がありませんでした。

<inputtype="checkbox" bind:group={selectedIds}value={id} />

なんとこれだけでSvelteがチェックボックスと配列の双方向バイディングを完成させてくれました。選択判定やトグル処理は自作する必要すらなかったです。

せっかく書いたコードが不要になりましたが、無駄な時間を過ごしたと嘆く必要は全くありません。むしろその不要なコードを順を追って作ったからこそ、フレームワークが提供してくれる機能に対してより深い理解が得られるというわけです。たとえcommitログに何も残らなくともすべてを血肉に変えていきましょう。そしてペアプロ・モブプロに慣れてくるとcommitの量なんて無意味です。より大事なのはコード品質です。

もしSvelteにこんな便利な機能がなかった場合にどういう展開にしようとしてたかを一応解説しておきます。

<inputtype="checkbox"checked=選択されている? onchange=選択をトグルする />

正しいHTMLになっていませんがイメージとしてはこのような形です。選択状態の判定とトグル処理は既に用意したので、あとはこの形を各々のフレームワークでどう表現するか探すだけです。ここから先はフレームワークの表面的な作法や記法の問題です。

まとめ

かなり単純化してしまいましたが、コンポーネントの実装に取り組むときの流れは大概このような流れです。早く見た目を作りたい欲はぐっと抑え、とにかく真っ先に状態を設計して形に落とします。状態さえ定めてしまえば後はUI実装を思う存分楽しむだけです。

レビューだけではどうしても完成した後のコードを見るというケースが多く、こうした実装の考え方はなかなか共有が難しいものです。一方でペア・モブプロをしてみると、まさにこういった実装者の思考と共にコードを追っていくことができます。

ペア・モブプロが好きな理由もこのあたりだったりします。他人の作業風景を覗き見て取り入れることで自らの作業効率を改善できるという副次作用もあったりします。普段そういった機会があまりない方々も、ここまで読んでいただけた縁と思ってぜひとも周囲の方を誘ってペア・モブプロを試してみてください。

そしてなんと、メドピアではエンジニアを絶賛募集中です。集合知というキーワードの元にチーム開発を楽しみたい、そんな方は是非ともお声がけください。


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html`

Ruby3.0でのパターンマッチ機能の変更点

$
0
0

こんにちは、メドピアのサーバサイドエンジニアの草分です。

2020年12月25日、ついに待望のRuby3.0がリリースされましたね。

以前、Ruby2.7で発表されたパターンマッチについての記事を執筆したのですが、Ruby3.0になりいくつか追加/変更が入っています。 この記事ではそれらの変更点を確認していきます。

「Rubyのパターンマッチとは何ぞや?」という方は是非前回の記事も合わせてご覧ください。 tech.medpeer.co.jp

Ruby3.0を使うには

rbenvなど、主要なサードパーティツールが既にRuby3.0に対応しています。
それらを使ってインストールするのが簡単でしょう。

# rbenv
rbenv install 3.0.0
# rvm
rvm install ruby-3.0.0

Windowsの方は RubyInstaller for Windowsなどをお使いください。

rubyを実行しRUBY_VERSIONが3以降になっていれば準備完了です。

irb(main):001:0> RUBY_VERSION
=> "3.0.0"

1行パターンマッチ(experimental)

# version 3.0
{a: 0, b: 1} => {a:}
p a # => 0# version 2.7
{a: 0, b: 1} in {a:}
p a # => 0

Ruby2.7ではパターンマッチのinを用いて右代入のようなことができていましたが、Ruby3.0からは=>を用いるように再設計されました。

下記のように分割代入をさせることもできます。

attrs = { name: 'メドピア太郎', email: 'med@example.com' }

attrs => { name:, email: }
p name # => "メドピア太郎"
p email # => "med@example.com"

一方、inは true/false を返すようになりました。 条件判定として使えそうですね。

{ a: 0, b: 1 } in { a: } # => true
{ a: 0, b: 1 } in { c: } # => false

Find Pattern(experimental)

case ["a", 1, "b", "c", 2, "d", "e", "f", 3]
in [*pre, String => x, String => y, *post]
  p pre  #=> ["a", 1]
  p x    #=> "b"
  p y    #=> "c"
  p post #=> [2, "d", "e", "f", 3]end

*を指定することで、複数要素から要素数に関わらずマッチする部分のみ抽出できるようになるパターンも追加されました。

下記のように、Hashに対して条件指定と属性の抽出を同時に行う。といったこともできるようになります。

case [{name: "sato", age: 18}, {name: "tanaka", age: 15}, {name: "suzuki", age: 17}]
in [*, {name: "tanaka", age: age}, *]
  p age # => 15end

case/inが実験的(experimental)な機能ではなくなった

irb(main):001:0> RUBY_VERSION
=> "3.0.0"
irb(main):002:1* case0
irb(main):003:1* in a
irb(main):004:1*   puts a #=> 0
irb(main):005:0> end0
=> nil

パターンマッチを利用してもexperimentalであることの警告が発生しなくなりました。

ただし、3.0で追加された1行パターンマッチやFind Patternについてはexperimentalのままです。要注意。

irb(main):001:0> {b: 0, c: 1} => {b:}
(irb):1: warning: One-line pattern matching is experimental, and the behavior may change in future versions of Ruby!

 

irb(main):001:1* case ["a", 1, "b", "c", 2, "d", "e", "f", 3]
irb(main):002:1*   in [*pre, String => x, String => y, *post]
irb(main):003:1*   p x    #=> "b"
irb(main):004:0> end
(irb):2: warning: Find pattern is experimental, and the behavior may change in future versions of Ruby!
"b"

おわりに

Ruby3.0のリリースノートからパターンマッチに関する部分を抜粋して紹介いたしました。参考になれば幸いです。

パターンマッチ自体の概要については前回の記事にまとめております。 こちらも是非ご覧ください。


是非読者になってください


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

ECS を利用した検証環境の自動構築 ~運用3年を経て得た知見~

$
0
0

CTO 室 SRE kenzo0107です。

以前執筆した ECS を利用した検証環境の自動構築について、運用開始から3年の時を経ました。
実運用とその上で頂いた要望を取り入れ変化してきましたので、その経緯を綴ります。

tech.medpeer.co.jp

本稿、議論を重ね改善を進めて頂いたチームメンバーの知見を集めた元気玉ブログとなっております。

前提

社内では、以下の様に呼び分けしています。

  • 本番相当の検証環境を STG 環境
  • 本記事で説明する自動構築される仕組みを持つ環境を QA 環境*1

検証環境の自動構築の目的

開発した機能を開発担当者以外でも簡易的に確認できる様にし、以下を促進します。

  1. ディレクターと開発者の仕様齟齬を減らす
  2. 改善のサイクルを高速化する

当時の検証環境の自動構築の仕組み

当時の検証環境の自動構築の仕組み

大まかな流れ

① ブランチ qa/fooを push
② CircleCI 実行
③ CodePipeline 作成 or 実行
④ Rails イメージビルドし ECR へ push
⑤ TargetGroup, Listner を 既存 STG 環境 LB に追加
⑥ ECS Service 作成 or 更新を実行

ブランチ qa/fooに push すると ECS Service を作成・更新する、 という仕組みです。*2

最新の仕組み

大まかな流れ

① ブランチ qa/fooを push
② CircleCI 実行
③ CodePipeline 作成 or 実行
④ Rails イメージビルドし ECR へ push
⑤ DB 更新
⑥ TargetGroup, Listner を QA 環境用 LB に追加
⑦ ECS Service 作成 or 更新を実行

当時と最新の検証環境の自動構築の仕組みの違い

  • QA 環境用の LB を用意
  • CodeBuild 内で DB 更新
  • 既存 STG DB に QA 環境用 スキーマ作成

この様な仕組みへと変わった歴史を見ていきたいと思います。

構築当初の問題と解決の歴史

問題1: デプロイする度に DB データが初期化される

問題1. デプロイする度に DB データが初期化される

当初、 QA 環境は上図の構成を取っていました。

role (app, admin) 毎に別々にタスク内にDB コンテナを起動し、参照しています。

DB コンテナはデータを永続化していません。
デプロイ毎にデータが初期化されてしまいます 😢

もう一つ問題があります。

app と admin で共通の DB を見ていない為、app で更新したデータを admin で参照できません。

「検証環境」と言っておきながら、role 間 (app と admin 間) のデータの検証はできないんですね(笑)
と、妄想で闇落ち仕掛けましたが、次の方法で解決しました。

解決1: ブランチ毎に role 間で共通の DB スキーマを作る

解決1. ブランチ毎に role 間で共通の DB スキーマを作る

STG 環境 DB に ブランチ qa/fooに紐づくスキーマを作成し参照します。

これにより、ブランチ毎に DB を新規作成することなく、低コスト且つ、 既存 STG DB への影響も少なく目的が達成されました。*3

  • config/database.yml
default: &default
  ...
  url: <%= ENV['DATABASE_URL'] %>

staging:
  <<: *default
-  database: 'hoge_rails_staging'
+  database: <%= ENV['DB_NAME'] || 'hoge_rails_staging' %>
  • タスク定義
{
  "containerDefinitions": [
    {
      "name": "web",
      "environment": [
        {
          "name": "DB_NAME",
          "value": "qa-foo"
        },
      ],
      "secrets": [
        {
          "name": "DATABASE_URL",
          "valueFrom": "xxxxxxx"
        }
      ],
      ...

問題2: ECS Service に oneshot で db:migrate 等実行エラー発生後も処理が停止されない

 ECS Service に oneshot で db:migrate 等実行エラー発生後も処理が停止されない

当初、AWS 公式の ECS 特化ツールである ecs-cliを採用していました。

ecs-cli compose runで ECS Service でタスクを起動し db:migrate, db:seedを oneshot で実行していました。

タスク起動時のリソース不足等で db:migrateが失敗しても処理が停止されない問題がありました。

実行結果のログこそ取れています。

ですが、ログを grep してエラー判定するのは、全エラーパターンを把握しておらず、取りこぼしがある可能性があります。*4

以下の方法で解決しました。

解決2: db:migrate 等 DB 操作は CodeBuild から VPC 接続し実行する

解決2: db:migrate 等 DB 操作は CodeBuild から VPC 接続し実行する

db:migrate 等 DB 操作は ECS Service 上でなく CodeBuild 上で実施する様にしました。

不要となった ecs-cli を削除し、コードも見通しがよくなりました。*5

  • buildspec.yml
  build:
    commands:
      - >-
        docker run --rm
        -e RAILS_ENV=${rails_env}
        -e RAILS_MASTER_KEY=$RAILS_MASTER_KEY
        -e DATABASE_URL=$DATABASE_URL/$DB_NAME
        ${repository_url}:$IMAGE_TAG bin/rails db:prepare db:seed

DB は Private Subnet 上にあるかと思いますが、
その場合、 CodeBuild から VPC 接続し起動する必要があります。

resource "aws_codebuild_project" "web" {
  ...
  # NOTE: rails のイメージビルド後にこの CodeBuild から db:migrate を実行する
  # RDSへ接続できるようにVPC内でCodeBuildを実行する
  vpc_config {
    vpc_id             = aws_vpc.main.id
    subnets            = aws_subnet.codebuild.*.id
    security_group_ids = [aws_security_group.codebuild.id]
  }

CodeBuild を VPC 接続し起動した副産物

CodeBuild で Docker Hub からイメージを pull しています。
通常 CodeBuild は任意の IP で起動します。

その為、既に Docker Hub へ多数のリクエストをした IP を引いてしまうことがあります。

所謂、「CodeBuild IP ガチャ問題」です。

CodeBuild IP ガチャ問題

VPC 接続したことで Nat Gateway で出口 IP を固定し、使い回し IP を利用することがなくなる為、現状の利用頻度では、Docker Hub リクエスト制限を回避できました。*6

CodeBuild IP ガチャ問題 回避

問題3: デプロイ毎に Redis データが初期化される

当初、DB 同様、Redis もタスク内にあり、role 毎に分離した構成になっていました。

主に Redis は Sidekiq のキュー管理で利用しています。

問題3: デプロイ毎に Redis データが初期化される

Redis も DB と同様、 QA 環境毎に role 間で共通のデータを参照できる様、対応が必要です。

解決3: Redis も role 間で同じデータを参照しよう!

解決3-1. Redis DB 番号で分ける

まず Redis の DB 番号で STG と QA 環境で分けます。

Rails に渡す Redis URL は以下の様にします。

  • STG: rediss://stg-hoge.xxxxxx.apne1.cache.amazonaws.com:6379
  • QA: rediss://stg-hoge.xxxxxx.apne1.cache.amazonaws.com:6379/9

redis(s) と s が 1 つ多いのはスペルミスでなく、伝送中 (In-Transit) の暗号化を有効化している為です。*7

QA 環境は DB 番号 9 を指定しています。*8

解決3-1. Redis DB 番号で分ける

ただこれだけでは、 qa/foo, qa/barの QA 環境は同じ 9 を利用し、干渉します。

解決3-2. gem redis-namespace で QA 環境毎の干渉を回避

gem 'redis-namespace'を採用し、同じ DB 番号 9 内で namespace を指定し QA 環境毎に分離し、データを干渉しない様にしました。

QA 環境の Rails.env = staging です。

Sidekiq 側で QA 環境が起動する stagingに namespace の指定をします。

  • config/application.yml
staging:
  redis:
    :url: <%= ENV['REDIS_URL'] %>
    # NOTE: BRANCH = develop もしくは、 qa/xxx が設定される。
    namespace: <%= ENV.fetch('BRANCH', nil)&.gsub('/', '_') %>
  • config/sidekiq.rb
Sidekiq.configure_client do |config|
  config.redis = Settings.redis.to_h
end

参考: https://github.com/mperham/sidekiq/blob/4338695727d0bf16c9bf90d4170c55232bfc0957/lib/sidekiq/redis_connection.rb#L53-L69

問題4: QA 環境のタスクが増え過ぎて DB が Too many connetions エラー

QA環境がカジュアルに利用される様になり、起動するタスクが増え、 DB で Too many connections エラーが多発する様になりました。

問題4: QA 環境のタスクが増え過ぎて DB が Too many connetions エラー

解決4: QA 環境のタスクのみ RAILS_MAX_THREADS を抑える

QA 環境のタスクのみ RAILS_MAX_THREADS を抑え、DB コネクション数を抑えることで暫定対応としました。

極力 DB スペックアップによるコスト増を避けたい意図です。

何かと相乗せ相乗せでコスト削減してますが、 弊社も希望と予算の合意が取れればスペックアップするんですよ 💸 *9

問題5: QA 環境にはどこまでリソースが必要か?

メドピアの最新のデファクトとなりつつあるアーキテクチャ

メドピアの最新のデファクトとなりつつあるアーキテクチャは上記図の通りです。

CloudFront を前段に配置しています。 レスポンスの高速化と AWS Shieldの恩恵を受ける為です。

QA 環境はどこまで検証の為のリソースを用意する必要があるでしょうか?

RDS, ElastiCache は STG 環境に相載せしてきましたが、 CloudFront, ElasticsearchService, S3 等も用意すべきでしょうか?

解決5: 機能の検証に必要なリソースのみ用意する

CloudFront は ALB の様にポートによるルーティングができません。*10
その為、QA 環境で CloudFront を利用するにはブランチ qa/xxx毎に構築する必要があります。*11

ですが、
CloudFront を毎回構築するのは時間が掛かります。
直ちに検証できる状態にならないデメリットがあります。

その為、
「CloudFront の機能を前提とした機能の検証は QA 環境ではしない」
という合意の元、QA 環境では CloudFront を採用しませんでした。

CloudFront の機能を前提とした機能の検証は QA 環境ではしない

本番相当の検証は STG 環境で実施します。

まとめ

元々、必要になった経緯は、以下の依頼からでした。 検証環境の自動構築を作るきっかけとなった依頼

依頼を言葉通りに受けていたら terraform で環境をコピーしたものを用意して終わっていたかもしれません。

ですが、依頼者の言葉を翻訳すると
「任意のブランチのコードが動く STG 環境相当の環境作って!」
でした。*12

この翻訳に掛けた時間がとても貴重だったと、運用を3年経過して思います。

そして、何より記事を書かねば!と至ったのは、このアーキテクチャが今まさに更なるアップデートを遂げている最中だからです。

その内容がブログに執筆されることを期待して筆を置こうと思います。

ご清聴ありがとうございました。


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html`

*1:Quality Assurance の手助けになれば、と当時発足した QA チームとかけて QA としました。「QA 環境って何ですか?」と新入社員に聞かれることも多く、誤解招く命名だったなと思う。ごめんなさい。名前大事。運用を経て用途として「気軽に試せる場所」という意味合いが強くなってきたので sandbox に改名することを検討中

*2:ブランチ qa/xxx 削除時に Webhook → Lambda 関数で作成した QA 環境のリソースを削除しています。

*3:ブランチ qa/xxx 削除時に Webhook → Lambda 関数で作成した DB スキーマ削除しています。

*4:capistrano でラップしていた影響もあるかもしれません。未検証です。すいません。

*5:ecs-cli は CodeBuild にプリインストールされていない為、インストールするコードを書く必要があります。

*6:あくまで暫定対応ではありますが、現状の利用頻度では効果覿面でした。

*7:Redis クライアントに hiredis を利用している場合、SSL サポートが安定してない為、注意が必要です。 https://github.com/redis/hiredis-rb/issues/58

*8:"Q"A 環境だから 9 がしっくりきたのもありますが、 0-8 は STG 用、 9-16 は QA 環境用に使えるかな、という今後の運用の予備をとっておきたい意図から 9 にしています。

*9:DB の max_connection を調整したりと極力スペックアップを回避しつつ、どうしてもというときは勿論インスタンスクラスをアップします。

*10:QA 環境のエンドポイントの分離にポートを採用したのは、非エンジニアでもアクセスを容易にしたい為です。ヘッダー情報や Cookie 等でルーティングする方法が非エンジニアには難易度が高く回避した経緯があります。

*11:Lambda@Edge でゴニョゴニョすればできそう?!とも思いましたが、アイディア浮かばず

*12:ブランチ駆動にしたのはこの依頼のまま受け取ってます。PR 単位でなくブランチ駆動にした方が構築される QA 環境の数が少なくて済み、コストが抑えられる為です💸

メドピアで開催している社内読書会と社内読書分科会について

$
0
0

こんにちは。メドピアのお手伝いをしています @willnetです。花粉が厳しい季節みなさんいかがお過ごしでしょうか。僕は毎年レーザー治療したいな、と思っているのですが気づくともう春になっています。次回こそは…。

さて、メドピアでは毎週水曜日の11時から12時の間に社内読書会を開催しています。これは社内でなるべく多くの人が仕事上で使える知見を獲得することを目的としています。進め方は

  • 予習不要
  • 音読する
  • キリのいいところで止めて感想を話す

という形です。詳しい内容は以前個人のブログで書いたので興味のある人は読んでみてください。

しかし読書会を定期開催するようになってから4年がすぎ、また社内のエンジニアがどんどん増えてくると「読書会をもっと改善したいな」と思うことが増えてきました。

すでに読んでしまった本は題材として扱いづらい

例えばメタプログラミングRuby 第2版は良い本なので、当然この本を題材にした読書会は開催済みです。最近入社したエンジニアにも読んで欲しいのですが、読書会はすでに開催済みのため社内にすでに読了済みの人がたくさんいて、どうしても次の題材選定の際に優先順位が下がってしまいます。

音読しづらい書籍は題材として扱いづらい

日本語訳されていない洋書は、仮にそれが良書でも音読形式で進めづらいので対象外になってしまいます。

ニッチな内容の書籍は題材として扱いづらい

読書会の効果を最大限に高めるために、どうしても最大公約数的な書籍を選びがちです。そのため、RubyやRails全般に関わる書籍、かつジュニア向けの書籍以外を選定するのが難しくなっています。

そこで読書分科会ですよ

現行の読書会は維持しつつ、ここまで出てきた問題を解決するために読書会の分科会(以降分科会)を開催しています。これは通常の読書会とは形式を変えています。

  • 予習前提。予め決めておいた範囲を読んでおく
  • 30分で読んだ感想を話す

これまで取り扱ったお題は次の通りです*1

最大公約数的な題材は通常の読書会、それ以外の題材は分科会とすることで広い範囲をカバーできているように思えます。それぞれ書いたコードを比較し合う、といった読書に限らないことを実施できるのも良い点です。

難点としては、予習前提であることから敷居が高く、脱落率も通常の読書会よりも高いことがあげられます。業務が忙しく予習する時間が取れないことがあるので、なるべく一回の範囲を狭くして、一度サボっても復帰しやすいように工夫しています。

まとめ

メドピア社内で開催している読書会と、そこから派生した分科会について紹介しました。

「どうやってエンジニアのスキルを底上げしていくと良いのか」は難しい問題です。読書会についてももっと良いやり方を模索しています。「うちはこうやっているよ!」という事例があったら教えてもらえると嬉しいです。


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

*1:Working with Ruby ThreadsとWorking with TCP Socketsは現時点で購入する場所がなくなってしまっている模様です。悲しい


SREチームのセキュリティインシデントゲームデー

$
0
0

CTO室SREの侘美です。最近はM5Stackを嗜んでおります。

ここ半年ほど、MedPeerグループ全体のAWSのセキュリティ改善に力を入れてきました。 その中で、AWS Well-Architectedのセキュリティのベストプラクティスにも記載があるゲームデーを実施したところ、とても学びが多かったので本記事にまとめました。

ゲームデーとは?

Well-Architectedには以下のように記載されています。

ゲームデーを実施する

ゲームデーを実施する: さまざまな脅威について、インシデント対応イベントのシミュレーション (ゲームデー) を実施します。このゲームデーには、主要なスタッフや管理者を参加させてください。
教訓から学ぶ: ゲームデーの実行から得られた教訓は、プロセスを改善するためのフィードバックに含まれている必要があります。

出典: https://wa.aws.amazon.com/wat.question.SEC_10.ja.html

実施したゲームデーの概要

今回行ったゲームデーの概要は以下の通りです。

  • 擬似的に社内のいずれかのサービスでAWS上のインフラに関するセキュリティインシデントを発生させる。
  • どのサービスで発生するかは発生するまで不明とする。
  • セキュリティインシデント対応計画に則りSREチームで対応する。
  • ステージング環境を本番環境とみなしてゲームデーを行う。

今回は初回ということもあり、ゲームデーへの参加はSREのみとし、 私がインシデントを擬似的に発生させ、他のSREメンバー5名で対応にあたるシナリオとしました。

ゲームデーのシナリオ

より実際のセキュリティインシデントを再現するため、次のようなシナリオを事前に作成しました。 SREチームのメンバーへはゲームデー終了後に公開しました。


シナリオ全体の図

f:id:satoshitakumi:20210308135840p:plain
ゲームデーのシナリオ

インシデント

IAMアクセスキーの流出。
ある開発者がバグ調査のためにIAMユーザーを作成し、IAM認証情報を作成した。
Slack上の社外のゲストユーザーが参加しているチャンネルでAWSのアクセスキーIDとシークレットアクセスキーを投稿してしまった。*1
認証情報には、S3やECRやSystem Managerパラメータストアの権限が含まれていた。

インシデントの検知

CTOのTwitterのDMに以下のメッセージがあった体とする。*2

XXX社のYYYです。
御社の<サービス名>というサイトのユーザーのリストと思われる情報がWebに公開されていました。
現在は公開されていないようです。
名前とメールアドレスが載っているので、個人情報流出的に大丈夫ですか??

サイト: https://example.com/path/to/user-list

掲載されている情報(抜粋)

1,テスト太郎,taro.test@example.com
2,テスト次郎,jiro.test@example.com

掲載されている情報は、流出したS3上のCSVファイルの一部となっている。

攻撃者が取得した情報

攻撃者はSlackに投稿されたIAM認証情報を利用して、以下の操作を行った。 (実際に事前に認証情報を利用してこの操作を行う)

  • S3バケットからユーザー情報が記載されたCSVファイルをダウンロード
  • ECRへログインし、サービスで利用しているECS用のRuby on Railsアプリケーションのイメージを取得

本シナリオを利用したゲームデーで確認したい点

  • 初動でIAM認証情報を無効化できるか
  • 以下を調査し適切に報告へ含めることができるか
    • 流出したIAM認証情報
    • 流出した情報
    • ECR上のイメージ流出による影響
    • 流出した経路
  • セキュリティインシデント対応計画の内容は適切か

ゲームデー当日

当日は2時間と時間を区切ってインシデント対応にあたることにしました。

私はシナリオ作成者なので調査は見ているだけで参加しなかったのですが、インシデント検知→作業を分担して各種ログを調査→IAM認証情報特定→他の操作調査とスムーズに対応が進んでいました。

具体的な調査としては、CloudTrail、ALB、ECS、S3アクセスログなどを調査し、流出したIAM認証情報の特定やその認証情報で実行された操作等を特定していきました。
必要なログが取得されて無いといったことは発生しませんでした。

特に初動で流出したデータ内容から、RDS、S3バケット、管理画面などが流出経路である可能性があると判断し、分担して対応にあたった場面は想定以上にスムーズに実行できていたと思います。

最終的な報告では、流出経路を含めほとんど特定でき、その上で流出した情報からサービスの緊急停止を検討すべきか等の議論も行われており、十分対応できた結果となりました。

学び

ゲームデー終了後にSREチームで振り返りを行ったところ、多くの学びや改善点が見つかりました。
すべては記載しきれないので、一部を掲載させていただきます。

  • AWSやGoogle等が説いているインシデント対応のゲームデーの重要さについて、真の意味で理解できた。
  • S3アクセスログ、CloudTrail、Athenaなどを利用して調査を行ったが、習熟度が人それぞれだったため、得意不得意が出た。
    • ログを1箇所に集約し、Athenaを事前に設定しておくことで解消できそう。
  • CloudTrailでのS3オブジェクトレベルのアクセスログ記録が有効になっていないため、S3側で設定しているアクセスログも調査する必要があった。
    • コストとの兼ね合いもあるが、一箇所で検索できると調査がスムーズになる。
  • IAMユーザーの総数が少ないと流出したIAM認証情報の特定は速い。
    • IAMユーザーの棚卸しはやはり大事。
  • (一部のエンジニアにIAMのフル権限を与えているので)IAMユーザー作成検知の仕組みが欲しい。
    • ゲームデー後にCloudTrailのログを利用して通知する仕組みを実装した。
  • 初動の分担等、陣頭指揮は重要。
  • ECRログイン後のアクセスキーが変化するという仕様に気づかず、ECRからイメージをダウンロードしたログを特定できなかった。
  • Ruby on Railsで扱う秘匿情報の管理方法について見直す必要がある。
  • 報告がSlackで散発的に行われたので、最後に報告をまとめる際に苦労した。
    • テンプレートが用意されており、共同編集可能な環境にまとめていくのが良さそう。
  • 次は各サービスのRailsエンジニア等と協力して調査する部分もシミュレートすべき。

AWSのソリューションアーキテクトの方からのアドバイス

AWSのソリューションアーキテクトの方に今回実施したゲームデーの内容を共有したところ、以下のようなコメントをいただきました。
(SAさんに気軽に相談できる環境は本当にありがたい!)

  • 目的・手段が噛み合っているシナリオで良い
  • CloudTrailから影響を絞っていく対応も良かった
  • セキュリティ系インシデントであれば、エンジニア系職種以外の他職種・他チームも巻き込んでシミュレートすると、より実際の対応に近づく
  • セキュリティ系以外には可用性の障害を発生させるシナリオもよくある

次回

上記を踏まえて次回は以下の点を事前に準備・検討した上で再度ゲームデーを夏頃に開催する予定です。

  • 報告書のテンプレートの準備
  • ログ検索の整備
  • SRE以外のエンジニア、非エンジニア職も巻き込んむ

まとめ

セキュリティインシデントのゲームデーを実施することで、実際にやってみないと見えてこない課題などがいくつも見つかりました。
サービスを運用している会社では是非実施することをおすすめします!


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html


*1:GitHubで公開してしまったというよくあるパターンも検討したが、すぐに通知が飛んできて準備中に攻撃がバレそうなので今回は却下しました。

*2:ブログ用に一部内容を修正しています。

Amazon ECSで動かすRailsアプリのDockerfileとGitHub Actionsのビルド設定

$
0
0

CTO室SREの@sinsokuです。

Dockerイメージのビルドを高速化するため、試行錯誤して分かった知見などをまとめて紹介します。

AWSのインフラ構成

assetsもECSから配信し、CloudFrontで /assets/packsをキャッシュする構成になっています。

f:id:sinsoku:20210527182858p:plain
Rails on ECS

デプロイ時にassetsが404になる問題

以前の記事に詳細が書かれているため、ここでは問題の紹介だけしておきます。

Rails等のassetsファイルをハッシュ付きで生成し配信するWebアプリケーションの場合、ローリングアップデートを行うと、アップデート時に404エラーが確立で発生してしまいます。

引用: メドピアのECSデプロイ方法の変遷

Dockerfile

実際のDockerfileには業務上のコード、歴史的な残骸などが含まれていたので、綺麗なDockerfileを用意しました。

rails newした直後の最小限のRailsアプリで動作確認していますが、業務のRailsアプリではapkでインストールするパッケージなどは少し変える必要があるかもしれません。

# == baseFROM ruby:3.0.1-alpine3.13 AS base

WORKDIR /app
ENV RAILS_ENV production
ENV BUNDLE_DEPLOYMENT true
ENV BUNDLE_PATH vendor/bundle
ENV BUNDLE_WITHOUT development:test

RUN gem install bundler --no-document --version 2.2.16

# == builderFROM base AS builder

# Add packagesRUN apk update && apk add --no-cache --update \
      build-base \
      postgresql-dev \
      tzdata \
      git \
      yarn \
      shared-mime-info

# == bundleFROM builder AS bundle

# Install gemsCOPY Gemfile Gemfile.lock .
RUN bundle install \
      && rm -rf $BUNDLE_PATH/ruby/$RUBY_VERSION/cache/*

# == npmFROM builder AS npm

# Install npm packagesCOPY package.json yarn.lock .
RUN yarn install --production --frozen-lockfile \
      && yarn cache clean

# == assetsFROM builder AS assets

COPY . .

COPY --from=bundle /app/vendor/bundle /app/vendor/bundle
COPY --from=npm /app/node_modules node_modules

# Set a dummy value to avoid errors when building docker image.# refs: https://github.com/rails/rails/issues/32947RUN SECRET_KEY_BASE=dummy bin/rails assets:precompile \
      && rm -rf tmp/cache/*

# == mainFROM base AS main

# Add packagesRUN apk update && apk add --no-cache --update \
      postgresql-dev \
      tzdata \
      nodejs \
      shared-mime-info

COPY . .

# Copy files from each stagesCOPY --from=bundle /app/vendor/bundle /app/vendor/bundle
COPY --from=assets /app/public/assets public/assets
COPY --from=assets /app/public/packs public/packs

ARG SHA
ENV SHA ${SHA}
ENV PORT 3000
EXPOSE 3000

CMD bin/rails server --port $PORT

rails newをMacで実行した場合

MacでBundler 2.2.3以上を使っている場合、Gemfile.lockにPLATFORMSが入ってしまいDockerイメージのビルドに失敗してしまいます。

> [bundle 2/2] RUN bundle install       && rm -rf vendor/bundle/ruby/3.0.1/cache/*:
#12 0.953 Your bundle only supports platforms ["x86_64-darwin-20"] but your local platform
#12 0.953 is x86_64-linux-musl. Add the current platform to the lockfile with `bundle lock
#12 0.953 --add-platform x86_64-linux-musl` and try again.

ビルドできるようにするため x86_64-linuxを追加する必要があります。1

$ bundle lock --add-platform x86_64-linux

マルチステージビルド

このDockerfileには以下のステージが含まれています。

  • base: 全てのステージの親ステージ
  • builder: bundle,npm,assetsの親ステージ
  • bundle: bundle installを実行する
  • npm: yarn installを実行する
  • assets: assets:precompileを実行する
  • main: 本番環境で使うステージ

ステージの依存関係は以下の通りです。

bundle ─┬- assets ─ main
        │
   npm ─┘

bundle stage

bundle installの引数を使うと、mainステージでも bundle configをする必要がありDRYでは無いため、baseステージで環境変数を指定しています。2

ENV BUNDLE_DEPLOYMENT true
ENV BUNDLE_PATH vendor/bundle
ENV BUNDLE_WITHOUT development:test

bundle installを実行します。

RUN bundle install \
      && rm -rf $BUNDLE_PATH/ruby/$RUBY_VERSION/cache/*

npm stage

特筆すべき点は特にないです。 yarn installを実行します。

RUN yarn install --production --frozen-lockfile \
      && yarn cache clean

assets stage

RAILS_ENV=productionを設定しているので、 assets:precompileを実行するときに SECRET_KEY_BASEが存在しないというエラーが起きてしまいます。
Railsのissueを参考にSECRET_KEY_BASE=dummyを指定すれば回避できます。3

RUN SECRET_KEY_BASE=dummy bin/rails assets:precompile \
      && rm -rf tmp/cache/*

main stage

rails serverの起動に必要なパッケージをインストールしたり、ソースコードや各ステージの成果物をコピーします。

assets:precompileでassetsファイルは結合されているため、 node_modulesはサーバ起動時に不要です。

RUN apk update && apk add --no-cache --update \
      postgresql-dev \
      tzdata \
      nodejs \
      shared-mime-info

COPY . .

# Copy files from each stagesCOPY --from=bundle /app/vendor/bundle /app/vendor/bundle
COPY --from=assets /app/public/assets public/assets
COPY --from=assets /app/public/packs public/packs

環境変数の定義位置には注意が必要です。 GitのSHAのように毎回変わる値をイメージに埋め込む場合、Dockerfileの下部に記載しておかないとレイヤーキャッシュが効かなくなってしまいます。

ARG SHA
ENV SHA ${SHA}

最後にポート番号とサーバの起動コマンドを設定します。

ENV PORT 3000
EXPOSE 3000

CMD bin/rails server --port $PORT

GitHub Actions

DockerイメージをビルドするGitHub ActionsのYAMLを紹介します。

name: build

on:pull_request:branches:- master
  push:paths:- '.github/workflows/build.yml'- 'Dockerfile'env:ECR_REPOSITORY: example
  HEAD_SHA: ${{ (github.event_name == 'pull_request'&& github.event.pull_request.head.sha) || github.sha }}

jobs:build:runs-on: ubuntu-latest
    timeout-minutes:10steps:- uses: actions/checkout@v2
      with:ref: ${{ env.HEAD_SHA }}

    - name: Configure AWS Credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ap-northeast-1

    - name: Login to Amazon ECR
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v1

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v1

    - name: Cache Docker layers
      uses: actions/cache@v2
      with:path: /tmp/.buildx-cache
        key: ${{ runner.os }}-buildx-${{ env.HEAD_SHA }}
        restore-keys: |
          ${{ runner.os }}-buildx-

    - name: Build bundle stage
      uses: docker/build-push-action@v2
      with:context: .
        target: bundle
        cache-from: type=local,src=/tmp/.buildx-cache/bundle
        cache-to: type=local,dest=/tmp/.buildx-cache-new/bundle

    - name: Build npm stage
      uses: docker/build-push-action@v2
      with:context: .
        target: npm
        cache-from: type=local,src=/tmp/.buildx-cache/npm
        cache-to: type=local,dest=/tmp/.buildx-cache-new/npm

    - name: Build and push
      uses: docker/build-push-action@v2
      with:context: .
        build-args: |
          SHA=${{ env.HEAD_SHA }}
        tags: |
          ${{ format('{0}/{1}:{2}', steps.login-ecr.outputs.registry, env.ECR_REPOSITORY, env.HEAD_SHA) }}
          ${{ format('{0}/{1}:{2}', steps.login-ecr.outputs.registry, env.ECR_REPOSITORY, 'latest') }}
        push: ${{ github.event_name == 'pull_request' }}
        cache-from: |
          type=local,src=/tmp/.buildx-cache-new/bundle
          type=local,src=/tmp/.buildx-cache-new/npm
        cache-to: type=inline

    # Temp fix # https://github.com/docker/build-push-action/issues/252 # https://github.com/moby/buildkit/issues/1896- run: |
        rm -rf /tmp/.buildx-cache
        mv /tmp/.buildx-cache-new /tmp/.buildx-cache

キャッシュ

docker/build-push-actionのCacheのページを参考に、GitHub cacheを使ってbundle,npmのステージをキャッシュします。4

エクスポートするのはbundle, npmのステージになるため、各ステージでもキャッシュを消してイメージサイズを小さく保つようにします。

マージした時のみpush

build-push-action ではレジストリへのプッシュを制御できるので、「git pushではレジストリにプッシュしないで、Actionsでビルド結果を確認してからプルリクを作る」という運用が簡単に実現できます。

- name: Build and push
  uses: docker/build-push-action@v2
  with:context: .
    build-args: |
      SHA=${{ env.HEAD_SHA }}
    tags: |
      ${{ format('{0}/{1}:{2}', steps.login-ecr.outputs.registry, env.ECR_REPOSITORY, env.HEAD_SHA) }}
      ${{ format('{0}/{1}:{2}', steps.login-ecr.outputs.registry, env.ECR_REPOSITORY, 'latest') }}
    push: ${{ github.event_name == 'pull_request' }}
    cache-from: |
      type=local,src=/tmp/.buildx-cache-new/bundle
      type=local,src=/tmp/.buildx-cache-new/npm
    cache-to: type=inline

改善の効果

MedPeerのリポジトリで改善したときは他に CodeBuild -> GitHub Actions化なども実施しているため、厳密な計測結果ではありませんが10%程度の高速化を実現できました。

  • 改善前: 🐢10〜11分程度(CodeBuild)
  • 改善後: 🚀8〜9分(Dockerfileの改善 + GitHub Actions化)

まとめ

Dockerイメージのビルドを高速化する知見をブログにまとめてみました。 これらの内容が何か参考になれば幸いです。


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html



  1. Bundler 2.2.3+ and deployment of Ruby appsの記事を参考にしました。

  2. bundle configで使える環境変数を調べられる。

  3. https://github.com/rails/rails/issues/32947

  4. GitHubのキャッシュは5GBまでしか使えないため、Registry cacheを使う方が良いケースもあります。

1枚岩なMPAプロダクトでWebpackのマルチエントリーをさらにグルーピングしてビルドする

$
0
0

Noita世界の理不尽をこの身をもって体験した末にバウンドルミナスで全てを切り刻んでクリアしました、フロントエンドグループの小宮山です。

以前からこれできたらいいのになぁと思いながら無理そうと諦めていた掲題の事柄を実現できた嬉しさの勢いのままに書き始めています。

状況

1枚岩なMPAプロダクトがどういうものかというと、

  1. ルーティングをRails側で管理するMPA(複数エントリーポイント)
  2. 異なる種別のユーザー向けシステムが複数内包されている

という構成です。

ルーティングについては要するにSPAではなく、ページ毎のhtmlファイルとmain.jsがあるということです。

異なる種別というのは、要するにユーザー向け画面と管理者向け画面が分かれているような状況です。場合によっては3種類、4種類以上の異なるシステムが内包されたりもします。BtoBtoCなサービスだったりする場合ですね。

課題感

このような状況で普通にWebpack設定を組み上げると、もちろん全てのフロントエンドアセットをひっくるめて同じ設定でビルドすることになります。幸いWebpackはマルチエントリーなビルドにも対応しているのでビルド自体に難しさはありません。

一方で、異なる種別のユーザー向けのフロントエンドアセットを一緒くたにビルドするとやや困ったことが起きてきます。

特に気になっていたのが、splitChunksによる複数画面利用モジュールの切り出しに関することです。
SPAと異なりMPAの場合は画面遷移毎にscriptファイルのロードが行われるため、例えばVue.jsなど、ほぼ全画面で利用するようなモジュールは個別のエントリーファイルに入れてしまうとパフォーマンスの低下が懸念されます。
そこでsplitChunksをいい感じに設定していい感じに切り出すわけですが(いい感じの切り出し方は無限に議論があるので今回は触れません)、困ったことにこの切り出しが全ユーザー種別を跨って行われてしまいます。

f:id:robokomy:20210528183805g:plain
従来のビルドイメージ

シンプルな例を挙げると、管理画面でしか使わない重厚なリッチエディタ用モジュールが、splitChunksの対象となることでそれを全く必要としないユーザー向け画面でも取得対象に含まれてしまったような状況です。
node_modules配下を丸ごとvendor.jsに切り出すような設定をしているとあるある状況だと思われます。

妥協案

Webpackビルドをユーザー種別ごと別々に行えば当然ですが上記のような問題は起きません。しかしこれはこれで様々な面倒事が付きまといます。

ルーティングをバックエンドで制御している場合、フロントエンドアセットはmanifest.jsonで管理することが多いと思われます。
Webpackビルドを分けた場合、当然このmanifest.jsonも複数種類出力されることになります。つまりバックエンドも複数のmanifest.jsonを読み分けるような処理をしないといけません。既にやりたくありません。

複数のWebpackビルドを実行しなければいけない点も見逃せません。複数のWebpackビルドを--watchモードで動かしながら開発することに喜びを見出す会の皆様には申し訳ないですが、複数のWebpackビルドを--watchモードで動かしながら開発することに喜びを見出すことは私のような一般フロントエンドエンジニアには不可能でした。

このような様々な不便が付きまとうことから、多少のパフォーマンス悪化には目をつぶってまるごと単一のWebpackビルドで片付けてしまっていたのが現状でした。

光明

1シーズンに1回くらいの頻度でこの課題感と無理感の再発見を繰り返してきていて、今シーズンも再発見に勤しもうと思ったら実はなんとかなりそうなピースが揃っていることに気がつきました。

ピースその1

実はWebpackは複数の設定を配列で持つことができます。

webpack.js.org

リンク先にもある通り、こういう書き方ができます。

module.exports = [
  {
    output: {
      filename: './dist-amd.js',
      libraryTarget: 'amd',
    },
    name: 'amd',
    entry: './app.js',
    mode: 'production',
  },
  {
    output: {
      filename: './dist-commonjs.js',
      libraryTarget: 'commonjs',
    },
    name: 'commonjs',
    entry: './app.js',
    mode: 'production',
  },
];

このビルドを実行すると、2つの設定に基づいたビルドを同時(内部的には順次かもしれません)に走らせてくれます。

サンプルコードのように用途別に生成物を分けたり、ユニバーサルJSなプロダクトでサーバーとクライアント環境それぞれをビルドしたいような場面で活躍しそうです。

ユーザー種別ごとに異なる設定を用意したいという場面も状況は同じなので、きっとそのまま適用できるでしょう。
「複数のWebpackビルドを実行しなければいけない」という問題はこれでなんとかなりそうです。

If you pass a name to --config-name flag, webpack will only build that specific configuration.

なんと名前を付けておくと特定の設定でだけビルドすることもできるようです。admin系画面が不要な開発中はビルド対象から外して高速化するといった使い方もできそうです。

ピースその2

Webpackプラグインとしてmanifest.jsonをいい感じに生成してくれるのがwebpack-assets-manifestで、このプラグインにはmergeというオプションがあります。

github.com

このmergeオプションを有効にするとその名の通り、同名のmanifest.jsonファイルが既に存在している場合、そこに追記する形で新たなmanifest.jsonを生成してくれるようになります。

前回生成したmanifest.jsonを一部引き継ぐなんて絶対面倒な何かを引き起こす厄介な機能じゃないと勘ぐりたくなりますが、実はこのオプションが欲しい状況が存在します。

今回なんとかしたかった状況が正しくそれでした。ユーザー種別ごとに異なる設定でWebpackビルドを行いつつ、最終的なmanifest.jsonは1つに統合することが可能となります。

マルチエントリーグルーピングビルド設定

以上のピースを当てはめるとこのようなWebpack設定を組み上げることが可能です。

app/javascript/packs配下にエントリーファイルが設置されるとして、さらに顧客用画面はcustomers、管理用画面はadminでネームスペースを切っているような例です。

- app
  - javascript
    - packs
      - entry.js
      - customers
        - entry.js
      - admin
        - entry.js

gist.github.com

そして生成されるmanifest.jsonはこのようになります。個別のエントリーファイルは以前同様に生成されつつ、splitChunksしたvendor系ファイルはネームスペース毎の個別で生成してくれています。

{/**/"entry": "/packs/entry.abc-hash.js",
  "customers/entry": "/packs/customers/entry1.abc-hash.js",
  "admin/entry": "/packs/admin/entry1.abc-hash.js",
  "vendor-root.js": "/packs/vendor-root.abc-hash.js",
  "vendor-customer.js": "/packs/vendor-customers.abc-hash.js",
  "vendor-admin.js": "/packs/vendor-admin.abc-hash.js",}

f:id:robokomy:20210528183900g:plain
改善後のビルドイメージ

注意点として、splitChunksしたファイルを実際に読み込むhtmlファイルはネームスペース毎に別で用意する必要があります。
とはいえ全ユーザー種別で単一のlayoutファイルを使い回すことは稀で、それぞれ専用のlayoutファイルを用意するケースがほとんどだと思われます。manifest.jsonをhelperで読み分けるような面倒さに比べたらきっと些細なことです。

おわり

異なるユーザー種別向けの設定を別で用意しつつ、ビルド自体は統合されているようにふるまわせたいという贅沢な願いはこうして無事に叶えることができました。 Webpackを素で触れる環境はやはりよいものです。皆様も素敵なWebpackライフを。


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html


入門 GitHub Actions

$
0
0

CTO室SREの @sinsokuです。

社内のGitHub ActionsのYAMLが複雑になってきたので、私が参考にしてる情報や注意点、イディオムなどをまとめておきます。

頻繁に参照するページ

新しい機能の説明が日本語ページに反映されていないため、基本的に英語ページを読むことを推奨。

よく使うaction

actions/checkout

イベントによってはデフォルトブランチをチェックアウトするため、 ワークフローをトリガーするイベントのページで GITHUB_SHAを確認する必要がある。

例えば pull_requestイベントの GITHUB_SHAはデフォルトブランチとのマージコミットになるため、ブランチのHEADを使う場合は以下のような指定が必要です。

- uses: actions/checkout@v2
  with:ref: ${{ github.event.pull_request.head.sha }}

actions/github-script

簡単なAPIの実行であれば、これで事足りる。

例えば、Issueコメントにリアクションをつけるコードは下記の通り。

name: reaction

on:issue_comment:types:[created]jobs:- name: Create a reaction
    uses: actions/github-script@v3
    with:script: |
        await github.reactions.createForIssueComment({
          owner: context.repo.owner,
          repo: context.repo.repo,
          comment_id: context.payload.comment.id,
          content:"+1",
        });

ワークフローを書く時に注意すること

文字列は一重引用符

RubyやJavaScriptを書いていると間違いやすいので注意。

"foo"はエラーになるので 'foo'にします。

タイムアウトの指定

Actionsは実行時間で課金されるため、意図しない長時間の実行を防ぐために基本的に設定しておく方が良い。

timeout-minutes:5

並列実行数の制御

ワークフローを無駄に実行しないように、基本的に設定しておく方が良いです。

ただ、 github.refだけ指定すると他ワークフローを意図せず止めてしまうことがあるため、 github.workflowを接頭辞につけておいた方が安全です。

concurrency:group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress:true

GITHUB_TOKENを使うと新しいActionは起動しない

意図せず再帰的にActionが起動するのを防ぐためですが、知らないとハマります。1

  1. Approveされたプルリクを自動マージ
  2. マージされた後に自動デプロイ

例えば上記のように2つのActionを作っても、2つ目のActionは起動しないです。

これを解決するには GITHUB_TOKENの代わりに personal access token を使う必要があります。

envは stepsの中でしか使えない

以下のコードは Unrecognized named-value: 'env'でエラーになります。

env:FOO: foo

jobs:run:runs-on: ubuntu-latest
    timeout-minutes:5env:BAR: ${{ env.FOO }}-bar

    steps:- run: echo ${{ env.BAR }}

同様に matrixの中でも envは使えないです。

success()はifでしか使えない

以下のコードは Unrecognized function: 'success'でエラーになります。

- name: Notify finish deploy to Rollbar
  uses: rollbar/github-deploy-action@2.1.1
  with:environment:'production'version: ${{ github.sha }}
    local_username: ${{ github.actor }}
    status: (success() &&'succeeded') || 'failed'

if条件は式構文 ${{ }}を省略できるケースがある

ドキュメントに記載されてはいるが、Web上の事例ではあまり書いてないので紹介する。

式に演算子が含まれていない場合は ${{ }}を省略できます。

if: always()

ただ、 ${{ }}をつけても特に問題はないため、常に ${{ }}で囲んでおいた方が良いかも。

outputs のデフォルト値

ドキュメントに記載されていないですが nullになります。

例えば、デプロイ処理の準備中にワークフローをキャンセルされることもあるため、以下のように if:でoutputsをチェックしておく必要があります。

deploy:outputs:deployment-id: ${{ steps.deploy.outputs.deployment-id }}
  steps:- name: Prepare for deployment
    run: echo "do something"- name: Deploy
    id: deploy
    run: echo "::set-output name=deployment-id::1"rollback:needs:[deploy]if: cancelled() && needs.deploy.outputs.deployment-id

イディオム

三項演算子

三項演算子と同等のことは以下の書き方で実現できます。

env:RAILS_ENV: ${{ (github.ref == 'refs/heads/main'&&'production') || 'staging' }}

ArrayとObjectの生成

リテラルの記法が存在しないため、 fromJSONを使う必要があります。

env:is_target: ${{ contains(fromJSON('["success","failure","error"]'), github.event.deployment_status.state) }}
  rollbar_status: ${{ fromJSON('{"success":"succeeded","failure":"failed","error":"failed"}')[github.event.deployment_status.state] }}

その他

Dependabotを設定する2

DependabotはGitHub Actionsに対応しているので、設定しておくと便利です。

version:2updates:- package-ecosystem:"github-actions"directory:"/"schedule:interval:"daily"

採用のリンク


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html



  1. GITHUB_TOKENの詳細な権限は Authentication in a workflowを参照してください。

  2. Keeping your actions up to date with Dependabot

【令和最新(当時)】メドピア開発からVueの プログレッシブを俯瞰する

$
0
0

CTO室の小宮山です。肉体系Youtuber巡りが最近の趣味です。

2020年6月5日に開催された令和最新(当時)なこちらのイベントにて、登壇者として発表させていただきました。

techplay.jp

そして発表資料は体裁を整えてこのブログにて大々的にドカンと投稿しようと考えていたのですが、そのまま忘れ去って気づけば早1年が経過しておりました。時の流れとは残酷なものです。

内容自体は色あせにくいものであり、このまま埋もれさせてしまうのももったいないと感じたため、そのまま投稿してお披露目とさせていただきたいと思います。

埋め込みスライドだと左右が見切れてしまうようなので、スライドのリンクを貼っておきます。

https://speakerdeck.com/tomoyakomiyama123/medopiakai-fa-karavuefalse-puroguretusibuwofu-kan-suru

一応埋め込みもここに。

speakerdeck.com

以上です、どうぞよろしくお願いいたします。


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html


Viewing all 210 articles
Browse latest View live