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

AWS + ngx_mruby で SSL 証明書の動的読み込みシステム構築

$
0
0

CTO室SREの @kenzo0107です。

2021年6月24日に「 kakari for Clinic ホームページ制作」がリリースされました。

f:id:kenzo0107:20210720154151p:plain
kakari for Clinic ホームページ制作

今回は上記サービスで採用した、
AWS + ngx_mruby で構築した SSL 証明書の動的読み込みシステムについてです。

SSL 証明書を動的に読み込みする理由

kakari for Clinic ホームページ制作の1機能で、制作したホームページに独自ドメインを設定する機能がある為です。*1

f:id:kenzo0107:20210728135315p:plain

複数ドメインでアクセスできる =複数ドメインの SSL 証明書を読み込む
を実現する必要があります。

動的に SSL 証明書を読み込むには?

以下いずれかのモジュールを組み込むことで SSL 証明書の動的読み込みが可能になります。

以下理由から ngx_mruby を採用しました。

  • 弊社は Ruby エンジニアの割合が高い!
  • 技術顧問 Matz さんに相談できる!*2

ngx_mruby での SSL 証明書動的読み込み 実装 参考資料

論文「高集積マルチテナントWebサーバの大規模証明書管理」を参考にさせていただきました。

p4 の「図3 動的なサーバ証明書読み込みの設定例 (KVS ベース)」を見ると実装概要がわかりやすいです。

server {
    listen 443 ssl;
    server_name _;
    ssl_certificate /path/to/dummy.crt;
    ssl_certificate_key /path/to/dummy.key;

    mruby_ssl_handshake_handler_code ’
        ssl = Nginx::SSL.new
        host = ssl.servername
        redis = Redis.new "127.0.0.1", 6379
        crt, key = redis.hmget host, "crt", "key"
        ssl.certificate_data = redis["#{host}.crt"]
        ssl.certificate_key_data = redis["#{host}.key"]
    ’;
}

通常、 Nginx の ssl_certificate, ssl_certificate_keyに変数を利用できません。 ngx_mruby を利用すると Redis or その他から証明書情報 crt, key を取得し、 設定することができます。

システム構成

右側のシステム管理者・運営者が管理画面から静的コンテンツを S3 に生成しています。
今回は ngx_mruby での証明書の動的配信についてフォーカスして紹介します。*3

f:id:kenzo0107:20210720220614p:plain

ユーザアクセスからのサイトのコンテンツ配信する大まかな流れは以下の通りです。

  1. 患者様 がクリニックサイトにアクセス
  2. ngx_mruby で SSL/TLS ハンドシェイク時にドメインを元に Redis から証明書(crt), 秘密鍵(key) を取得
    • Redis に存在しない場合は DynamoDB から取得し、 Redis にキャッシュ登録
  3. 取得した crt, key を元に SSL/TLS ハンドシェイク
  4. 静的ウェブサイトとしてホスティングされた S3 へ proxy し HTML を表示
    • HTML 内の各種 css, js, img は CDN で配信

システムの詳細・工夫点を以下に記載して参ります。

Nginx を Fargate で起動させる

ngx_mruby を組み込んだ Nginx は Fargate 上で起動させました。

サーバ管理・デプロイやスケーリングの容易さのメリットが大きい為、Fargate を採用しました。

Fargate では net.core.somaxconn が変更できませんが、 リクエスト詰まりしない様、タスク数には余裕を持たせています。

Docker イメージは https://github.com/matsumotory/ngx_mruby/blob/master/Dockerfileを参考に alpine でマルチステージビルドし軽量化 (850 MB → 26 MB) しました。

イメージビルドや ECS へのデプロイは GitHub Actions で実施しています。

SSL 終端を Nginx で実施すべく NLB を採用

ALB, CLB では HTTPS (443) 通信する場合は、証明書の設定が必須です。
NLBは TCP (443) を指定し SSL 終端を Target で実施でき、Fargate との親和性も高い為、採用しました。

f:id:kenzo0107:20210720223917p:plain
NLB Listeners TCP:443 で設定すると証明書の設定が不要

ALB は ロードバランサーあたりの証明書 (デフォルト証明書は含まない): 25であること等、クォータ制限がある為、AWS LB シリーズでの SSL 終端はサービスがスケールすることを考慮すると採用できませんでした。

証明書発行は ACM でなく Let's Encrypt を採用

ACM 証明書数 クォータ制限がある為、サービスがスケールすることを考慮して証明書の発行は Let's Encryptで実施することとしました。*4

過去に業務で利用経験があり、また本件で参考にさせていただいたはてなブログさんでも採用していること、また、プロジェクトが開始される頃に Software Design 2021年4月号で特集されており、発行の手軽さと信頼性から採用しました。

NLB 利用時の注意点

NLB は ALB と異なり、以下を注意する必要がありました。*5

  • セキュリティグループがアタッチできない
  • WAFがアタッチできない
  • 4xx, 5xx 等のメトリクスがない

対策: セキュリティグループがアタッチできない

セキュリティグループで実施していた IP 制限は ngx_mruby で実装しました。

  • allow_request.rb
# frozen_string_literal: true# リクエスト許可処理クラスclassAllowRequestdefinitialize(request, connection)
    @r = request
    @c = connection
  enddefallowed_ip_addressesENV['ALLOW_IPS'].split(',')
  enddefallowed?returntrueunless (allowed_ip_addresses & [
      @c.remote_ip,
      @r.headers_in['X-Real-IP'],
      @r.headers_in['X-Forwarded-For']
    ].compact).empty?

    falseendAllowRequest.new(Nginx::Request.new, Nginx::Connection.new).allowed?
end

nginx.conf

env ALLOW_IPS;

...

# 許可 IP でない場合、 404 を返す
mruby_set $allow_request /etc/nginx/hook/allow_request.rb cache;
if ($allow_request = 'false') {
    return 404;
}

環境変数 ALLOW_IPS に許可したい IP を渡すと ngx_mruby で許可 IP 以外は 404 を返します。

NLB + Nginx on Fargate でクライアント IP を渡す方法

NLB は Target Group のプロトコルが TCP or TLS の場合、 クライアント IP 保持はデフォルトで無効化されています。*6
その為、明示的にクライアント IP の保持を有効化する必要があります。

f:id:kenzo0107:20210721004931p:plain
NLB > Target Group > Attributes 設定

Proxy protocol v2 も有効化し、Nginx で proxy_protocolを設定することで、Nginx でクライアント IP を解釈できる様になります。

server {
    listen 443 ssl proxy_protocol;
    server_name _;

対策: WAF がアタッチできない

NLB には WAF がアタッチできません。
XSS, SQLi 等の WAF は Nginx に NAXSI*7を導入することで対応しました。*8

location / {
    # NAXSI による SQLi, XSS 等検知しブロックした場合、403 を返す
    SecRulesEnabled;
    DeniedUrl /request_denied;
    CheckRule "$SQL >= 8" BLOCK;
    CheckRule "$XSS >= 8" BLOCK;
    CheckRule "$RFI >= 8" BLOCK;
    CheckRule "$TRAVERSAL >= 4" BLOCK;
    CheckRule "$EVADE >= 4" BLOCK;

    # whitelist: XSS double encoding が誤検知された為、許容する
    BasicRule wl:1315;

    ...
}

# WAF でブロックした際に 403 を返す
location = /request_denied {
    return 403;
}

誤検知した際には特定ルールをホワイトリストとして登録し許容することが可能です。*9

ブロック時には Nginx エラーログに出力されます。*10

2021/06/11 17:53:32 [error] 7#0: *53 NAXSI_FMT: ip=172.21.0.1&server=example.com&uri=/%25U&vers=1.3&total_processed=13&total_blocked=11&config=block&cscore0=$EVADE&score0=4&zone0=URL&id0=1401&var_name0=

対策: 4xx, 5xx メトリクスがない

NLB は ALB とは異なり 4xx, 5xx メトリクスがなく、エラー検知ができません。

以下の様に対応しました。

f:id:kenzo0107:20210722222748p:plain

  1. fluentbit で Nginx のログを CloudWatch Logs へ配信
  2. CloudWatch Metric Filter で 4xx, 5xx エラーをフィルタリング*11
  3. CloudWatch Alarm で 4xx, 5xx の数が閾値を超えると SNS 経由で Chatbot へ通知*12
  4. Chatbot と連携した Slack へ通知

CloudWatch Logs は通知用に利用し
Kinesis Firehose + S3 は Athena でログ捜査時に利用します。

RDS でなく DynamoDB でデータ永続化

ngx_mruby のサンプルコードでは、証明書情報を Redis でキャッシュし、 RDS で永続化するパターンがよく見られました。

ですが、今回は DynamoDB を採用しています。

理由は、ドメイン名をキーに証明書情報を取得する今回のケースでは複雑なクエリを実行する必要がなく、リレーショナル DB と比較して NoSQL の特徴である以下メリットを享受できる為です。

  • 柔軟でスキーマレスなデータモデル
  • 水平スケーラビリティ
  • 分散アーキテクチャ
  • 高速な処理

参考: 何が違う?DynamoDBとRDS - サーバーワークスエンジニアブログ

DynamoDB へのアクセスは API Gateway + Lambda

ngx_mruby は https://rubygems.org/の gem を利用できません。 *13
低レベル APImattn/mruby-curlで実現できないこともなさそうですが、難易度が高く検証工数を確保できそうにない点から見送りました。

その代わりに
Lambda で aws-sdk を利用し DynamoDB へアクセスする様にしました。
API Gateway で Lambda のエンドポイントを設定し ngx_mruby から mattn/mruby-curlでエンドポイントを叩き Lambda を実行する様にしました。

f:id:kenzo0107:20210722223219p:plain

上記構成で数十ミリ秒程度でレスポンスが返り商用環境の利用は問題ありませんでした。

ちなみに、 永続化データを担保する DynamoDB へのアクセスは以下の場合となり、基本的に頻度は低いです。

  • ElatiCache Redis にアクセスできない
  • ElastiCache Redis のデータが揮発した*14

証明書の自動更新 システム構成

f:id:kenzo0107:20210722231120p:plain

概要は以下の通りです。

  1. EventBridge (cron) で Lambda cert-lifecycle-storeを定期実行
  2. cert-lifecycle-storeで証明書の有効日数が 30日以下の証明書のドメインリストを取得*15
  3. cert-lifecycle-storeから cert-updaterにドメイン名を渡し証明書の更新を実行
  4. cert-updatergo-acme/legoを利用し Let's Encrypt で証明書を発行
  5. SSL 証明書 (crt) と 秘密鍵 (key) を DynamoDB, ElastiCache Redis に保存、バージョン管理として S3 に証明書発行時のレスポンスを JSON ファイルに保存

証明書の新規発行は管理画面から cert-updaterを実施できる様にしており、運用者が証明書を発行できる様にしています。

参考

おまけ

mruby 仲間を増やしたい気持ちから今回の ngx_mruby を用いた証明書の動的読み込みを簡易的に体験できるリポジトリを用意しました。

github.com

ngx_mruby 初めましての方もそうでない方も遊んでいただけると幸いです。

以上です。

採用のリンク


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

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

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

■開発環境はこちら

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


*1:弊社テックブログでも利用しております、はてなブログの「独自ドメイン」の設定と同様の機能です。

*2:弊社では定期的に Matz さんへ聞きたいこと!会を開催頂いております。

*3:Ruby on Rails で構成される管理画面で静的コンテンツをS3にアップロードする仕組みについては別途本ブログで紹介予定です。お楽しみに✨

*4:プロジェクト開始前に弊社担当の AWS ソリューションアーキテトに相談したところ、サービスがスケールすることを考慮すると ACM でなく別途証明書発行システムを採用することを推奨されました。

*5:弊社では NLB は本プロジェクトが初採用でした。

*6:NLB Client IP preservationにて「If the target group protocol is TCP or TLS, client IP preservation is disabled by default. 」と記載がある通りです。

*7:NAXSI は Nginx Anti XSS & SQL Injection の略で Nginx 特化の WAF モジュールです。

*8:Nemesida WAF Freeは alpineベースだと導入方法がわからなかった(できなかった)。Nginx Plus ModSecurityは年間40万円以上の有償サービスで検証工数が確保できず、断念しました。

*9:w:1315 の 1315 は ルールに採番されているIDで https://github.com/nbs-system/naxsi/blob/master/naxsi_config/naxsi_core.rulesに記載されています。

*10:LOG を設定するとブロックせずログに出力するモードがある様ですが、LearningMode (学習モード)を設定しないと「Assertion failed: strlen(fmt_config) != 0 (/usr/local/src/naxsi/naxsi_src//naxsi_runtime.c: ngx_http_nx_log: 1076)」というエラーが発生することを確認しています。AWS WAF の count の様な機能を期待していましたが違いました。

*11:CloudWatch Metric Filter のアイコンが見つからなかった

*12:SNS 連携先を Lambda でなく Chatbot にした場合、通知内容を

*13:その代わり https://github.com/mruby/mgem-listにある gem を利用できます

*14:よくある質問 - Amazon ElastiCache | AWSにて「エンジンのアップグレードプロセスは、既存のデータをベストエフォートで保持するように設計されており、Redis レプリケーションに成功する必要があります。」とあり、データは揮発する可能性があることを前提に設計しています。

*15:Let's Encrypt の証明書の有効期間は 90 日間で 60日毎の更新を推奨している為です


今年もRubyKaigi Takeout 2021にプラチナスポンサーとして参加しました!

$
0
0

f:id:ryoheikurisaki:20210915104735p:plainサーバーサイドエンジニアの栗崎です。

『MedPeer』の製薬企業向けのサービスの開発を担当しています。

2021年9月9日~11日に RubyKaigi Takeout 2021が開催されました。メドピアは今年で通算4回目のスポンサー参加となります。

私はRubyKaigi, RubyKaigi Takeoutに初めての参加でした。今回はメドピアのメンバーでSlackチャンネルを作り、「社内感想戦」をしながら視聴しました!

プラチナスポンサーとして

オンライン開催になったのは2020年に続き2回目ですが、今回もプラチナスポンサー💎として協賛しています。

スポンサー特典として15名分の招待枠がありました。

枠はすぐに埋まりましたが、メドピアでは技術研鑽活動の一環として経費でチケットを購入できるので、希望者は全員視聴しています。

Kaigi中には、プレゼンテーションの幕間に15秒の動画CMも(初制作)も流れました。

f:id:ryoheikurisaki:20210916191312p:plain

CMが流れた瞬間、コメントに「メドベア~」と入れてくださった方が複数いました。過去もこのキャラクターをあしらったノベルティを配っていたので、非公式キャラクターですが意外と認知されているようです。 これを機に、医師と患者を支えるメドピアという会社の存在をより多くの方に知っていただけたらと思います。

Kaigiのようす ー社内感想戦ー

3日間の開催期間中に計37セッションが行われていました。タイムテーブル上は2つのトラックにわかれていて、それぞれ視聴したものを専用Slackチャンネルでコメントしあいました。

社内感想戦で話題に挙がっていたことのひとつは、Rubyの型解析ツールの「The newsletter of RBS updates」のセッションについてでした。RBS導入のヒント、Ruby3.1に搭載予定のRBSの新機能のお話でした。

f:id:ryoheikurisaki:20210916191247p:plain

セッションでは、RBSの概要を始め、関連ツールの紹介、Ruby3.1搭載機能のお話しがされていました。また、課題の例として、Railsアプリケーションへの導入をお話されており、Railsアプリケーションでの型の導入について、社内のリポジトリで試してみたいといった会話などがありました。社内での導入も近いうちに行えたらなと思います!!

印象的だったこと

私が印象的だったのは、「The Art of Execution Control for Ruby's Debugger」のセッションでした。

普段のアプリケーション開発でも使用しているdebuggerについてのお話で、現在Rubyに標準添付されているdebuggerの lib/debug.rbを置き換える目的で新たなdebuggerである ruby/debug *1を作成されたお話でした。

infoコマンドでローカル変数の一覧が見れたり、backtraceコマンドで、どこでメソッド定義がされているかを見れたりと普段の開発業務の中でも便利になりそうなものが紹介されていました。 binding.breakは、自分がdebugしたい箇所にセットし、do:にコマンドを渡すだけで、渡したコマンドをdebugコマンドとして実行でき、わざわざブレークポイントで停止することなく実行でき便利だなと思いました。

f:id:ryoheikurisaki:20210915110339p:plain

また、社内Slackではstep backの機能についてもお話しされていましたね。debugしている箇所の前の状態がどういうローカル変数を持っているかどうかを調べるのに使えるようです。 これらの機能が、Visual Studio Codeのextensionとしても使うことができ、なお魅力的だなと感じました。

先日、Ruby on Rails7では、debuggerがbyebugに代わり、ruby/debugに置き換わったようです。*2

まとめ

今回のRubyKaigi Takeout 2021に参加して多くのことを感じました。

Rubyを使った多岐にわたる発表により、Rubyの世界の広さを知りました。 IDE、パフォーマンス、アップデート予定の機能、ツールライブラリ、データ処理、キーボードなど様々な発表が繰り広げられていました。正直、私個人にとっては難しい話も多かったですが、自分の技術スキルの立ち位置を知る機会になりました。

また、最前線で活躍されているRuby committer、馴染み深いGemのcommitter、海外で活躍されている有名企業のRuby commiterなど、グローバルカンファレンスでないと聞くことのできなさそうな方々の話を聞けたことはとても貴重な機会でした。

そして、カンファレンス内のチャット欄で親しみの込もったリアクション、最後のMatzさんのお話からもRubyコミュニティの温かさを随所に感じました😭

本格的にオンラインとなったのは今年からで、運営の方は動画配信も大変そうでしたが、来年も開催されるとの予告があったので是非参加したいです。 メドピアチームとしては今後もRubyコミュニティ・Rubyist仲間の皆様に貢献できるよう積極的に活動していきます!リアルイベントもいつかまた再開できたら… Rubyコミュニティの一員として、Rubyを駆使して、医療を再発明することに貢献したいと思います!!


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

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

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

■開発環境はこちら

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


iOSDC Japan 2021にダイアモンドスポンサーとして登壇しました

$
0
0

f:id:ichi6161:20210930144331p:plain

エンジニアの市川です。 メドピアでは保険薬局と患者さまを繋ぐ「かかりつけ薬局」アプリ「kakari」のiOS開発を担当しています。 2021年9月17日~19日に開催されたiOSDC japan2021にて、スポンサーセッションとして登壇し、kakariのiOS開発について話しました! 企業としては、今年で4回目、ダイアモンドスポンサーとしては3回目の協賛参加となりましたが、セッションへの登壇は初めてでした。 登壇の内容も含め、イベントのレポートをしたいと思います。

発表内容
アプリ間連携で薬局・クリニックのユーザー体験価値を最適化した話~MedPeer iOS開発~

かかりつけ薬局支援サービス「kakari」と、去年リリースした姉妹アプリである、かかりつけクリニック支援サービス「kakari for Clinic」のアプリ間で、ユーザー情報連携を簡単にできるようにした実装について紹介しています。 FirebaseのDynamicLinkを使った実装の話です。


資料 speakerdeck.com

※資料内にデモ動画を載せていますが埋め込み資料内だと動画が再生されません。 iOSDC Japan 公式YouTube www.youtube.com

にライブのプレゼン動画が掲載されると聞いていますので、詳しくはそちらでもご覧いただけます。

 他のスポンサーセッションでは各企業さんの「エンジニアの働き方」や「開発体制」などについて話されているところも多かったですが、メドピアは視聴されているエンジニアの方々にとって何らかの技術還元になればと思い開発事例の話をしました。 kakariはコンセプトが明確で、ユーザー目線に立って、改良をし続けているサービスだと思います。 そこにプロダクト開発の面白さを感じているので、今回の登壇でkakariを紹介することができたのは、とても嬉しく思います。

f:id:ichi6161:20210930144957p:plain


スポンサーとしてのその他の取り組み ―ノベルティ―

iOSDC Japanは毎年豪華でたくさんのノベルティも注目されているようですが、今回メドピアからは宣伝フィルムでパッケージングしたペットボトルのミネラルウォーター(通称:Peerウォーター)と、ステッカーと、マスクケースをお送りしました。


f:id:ichi6161:20210930144917j:plain


マスクのノベルティは複数ありましたが、マスクケースはオンリーワンだったようです。 また、箱を開けてすぐに、公式パンフレット以外はPeerウォーターだけが目に入る位置にあり、宣伝効果は抜群?!ファーストビューは大事ですね。


f:id:ichi6161:20210930144937j:plain


医師と患者を支えるメドピアという会社のことをより広く知っていただくと共に、コミュニティを同じくする企業の皆さんとも知見を共有しあい、iOS開発技術の向上に貢献したいです。

まとめ
 イベント配信時のチャットで「来年こそはリアルで開催したい」と望むコメントが多く見られましたが、メドピアとしても来年のiOSDC Japanがリアルで開催されることを願いつつ、会社としてiOSエンジニアコミュニティにまた何か還元できるような技術力を培い、私個人としても「kakari」の事業をもっと発展させたいと思います。



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

■募集ポジションはこちら
https://medpeer.co.jp/recruit/entry/
■開発環境はこちら
https://medpeer.co.jp/recruit/workplace/development.html

AWS Config + Athena + QuickSightによる複数AWSアカウント横断でのセキュリティ状態の可視化

$
0
0

CTO室SREの侘美です。最近は社内のセキュリティ対策関連を生業にしております。
今回は最近進めていた社内のAWSアカウントのセキュリティ可視化がある程度形になったので記事にしたいと思います。

課題:多数のAWSアカウントのセキュリティをチェックしたい

サイバー攻撃が増加している昨今、AWSなどのPaaS環境においても構築時にセキュリティの観点で注意すべき点がいくつもあります。 例えば、不必要なサーバー/ポートがインターネットに公開されていないか、アカウントにMFAが設定されているか、等々実施しておきたいセキュリティ対策は多岐にわたります。

弊社では、AWSを用いてインフラを構築する際にセキュリティ上守るべきルール集を、インフラセキュリティポリシーというドキュメントを定義しています。 しかし、あくまでドキュメントベースなので、実際にこのドキュメントに書かれたルールに準拠した構成になっているかどうかのチェックは手作業で実施しなければならない状態でした。

また、サービスも年々増加しており、現在では約40のAWSアカウントを6名のSREで管理している状態であり、今後すべてのアカウントのセキュリティまわりをSREが手動でチェックしていくのは現実的ではありません。

さらに、現在はSREが中心に行っているインフラの構築/運用も各サービスチームへ徐々に移譲している途中であり、これらのセキュリティルールのチェックの自動化の必要性が上がってきました。

対策:セキュリティルール準拠状態の可視化

幸いAWSにはこういった課題を解決するためのサービスがいくつもあります。

これらサービスを最大限活用し、社内のセキュリティポリシーで定義したルールへの準拠状態を可視化することを目標としました。

要件の整理

今回構築したアーキテクチャは以下の要件を元に作成しています。

  • どのアカウントがどの程度セキュリティルールに準拠できているかがわかりやすく可視化できる
  • できればフルマネージド
  • Organizationにアカウントが増えてもメンテナンス不要で自動で対応される
  • リソースの自動修復は現状考えていない

構成の検討

上記の要件を満たす構成を検討していく課程で、悩んだ・ハマったポイントをいくつかご紹介します。

AWS Config vs Security Hub

Organization配下のアカウント全体に対して、リソースがルールに準拠した設定になっているかをチェックするサービスとして、AWS ConfigAWS Security Hubがあります。

AWS Configは「AWSが提供する151(2021/09/08現在)のマネージドルール」または「自身でLambda関数で実装したカスタムルーム」を使い、リソースのルールへの準拠状態をチェックすることができます。

また、AWS Organizationsにも対応しており、Organization配下の全アカウントに一括でルールを作成することができます。 ただし、アカウント横断でルール準拠状態を閲覧する方法は無いため、別途何らかの方法で可視化する必要があります。

Security Hubはセキュリティチェックの自動化とセキュリティアラートの一元化を主眼においたサービスです。 いくつか提供されているベストプラクティスを選択し、そのベストプラクティスに含まれるルールへの準拠がチェックされます。

f:id:satoshitakumi:20210915154330p:plain
マネジメントコンソールにおけるSecurity HubのUI

こちらもOrganizationsに対応しており、配下のアカウントすべてを横断的にチェックすることができます。

Security Hubなら比較的少ない工数でOrganization配下のアカウントに対して横断的にチェックを実行できます。しかし、提供されたベストプラクティスのルール集でのチェックなので、今回実現したい社内のセキュリティポリシーに準拠しているかというチェックとは少しズレてしまいます。 また、ビューも固定なので「どのアカウントがセキュリティ的に弱いか」等を即座に判断するのは難しいです。

AWS Configはマネージドルールが豊富であり、社内でチェックしたい項目に合わせて柔軟に対応できそうです。 アカウント横断でのビューは無いため、何らかの方法で用意する必要があります。

弊社では既にSecurity Hubを全アカウントで有効化し、重要なセキュリティ項目に関しては適宜チェックし修正する運用を行ってはいましたが、今回はより柔軟なルールと求めているデータを可視化できるという点を優先し、AWS Configを採用することにしました

QuickSight vs Elasticsearch Service

AWS Configを採用したので、アカウント横断の評価結果をいい感じに可視化する仕組みを別途用意する必要があります。

AWS Configは各リソースの評価状態をスナップショットログとして定期的にjson形式でS3に出力することができるので、このログを利用して可視化を行います。 スナップショットログは可視化のために一箇所に集めたいため、以下の図のような構成をとることで1つのS3バケットに全アカウント分集約しています。

f:id:satoshitakumi:20210915154439p:plain
Organization配下のアカウントへのConfigの設定とスナップショットの集約

上記のような方法でS3に格納されたログを可視化するソリューションはいくつも存在します。 AWS上で実現するメジャーな方法としては、「S3 → Athena → QuickSight」や「S3 → Lambda → Elasticsearch Service → Kibana」のような構成があげられます。 後者はSIEM on Amazon Elasticsearch Serviceというソリューションとして知られています。

f:id:satoshitakumi:20210915154628p:plain
代表的な2パターンの可視化方法

どちらの構成にもメリット・デメリットは存在しますが、今回は以下の理由からQuickSightを利用する構成を採用することにしました。

  • IAMと連携したユーザー管理の容易性
  • インスタンス管理の有無
  • データ取り込み部分の実装コスト
  • レポートメール機能

AWS Config マネージドルールの選定

2021/09/08時点で151のマネージドルールが提供されています。 docs.aws.amazon.com

ルール自体は「ルートユーザーのアクセスキーが存在しないこと」等様々な項目が用意されています。

この中からチェックしたい項目をピックアップし、 またセキュリティ以外の観点でもバックアップ、削除保護、可用性の観点などで設定が推奨していきたいルールもいくつかピックアップしました。
今回は合計で68ルールを採用しています

これらをセキュリティ、コスト、パフォーマンス、バックアップ、削除保護の5つに分類し、それぞれの接頭詞(例:セキュリティなら security-)を決めた上で、ConfigのOrganization Config Ruleとして社内の全AWSアカウントへ登録しました。 接頭詞をルールを作成する際の名前に指定することで、スナップショット中のルール名から何の目的で導入したルールかを判別可能にし、ダッシュボードで可視化する際に「セキュリティルールに非準拠であるリソース数」のような表示も可能にしています。

最終的なアーキテクチャ

今回構築したアーキテクチャの全体像がこちらになります。

f:id:satoshitakumi:20210915154524p:plain
全体のアーキテクチャ

弊社ではAWS上のリソースはTerraformで管理しています。 また、Terraform CloudでStateの管理やapplyの実行を行っています。

Terraform Cloudのトリガー機能とWorkspace間のoutput参照機能を利用することで、Organizationを管理しているTerraformが出力する、アカウント一覧に変更があった場合、Log AccountのAthenaのテーブル定義を管理しているTerraformを実行するといった連携が可能になります。(詳細は後述します)

この手のダッシュボードでは、能動的な閲覧のみで運用を続けていると、閲覧するメンバーが固定化され仕組みが風化していく懸念があります(体験談)。 そこで、QuickSightの機能で定期的にダッシュボードをレポートとして送信することで、関与するメンバーが定期的に閲覧してくれるように試みています。

ハマりポイント:AthenaのProjection Partition

S3に保存したConfigのスナップショットに対してAthenaでクエリを実行するためにテーブルを作成する必要があります。 その際のテーブル定義で一部ハマった箇所があったのでご紹介します。

前提:Partition Projectionの型

Athenaのテーブル設定の一つの項目に、パーティションという概念があります。 パーティションを簡単に説明すると、S3のキー中のどの位置にどのような変数が含まれるかを設定し、SQL中でその値を指定することでスキャン対象となるS3上のオブジェクトを限定することができます。

例えば、S3バケットのキーに /AWSLogs/111111111111/Config/ap-northeast-1/2021/9/1/ConfigSnapshot/のように日付が含まれる場合、日付部分をパーティションとして登録することで、 WHERE date = '2021/9/1'のようなクエリを実行できるようになります。 大量のオブジェクトがあるS3に対してパーティションを適切に設定せずにAthenaでクエリを実行するとコストがかかったりエラーが発生したりするのでAthenaを使う上では必須のテクニックとして知られています。

パーティションの設定方法にはいくつか種類があります。

  1. Hive形式のキーを利用する
  2. ADD PARTITIONクエリを実行する
  3. Projection Partitionを利用する

各方法の細かい違いに関しては公式ドキュメントを参照していただくのが良いかと思います。

今回はConfigのスナップショットが対象になるため、1のHive形式のキーではないのでこの方法は使えません。 2と3で迷い、AWSのSAやプロフェッショナルの方とディスカッションさせていただき、 Projection Partitionを利用する方が良さそうという結論に至りました。

議論のポイントとなったのは、S3オブジェクトのキーに含まれるアカウントID部分をProjection Partitionのどの型で表現するかという点です。 Projection Partitionでサポートされる型には、 Enum, Integer, Date, Injectedの4種があります。

参考:Supported Types for Partition Projection - Amazon Athena

アカウントIDは12桁の数字なので、 Enum, Integer, Injectedが候補となります。 それぞれの特徴は以下のようになっています。

  • Enum : テーブル定義時に取りうる値を列挙する。検索時の指定は任意。
  • Integer : テーブル定義時に取りうる値の範囲を指定する。範囲が広すぎる場合検索クエリがタイムアウトする。検索時の指定は任意。
  • Injected : テーブル定義時に値の指定は不要でキーに含まれる値を自動的に判定してくれる。検索時の指定は必須

AWSアカウントは任意のタイミングで増減するため、できれば現存するアカウントを列挙し指定するようなパーティションの設定は避けたいです。これはAWSアカウントをOrganizationに追加した際に特にメンテナンスすることなく可視化用のダッシュボードに反映されて欲しいからです。

そうなると、 Injected型が候補になってきますが、検索時に値の指定が必須となってしまうため、アカウント横断で検索するようなクエリを実行できなくなってしまうため却下となります。

Integer型なら12桁の整数も対象なので、これで条件を満たせると思い、実際にPartition Projectionを設定してAthenaから検索クエリを実行してみました。 ところが、検索クエリがタイムアウトしてしまいました。 Integer型の取りうる範囲の指定を調整して何度か実験したところ、12桁のAWSアカウントの範囲を値域として指定すると範囲が広すぎるためかタイムアウトとなることがわかりました。

課題:アカウントの増減への対応

ということで、 Enum型でテーブル定義時に現存するOrganization配下のアカウントを列挙する必要がでてきました。 この Enum型でのPartition Projectionを設定した状態での検索クエリの挙動は特に問題なく、想定している結果を得ることができました。 つまり、このEnum型の場合、Organization配下にAWSアカウントが増えた場合に如何に自動でPartition Projectionの定義にアカウントIDを反映するかという課題が残ります。 (Partition Projectionの変更はテーブル作成後でも実行できます)

対策:TerraformのRemote Stateの活用

この課題を解決するにあたり、弊社で利用しているTerraform Cloudの機能を利用するのが最もスマートであることに気づきました。

Terraform CloudはHashiCorp社が提供している、Terraformの実行環境です。指定したブランチに反映されたTerraformのコードを使い自動でapplyを実行してくれます。 弊社ではAWSのリソースはTerraformで管理しており、SREが管理する全てのTerraformのリポジトリをTerraform Cloud上でapplyしています。 また、Organization配下にアカウントを新規に開設する場合も、Terraformで実装しています。 今回のConfigやAthenaに関しても同様です。

Terraform Cloudを利用すると、あるworkspaceの出力( output)を別のworkspaceから参照するRemote State機能を利用することができます。

参考: Terraform State - Workspaces - Terraform Cloud and Terraform Enterprise - Terraform by HashiCorp

また、特定のworkspaceのapplyが完了したのをトリガーに、別のworkspaceのapplyをキックすることが可能です。

参考: Run Triggers - Workspaces - Terraform Cloud and Terraform Enterprise - Terraform by HashiCorp

つまり、以下のような構成にすることで、Organizationにアカウントが追加された場合に自動でAthenaのProjection Partitionの設定を変更することが可能になります。

  • Organizationを管理するTerraform Workspaceにて、アカウントの一覧を outputで出力する
  • AthenaのPartition Projectionを構築するTerraform Workspaceにて、上記のアカウント一覧を参照して Enum型の値に設定する
  • Organizationを管理するTerraform Workspaceが実行されたら、Athenaを管理するTerraform Workspaceのapplyが実行されるように、Run Triggerを設定する

全体のアーキテクチャから抜粋すると、以下の部分がこの仕組を表しています。

f:id:satoshitakumi:20210915154734p:plain
Terraform Cloudによるアカウント追加時のAthenaテーブル定義の自動更新

テーブル定義は最終的に以下のTerraformコードにより作成しました。

resource "aws_glue_catalog_table" "config" {
  name          = "aws_config"
  owner         = "hadoop"
  database_name = aws_glue_catalog_database.config.name

  table_type = "EXTERNAL_TABLE"

  parameters = {
    EXTERNAL = "TRUE"

    "projection.enabled"          = "true"
    "projection.account.type"     = "enum"
    "projection.account.values"   = join(",", values(data.terraform_remote_state.root.outputs.accounts)) # Remote Stateで別WorkspaceからアカウントIDの配列を参照
    "projection.region.type"      = "enum"
    "projection.region.values"    = "ap-northeast-1,us-east-1"
    "projection.dt.type"          = "date"
    "projection.dt.range"         = "2021/4/1,NOW"
    "projection.dt.format"        = "yyyy/M/d"
    "projection.dt.interval"      = "1"
    "projection.dt.interval.unit" = "DAYS"
    "projection.itemtype.type"    = "enum"
    "projection.itemtype.values"  = "ConfigHistory,ConfigSnapshot"
    "storage.location.template"   = "s3://<your bucket name>/<prefix>/AWSLogs/$${account}/Config/$${region}/$${dt}/$${itemtype}"
  }

  partition_keys {
    name = "account"
    type = "string"
  }

  partition_keys {
    name = "region"
    type = "string"
  }

  partition_keys {
    name = "dt"
    type = "string"
  }

  partition_keys {
    name = "itemtype"
    type = "string"
  }

  storage_descriptor {
    location      = "s3://<your bucket name>/<prefix>/AWSLogs"
    input_format  = "org.apache.hadoop.mapred.TextInputFormat"
    output_format = "org.apache.hadoop.hive.ql.io.IgnoreKeyTextOutputFormat"

    ser_de_info {
      serialization_library = "org.openx.data.jsonserde.JsonSerDe"
      parameters = {
        "serialization.format"                 = "1"
        "case.insensitive"                     = "false"
        "mapping.arn"                          = "ARN"
        "mapping.availabilityzone"             = "availabilityZone"
        "mapping.awsaccountid"                 = "awsAccountId"
        "mapping.awsregion"                    = "awsRegion"
        "mapping.configsnapshotid"             = "configSnapshotId"
        "mapping.configurationitemcapturetime" = "configurationItemCaptureTime"
        "mapping.configurationitems"           = "configurationItems"
        "mapping.configurationitemstatus"      = "configurationItemStatus"
        "mapping.configurationitemversion"     = "configurationItemVersion"
        "mapping.configurationstateid"         = "configurationStateId"
        "mapping.configurationstatemd5hash"    = "configurationStateMd5Hash"
        "mapping.fileversion"                  = "fileVersion"
        "mapping.resourceid"                   = "resourceId"
        "mapping.resourcename"                 = "resourceName"
        "mapping.resourcetype"                 = "resourceType"
        "mapping.supplementaryconfiguration"   = "supplementaryConfiguration"
      }
    }

    skewed_info {
      skewed_column_names               = []
      skewed_column_value_location_maps = {}
      skewed_column_values              = []
    }

    number_of_buckets = -1

    columns {
      name = "fileversion"
      type = "string"
    }

    columns {
      name = "configsnapshotid"
      type = "string"
    }

    columns {
      name       = "configurationitems"
      parameters = {}
      type       = "array<struct<configurationItemVersion:string,configurationItemCaptureTime:string,configurationStateId:bigint,awsAccountId:string,configurationItemStatus:string,resourceType:string,resourceId:string,resourceName:string,ARN:string,awsRegion:string,availabilityZone:string,configurationStateMd5Hash:string,configuration:string,supplementaryConfiguration:map<string,string>,tags:map<string,string>,resourceCreationTime:string>>"
    }
  }
}

可視化した内容

最後にQuickSightで構築したダッシュボードの一部を紹介します。(一部加工しております)

f:id:satoshitakumi:20210915184242p:plain
作成したダッシュボードの一部

主に、「次にテコ入れすべきAWSアカウントの特定」や「全体的に実践できていないルール = 社内にノウハウがないルール」の特定などに利用する想定で作成しております。

課題

今回構築した構成の中で課題として残っている部分もあるので掲載しておきます。

  • QuickSightのリソースのほとんどがTerraformに対応してない
    • SQLのみリポジトリ管理している状態で、aws providerの対応待ち
  • 特定のリソースを除外する対応が難しい
    • タグでの除外とかができれば嬉しいが現状はできない

まとめ

Organization配下のAWSアカウントのルールへの準拠状態を、AWS Config + Athena + QuickSightで可視化することができました。 これで今後AWSアカウントが増加したり、各アカウントの管理をサービス開発チームへ移譲していってもある程度のガバナンスが効いた状態を担保することができるようになったかと思います。


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

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

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

■開発環境はこちら

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

『medpeer.jp 開発を加速させるエンジニアリング施策』をKaigi on Rails 2021で話しました

$
0
0

こんにちは、DCP事業部エンジニアの榎本です。普段の業務ではMedPeer Channelの開発を担当しています。

メドピアはKaigi on Rails 2021にRubyスポンサーとして参加し、そのスポンサーLT枠にて 『medpeer.jp 開発を加速させるエンジニアリング施策』という発表をしてきました。

今日はその内容を補足するとともに改めて本テックブログでも紹介したいと思います。

medpeer.jp の rails stats

medpeer.jp (医師専用コミュニティサイト「MedPeer」のことです)自体は2007年にローンチしたサービスで、サービスの歴史としては10年以上(もうすぐ15年!)続くサービスです*1

サービスイン当時の開発言語はPHPでしたが、2016年に medpeer.jp のRails化が開始されました。Rails化をスタートさせてから既に5年が経過しており、Railsのコードベースも巨大になりつつあります。

2021年10月時点の medpeer.jp の rails stats取得してみました。その結果が下記です。

$ rails stats
+----------------------+--------+--------+---------+---------+-----+-------+
| Name                 |  Lines |    LOC | Classes | Methods | M/C | LOC/M |
+----------------------+--------+--------+---------+---------+-----+-------+
| Controllers          |  29336 |  22890 |     547 |    3078 |   5 |     5 |
| Helpers              |    861 |    676 |       0 |     114 |   0 |     3 |
| Jobs                 |   1412 |   1164 |      49 |     166 |   3 |     5 |
| Models               |  64144 |  43039 |    1218 |    4945 |   4 |     6 |
| Mailers              |    599 |    483 |      26 |      59 |   2 |     6 |
| JavaScripts          |    665 |    292 |       0 |      51 |   0 |     3 |
| JavaScript           |  22963 |  15936 |       0 |    1234 |   0 |    10 |
...(以下略)...

そんな巨大な medpeer.jp 開発を支えるエンジニアリング的な工夫をいくつか紹介したいと思います。

コンテナベースの開発環境

もはや常識となったdockerコンテナ開発ですが、弊社も例に漏れずdockerコンテナをベースとして開発しています。

ローカル開発環境

ローカル開発環境は dipを利用しています。

dip とは Docker Compose をラップしたコマンドラインツールです。これを利用することで複雑な docker composeコマンドを大幅にショートカットできます。例えばmedpeer.jp 開発の場合、 Rails Server の起動は dip rails sとすれば立ち上がるように設定しています。

dip に関しては下記の翻訳記事が詳しいのであわせてご参照ください。

techracho.bpsinc.jp

QA環境

medpeer.jp 開発チームではQA環境という環境を用意しています。QA環境は何かというとブランチ単位のプレビュー環境のことです。

GitHub Pull Request 上で /deploy-qaとコメントをすると GitHub Actions でデプロイジョブが走り、当該ブランチがQA環境へデプロイされる仕組みになっています。

f:id:toshimaru-medpeer:20211026141513p:plain
PRコメントでQA環境へと自動デプロイされる様子

ステージング環境・プロダクション環境

ステージング環境、プロダクション環境ともにECSを使ってインフラを構築しています。また、メインとなるRails AppのサーバーはAWS Fargate を使って構築しています。

ちなみに medpeer.jp だけでなくメドピア会社全体としてECSを採用しており、弊社のSRE・侘美がECS勉強会を開催したときの資料は下記になります。

フィーチャートグル

flipperというgemを使いフィーチャートグルを実現しています。

今まで巨大な長命のフィーチャーブランチを頑張ってメンテし、リリースタイミングでドカンと危険なビッグバンリリースを発動させていたのですが、このフィーチャートグルの仕組みが導入されたことにより、ビッグバンリリースを回避することができるようになりました。

f:id:toshimaru-medpeer:20211026141600p:plain
flipper-ui によるフィーチャートグル制御UI

Railsアプリケーション設計

巨大なRailsアプリケーションコードをメンテナブルにする工夫は各社いろいろアプローチあると思いますが、弊社は PORO (Plain Old Ruby Objects) のアプローチを使うことが多いです。

POROに関しては弊社の技術顧問である willnet さんが下記記事で紹介しておりますので、よければご覧ください。

tech.medpeer.co.jp

tech.medpeer.co.jp

また、フォームオブジェクトのアプローチについても下記記事で解説しておりますのであわせてどうぞ。

tech.medpeer.co.jp

CI/CD

medpeer.jp の CI/CDですが、CIサービスとして CircleCI + GitHub Action を利用しており、CDは主に GitHub Action を利用しています。

中でも GitHub Actions + GitHub Deployment の仕組みが便利で、GitHub ネイティブのデプロイ通知が見れたり、Activeなデプロイメントステータスが GitHub UI 上でさっと確認できるのが便利です。

f:id:toshimaru-medpeer:20211026143514p:plain
GitHub Deployment 通知

f:id:toshimaru-medpeer:20211026143600p:plain
Active な Production Deployment の確認

これに関して詳しくは、弊社SRE・正徳が発表した資料がありますので、下記資料をご覧ください。

Rails Upgrade 戦略

スムーズなRails Upgradeのために、複数RailsバージョンでCIを実行しています。つまり、現行バージョンに加えて次のRailsバージョンでCI上を動かすことで、次のバージョンでも正しく動くことをCIで担保しています。

これに関しても正徳が発表した資料がありますので、詳しくは下記資料をご覧ください。

技術負債・今後の課題

medpeer.jp は長年運用しているサービスですので、もちろん技術負債もあります。具体的には下記のようなものです。

  • まだRails 6.0(年内には Rails 6.1 にアップグレード予定)
  • 残存するPHPコードベース
  • PHPから移植した古いRailsコード
  • マイクロサービスっぽいアーキテクチャの残滓

現状はこれらの技術負債に対して、きれいにできるところからきれいにしていき徐々に減らしていっているという段階です。

Kaigi on Rails に参加してみて

前年度の Kaigi on Rails にも私は参加していましたが、間違いなく去年よりカンファレンス特有の"ワイワイ感"が演出できていたように感じます。

そのワイワイ感を演出するのに一役買っていたのは reBakoであったと思います。雑談テーブルで開発者同士がいろいろ話していたり、登壇者が登壇後QAテーブルでいろいろ質問に受け答えをしていたりして盛り上がっていました。

一方で企業ブース出展側でいうと、一見さんが入りやすいような雰囲気作りをするのに課題を感じました。リアルのブース出展だとフラッと立ち寄ってくれる方がいらっしゃったり、こちらからノベルティを渡しに行ったりとある程度インタラクションが発生する機会がありますが、オンラインだとなかなかブースの中までは呼び込むのにハードルを感じました。このあたりは再度 reBako 出展する機会があればいろいろ工夫したいところです。

f:id:toshimaru-medpeer:20211026145841p:plain
reBako会場

最後になりますが、とても素敵な"Kaigi"を開催してくださった Kaigi on Rails 2021スタッフの皆様、ありがとうございます!


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

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

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

■開発環境はこちら

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

RubyWorld Conference 2021 にプラチナスポンサーとして出展しました!(Matzの後日談つき)

$
0
0

サーバーサイドエンジニアのミナトです。 医師とMRをつなぐコミュニケーションツールとなるMedPeer Talkというサービスの担当をしています。

昨年の 12/16に開催された RubyWorld Conference(RWC) に、メドピアはプラチナスポンサーとして参加しました。

f:id:minato_nkmr:20220108062103p:plain
RWC2021スポンサーセッション(メドピア)

スポンサーセッションはこちらからご覧いただけます!

当日は、GALIMO を利用したオンライン会場でブース出展もいたしました。

f:id:minato_nkmr:20220108062207p:plain
RWC2021スポンサーブース

この記事では技術的な内容がほとんどないのですが、RWCを振り返ってみた内容をまとめてみました。

個人的にRWCは比較的ビジネス寄りの発表が多いイメージがありました。 ですが、今年の発表ではメソッドオブジェクトの理解を深めるような技術寄りの話からRubyコミュニティについての話まで幅広くあり、改めてRubyの多様な側面を見れました。

Rubyコミュニティという軸でみたRWC2021

プログラミング未経験者でも利用できるSmalrubyをベースとして作成されたツールのセッションがありました。このツールを利用する方の中でRubyそのものに深く入り込んでいくという人は少ないのかも知れません。ですが、こういったプログラミング自体の敷居を下げていくきっかけとしてRubyが利用されていくということで、社会にRubyが入り込んでいくことが増えてくるのではと思いました。

アフリカでのRubyエンジニア育成のセッションRailsチュートリアル×遠隔学習のセッションを聞き、未経験からRubyを利用したエンジニアとして成長していく過程でこういったエコシステムが存在することで、Rubyコミュニティが形成されていくのだとも感じました。

私が一番共感してしまったのは、リンカーズ株式会社さまの「Railsバージョンを倍にした」セッションでした。 なぜ技術的負債が溜まってしまったのかという考察から、どうやってリファクタリングを進めていったのかという具体的なお話の中で、

  • MVC全部遷宮
  • テストカバレッジ0→95%まで上げた
  • 3ヶ月の動作確認
  • 負債を溜めないための仕組み

などなど、聞いている中で大変だったんだな…すごくわかる…といった部分が随所にありました。 Rubyを利用しているからこそ共感できるこういったものが、松田さんの発表「Me and My Ruby Friends」にあったような「Rubyを利用していることで芽生える友情」に育っていくのかなとも思いました。

Matzさん後日談

弊社ではMatzさんを技術顧問として毎月1回オンラインMTGをさせていただいております(エンジニアにとっての福利厚生ですね!)。

そのMTGの中でMatzさんに、特に印象に残ったセッションはどれでしたか、とお聞きしたところ松田さんの「Me and My Ruby Friends」とのことでした。他にもいくつか挙げていただいていましたが、全部は書ききれないので省略しました。

このセッションの感想をお聞きする中で、

「純粋に技術だけの話をすると投資しているリソースのサイズによってしまう。例えばV8エンジンにはどうやっても勝てない。しかし、RubyはRWCのようなものだったり、RubyKaigiのような多様なコミュニティがある。良いコミュニティ作るという意味では Ruby はなかなか良いのではと思っている」

と仰っていました。 私自身はJava、PHP、Rubyと変遷してきた中でのRubyコミュニティの多様さを本当に感じます。

他にも、

  • 当日の朝にmrubyのバグレポートが来ていて、開会〜キーノートの間に準備をしていると見せかけてデバッグして 5min前にcommitしていたこと
  • オンラインカンファレンスは「オフラインでできてた廊下でちょっと話したりとかフラッとブースに立ち寄ったりが難しいよね。副音声とか面白いかもね」

といった面白いこぼれ話も聞けました。

まとめ

とりとめもなく書いてしまいましたが、今回のRWCではRubyコミュニティ、OSSとしてのあり方、ビジネスとしての可能性の広さを改めて感じることができた1日でした。

最後になりますが、今回のRWCを開催・運営してくださったスタッフの皆様ありがとうございました!


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

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

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

■開発環境はこちら

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

Ruby × jemallocのすすめ

$
0
0

集合知プラットフォーム事業部・エンジニアの榎本です。コロナ禍の運動不足を解消すべく筋肉体操で筋トレを続けてますが、上腕三頭筋がいい感じに成長しており継続の大切さを身に沁みて実感しております。

f:id:toshimaru-medpeer:20220113141210j:plain

目次

TL;DR(三行要約)

  • jemalloc でRubyアプリのメモリ効率改善
  • jemalloc でRubyアプリのパフォーマンス改善
  • jemalloc の導入も簡単

Rubyアプリケーションのメモリ肥大化問題

Ruby on RailsなどのRubyアプリケーションを運用する上で、メモリ使用量の肥大化に頭を悩ませた方は多くいらっしゃるのではないでしょうか。

下記は典型的なRailsアプリケーションのメモリ使用量のグラフです。メモリの使用量が対数関数のグラフのように時間とともに100%に近づいていく様子が見て取れます。

f:id:toshimaru-medpeer:20220112085652p:plain
Railsアプリケーションのメモリの使用率の増加

この問題の素朴な対処法としては、しきい値を定義して定期的にworkerプロセスを再起動してやることです。実際にそれを実現するためのgemがいくつか存在します。

しかし puma_worker_killerの README 冒頭で注意喚起されているとおり、頻繁な再起動はCPUリソースを消費させパフォーマンス劣化の要因にもなることから、あくまで応急処置であるべきです。メモリ肥大化の根本的な原因となっているコードがあるのであれば、それをきちんと調査し修正・対応すべきでしょう。

jemalloc を使ってみる

こういったメモリの肥大化・断片化がなぜ起こるのか、それにどう対処すべきかについては書籍・『Complete Guide to Rails Performance』(ちなみに本書は弊社で行っている分科会で取り上げた書籍の1冊です)の中で詳しく解説されています1

細かい話は本を読んでいただくとして、本書の中ではRubyアプリケーションのメモリ使用を効率化する方法として、メモリアロケーターを jemallocに切り替える方法が紹介されています。

前置きが少し長くなってしまいましたが、本記事では jemalloc を実際にプロダクション投入してみた結果とともに jemalloc について紹介したいと思います。

jemalloc とは?

jemallocとは Meta(旧・Facebook)社が中心となって開発されているメモリアロケーターです2。その特徴として、メモリの断片化の回避とスケーラブルな並行性サポートを謳っています。

github.com

jemalloc で改善するのか?

気になるのは jemalloc 導入によって実際にメモリ使用率は改善するのか?というところです。

検索してみるとすぐに jemalloc 導入で実際にメモリの使用量が改善したという事例がいくつか見つかりました。

またこちらのベンチマークによると、メモリだけではなくパフォーマンスも10%程度 jemalloc によって向上することが示されています。

CRuby 2.5.0 jemalloc tcmalloc increase w/ tcmalloc increase w/ jemalloc
Median Throughput 175.13 req/sec 197.49 req/sec 183.33 req/sec 4.68% 12.77%

jemalloc の設定方法

Rubyはデフォルトのメモリアロケーターとして、 glibc malloc を使います。ではどのようにメモリアロケーターをデフォルトから jemalloc に切り替えることができるのでしょうか?

--with-jemallocオプションをつけてRubyをコンパイルする方法もありますが、一番手軽な方法は 環境変数 LD_PRELOADを設定することです。このLD_PRELOADに jemallocのsoファイルのパスを指定してやればOKです。

具体例を示しましょう。下記は alpineベースのRuby dockerイメージにおける jemalloc 設定方法になります(マルチステージビルドを使って jemalloc のインストールを行っていることに注意してください)。

FROM ruby:X.X.X-alpine as base
...
FROM base as jemalloc
RUN wget -O - https://github.com/jemalloc/jemalloc/releases/download/5.2.1/jemalloc-5.2.1.tar.bz2 | tar -xj && \
    cd jemalloc-5.2.1 && \
    ./configure && \
    make && \
    make install
...
COPY --from=jemalloc /usr/local/lib/libjemalloc.so.2 /usr/local/lib/
ENV LD_PRELOAD=/usr/local/lib/libjemalloc.so.2
...

jemalloc インストール後、ENV命令にてLD_PRELOADにsoファイルのパスを指定しています。この状態でRubyアプリケーションを起動すれば、Rubyのメモリアロケーターは jemalloc に切り替わります。

ただ、この環境変数はDockerイメージ全体のソフトウェアに影響するグローバルな設定なので、設定して問題ないかはきちんとステージング環境などで動作確認・検証しましょう。

jemalloc をプロダクション導入してみた結果

実際に本番環境で稼働するRailsアプリケーションに jemalloc を適用させてみました。その結果をご紹介します。

下記は sidekiq (v6.3)の jemalloc 導入前後一週間のメモリ使用率の比較です。青い線が導入後のメモリ使用率、点線が導入前のメモリ使用率となっております。

f:id:toshimaru-medpeer:20220112092451p:plain
sidekiq jemalloc化 before(点線)/after(青線)

少しわかりにくいのですが、平均して5%程度メモリの使用率が改善したことが確認できました。しかし、先に紹介した改善事例のようにグラフにはっきりと改善が現れると期待していたので、正直なところ少し期待外れ感は否めませんでした。

パフォーマンスについてはどうでしょうか? 下記はRailsアプリケーション(Rails v6.1, Webサーバーはunicorn)のレイテンシーのグラフです。上から順に p99, p95, p90のレイテンシーとなっています。

f:id:toshimaru-medpeer:20220112092844p:plain
Railsアプリケーションのjemalloc化 before/after

こちらはグラフにはっきりとした改善(10-20%程度のレイテンシーの改善)が現れました。Dockerfileを少しイジっただけでこれだけのパフォーマンスが改善したのは、期待以上の結果と言えます。

まとめ

Rubyのメモリアロケーターを jemalloc に切り替えることで、アプリケーションコードの変更なしにメモリ使用およびパフォーマンスを改善できました。

導入もさほど難しくないので、Rubyアプリケーションのメモリおよびパフォーマンスにお困りの方は一度試してみてはいかがでしょうか。

おまけ:jemalloc についてMatzに聞いてみた

弊社の技術アドバイザリーとしてMatzさんがおりますので、Matzさんにもjemalloc について見解を伺ってみました。

Q. jemalloc コアチーム的にどう考えている?

  • どのアロケーターでパフォーマンス向上するかは、アプリケーションの特性次第。一概にどれがいいとは言えないと思う
    • jemalloc に変える提案も来た3が、Rubyとして取り込む予定はない
    • LD_PRELOADでメモリアロケーターを変更できる口は用意してあるので、変更したい場合はそちらを使ってほしい
  • Ruby として jemalloc を推奨することはしないが、Railsアプリケーションでメモリがボトルネックになりやすいということは理解しているので、改善は進めている。すでに入れた変更だと下記のようなもの。
    • 世代別GC
    • インクリメンタルGC
    • Object Compaction
  • Rubyとして推奨はしないがコミュニティの中で jemalloc のほうが良さそうだといった知見が公開されるのは大歓迎である

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

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

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

■開発環境はこちら

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

Ruby v3.1.0のSegmentation faultに遭遇した話

$
0
0

こんにちは。サーバーサイドエンジニアの三村です。

保険薬局と患者さまを繋ぐ「かかりつけ薬局」化支援アプリ kakariやその姉妹サービスである患者接点を資産化する診療予約システム かかりつけクリニック支援サービス kakari for Clinicの開発を担当しています。

目次

はじめに

kakariをRuby v3.1.0にアップグレードする作業をしていたところSegmentation faultに遭遇したので、bugs.rubyへの報告や再現コード作成などの経緯をまとめました。

賢い方法では無い部分も多々ありますが、温かい目で見守ってください。

bugs.rubyのissueはこちらです。

bugs.ruby-lang.org

f:id:t_mimura:20220119185723p:plain
bugs.ruby issue

※ Ruby本体の実装の話は出てきません。

現象

kakariでRuby v3.1にアップデートをしたら、CircleCIで実行しているRSpecでたまにSegmentation faultが発生するようになりました。

f:id:t_mimura:20220119185413p:plain
CircleCIログ

以下のような状態でした。

  • ActiveDecoratorの内部処理で発生
  • 複数回発生
  • エラー箇所は1箇所ではない
  • 再実行すると直る

bugs.rubyに報告

何はともあれ、Segmentation faultはRubyのバグなので報告します。 が、原因分からず再現コードも全然整理出来ていなかったので報告を躊躇いました。 そんなときに便利なruby-jp Slack

f:id:t_mimura:20220117233548p:plain
ruby-jp Slackでの相談の様子

温かく受け入れてくれそうだったので、安心してissueを作成することが出来ました。https://bugs.ruby-lang.org/issues/18489

issue起票当時は再現コードを整理する余裕がなかったので少しでも情報量を多くしようと思い、遭遇したSegmentation faultのログを複数個添付しました。

原因究明までの道のり

環境依存の問題かどうかを切り分け

これまでCircleCIでのみ再現していたので別の環境でも発生するのかをまず切り分けました。

ローカル開発環境(Docker Desktop for Mac)で複数回RSpecを全件実行し、無事に?再現することが確認出来ました。 Docker Image( cimg/ruby:3.1.0-browsersを利用 )依存ならまだしもCircleCI環境依存だった場合は原因究明までの試行錯誤が恐ろしく大変だったので一安心。

エラー発生ファイルの特定

特定のrequest specで再現することがすぐに判明出来ました。 が、このrequest specを単体で何回も実行しても再現しないことからファイルの読み込み順依存の問題であることも同時に分かり悲しみ。(ファイルの読み込み順問題よくありますよね)

specの実行順をランダムから定義順に変更

specは config.order = :randomが指定されランダム順に実行されるようになっていたので、これを definedに変更し読み込み順を定義順にしました。 この状態でも本問題が再現することが確認できたのはかなりラッキーでした。 「randomの場合でのみたまに遭遇」みたいな状態だったらseed固定などもう一工夫必要になり面倒で諦めていたかもしれません。

因果関係のあるテストを特定

ここからは地道な試行錯誤の繰り返しです。

以下のspec群から本問題に関係のあるものを特定していきます。

$ tree -L 1 spec/
spec/
├── controllers
├── decorators
├── helpers
├── lib
├── mailers
├── models
├── push_notifiers
├── requests
├── serializers
├── system
├── uploaders
├── validators
└── workers

まずは試行時間の短縮の為に重たいspecを除外を試みました。 特にテスト数の多いmodelsと一つ一つが重たいsystemsを除外しました。 幸いこれらを除外しても変わらずSegmentation faultは再現できたのもラッキー。

脱Docker Desktop for Mac

この後もひたすらspecを実行しまくることが想像できたので少しでもspec実行時間を短くするように先に工夫しました。

kakariではDocker Desktop for Macを利用してローカル開発環境を構築しています。 が、これは色々な要因で重いことが有名ですね(詳細は割愛)。

ということで、脱Docker Desktop for Macを試みました*1。 (恒例の)mysql2のbuildでエラーになるなどちょいハマりポイントはあったものの、すんなり対応出来ました。 真面目に計測したわけではないですが、1.5~2倍くらい早くなった気がします。(かなりうろ覚えなので気になる方はご自身でお確かめください。)

MySQL -> SQLiteに変更

次にMySQLからSQLiteに変更をしました。

本事象は十中八九データベースは無関係だろうと予想していました。 MySQLが重たいわけではないですがSQLiteにすることでインメモリーなデータベースを利用することができる ( database: ':memory:'こんなやつ ) のでspecの並列実行が容易になりました。

基本的にDBの差異はActiveRecordが吸収しているのでadapterを切り替えるだけで済みました。 一部、外部キー制約やindex周りの挙動の違いはあったものの取り上げるほどのものはありませんでした。 (というか本事象と無関係だろうと思い脳死でコメントアウトしたりして対応していました。)

↓はイメージですが、こんな感じの頭の悪い方法でspecを並列実行することが出来ました。

f:id:t_mimura:20220117234303p:plain
spec並列実行の様子
※ iTerm2の画面分割と「Broadcast Input to All Panes in Current Tab」を利用

再現コードの特定

上記の工夫のおかげもあり、因果関係のあるファイルを数個に特定することができたのでミニマムな再現コードの調査に切り替えました。 特に「request specでのみ再現」という状況が色々と面倒だったので何とかシンプルなRubyスクリプトコードを書けないかを模索。

数行のRubyスクリプトコードで再現したりしなかったりする状態までたどり着いたものの、再現有無の条件が全く分かりませんでした。 が、唐突に「GCか?」と思いついたため試しに GC.startを差し込んでみたところめでたく再現しました。

ここから先は簡単で、ActiveDecoratorを利用して書かれた再現コードをplainなRubyコードに変換するだけです。 ActiveDecoratorのコードは予め目を通していたお陰でここは数分で出来ました。

その結果出来た再現コードがこちらです。

M = Module.new
Object.new.extend(M)
GC.start
M.include(Module.new)

二日くらい費やした結果、美しく短い再現コードを作れた時は若干の感動がありました。

再現コード報告

早速「再現コード作れたよ」と追加報告です。

「ActiveDecoratorでSegmentation fault発生」という状況から一気にシンプルな再現コードに飛躍してしまったので「ActiveDecoratorをこんな感じに使ったら再現するよ」というのを添えてコメントしたのは我ながら親切ポイントです。

そして、再現コードをコメントしてから僅か2時間足らずでn0kadaさんが修正PRを作成してくれました。(流石だ)

修正確認

その後、issue上で 「Does https://github.com/ruby/ruby/pull/5455 fix it?」と聞かれてしまったので確認するしかありません。

Rubyをcloneして自前でビルドするのは初めてだったので不安がありましたが、実際にはruby/rubyのREADME手順通りにコマンド打つだけで大きなハマりどころもなくすんなり出来ました。 強いて言えばopensslのパス設定が必要だったかな程度です。あまり覚えていないくらい些細な問題でした。

上記の「脱Docker Desktop for mac」をしておいたのも地味に良かったです。

ruby-jpで相談していたところ、 k0kubunさんのブログを紹介してもらいとても参考になりました(ありがとうございます!!) ※ 特に「ビルドしたrubyをrbenvから使うには」の所は便利

というわけで、大した苦労もなく無事に修正確認できました。

work around

無事に修正PRもマージされたとは言え流石にheadなrubyコードを利用するわけにはいかないので別途回避する方法を模索します。

これに関しては再現コードの特定が既に出来ているため簡単な話で、以下のような(不適切な形で)ActiveDecoratorを利用している箇所の修正するだけで済みました。

let!(:user) do
-  build(:user).extend UserDecorator+  ActiveDecorator::Decorator.instance.decorate(build(:user))
end

本事象が発生していたのはspecのみで、実際のプロダクトコードでは同様の事象はなさそうでした。

まとめ

  • Rubyを触り始めて6年近く経ちますが、初めて Ruby本体にまともな貢献が出来たかなと実感できとても良い経験でした。
  • ruby-jp Slackで気軽に相談できたのがとても有り難かったです。感謝感謝です。
  • 自分のコミットではないにしろ、ほぼ同等のものが「テストコード」としてRubyに入ったの嬉しい。
  • 褒められたの嬉しい。
    f:id:t_mimura:20220117235023p:plain
    ruby-jp Slackで褒められた様子

おまけ

無事にkakariはRuby v3.1.0にアップグレードすることができました。 本件は色々とありましたが、それ以外に必要な対応は殆どなくRubyの後方互換性の高さに感謝です。

以上です。 もし「Segmentation faultに遭遇して困った」なんて時に本記事が参考になれば幸いです。


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

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

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

■開発環境はこちら

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

*1:今回の調査中だけの話です


The Complete Guide to Rails Performance読書会をしました

$
0
0

こんにちは。メドピアのお手伝いをしています@willnetです。最近車を買いました。これまでペーパードライバーだったので自信を持って運転できるように運転の練習を頑張っています。

今日は社内読書分科会で読んだThe Complete Guide to Rails Performanceという本の話を書きたいと思います。社内読書分科会って何?という人はこちらのエントリを参考にしてください。

tech.medpeer.co.jp

The Complete Guide to Rails Performanceとは

タイトルの通り、Railsアプリケーションのパフォーマンスを向上させるための知識やテクニックについて書かれている書籍です。書いている人は Nate Berkopecさんで、パフォーマンスに関するコンサルやワークショップをしているようです。パフォーマンスに関するメールマガジン*1の発行もしています*2

目次

こちらのgistから引用します。

  • An Economist, A Physicist, and a Linguist Walk Into a Bar...
  • Little's Law (+ screencast)
  • The Business Case for Performance
  • Performance Testing (+ screencast)
  • Profiling (+ screencast)
  • Memory - How to Measure (+ screencast)
  • Rack Mini Profiler (+ screencast)
  • New Relic
  • Skylight
  • Optimizing the Front-end
  • Chrome Timeline (+ screencast)
  • The Optimal Head Tag (+ screencast)
  • Resource Hints (+ screencast)
  • Turbolinks and View-Over-The-Wire
  • Webfonts (+ screencast)
  • HTTP/2 (+ screencast)
  • JavaScript
  • HTTP Caching (+ screencast)
  • Memory Bloat (+ screencast)
  • Memory Leaks (+ screencast)
  • ActiveRecord
  • Background Jobs
  • Caching
  • Slimming Down Your Framework (+ screencast)
  • Exceptions as Flow Control
  • Webserver Choice
  • Idioms - writing faster Ruby
  • Streaming
  • ActionCable
  • CDNs (+ screencast)
  • Databases
  • JRuby
  • Memory Allocators
  • SSL (+ screencast)
  • Easy Mode Stack - "What stack should I choose?"
  • The Complete Checklist - a 75+ item checklist for Ruby/Rails apps.

良かったポイント

Railsでアプリケーションを開発するときに知っておくと良い知識や気をつけるべきポイントについて網羅的にまとめられています。特にRailsアプリケーションだけの話にとどまらず、フロントエンドやCDNなどのwebアプリケーションを構成する要素全体について記述されているのが良いところだと感じています。ユーザが感じるwebアプリケーションの速度はサーバサイドのレスポンス速度だけではなくレイテンシやフロントエンドも含めた総合的な体験であり、通常の構成のwebアプリケーションではアセット類を取得したりパース、実行する時間のほうがユーザが待つ時間の大きい比率を占めるのでまずそちらを見てボトルネックを見つけると良い、というのは言われるとそうですね、となるのですがRailsエンジニアとしてはまずスロークエリとかN+1とかに目がいってしまうのですよね…。

一定規模以上のRailsアプリケーションを運用していると、アプリケーションサーバやワーカのメモリが急に増える現象に遭遇することがあります。そうなったときになにが原因なのかよくわからないのでとりあえず再起動しよう、とpuma_worker_killersidekiq-worker-killerなどのツールを使ってお茶を濁すことも多いです。この本ではお茶を濁さずに、原因をメモリリークなのかメモリ肥大化なのかを切り分けるところから、具体的にどうやって調査、解決していったり良いのか方針を提示してくれています。

最近 メドピア内でjemallocを採用した記事が公開されましたが、この本ではjemallocとそれ以外のメモリアロケータ (tcmallocHoardについてベンチマークが取られておりjemalloc以外の選択肢について考えさせられました(とはいえ採用事例の多いjemalloc側に寄せておくのがいいのかな、というのが現時点での個人的な見解です)。

などなど。もちろんActiveRecordで効率的なクエリを書くのはどうしたらよいか、とかキャッシュの使い方など基本的なトピックも抑えています。洋書を事前に予習しておくというのがハードル高く大変でしたが、それを超えたメリットがある本です。

良くなかったポイント

良いことばかり書くと信憑性に欠けそうなので、良くなかったポイントについても書いておきます。

何度か改訂はされていますが初出が2016年なので、当時の状況と現在の状況を読み替えないといけない箇所があります。具体的にはHTTP/1.1が広く使わている前提になっていてjsを結合して配信しましょう、という文章があちこちにあります。2022年現在ではHTTP/2が広く使われており、jsを結合する必要は薄れているのでそこを意識しつつ読み進める必要があります。

あとはとても細かい点です。HTML, PDF, epub, mobi, txt など複数のフォーマットで本が読めるのは良いのですが、PDF, epub, mobiで読むと横に長いソースコードが全部掲載されていない(右側が切れてしまう)ことがあってtxtなどを参照しにいかないとソースコードを確認できなくてつらい、となりました。あとは編集ミスのような文章の構成(2章分の全然別のトピックが1章にまとめられてる)があるので注意が必要です。

まとめ

The Complete Guide to Rails Performanceの紹介でした。Railsエンジニアでパフォーマンスに興味のある人であれば広くおすすめできる本ですのでよければ読んでみてください!


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

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

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

■開発環境はこちら

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

*1:ここから購読できます

*2:参考になる知見が多いので、僕はずっと購読しています

ECR拡張スキャンでRailsアプリを診断した際の脆弱性警告(偽陽性)への対策

$
0
0

皆様こんにちは、サーバーサイドエンジニアの草分です。 最近ポケモン最新作を買ってしまったのでひたすら野原でボールを投げ続ける日々を送っています。

さて本題に入りましょう。

Amazon ECRには、pushしたコンテナイメージへのイメージスキャン(脆弱性診断)機能があります。

Image scanning - Amazon ECR

メドピアではこれを利用して全社横断的にアプリの脆弱性の検知および可視化を行っています。

f:id:yuyakusawake:20220131191845p:plain
脆弱性が検知された場合の表示

この記事では、Railsアプリをイメージスキャンした際の【偽陽性】警告の問題と、その解決策について紹介いたします。

問題

ECRのイメージスキャンには「基本スキャン」「拡張スキャン」の2種類があり、この内の拡張スキャン(Enhanced scanning)では、Rubyのgemやnpmのパッケージなども診断の対象となります。
Gemfile.lockに脆弱性のあるライブラリの記載があった場合でも、拡張スキャンを使えば自動検知&一覧化することができます。

ただし現時点(2022/01)ではアプリが使用するGemfile.lockだけでなく、インストールしたgemのディレクトリまで無差別に診断してしまうようです。

f:id:yuyakusawake:20220130162016p:plain

Rubyのgemは.gemspecファイルの記載に従って動作するため、gemパッケージ内にGemfile.lockが含まれていても通常は使われることはありません。

しかしそんなことはお構いなしに脆弱性警告を発してくるため、使っているgemによっては身に覚えのない警告を大量に受け取ることになるでしょう。困りましたね。

Inspector側でなんとかして欲しいという思いもありますが、この問題に対して以下の対策を実施しました。

対策

gemのパッケージからGemfile.lockを除外する

冒頭のケースでは、gem内にサンプルアプリ(Gemfile.lock入り)が含まれていたため警告対象となっていました。

こういった不要なGemfile.lockについては、gemから除外してしまってもよいでしょう。
.gemspecファイルのspec.filesから除外することで、gemパッケージから除外することが可能です。

こちらについてはgem側にPRを送って対処しました。
Exclude sample directories from spec.files by lnit · Pull Request #12 · k0kubun/rack-user_agent · GitHub

迅速にマージ&リリースいただけたので非常に助かりました。ありがとうございました。

リリース後に対象gemをアップデートすれば警告除去完了です。

Dockerイメージから不要なGemfile.lockを除外する

他にも複数のgemが警告対象となっていたのですが、gem側への対応はいつリリースされるか分からず待ち状態となってしまいます。

そこで、アプリのDockerイメージから使用していないGemfile.lockを一括削除することとしました。

RUN bundle install
RUN find$(gem environment gemdir)-name"*.lock" | xargs rm-vf

といっても、上記のようにbundle install後にファイルを削除するのみです。Dockerfileごとに追加する必要はありますが、ひとまずこの対応で警告は抑制できました。

うっかりアプリで使用するGemfile.lockを消さないように削除対象には注意しましょう。

まとめ

自動検知ゆえの困ったことはいろいろありましたが、手動で脆弱性やバージョンのチェックを逐一していくのはさすがに無理があります。うまくAmazon Inspectorを手懐けて、セキュアな生活を送っていきましょう!

補足: 採用しなかった対策

forkしたgemを使う

gemをforkしてspec.filesを変更したものをインストールする方法も試してみましたが、gitからインストールしたgemはリポジトリ内の全ファイルがイメージ内に入ってしまうようで、うまくGemfile.lockが除去できませんでした。

forkしたgemを手元でビルド→アプリ内に取り込む という手順を踏めば除去できますが、手間が大きいため没としました。

AWS側で警告を抑制する

Amazon Inspector側にも警告の抑制機能はあるようです。
Suppression rules - Amazon Inspector

しかし、さすがに特定ディレクトリの警告を抑制する設定はなく、今回の問題には対処できませんでした。


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


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

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

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

■開発環境はこちら

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

Google ドキュメントを会議メモとして使う

$
0
0

こんにちは。サーバーサイドエンジニアのティーチです。最近の趣味は生後半年の娘にウケる動作の探求です。直近だと背筋が手応えありました。髪の毛がふわふわするのが面白かったようです。

本記事について

f:id:taichisato:20220304134701p:plain

■書く

  • Google ドキュメントを会議メモとして使うときの設定
  • Google ドキュメントを会議メモとして使う提案をする方法例

■書かない

会議メモを作る意義に触れると本記事の趣旨(Google ドキュメントの具体的な設定の仕方、使用の提案の仕方)以外の部分が長くなるので以下は割愛します。

  • 会議においてアジェンダ、議事録を作る意義
  • Google ドキュメントって何?

対象

  • 会議でアジェンダ、議事録を使いたいなと思ってる方
  • よその会議の仕方知りたいなという方

Google ドキュメントの強み

私のプロジェクトではオンライン会議をするためのツールとしてGoogle Meetを用いています。
そして、会議を入れる時はGoogle カレンダーに予定を入れるようになっています。

Google カレンダーには会議メモという機能があり、予定からGoogle ドキュメントを作成することが容易です。
作成後は予定にGoogle ドキュメントへのリンクが作られるので会議参加者への共有も容易です。

作成前 作成後
f:id:taichisato:20220304132153p:plainf:id:taichisato:20220304132217p:plain

f:id:taichisato:20220304132235p:plain

※2022/3/3時点 生成場所がマイドライブ固定のようなので共有ドライブに移動させるか、「共有」から編集権限を付与する必要があります。

作成、紐付けが容易な上
機能的には画像の挿入、見出し、同時編集機能ができるので良きです。

具体的な使い方

ファイル >ページ設定 から「ページ分けなし」に設定します。(この機能を紹介するためにこの記事書きました!) f:id:taichisato:20220304132252p:plain

ページ設定分けなしにすることで、なんと!例のあの空白をなくすことができます!ステキ! f:id:taichisato:20220304132303p:plain

f:id:taichisato:20220304132317p:plain

そして、1. 左上の灰色の部分をクリックしてテキストの幅を幅広にします。

あとは会議の内容に応じて構成を作る

f:id:taichisato:20220304132329p:plain

見出しは「見出し3」推しです。(MacならCommand + option + 3)

Google ドキュメントを会議メモとして使う提案をする方法例

私はすでに登録してある会議に対して上記のような会議メモを作って例えば以下のように共有しています。

前提

私はスギサポdeliというプロジェクトに携わっており、メンバーは本サービスを「deli」と呼称しています。

最近具だくさん食べるスープのBセットが出ました。私もまだ食べてないのですが、AセットはかなりおいしかったのでBセットもぜひ!(宣伝)

deliでは GitHubのプロジェクトボード を画面共有して会議していました。

Slackで投稿した文(ポイントがわかりやすいように修正しています)

Taichi Sato(teach)

deliの週次定例は内容充実してて良いのですが、
事前準備やissue外のトピックについての話し合いがやりづらいので、  .. (1)
マイブームの会議メモを作りました。.. (2)

[会議メモのURL]

deli projectとの2画面を画面共有するのはやりにくい部分あるかもしれませんが、
とりあえずやってみたいです! ..(3)

ポイント

(1) 会議メモがあると良い理由を一行で書く。

長いと読みづらいため。提案する相手によって詳細度は変えても良いと思います。

(2) 提案する人が会議メモのたたき台を作る。

理想的には、会議を招集する人が会議メモ、アジェンダ、構成全部つくるのが一番早いと思います。
しかし、登録された会議に対して
「アジェンダ作ってもらっていいですか?」
よりも
「会議メモとアジェンダ作りました!XXとYYはわからないので追記お願いしたいです!」
といった一緒に会議スタイルを作り上げていく方が現実的には効率よいのではないかなと私は思います。

(3) とりあえず一回!

「習うより慣れよ」
実際に会議メモを使って会議してみて、よかったら継続、合わなかったらやめれば良い。 そのくらいライトに提案した方が、提案された側も「とりあえずなら..」ってなる気がします。

deliでは今後も継続してGoogle ドキュメントを使用して会議をすることになりました。

まとめ

ページ分けなし設定ステキなのでぜひ広めて欲しい..!
Google ドキュメントの使用の提案方法が参考になれば幸いです!


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


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

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

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

■開発環境はこちら

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

Vue.js 公式ドキュメントのモブ翻訳をやりました!

$
0
0

こんにちは!
週一のサウナは欠かさない、フロントエンドエンジニアの土屋です。

先日、Vue.js の公式ドキュメントがリニューアルされました。
Vue.js 日本ユーザーグループ主導で翻訳プロジェクトが立ち上がっているのはご存知でしょうか?

メドピアでは、普段お世話になっている Vue.js に貢献したいという思いから、数回に渡って、Vue.js 公式ドキュメントのモブ翻訳を行ないました。

モブ翻訳の方法

  • Google Meet でファシリテーターの画面を共有しながら、Vue.js 公式ドキュメントの翻訳を行う
    • OSS へのコントリビュートに慣れていない方は、ファシリテーターが共有した画面を見ながら一緒に作業する。
    • 不明点がある場合、適宜ボイスやテキストチャットで相談しながら行う。
  • OSS へのコントリビュートに慣れてる方は、自分のペースでもくもくと作業する。

モブ翻訳の流れ

ここからは実際のモブ翻訳の流れとともに、その様子を紹介します。

まず、リポジトリの READMEVue.js 公式サイト日本語翻訳ガイドを確認します。
翻訳の方法や注意点が記載されているので、必ず目を通しましょう。

その後、翻訳するページを決めます。
Vue.js ビギナーの参加者が多かったので、Tutorialを翻訳することにしました。 翻訳を通して、Vue.js の知識を獲得するのが狙いです。

GitHub Issuesで翻訳タスクが管理されているので、翻訳するページが決まったら、Issue に翻訳する旨をコメントします。
Tutorial を翻訳するので、Tutorial 翻訳まとめという Issue にコメントしました。

f:id:doyahiro:20220330165158p:plain
弊社のメンバーがこぞってコメントする様子

その後、リポジトリをフォークしてローカル開発環境を構築します。
ローカルで立ち上げることに成功したら翻訳開始です!

f:id:doyahiro:20220330165401p:plain
モブ翻訳中の様子。Google Meet で画面を共有しながらワイワイ。テキストチャットも盛んです。モブ翻訳だと、ここはこう訳した方が良いなど、色々な人の意見を聞けるのがありがたいですね。

訳し方に迷ったら Wikiをチェックしましょう。よくあるNGが記載されています。
また、Vue2 の公式ドキュメントではどのように訳されているか確認するのも有用でした。

訳し方に自信が持てない箇所は DeepLを使いました。

f:id:doyahiro:20220330165552p:plain
DeepL は強力ですが、意訳になりすぎたり、文中、文末の : (コロン) が削除されることがあるので注意が必要です。頼りきりにならないようにしましょう。

翻訳が完了したら、フォークした自分のリポジトリにプッシュします。
その後、フォーク元のリポジトリに Pull Request を出します。

f:id:doyahiro:20220330165703p:plain

PRを出すと、メンテナーの方がレビューしてくれます。
修正箇所がある場合は修正して再度コミットします。

f:id:doyahiro:20220330165735p:plain

問題がなければメンテナーの方がマージしてくれます。

f:id:doyahiro:20220330165813p:plain
晴れてマージされました。やったね!

これで一つの翻訳タスクが完了です。

f:id:doyahiro:20220330165902p:plain
初めてのOSSへのコントリビュートに喜びを隠しきれない様子

この流れで数回モブ翻訳を行い、弊社の社員で Tutorial を全て翻訳することができました!

まとめ

OSSにコントリビュートするのが初めての参加者が多かったので、リポジトリをフォークしてPRを出すといった、一般的なOSSへのコントリビュートの流れを体験できたのは良い経験になったと思います。

OSSにコントリビュートしてみたいけど、本体のコードに手を入れるのはハードルが高いと感じている方は、まずドキュメントの翻訳からトライしてみるのはいかがでしょうか。

普段使っているOSSには、今後も積極的にコントリビュートしていきたいですね。


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


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

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

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

■開発環境はこちら

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

SwiftUI / UIKit (Storyboard) ハイブリッド対応、Needle + RIBs インスパイアな iOS アプリケーションデザイン

$
0
0

こんにちは、モバイルアプリを開発しています高橋です。交互に仕事場に猫二匹がやってきて監視されながら仕事しています。

先日リリースしたとある iOS アプリは、

  • 機能は機能ごとに分割して実装したい
  • 依存解決のコードは自動生成したい
  • ライトウェイトな設計としたい

というコンセプトの元、コンパイルセーフな DI フレームワークの uber/needleを使い、uber/RIBsのようなアプリケーションアーキテクチャでデザインすることで、各コンポーネントをコンパクトに分割することができました。

Needle や RIBs が前提知識となります。そのため本記事ではざっと Needle と RIBs を解説したのちに、具体的なコードを交えて SwiftUI + UIKit (Storyboard) ハイブリッド対応でかつ Needle + RIBs インスパイアなアプリケーションアーキテクチャの一端を解説してみます。

動作環境

  • Xcode 13.2
  • Swift 5.5
  • macOS 12 Monterey (macOS 11 BigSur でも確認済み)

Needle

まず、簡単に Needle を説明します。

Needle は各モジュールをコンポーネントとしてツリーで表現し DI することができるものです。

コードジェネレーターの cli ツール→①と、ジェネレートされたコードを引き回すライブラリ→②がセットになっており、①をビルド時に実行するようにして使います。

Swift Package Manager でも Carthage でも CocoaPods でも使うことができます。ここではサクッと①を Homebrew 、②を Xcode の Swift Package Manager で入れてみます。

①Needle コードジェネレータ

% brew install needle

次に、Build Phase で Compile Sources の前に以下を設定します。

mkdir"$TEMP_DIR"touch"$TEMP_DIR/needle-lastrun"SOURCEKIT_LOGGING=0 needle generate $SRCROOT/アプリフォルダ名/Needle.swift アプリフォルダ名

※Carthage で入れた場合はチェックアウトディレクトリの中にあるので、それ (Carthage/Checkouts/needle/Generator/bin/needle) を実行します。

ビルドしてみると、registerProviderFactories()という空のメソッドが生えている Needle.swiftファイルができます。このメソッドは後ほど使用します。

Needle.swiftファイルを .xcodeproj に入れたら、下準備完了です。

コラム: SOURCEKIT_LOGGING=0

SourceKit ロギングをサイレントモードにしています。 通常 Xcode はログを表示しますが、Xcode で作業している際のノイズを減らすために入れています。

②Needle ライブラリ

ライブラリの方もプロジェクトに入れます。Xcode → File → Add Packages... より Needle のレポジトリ(https://github.com/uber/needle.git)を指定して入れます。

Needle を Swift Package Manager で入れる
Needle を Swift Package Manager で入れる

Needle の使い方の簡単な説明をするとこのようになります。

  • コンポーネントをツリー構造にします。ルートだけ BootstrapComponentのサブクラスにします。
  • 親コンポーネントで定義したコンポーネントを Dependency にするので、上位コンポーネント(ルートとか)で諸々注入します。
  • 子コンポーネントでは親で注入されたものを protocol Dependencyで定義することで利用することができます。
  • 子コンポーネントは Component<***Dependency>のサブクラスにします。

Needle を使ったサンプル

雰囲気を掴むために、雑ですがコードを示します。

RootComponent.swift

import NeedleFoundation

finalclassRootComponent:BootstrapComponent {
    varpoint:Int {
        return100
    }
    varfeatureABuilder:FeatureABuilder { FeatureAComponent(parent:self) }
}

FeatureAComponent.swift

import NeedleFoundation

protocolFeatureADependency:Dependency {
    varpoint:Int { get }
}
protocolFeatureABuilder {
    funcshowPoint()
}
classFeatureAComponent:Component<FeatureADependency> {
}
extensionFeatureAComponent:FeatureABuilder {
    funcshowPoint() {
        print(dependency.point)
    }
}

これで子の FeatureAComponentの中で、親の RootComponentで定義した pointにアクセスすることができています。Component<***Dependency>の中で dependencyというプロパティが生えているので、これ経由でアクセスすることができます。

  • 注:***Builderという protocol を登場させています。 Componentの protocol として定義しています。(後述)

AppDelegateあたりで先ほど触れた registerProviderFactories()を呼び出すことで Needle との接続を行います。

AppDelegate.swift

import UIKit
@mainclassAppDelegate:UIResponder, UIApplicationDelegate {
    private(set) varrootComponent:RootComponent!funcapplication(_ application:UIApplication, didFinishLaunchingWithOptions launchOptions:[UIApplication.LaunchOptionsKey: Any]?) ->Bool {
        registerProviderFactories() // Needle.swift で定義されている
        rootComponent = RootComponent()
        returntrue
    }
    // MARK: UISceneSession Lifecyclefuncapplication(_ application:UIApplication, configurationForConnecting connectingSceneSession:UISceneSession, options:UIScene.ConnectionOptions) ->UISceneConfiguration {
        return UISceneConfiguration(name:"Default Configuration", sessionRole:connectingSceneSession.role)
    }
    funcapplication(_ application:UIApplication, didDiscardSceneSessions sceneSessions:Set<UISceneSession>) {}
}

RootComponentを経由して featureAComponentを取り出し、そのメソッドを呼び出して挙動を確認します。

SceneDelegate.swift

import UIKit
classSceneDelegate:UIResponder, UIWindowSceneDelegate {
    varwindow:UIWindow?funcscene(_ scene:UIScene, willConnectTo session:UISceneSession, options connectionOptions:UIScene.ConnectionOptions) {
        guardlet_= (scene as?UIWindowScene) else { return }
        letrootComponent= (UIApplication.shared.delegate as!AppDelegate).rootComponent
        letfeatureABuilder= rootComponent!.featureABuilder
        featureABuilder.showMoney()
    }
}

実行結果:

100

雰囲気だけで申し訳ありませんが、 Needle は以上のような感じで (point が Component になった時とか、孫 Component を持った時なども同様に) 自動で DI することができます。

RIBs

今回のプロジェクトはあくまでインスパイアされているだけなので実際は使用していませんが、軽くどういうものが触れておくと、こちらは VIPER や MVC のようなアプリケーションアーキテクチャであり、フレームワークでもあります。

実際に使用する際は、uber/RIBsに則ってプロジェクトにインストールします。Xcode テンプレートも利用することができ、スクリプトを実行して Xcode にインストールすることで新規コンポーネントを作成するときに自動で必要なファイルが生成されるようになっているため、効率的に開発が進められるとのことです。

RIB

RIBs は Router-Interactor-Builder(RIB) が中心となったデザインになっており、RIB 単位でコンポーネントとして分割します。画面であればこれに View を付け足します。

  • Router: RIB 間のやりとりを担当し、画面系の RIB であれば画面遷移なども担当します
  • Interactor: ビジネスロジックを担当します
  • Builder: Router / Interactor / View を作ります
  • View: データを表示したり、ユーザーインタラクションを担当します

出典: https://github.com/uber/RIBs/wiki/iOS-Tutorial-1

ルール

大枠のルールとしてはこのような感じです。

  • 別 RIB にアクセスする場合は、Router から別 RIB の Builder を参照する
  • 直接 View から Router にアクセスせず、Interactor で Router のメソッドへアクセスしてもらう

今回のアプリケーションアーキテクチャ(Needle + RIBs インスパイア)

ようやく本題です。

前述した Needle を使用し、RIBs の設計とルールを取り入れたアプリケーションアーキテクチャデザインになっており、Needle + RIBs インスパイアとこの記事では命名しております。(現場では特に名前をつけていません)

Needle + RIBs inspired App Architecture Example
Needle + RIBs inspired App Architecture Example

ベース部分の実装イメージ

あくまでイメージですが、RootComponent.swift や SceneDelegate.swift は以下のようになっています。

RootComponent.swift

import NeedleFoundation

finalclassRootComponent:BootstrapComponent {
    // DIvarapiClient:APIClient { APIClient() }
    varerrorLogger:ErrorLogger { ErrorLogger() }
    
    // 子コンポーネントvarfeatureABuilder:FeatureABuilder { FeatureAComponent(parent:self) }
    varfeatureBBuilder:FeatureBBuilder { FeatureBComponent(parent:self) }
}

SceneDelegate.swift

import UIKit
classSceneDelegate:UIResponder, UIWindowSceneDelegate {
    varwindow:UIWindow?funcscene(_ scene:UIScene, willConnectTo session:UISceneSession, options connectionOptions:UIScene.ConnectionOptions) {
        guardletwindowScene= (scene as?UIWindowScene) else { return }
        guardletappDelegate= UIApplication.shared.delegate as?AppDelegateelse { return }
        guardletrootComponent= appDelegate.rootComponent else { return }
        letfeatureABuilder= rootComponent.featureABuilder
        letwindow= UIWindow(windowScene:windowScene)
        self.window = window
        window.rootViewController = featureABuilder.viewController
        window.makeKeyAndVisible()
    }
}

AppDelegate.swift

import UIKit
@mainclassAppDelegate:UIResponder, UIApplicationDelegate {
    private(set) varrootComponent:RootComponent!funcapplication(_:UIApplication, didFinishLaunchingWithOptions _:[UIApplication.LaunchOptionsKey: Any]?) ->Bool {
        registerProviderFactories()
        rootComponent = RootComponent()
        returntrue
    }
    // MARK: UISceneSession Lifecyclefuncapplication(_:UIApplication, configurationForConnecting connectingSceneSession:UISceneSession, options _:UIScene.ConnectionOptions) ->UISceneConfiguration {
        UISceneConfiguration(name:"Default Configuration", sessionRole:connectingSceneSession.role)
    }
    funcapplication(_:UIApplication,
                     didDiscardSceneSessions _:Set<UISceneSession>) {}
}

(Needle の紹介をしていたときに、 Builderという protocol を急に登場させていましたが)RIBs で言うところの Builderとして Componentは振る舞ってもらう、という発想に基づいて Builderと命名し、これを使って初期の画面を作成しています。

このとき、Builderはよく build()と命名されているメソッドではなく、エンドポイントの内容を示す意味で viewControllerを持つようにしています。

コンポーネント部分の実装イメージ

実際のコンポーネントは Builder、Interactor、Router (、View) で構成するので、コンポーネントごとにそれらの protocol と実装を作成し、ファイル分割します。(今回は雑ですが、コンポーネントごとにファイル分割して以下に掲載してしまいます)

FeatureA コンポーネントの View (SwiftUI) から FeatureB コンポーネントの View (UIKit) を画面遷移するサンプルを作ってみました。

ちなみに Interactor は今回は Router へのただの橋渡し役になっています。(API 通信やデータベースアクセスなどデータ処理・ドメインロジックが入るときはここで対応する)

FeatureAComponent.swift

import NeedleFoundation
import UIKit
import SwiftUI

// MARK: -// MARK: BuilderprotocolFeatureADependency:Dependency {
    // 説明用にここで注入。ただしこのサンプルであれば、FeatureAComponent の中で生成してもよいvarfeatureBBuilder:FeatureBBuilder { get }
}
protocolFeatureABuilder {
    varviewController:UIViewController { get }
}
finalclassFeatureAComponent:Component<FeatureADependency> {}
extensionFeatureAComponent:FeatureABuilder {
    varviewController:UIViewController {
        letnavigationController= UINavigationController()
        letrouter= FeatureARouter(viewController:navigationController,
                                    featureBBuilder:dependency.featureBBuilder)
        letinteractor= FeatureAInteractor(router:router)
        letviewController= FeatureAViewController(interactor:interactor)
        navigationController.viewControllers = [viewController]
        return navigationController
    }
}

// MARK: -// MARK: RouterprotocolFeatureARouting {
    funcshowFeatureB()
}
finalclassFeatureARouter:FeatureARouting {
    privateweakvarviewController:UIViewController?privateletfeatureBBuilder:FeatureBBuilderinit(viewController:UIViewController, featureBBuilder:FeatureBBuilder) {
        self.viewController = viewController
        self.featureBBuilder = featureBBuilder
    }

    funcshowFeatureB() {
        guardletviewController= viewController else { return }
        viewController.present(featureBBuilder.viewController, animated:true)
    }
}

// MARK: -// MARK: InteractorprotocolFeatureAInteracting {
    funcshowFeatureB()
}
finalclassFeatureAInteractor:FeatureAInteracting {
    privateletrouter:FeatureARoutinginit(router:FeatureARouting) {
        self.router = router
    }
    funcshowFeatureB() {
        router.showFeatureB()
    }
}

// MARK: -// MARK: ViewfinalclassFeatureAViewController:UIHostingController<FeatureAView> {
    privateletinteractor:FeatureAInteractinginit(interactor:FeatureAInteracting) {
        letview= FeatureAView(interactor:interactor)
        self.interactor = interactor
        super.init(rootView:view)
    }
    @MainActor@objcrequireddynamicinit?(coder aDecoder:NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
structFeatureAView:View {
    letinteractor:FeatureAInteracting?varbody:some View {
        Button {
            guardletinteractor=self.interactor else { return }
            interactor.showFeatureB()
        } label: {
            Text("B 画面開けるかな 🤔")
        }.padding()
    }
}

FeatureB では View を Storyboard で作成しています。Storyboard の説明は割愛します。

FeatureBComponent.swift

import NeedleFoundation
import UIKit

// MARK: -// MARK: BuilderprotocolFeatureBDependency:Dependency {
}
protocolFeatureBBuilder {
    varviewController:UIViewController { get }
}
finalclassFeatureBComponent:Component<FeatureBDependency> {
}
extensionFeatureBComponent:FeatureBBuilder {
    varviewController:UIViewController {
        letrouter= FeatureBRouter()
        letinteractor= FeatureBInteractor(router:router)
        letstoryboard= UIStoryboard(name:"FeatureB", bundle:nil)
        guardletviewController= storyboard.instantiateInitialViewController() as?FeatureBViewControllerelse { return UIViewController() }
        viewController.interactor = interactor
        router.viewController = viewController
        return viewController
    }
}

// MARK: -// MARK: RouterprotocolFeatureBRouting {
    funcdismiss()
}
finalclassFeatureBRouter:FeatureBRouting {
    weakvarviewController:UIViewController?init() {
    }
    funcdismiss() {
        viewController?.dismiss(animated:true)
    }
}

// MARK: -// MARK: InteractorprotocolFeatureBInteracting {
    funcdismiss()
}
finalclassFeatureBInteractor:FeatureBInteracting {
    privateletrouter:FeatureBRoutinginit(router:FeatureBRouting) {
        self.router = router
    }
    funcdismiss() {
        router.dismiss()
    }
}

// MARK: -// MARK: ViewfinalclassFeatureBViewController:UIViewController {
    varinteractor:FeatureBInteracting?@IBActionfunc dismissButtonWasTapped(_ sender:UIButton) {
        interactor?.dismiss()
    }
}

Demo

Needle で SwiftUI の画面から UIKit (storyboard) の画面に遷移するデモの動画キャプチャ

SwiftUI 画面 → UIKit 画面への画面遷移ができました!

Demo ソースコード一式:

github.com

ちなみに

  • データモデル (Entity) などはこれらと別に structを定義し Needle の管理下とは関係なくあちこちで利用します。

まとめ

  • ライトウェイトなアーキテクチャである
  • コンパイルセーフでバシバシ DI できる
  • SwiftUI も UIKit でも柔軟に対応できる

よかったらお試しください!


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


メドピアでは一緒に働く仲間を募集しています。 iOS / Android のモバイルエンジニアも募集しています。カジュアル面談からでも OK です! ご応募をお待ちしております!

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

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

■開発環境はこちら

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

Feature Toggleを用いたRailsアプリの継続的なリリースと要注意事項

$
0
0

はじめに

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

突然ですが、開発者の皆様、実装したソースコードはこまめにリリースしていますか? 「大きい機能なので開発に時間がかかる」「障害が起きないよう念入りにテストする必要がある」などの理由で、Featureブランチのままコミットグラフが伸びに伸びたりしていませんか?

大きな機能を作ること自体は悪いことではありませんが、大きすぎるFeatureブランチは、本流ブランチとの挙動の乖離やコードの衝突が発生しやすく、レビューやマージに多大な苦労を伴います。

この記事では、この問題の解決策の1つとなる「Feature Toggle」を、Ruby on Railsにおける実装方法と共にご紹介します。 Feature Toggle自体は開発手法の一種であるため、言語/フレームワークを問わず広く活用されています。

Feature Toggleとは

  • 「機能が動作する/動作しない を切り替える」機構です。
  • ソースコードは存在しますが、トグルを「ON」にするまで機能が動作しないよう制御します。

Feature Toggle 動作イメージ

効果

Featureブランチの早期マージが可能となる

通常、作りかけの機能を本流ブランチにマージしてしまうと、ユーザーに未完成の機能を提供することになるため、問題になってしまいます。

そこでFeature Toggleを用いて、開発中の機能はトグル「OFF」の場合動作しないように実装します。そうすることで、開発中の機能をユーザー環境に影響を出さずにマージできるようになります。

本番環境でのテストが可能となる

本番環境では、開発環境では見つからなかった問題が多々発生します。 例えば、既存データに予期せぬレコードが入っていたり、アプリではない別のレイヤーでトラブルが起きたり...

Feature Toggleで社内向けのテストユーザーのみ機能を利用できるよう制御すれば、実際のユーザー環境にリリースする前に、テストユーザーで機能の動作確認ができるようになります。

Railsでの実装例

この記事では、Feature Toggle実装の助けとなってくれる「Flipper」 gemをご紹介します。 このgemはフラグ管理と分岐制御の仕組みを提供しています。

github.com

Feature Toggleは独自に実装することもできますが、このgemを使えばフラグの動的切り替えやフラグの管理画面などを比較的簡単に導入することができます。

Flipperの基本機能

以下のようにフラグがONかOFFかによって処理を分岐させることが可能になります。

ifFlipper.enabled?(:search)
  search_hoge
else
  puts 'nothing...'end

フラグの有効化のメソッドも提供されています。

Flipper.enable(:search)

フラグの保存先

フラグ状況の保存先はプラグインによって切り替えることができます。 ActiveRecord, Redisなどから好きな保存場所を選びましょう。

$ gem 'flipper-active_record'
$ rails g flipper:active_record

フラグ管理ページを作成するFlipper UI

画面からフラグを制御するflipper-ui gemも提供されています。 サービスの管理画面の1つとして組み込んでおけば運用が楽になるでしょう。

Flipper UI

また、当たり前の話ですが、管理ページは一般公開しないように実装しましょう。 参考: Flipper UI#security

しかしこのFlipper UIですが、ページ内に動画が表示されるfun modeが存在します。 開発者の遊び心かと思いますが、業務利用であれば必要ないのでOFFにしてしまいましょう。

Flipper::UI.configure do |config|
  config.fun = falseend

特定ユーザーのみの分岐

フラグON/OFFだけでなく、「特定ユーザーのみON」といった制御も可能です。 Flipperでは「Actor」という名前で機能が提供されています。 https://www.flippercloud.io/docs/features#enablement-actor

これにより、テスト用のユーザーでのみ機能を有効化、一般ユーザーでは無効のままにする。といった運用ができます。

user = User.find(...)
other_user = User.find(...)

Flipper.enabled?(:verbose_logging) # falseFlipper.enabled?(:verbose_logging, user) # falseFlipper.enable_actor(:verbose_logging, user)

Flipper.enabled?(:verbose_logging) # falseFlipper.enabled?(:verbose_logging, user) # trueFlipper.enabled?(:verbose_logging, other_user) # false

ただし、FlipperでActor機能を使うには、対象が一意の flipper_idを持つ必要があります。 それ用のmoduleを利用するか、自前でメソッドを定義しておきましょう。

classUser< Struct.new(:id)
  includeFlipper::IdentifierendUser.new(5).flipper_id # => "User;5"

classUser< Struct.new(:uuid)
  defflipper_id
    uuid
  endendUser.new("DEB3D850-39FB-444B-A1E9-404A990FDBE0").flipper_id
# => "DEB3D850-39FB-444B-A1E9-404A990FDBE0"

どのレイヤーでFeature Toggleを利用すべきか

Controller

Controllerは最も分岐させやすいでしょう。 before_actionを活用して新機能を封じる分岐を設置しておけば、本番環境では一切動作させずに開発環境で実装を進めることができます。

classSugoiNewFeatureController< ApplicationControllerdefindexifFlipper[:sugoi].enabled? current_user
      # 新機能向けの処理else# 新機能を表示させたくないときの処理
      render_404
    endendend

筆者はこういったラッパーを用意してController/Viewから簡単に分岐できるようにしています。

moduleDarkLaunchextendActiveSupport::Concern

  included do
    helper_method :authorized_feature?enddefauthorized_feature?(sym)
    Flipper[sym].enabled? current_user
  enddefauthorized_feature!(sym)
    raiseUnavailableFeatureErrorunless authorized_feature?(sym)
    # 注: ↑任意の例外クラスとそれを拾って404ページを表示する機構を別途設ける必要ありendend

使い方はこんな感じです。

classSugoiNewFeatureController< ApplicationController
  before_action -> { authorized_feature!(:sugoi) }

  defindex; endend

View

View側で新機能への画面遷移などを設置する場合は、ここにも分岐が欲しくなります。

%ul%li= link_to 'メニュー項目A', ****_path
  %li= link_to 'メニュー項目B', ****_path
  -ifFlipper[:sugoi].enabled? current_user
    %li= link_to 'すごい新機能!', ****_path

実装は容易ですが、遷移元それぞれに分岐を設置する必要があるため、塞ぎ忘れが発生しやすくなります。 導入するかどうかは慎重に検討しましょう。

Model

Model内でもFlipperを使うことはできますが、他の箇所よりはオススメできません。 利用ユーザーが参照しづらいためFlipperの機能が活かしづらくなります。

また、モデル内の処理は参照タイミングが多くなりがちで、テストすべき分岐が膨大になる可能性もあります。 コードの見通しも悪くなりがちなので、慎重に利用するのがよいでしょう。

よくあるトラブル

制御漏れによる不具合

Feature ToggleはON/OFFの制御を提供するのみの機構なので、開発者の塞ぎ忘れには無力です。 しっかりとテストしましょう。 「開発中のコードがデプロイされる」というリスクは確実に発生します。リリース戦略はしっかり立てましょう。

Feature Toggleで防げないレイヤーの不具合

機能自体をOFFにしたつもりでも防げないトラブルもあります。 例えばDBレイヤーの問題。 新機能向けのALTER TABLEを発行したところ、重要なテーブルがロックされてしまい、サービス障害に繋がることもあるかもしれません。 Toggleを過信せずに開発を行いましょう。

大量のif分岐が生まれる

Feature Toggleが必要になる大きな新機能は、リリース後も継続的に改善していくケースがあります。 新機能リリースのたびに分岐を追加していると、あっという間に分岐まみれになってしまいます。

-if flag1
  .hoge-wrapper-if flag2
      .feature-2-if flag3
      = render 'hogehoge'-if flag4
        = link_to fugafuga

使い終わったフラグはなるべく削除するようにしましょう。

新任メンバーがフラグの存在に気が付かない

新任メンバーがフラグの存在を知らず、機能そのものに気づかないケースもあります。 開発用のseedデータではフラグが全てONになるようにしておくと自動で機能が使えるようになるので便利です。

まとめ

以上、RailsにおけるFeature Toggleの導入方法の1つを紹介させていただきました。 比較的簡単に導入できますが、安易に利用するとトラブルに見舞われる場合があります。

「ユーザーに見えないAPIからリリースする」「モデルやテーブルだけ先に作っておく」など、まずはFeature Toggleを使わない選択肢を検討してから、必要な部分にのみToggleを導入していくのが賢い使い方でしょう。

この記事が、開発者の皆様の安定したリリースの助けとなれば幸いです。

また、今回は紹介できませんでしたが、Feature Toggleのためのgemは他にもRolloutなどが存在します。 皆様のお好みの方法で導入してみてください。


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


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

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

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

■開発環境はこちら

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

Private GitHub Pagesで社内ドキュメントを公開しよう!

$
0
0

集合知プラットフォーム事業部の榎本です。筋トレのお供のプロテインが切れそうなので、次に購入するプロテインのメーカーとフレーバーに悩んでます。

GitHub Pages でアクセス制限

今まで GitHub Pagesというと静的サイトをインターネットへ全世界公開するしかできなかったのですが、2021年に Private GitHub Pages の機能がリリースされ、限定されたユーザーのみに制限してページを公開することが可能になりました

GitHub Pages を使って社内のドキュメントやナレッジを特定のユーザーだけに公開したり、企業内だけで共有したりすることができます。…(中略)… 今回の変更で、PrivateとInternalリポジトリでは、Private Pagesを使うことで、そのリポジトリを見れる人だけがそこから生成されるPagesのサイトを見られるという設定を行えるようになりました。

GitHub Pagesのアクセス管理 - GitHubブログ

今日は Private な GitHub Pages により社内ドキュメントをカジュアルに公開できるようになって捗りました、という話を紹介したいと思います。

Private GitHub Pages設定方法

Private GitHub Pages の設定方法は簡単です。下記のGitHub公式ドキュメント通りに進めれば OK です。

Changing the visibility of your GitHub Pages site - GitHub Docs

設定手順のサマリとしては下記の通りです。

  • リポジトリTOPへ
  • Settings (リポジトリ設定画面)へ
  • 左メニューから Pages を選択
  • GitHub Pages visibility で "Private"を選択

公開されるページのURL

下記のようなURLが GitHub 側で自動で生成され、 Private GitHub Pages としてアクセス可能になります。

https://xxx.pages.github.io/

一度発行されたURLは(設定を変えない限りは)基本的に変わらないようですので、安心して使えます。また、閲覧権限のない Private GitHub Pages にアクセスした場合は、 Unauthorized access errorメッセージが表示されます。

(わかりやすい Custom Domain をページに割り当ててもよいかと思います。私のチームでは開発者向けの社内ドキュメントだしそのままでいいか、ということでGitHubから自動で割り当てられたURLをそのまま使用しました)

GitHub Pages の運用について

GitHub Pages の運用方法にはいくつか流派があります。

  • GitHub Pages 用のブランチ(gh-pagesブランチ)を用意してページを公開
  • main/masterブランチに docsディレクトリを用意してページ公開

私のチームの場合、わざわざ公開用のブランチを用意して運用するのも手間がかかりそうだったので、後者の docs ディレクトリに html を放り込んで公開するかたちにしています。

Private GitHub Pages 設定例

Advanced な使用例として、peaceiris/actions-gh-pagesみたいなものを使って、静的ページの生成および公開を自動化するのも良いでしょう。

使用例

実際の使用例を紹介したいと思います。私のチームでは下記のドキュメントを開発者向けドキュメントとして公開しています。

OpenAPI を redocでhtml化した API ドキュメントを公開

redocで生成したAPIドキュメント

APIの仕様などについて話すときや、Pull Request からAPI仕様について言及するときなどに、GitHub Pages の URL で該当部分の仕様を参照できて便利でした。

Vue.js のコンポーネントをまとめた Storybook を公開

Vue.js コンポーネント Storybook

新しいページ追加のときに、既存のコンポーネントをStorybookのページから簡単に参照できて便利でした。

注意点

  • Private GitHub Pages 閲覧のためには、GitHub アカウントで認証を通している関係上、GitHubユーザー登録およびOrganizationへの登録が必要
    • ドキュメントを全社員が閲覧できる形で公開するのは、GitHub の金銭コスト増、アカウント管理コスト増につながりなかなか辛いかも
    • コストを抑えたいなら開発者ドキュメントのみ Private GitHub Pages に公開すると良さそう
  • Private リポジトリであっても GitHub Pages の設定を Public にすると全世界公開されるので気をつけましょう

まとめ

Private GitHub Pages 登場前は静的サイトを社内だけに公開しようにも、自前で S3 を用意したり認証の仕組みを用意したりと、なかなか骨が折れることが多かったように思います。しかし Private GitHub Pages が登場したことで、社内向けのinternalな静的ページ・ドキュメントであればカジュアルに公開可能になりました。

みなさんも社内ドキュメントを Private GitHub Pages で公開して、ナイスな開発体験を手に入れてみてはいかがでしょうか。


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

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

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

■開発環境はこちら

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


テックサポート制度の用途をまとめてみました!

$
0
0

グループ戦略室でエンジニアのマネジメントをしている平川 a.k.a @arihh です。
リモートワークで酒量が0になりましたが体重は減らない日々です。

弊社にはテックサポート制度というものがあります。 テックサポート制度とは、1人あたり年間12万円までスキルアップ・生産性向上に関わるサポートを行う制度です。

制度導入から3年以上が経過し、申請された制度の数は1,000を超える数となりました。
今回は実際にどういうことに使われたのかをまとめてみました。

導入時のブログあるのでそちらもご覧いただけたらと思います。 tech.medpeer.co.jp

どんなものに使われていたの?

カテゴリーに分けた分類はこのようになりました!

金額での割合で分類を出していますが、件数でみると技術書が8割程度で圧巻でした。 技術書以外ではキーボード購入・ソフトウェアのライセンスと続いています。

メドピアで人気の技術書は?

技術書の本は紙の本でも電子書籍でもOKとなっています。
制度を使って買われた技術書ランキングは以下となりました!

毎週輪読会を行っているので、輪読会の対象となった技術書についてはランキングが上位になりました。

技術書以外で使われていたものは?

技術書以外で使われたものランキングTOP5は以下となりました!

  1. Happy Hacking Keyboard
  2. Dash
  3. IntelliJ IDEA All Product Pack
  4. RubyMine
  5. REALFORCE

技術書以外ではキーボードやIDEの用途が多かった中、Dashの人気が目立ちました。

他にもこんな使われ方も!

エンジニアのスキルアップ・生産性向上のために、いろいろな使われ方をしています。
いくつか気になった使われ方をピックアップしてみました!

AWSの学習のほか、RaspberryPiでクラスタを組むような使い方をしている人がいます。

さいごに

詳しいことが気になった方はカジュアル面談でお話できればと思っています! メドピアでは引き続きテックサポートのような制度でエンジニアへの投資を続けていきます!


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


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

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

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

■開発環境はこちら

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

Swift Concurrencyのキャンセルと向き合う

$
0
0

メドピアでモバイルアプリを開発している小林(@imk2o)です。 惑星直列に興奮しています。

Xcode13.2より、iOS13以降を対象にしたアプリであればSwift Concurrencyを利用できるようになりました。従来RxSwiftやCombineなどのReactive Streamを利用しているプロジェクトのSwift Concurrencyへの移行を進める際、考慮しなければならないことのひとつが「キャンセル」の扱いです。

以下に記載するコードとその挙動は、Swift5.6(Xcode13.4)とSwift5.7(Xcode14 beta)で確認しました。

Taskのキャンセルの仕組み

非同期処理を実行する Taskオブジェクトは cancel()メソッドを持っており、これを明示的に呼び出すことでキャンセルができます。 Taskがキャンセルされると通常 CancellationErrorが投げられますが、catchブロック内では Task.isCancelledの値をチェックする方法が推奨されています。キャンセルされた場合は直ちに returnするほうがよいでしょう。

lettask= Task {
  do {
    letresult=try await someRequest()
  } catch {
    guard!Task.isCancelled else { return }
    // TODO: Handle error
  }
}

...

task.cancel()

Combineの AnyCancellableは破棄のタイミングで自動的にキャンセルされていましたが、 Taskcancel()を呼ばない限り、キャンセルされません。 Combineにおいては購読するストリームを Set<AnyCancellable>などで持っておき、ViewController(以降、VC)やViewModel(以降、VM)の破棄とともにキャンセルされるものと考えていましたが、Swift Concurrencyにおいては、

  1. VC/VMの破棄とともにキャンセルされる仕組みを導入する
  2. キャンセルを諦め、割り切る

のどちらで実装するかを考える必要があります。

Taskのキャンセル考慮は意外と険しい

VC/VMクラスのdeinitで cancel()すればよいかと思いきや、実はいくつかのハマりどころがあるのです。こんなViewModelクラスを考えます。

@MainActorfinalclassEchoViewModel {

    init(echoService:EchoService) {
        self.echoService = echoService
    }
    
    deinit { task?.cancel() }

    // Output@Publishedprivate(set) varechoBack=""privateletechoService:EchoServiceprivatevartask:Task<Void, Never>?

    // 一定時間経過後にstringをエコーバックするfuncecho(_ string:String) {
        task = Task { [unowned self] indo {
                echoBack =try await echoService.echo(string)
            } catch {
                guard!Task.isCancelled else { return }
                dump(error)
            }
        }
    }
}

この echo("Hello!")を呼び出すと、一定時間後 echoBackプロパティが "Hello!"に変化するコードです。このときエコーバックされる前に画面を閉じてしまうと、VC/VMは直ちに破棄され、Taskはキャンセルされるか...というとそうはなりません。

Taskブロック内は [unowned self]でVMの参照を持たないようにしているのですが、

echoBack =try await echoService.echo(string)

と書くことで、SwiftコンパイラがVMの参照カウントを増やす実行プログラムを生成してしまうのです。 従ってVCは破棄されてもVMは破棄されず、エコーバックされた後に破棄されるため、結果的にキャンセルが発生しません。

このコンパイラ仕様を回避するため、echo()メソッドを以下のように変えてみます。

// EchoViewModel.swiftfuncecho(_ string:String) {
        task = Task { [unowned self] indo {
                letechoBack=try await echoService.echo(string)
                self.echoBack = echoBack
            } catch {
                guard!Task.isCancelled else { return }
                dump(error)
            }
        }
    }

一度ローカル変数で受けることでコンパイラが生成する実行プログラムが変わり、この場合は画面を閉じるとVC/VMが破棄され、キャンセルが発動します。

実質的に同じ意味のコードを書いているのに挙動が変わるのは困りものですが、実はもっと危険なケースがあります。それは、 非同期処理がキャンセルに対応していない場合です。

EchoServiceecho()メソッドがもし以下のような実装だとしたら、例えキャンセルしたとしても無視され、一定時間後にエコーバックしてしまいます。

finalclassEchoService {
    privateletdelay:TimeInterval=3funcecho(_ string:String) async throws->String {
        // FIXME: withTaskCancellationHandlerでキャンセル対応するreturntry await withCheckedThrowingContinuation { continuation in
            DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
                continuation.resume(returning:string)
            }
        }
    }
}

このようにキャンセルが考慮されていない非同期メソッドを呼び出してしまうと、先ほどのVMのような実装にしたことで、クラッシュを発生させる要因を生み出してしまいます。 (self.echoBack = echoBackのところでクラッシュします)

この問題の対処法として、ここまでVMの echo()の中で Taskを作り、同期/非同期の境界点としていましたが、VMの echo()を非同期メソッドにし、VC側で Taskを作る形に変更してみます。

// EchoViewModel.swiftfuncecho(_ string:String) async {
        do {
            letechoBack=try await echoService.echo(string)
            self.echoBack = echoBack
        } catch {
            guard!Task.isCancelled else { return }
            dump(error)
        }
    }
finalclassEchoViewController:UIViewController {
    ...deinit { task?.cancel() }
    
    funcsendEcho() {
        task = Task { [unowned self] in
            await viewModel.echo("Hello!")
        }
    }

この場合の画面を閉じたときの挙動は、VCは直ちに破棄されるので Taskはキャンセルされます。VMはコンパイラによって参照カウントが増えており、少なくともVMの echo()メソッドのスコープにいる間はオブジェクトが生きているため self.echoBack = echoBackのところでもクラッシュしないので、致命的な問題は回避できそうです。

非同期処理がキャンセルに対応していない場合

非同期処理がキャンセルに対応している場合

まとめ

Swift Concurrencyにおいてはキャンセルは明示的に行う必要があること、またいくつかの留意点があることを紹介させていただきました。 ただ、「こんなに厄介ならキャンセルしない」と割り切るのもアリかなと思っています。

非同期処理の多くがUnaryなRest APIの呼び出しである場合、このリクエストをキャンセル可能にしたところで、通信や端末リソースの削減になるかというと微々たるもの...とも言えます。 Swift Concurrencyの設計思想からも、キャンセルはオプション的扱いで、必要であれば使ったらいいよという位置付けなんだろうなと思いました。

Combineと比較されがちですが、どちらにも長所・短所があると思うので適宜使い分けていくのが良いでしょう!


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


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

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

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

■開発環境はこちら

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

GitHub Appsの作成とOrgへの所有権の委譲手順

$
0
0

CTO室SREの @sinsokuです。

ドキュメントをテックブログとして書いておくと一石二鳥なことに気づいたので、ここに書きます。

GitHub Appsが必要なユースケース

GitHub Actions で使用できる secrets.GITHUB_TOKENには以下の制限があります。

  • secrets.GITHUB_TOKENを使用した操作では新しいワークフローが実行されません
    • Actions で作成したプルリクで Actions のテストが動かない
  • 他リポジトリのコードを参照できません
    • プライベートリポジトリのgemやnpmの取得ができない

この問題を回避するため、GitHub Appsで一時的なアクセストークンを生成して使用します。

なぜ Personal Access Token(PAT)を使うべきではないか?

PATを使用することは以下の理由から推奨していません。

  • PATを作成した人が退職した場合、引き継ぎを忘れると動かなくなる
  • PATの有効期限の管理が煩雑になる
    • 無期限にするのはセキュリティ上好ましくない
  • マシンユーザーの人数分の費用が増加する
    • アカウントを使い回すのはセキュリティ上好ましくない*1

GitHub Apps の作成手順

公式ドキュメント

GitHub Docs に手順が記載されているので、まだ読んだことない方は一度目を通しておいてください。

作成手順

  1. Register new GitHub Appで各項目を埋めてください
    • 項目
      GitHub App name 適当な名前
      (弊社だと mpg-xxx-botの命名規則)
      Homepage URL 適当なURL
      (弊社だと https://github.com/medpeer-dev
      Webhook 不要なので Activeのチェックを外す
      Repository permissions Permissions required for GitHub Appsを参照して必要な権限を選択してください。
      例: コードを参照する場合はContents、プルリクを作る場合は Pull requestsなど
  2. GitHub Apps 作成したら、秘密鍵を生成する
  3. 使用したいリポジトリの Secrets に以下を設定する
    • GitHub AppsのID
    • 生成した秘密鍵

Orgに所有権を委譲する

作成したGitHub Appsの所有権をOrgに委譲し、必要なリポジトリで使えるようにOrgにインストールする必要があります。

公式ドキュメント

GitHub Docs に所有権の委譲の手順があるため、参照してください。

Ownerへの依頼手順

弊社ではSREメンバーがOwner権限を持っているため、以下のような運用にしています。

  1. 作成したGitHub Appsの所有権をOrgに委譲する
  2. Backlogで以下を記載したチケットを作成する*2
    • GitHub Appsの用途
    • GitHub Appsをアクセスするリポジトリ一覧
    • GitHub Appsの管理者アカウント

SREメンバーは 用途権限に問題がなければ、所有権の委譲リクエストを承認します。 承認した後、以下の作業をします。

  • GitHub Apps に管理者を追加する
  • GitHub Apps をOrgにインストールする
    • Only select repositoriesで指定のリポジトリだけ許可する

アクセストークンの使い方

GitHub Actionsでの利用

tibdex/github-app-tokenを使用します。

README の引用ですが、以下のように簡単に使用できます。

- name: Generate token
  id: generate_token
  uses: tibdex/github-app-token@v1
  with:app_id: ${{ secrets.APP_ID }}
    private_key: ${{ secrets.PRIVATE_KEY }}
    # Optional (defaults to ID of the repository's installation). # installation_id: 1337 # Optional (defaults to the current repository). # repository: "owner/repo"- name: Use token
  env:TOKEN: ${{ steps.generate_token.outputs.token }}
  run: |
    echo "The generated token is masked: ${TOKEN}"

CircleCIでの利用

CircleCI では簡単に扱う方法がないため、以下のスクリプトを使用します。

#!/usr/bin/env ruby# frozen_string_literal: true# GitHub Appsで使うアクセストークンを生成し、標準出力に表示するスクリプト。# デフォルトgemではないjwtを入れるrequire'bundler/inline'
gemfile do
  source 'https://rubygems.org'
  gem 'jwt'endrequire'openssl'require'net/http'require'json'require'jwt'# 環境変数からAPP_ID, PRIVATE_KEYを読み込む。
gh_app_id = ENV['GITHUB_APPS_ID']
# Circleでは複数行の値を環境変数に使えないため、Base64でエンコードして設定
gh_private_pem_base64 = ENV['GITHUB_APPS_KEY_BASE64']
gh_private_pem = Base64.decode64(gh_private_pem_base64)

payload = {
  iat: Time.now.to_i - 60,
  exp: Time.now.to_i + (10 * 60),
  iss: gh_app_id
}
private_key = OpenSSL::PKey::RSA.new(gh_private_pem)
jwt = JWT.encode(payload, private_key, "RS256")

# httpリクエストを投げるのに必要な変数を用意
headers = { Authorization: "Bearer #{jwt}", Accept: "application/vnd.github.v3+json" }
http = Net::HTTP.new('api.github.com', 443).tap { |h| h.use_ssl = true }

# GitHubのAPIでアクセストークンを生成する## - https://docs.github.com/en/rest/apps/apps#get-a-repository-installation-for-the-authenticated-app# - https://docs.github.com/en/rest/apps/apps#create-an-installation-access-token-for-an-app
installation = http.get("/repos/medpeer-dev/medpeer/installation", headers).then { |r| JSON.parse(r.body) }
access_token = http.post("/app/installations/#{installation["id"]}/access_tokens", {}.to_json, headers).then { |r| JSON.parse(r.body) }

# アクセストークンを出力
puts access_token["token"]

スクリプトの出力を環境変数に設定することで、プライベートリポジトリにアクセスできます。

- run:name: Set GitHub access token
    command: |
      export GITHUB_ACCESS_TOKEN="`./bin/gh_apps_token`"
      export BUNDLE_GITHUB__COM="x-access-token:${GITHUB_ACCESS_TOKEN}"

参考ページ


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

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

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

■開発環境はこちら

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

*1:全てのプロジェクトのリポジトリへのアクセス権限を持つことになってしまうため

*2:社内の方はSREプロジェクトの「依頼: GitHub Appsの委譲」のテンプレを参照してください。

「はじめて学ぶソフトウェアテスト技法」読書会を開催しました

$
0
0

こんにちは。メドピアのお手伝いをしている@willnetです。平日朝はだいたいジョギングをしているのですが、梅雨から夏にかけてジョギングしづらい日々が続くので変わりに何をすべきか考え中です。

今日は社内読書会で読んだはじめて学ぶソフトウェアのテスト技法の話を書きたいと思います。

メドピアでは、サーバサイドのテストにRSpecを利用しています。RSpecの設定や使い方などは検索すればたくさんの記事を目にすることができます。

しかし、いざテストコードを書く際にどう設計すべきなのか(具体的になにをどのように、どれくらいテストするのか)はツールの使い方と比べて検索などで調べるのが難しく、他のテストコードを参考になんとなく雰囲気で書いてしまう事が多いのではないかと思います。その結果、テストケースに抜けがでたり、逆に過剰なテストケースを作ってしまいCIの時間が伸びたりしてしまいます。

そこで、そんなテスト設計の基礎を学ぶ書籍として「はじめて学ぶソフトウェアのテスト技法」を選び、毎週の読書会で読み進めてきました。

読書会の進め方は普段と同様に、音読でキリのいいところまで読み、その後感想を話し合う形式で行いました。この本は章単位で話が独立しており、また1章も短めなのでこの方式に向いていました。

感想

特に序盤が参考になりました。この本は5つのセクションに分かれていますが、最初のセクションである「ブラックボックステスト技法」は次のような章構成になっています。

  • 同値クラステスト
  • 境界値テスト
  • デシジョンテーブルテスト
  • ペア構成テスト
  • 状態遷移テスト
  • ドメイン分析テスト
  • ユースケーステスト

これらのワードは、それなりに経験のあるエンジニアであれば一度は目にしたことがあるのではないでしょうか。このセクションを読むと、どのようなテストケースを作ると、少ない労力で大きな成果を得られるかの指針を理解することができます。

次のセクションのホワイトボックステストでは、カバレッジを測る方法(いわゆるC0, C1, C2)の概要が理解できます。

後半のセクションは、ウォーターフォールによる開発や、大規模な開発環境を前提としている記述が多く見られます。この本が書かれたのは2000年代前半(原著の初版は2003年12月31日発売)なので当然ではありますが、web開発におけるテストに直接影響するものは少ないので時間がない人は前半2つのセクションを読むだけでもよいかもしれません。とはいえ、テスト設計を広く知るためには後半のセクションも必読なので時間があればぜひ読んでみてほしいです。

まとめ

どのようにテストを設計したらいいのかわからない、という人に「はじめて学ぶソフトウェアのテスト技法」はオススメです!


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


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

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

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

■開発環境はこちら

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

処方せんの自動印刷アプリを Electron でつくった話

$
0
0

フロントエンドエンジニアの小林和弘です。

kakari という薬局向けに提供しているサービスで Electron を使って Windows アプリケーションを作成したので、そのことについてお話しようかと思います。

事の発端

これまで、「kakari」では、患者さまから薬局へ処方せんが送信されたときに薬剤師さまが処方せんの受信に気づけるように FAX で処方せんを送信していました。

しかし、FAX 送信に利用していた Twilio Programmable Fax サービスが 2021 年 12 月 17 日をもってサービス終了になってしまいました。

このサービス終了に対応すべく、薬局向けに処方せんが送信されたらプリンターで処方せん印刷ができる Windows アプリケーションを Electronで作成することになりました。

そもそもElectronとは

HTML, CSS, JS のフロントエンドの技術だけで Windows, Mac, Linux 向けにアプリケーションを作成できるツールです。

アプリケーションの中身は Node.js と Chromium ブラウザになっています。画面表示には Chromium ブラウザが使われ、OS 側の処理は Node.js で行えるようになっています。

有名なアプリケーションだと SlackDiscordVSCodeなども Electron で作成されています。

なぜ Electron なのか

ブラウザにも印刷機能は搭載されていますが、JavaScript 上で window.print()を実行すると印刷ダイアログが表示されて、ダイアログ内の OK ボタンを押す必要があります。薬局の業務中に薬剤師さまにこの作業をしていただくのは現実的ではありません。

これを回避する方法として、--kiosk-printingオプションを付けた状態で Chrome ブラウザを起動するという突破口はありますが、すべての薬局が必ず Chrome を導入しているわけではないのと、すでに導入いただいている多くの店舗に導入支援することのコストの高さから、Web アプリは選択肢から外れています。

外部の会社に Windows アプリを開発していただくという選択肢もありました(実際に何社かに概算見積もりを出していただきました)が、運用も視野に入れるとフロントエンド技術で保守できる Electron が良いだろうという結論に至りました。

Electron の世界

Electron は先ほども述べた通り、ブラウザと Node.js で実装されています。Node.js 側からブラウザに命令を飛ばすことも、ブラウザ側から Node.js 側に命令を飛ばすこともできます。

当然、データのやりとりもできるわけですが、OS 側の処理を担う Node.js にブラウザ側から直接アクセスできる状態というのは非常に危険な状態です。

この危険な状態を避けるために、Electron ではコンテキストの分離というものを行っています。

コンテキスト分離を説明する前に Electron のプロセスモデルについて軽く説明します。

Electron には 2 つのプロセスが存在します。1 つ目が Node.js のランタイム上で動いているメインプロセス、2 つ目が Chromium ブラウザのランタイム上で動いているレンダラープロセスです。

Electron のデフォルトのセキュリティ設定ではメインプロセスとレンダラープロセス間でデータのやり取りができないようになっています。

ではどのようにしてアプリ画面のブラウザと Node.js でデータのやり取りを行うかというと、Electron に用意されているプリロードスクリプトというものを利用します。プリロードスクリプトはレンダラープロセス内で実行されるコードで、レンダラープロセスからメインプロセスにアクセスするための許可リストを定義しています。レンダラープロセスからメインプロセスにアクセスできる部分をプリロードスクリプトというレイヤーに分離することで、マシンを操作するメインプロセスの強力な API が無闇にレンダラープロセスに露出するのを防いでいます。

Electron のメインプロセスとレンダラープロセスとプリロードスクリプト

前準備

メインプロセス、レンダラープロセスという 2 つのプロセスがあることと、そのプロセス間を安全に接合するためのレイヤーとしてプリロードスクリプトが存在するというお話をしました。

Electron の知見が乏しい状態で、この三者のスクリプトをビルドする環境を用意するのは骨が折れます。

Electron の公式サイトではボイラープレートと CLIの利用を提案していますが、CLI の electron-forgeは必要最低限のファイルを出力するだけで正直ほとんど役に立ちませんでした(簡単なツールをローカルでサクッとつくるだけならこれでもいいかもですが)。electron-forge は簡易的な開発環境は提供してくれますが、アプリのビルドや配布などの CI/CD の設定は一切行われていません。

公式サイトからリンクが貼られているボイラープレートには GitHub Actions の CI/CD も事前に準備されたものがいくつか用意されていました。

メドピア内では Vue.js がよく使われており、別プロジェクトから Vue ファイルを流用することも可能であろうと判断して vite-electron-builderを選択しました。

vite-electron-builder はメインプロセス、レンダラープロセス、プリロードスクリプトがそれぞれ別のディレクトリに切り出されており、Electron の構造を意識した、見通しのよい開発を行うことができています。

どのように自動印刷を実現しているのか

実際に自動印刷アプリがどのように動いているかは、ざっくりと下記のような流れになっています(薬局のログイン等はもろもろ省略)。

  1. 患者さまが kakari のモバイルアプリから処方せんを送信
  2. 処方せんを受け取った Rails サーバーは Pusher(リアルタイム通知の SaaS)にリクエスト
  3. Electron で処方せん到着の通知を受け取って、印刷用 URL を取得する API にリクエスト
  4. レスポンスに含まれた印刷用 URL をレンダラープロセスでレンダリングして印刷処理

蛇足ですが、リリース当初の Electron のバージョンだと Electron 側からプリンターの選択ができない(OS でデフォルト設定したプリンターが選択される)状態でした。しかし、Electron v17でバグが修正されて Electron のアプリ画面に OS に接続されたプリンターを選択できる UI を追加してよりアプリらしさが増しています。

印刷アプリの構成図

印刷回数 100 万回突破

今回つくった Electron アプリから印刷された処方せんの印刷回数が 100 万回を突破しました。

仮に 1 回で 1 枚の処方せんが印刷されるとすると、A4 用紙の厚さが約 0.1mm なので積み上げると 10万 mm = 100 m になります。ビルでいうと 30 階建てくらいです。これだけの規模の患者さまと薬剤師さまのお役に立てたと思うと感慨深いものがあります。

Slack の投稿「自動印刷アプリの印刷枚数が 100 万超えるタイミングで Electron のテックブログ書こうかな」

丁度いい節目なのでテックブログを書こうと思ったんですが、ネタを温めていたら直に 130 万回を超えようとしていました(レビュー通過して公開されたときには絶対に超えてる)。

まとめ

Electron の 2 つのプロセスと、そのプロセスを安全に使うコンテキスト分離を理解した上でボイラーテンプレート使えば比較的簡単にデスクトップアプリがつくれてしまいます。

ブラウザでは対処できない課題を解決するための選択肢のひとつとして Electron を検討してみるのも面白いかもしれません。


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

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

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

■開発環境はこちら

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

Viewing all 211 articles
Browse latest View live