メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックカンパニーのエンジニアブログです。読者に有用な情報発信ができるよう心がけたいので応援のほどよろしくお願いします。

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

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 環境の数が少なくて済み、コストが抑えられる為です💸

Ruby3.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 # => 15
end

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

irb(main):001:0> RUBY_VERSION
=> "3.0.0"
irb(main):002:1* case 0
irb(main):003:1* in a
irb(main):004:1*   puts a #=> 0
irb(main):005:0> end
0
=> 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

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

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

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

メドピアの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.length
  
  function toggleAllSelected() {
    if (isAllSelected) {
      selectedIds = []
    } else {
      selectedIds = dataList.map(data => data.id)
    }
  }
</script>

<p>
  選択中: {selectedIds.join(', ')}
</p>

<table>
  <thead>
    <tr>
      <th><input type="checkbox" checked={isAllSelected} on:change={toggleAllSelected} /></th>
      <th>ID</th>
      <th>Name</th>
    </tr>
  </thead>
  <tbody>
    {#each dataList as { id, name }}
      <tr>
        <td><input type="checkbox" bind:group={selectedIds} value={id} /></td>
        <td>{id}</td>
        <td>{name}</td>
      </tr>
    {/each}
  </tbody>
</table>

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

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

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

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

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

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

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

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

まとめ

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

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

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

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


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

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

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

■開発環境はこちら

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

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

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

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

今月の一日でメドピアに入社してちょうど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

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

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`

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

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

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 Image Output Image
f:id:nyagato_00_miya:20200727190552p:plain f: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 Image Mono Image(Otsu) Mono Image(adaptiveThreshold)
f:id:nyagato_00_miya:20200727190552p:plain f:id:nyagato_00_miya:20200727190644p:plain f: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 Image Output Image(OpenCV) Output Image(ruby-vips)
f:id:nyagato_00_miya:20200727190552p:plain f:id:nyagato_00_miya:20200727190819p:plain f: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