メドピア開発者ブログ

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

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

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

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

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

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

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


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

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

# リクエスト許可処理クラス
class AllowRequest
  def initialize(request, connection)
    @r = request
    @c = connection
  end

  def allowed_ip_addresses
    ENV['ALLOW_IPS'].split(',')
  end

  def allowed?
    return true unless (allowed_ip_addresses & [
      @c.remote_ip,
      @r.headers_in['X-Real-IP'],
      @r.headers_in['X-Forwarded-For']
    ].compact).empty?

    false
  end

  AllowRequest.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日毎の更新を推奨している為です

【令和最新(当時)】メドピア開発からVueの プログレッシブを俯瞰する

CTO室の小宮山です。肉体系Youtuber巡りが最近の趣味です。

2020年6月5日に開催された令和最新(当時)なこちらのイベントにて、登壇者として発表させていただきました。

techplay.jp

そして発表資料は体裁を整えてこのブログにて大々的にドカンと投稿しようと考えていたのですが、そのまま忘れ去って気づけば早1年が経過しておりました。時の流れとは残酷なものです。

内容自体は色あせにくいものであり、このまま埋もれさせてしまうのももったいないと感じたため、そのまま投稿してお披露目とさせていただきたいと思います。

埋め込みスライドだと左右が見切れてしまうようなので、スライドのリンクを貼っておきます。

https://speakerdeck.com/tomoyakomiyama123/medopiakai-fa-karavuefalse-puroguretusibuwofu-kan-suru

一応埋め込みもここに。

speakerdeck.com

以上です、どうぞよろしくお願いいたします。


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

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

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

■開発環境はこちら

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


入門 GitHub Actions

CTO室SREの @sinsoku です。

社内のGitHub ActionsのYAMLが複雑になってきたので、私が参考にしてる情報や注意点、イディオムなどをまとめておきます。

頻繁に参照するページ

新しい機能の説明が日本語ページに反映されていないため、基本的に英語ページを読むことを推奨。

よく使うaction

actions/checkout

イベントによってはデフォルトブランチをチェックアウトするため、 ワークフローをトリガーするイベント のページで GITHUB_SHA を確認する必要がある。

例えば pull_request イベントの GITHUB_SHA はデフォルトブランチとのマージコミットになるため、ブランチのHEADを使う場合は以下のような指定が必要です。

- uses: actions/checkout@v2
  with:
    ref: ${{ github.event.pull_request.head.sha }}

actions/github-script

簡単なAPIの実行であれば、これで事足りる。

例えば、Issueコメントにリアクションをつけるコードは下記の通り。

name: reaction

on:
  issue_comment:
    types: [created]

jobs:
  - name: Create a reaction
    uses: actions/github-script@v3
    with:
      script: |
        await github.reactions.createForIssueComment({
          owner: context.repo.owner,
          repo: context.repo.repo,
          comment_id: context.payload.comment.id,
          content: "+1",
        });

ワークフローを書く時に注意すること

文字列は一重引用符

RubyやJavaScriptを書いていると間違いやすいので注意。

"foo" はエラーになるので 'foo' にします。

タイムアウトの指定

Actionsは実行時間で課金されるため、意図しない長時間の実行を防ぐために基本的に設定しておく方が良い。

timeout-minutes: 5

並列実行数の制御

ワークフローを無駄に実行しないように、基本的に設定しておく方が良いです。

ただ、 github.ref だけ指定すると他ワークフローを意図せず止めてしまうことがあるため、 github.workflow を接頭辞につけておいた方が安全です。

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

GITHUB_TOKEN を使うと新しいActionは起動しない

意図せず再帰的にActionが起動するのを防ぐためですが、知らないとハマります。1

  1. Approveされたプルリクを自動マージ
  2. マージされた後に自動デプロイ

例えば上記のように2つのActionを作っても、2つ目のActionは起動しないです。

これを解決するには GITHUB_TOKEN の代わりに personal access token を使う必要があります。

envは steps の中でしか使えない

以下のコードは Unrecognized named-value: 'env' でエラーになります。

env:
  FOO: foo

jobs:
  run:
    runs-on: ubuntu-latest
    timeout-minutes: 5

    env:
      BAR: ${{ env.FOO }}-bar

    steps:
    - run: echo ${{ env.BAR }}

同様に matrix の中でも env は使えないです。

success() はifでしか使えない

以下のコードは Unrecognized function: 'success' でエラーになります。

- name: Notify finish deploy to Rollbar
  uses: rollbar/github-deploy-action@2.1.1
  with:
    environment: 'production'
    version: ${{ github.sha }}
    local_username: ${{ github.actor }}
    status: (success() && 'succeeded') || 'failed'

if条件は式構文 ${{ }} を省略できるケースがある

ドキュメントに記載されてはいるが、Web上の事例ではあまり書いてないので紹介する。

式に演算子が含まれていない場合は ${{ }} を省略できます。

if: always()

ただ、 ${{ }} をつけても特に問題はないため、常に ${{ }} で囲んでおいた方が良いかも。

outputs のデフォルト値

ドキュメントに記載されていないですが 空文字列 になります。

例えば、デプロイ処理の準備中にワークフローをキャンセルされることもあるため、以下のように if: でoutputsをチェックしておく必要があります。

deploy:
  outputs:
    deployment-id: ${{ steps.deploy.outputs.deployment-id }}
  steps:
  - name: Prepare for deployment
    run: echo "do something"
 
  - name: Deploy
    id: deploy
    run: echo "::set-output name=deployment-id::1"

rollback:
  needs: [deploy]
  if: cancelled() && needs.deploy.outputs.deployment-id

イディオム

三項演算子

三項演算子と同等のことは以下の書き方で実現できます。

env:
  RAILS_ENV: ${{ (github.ref == 'refs/heads/main' && 'production') || 'staging' }}

ArrayとObjectの生成

リテラルの記法が存在しないため、 fromJSON を使う必要があります。

env:
  is_target: ${{ contains(fromJSON('["success","failure","error"]'), github.event.deployment_status.state) }}
  rollbar_status: ${{ fromJSON('{"success":"succeeded","failure":"failed","error":"failed"}')[github.event.deployment_status.state] }}

その他

Dependabotを設定する2

DependabotはGitHub Actionsに対応しているので、設定しておくと便利です。

version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "daily"

採用のリンク


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

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

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

■開発環境はこちら

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



  1. GITHUB_TOKEN の詳細な権限は Authentication in a workflowを参照してください。

  2. Keeping your actions up to date with Dependabot

1枚岩なMPAプロダクトでWebpackのマルチエントリーをさらにグルーピングしてビルドする

Noita世界の理不尽をこの身をもって体験した末にバウンドルミナスで全てを切り刻んでクリアしました、フロントエンドグループの小宮山です。

以前からこれできたらいいのになぁと思いながら無理そうと諦めていた掲題の事柄を実現できた嬉しさの勢いのままに書き始めています。

状況

1枚岩なMPAプロダクトがどういうものかというと、

  1. ルーティングをRails側で管理するMPA(複数エントリーポイント)
  2. 異なる種別のユーザー向けシステムが複数内包されている

という構成です。

ルーティングについては要するにSPAではなく、ページ毎のhtmlファイルとmain.jsがあるということです。

異なる種別というのは、要するにユーザー向け画面と管理者向け画面が分かれているような状況です。場合によっては3種類、4種類以上の異なるシステムが内包されたりもします。BtoBtoCなサービスだったりする場合ですね。

課題感

このような状況で普通にWebpack設定を組み上げると、もちろん全てのフロントエンドアセットをひっくるめて同じ設定でビルドすることになります。幸いWebpackはマルチエントリーなビルドにも対応しているのでビルド自体に難しさはありません。

一方で、異なる種別のユーザー向けのフロントエンドアセットを一緒くたにビルドするとやや困ったことが起きてきます。

特に気になっていたのが、splitChunksによる複数画面利用モジュールの切り出しに関することです。
SPAと異なりMPAの場合は画面遷移毎にscriptファイルのロードが行われるため、例えばVue.jsなど、ほぼ全画面で利用するようなモジュールは個別のエントリーファイルに入れてしまうとパフォーマンスの低下が懸念されます。
そこでsplitChunksをいい感じに設定していい感じに切り出すわけですが(いい感じの切り出し方は無限に議論があるので今回は触れません)、困ったことにこの切り出しが全ユーザー種別を跨って行われてしまいます。

f:id:robokomy:20210528183805g:plain
従来のビルドイメージ

シンプルな例を挙げると、管理画面でしか使わない重厚なリッチエディタ用モジュールが、splitChunksの対象となることでそれを全く必要としないユーザー向け画面でも取得対象に含まれてしまったような状況です。
node_modules配下を丸ごとvendor.jsに切り出すような設定をしているとあるある状況だと思われます。

妥協案

Webpackビルドをユーザー種別ごと別々に行えば当然ですが上記のような問題は起きません。しかしこれはこれで様々な面倒事が付きまといます。

ルーティングをバックエンドで制御している場合、フロントエンドアセットはmanifest.jsonで管理することが多いと思われます。
Webpackビルドを分けた場合、当然このmanifest.jsonも複数種類出力されることになります。つまりバックエンドも複数のmanifest.jsonを読み分けるような処理をしないといけません。既にやりたくありません。

複数のWebpackビルドを実行しなければいけない点も見逃せません。複数のWebpackビルドを--watchモードで動かしながら開発することに喜びを見出す会の皆様には申し訳ないですが、複数のWebpackビルドを--watchモードで動かしながら開発することに喜びを見出すことは私のような一般フロントエンドエンジニアには不可能でした。

このような様々な不便が付きまとうことから、多少のパフォーマンス悪化には目をつぶってまるごと単一のWebpackビルドで片付けてしまっていたのが現状でした。

光明

1シーズンに1回くらいの頻度でこの課題感と無理感の再発見を繰り返してきていて、今シーズンも再発見に勤しもうと思ったら実はなんとかなりそうなピースが揃っていることに気がつきました。

ピースその1

実はWebpackは複数の設定を配列で持つことができます。

webpack.js.org

リンク先にもある通り、こういう書き方ができます。

module.exports = [
  {
    output: {
      filename: './dist-amd.js',
      libraryTarget: 'amd',
    },
    name: 'amd',
    entry: './app.js',
    mode: 'production',
  },
  {
    output: {
      filename: './dist-commonjs.js',
      libraryTarget: 'commonjs',
    },
    name: 'commonjs',
    entry: './app.js',
    mode: 'production',
  },
];

このビルドを実行すると、2つの設定に基づいたビルドを同時(内部的には順次かもしれません)に走らせてくれます。

サンプルコードのように用途別に生成物を分けたり、ユニバーサルJSなプロダクトでサーバーとクライアント環境それぞれをビルドしたいような場面で活躍しそうです。

ユーザー種別ごとに異なる設定を用意したいという場面も状況は同じなので、きっとそのまま適用できるでしょう。
「複数のWebpackビルドを実行しなければいけない」という問題はこれでなんとかなりそうです。

If you pass a name to --config-name flag, webpack will only build that specific configuration.

なんと名前を付けておくと特定の設定でだけビルドすることもできるようです。admin系画面が不要な開発中はビルド対象から外して高速化するといった使い方もできそうです。

ピースその2

Webpackプラグインとしてmanifest.jsonをいい感じに生成してくれるのがwebpack-assets-manifestで、このプラグインにはmergeというオプションがあります。

github.com

このmergeオプションを有効にするとその名の通り、同名のmanifest.jsonファイルが既に存在している場合、そこに追記する形で新たなmanifest.jsonを生成してくれるようになります。

前回生成したmanifest.jsonを一部引き継ぐなんて絶対面倒な何かを引き起こす厄介な機能じゃないと勘ぐりたくなりますが、実はこのオプションが欲しい状況が存在します。

今回なんとかしたかった状況が正しくそれでした。ユーザー種別ごとに異なる設定でWebpackビルドを行いつつ、最終的なmanifest.jsonは1つに統合することが可能となります。

マルチエントリーグルーピングビルド設定

以上のピースを当てはめるとこのようなWebpack設定を組み上げることが可能です。

app/javascript/packs配下にエントリーファイルが設置されるとして、さらに顧客用画面はcustomers、管理用画面はadminでネームスペースを切っているような例です。

- app
  - javascript
    - packs
      - entry.js
      - customers
        - entry.js
      - admin
        - entry.js

gist.github.com

そして生成されるmanifest.jsonはこのようになります。個別のエントリーファイルは以前同様に生成されつつ、splitChunksしたvendor系ファイルはネームスペース毎の個別で生成してくれています。

{
  /**/
  "entry": "/packs/entry.abc-hash.js",
  "customers/entry": "/packs/customers/entry1.abc-hash.js",
  "admin/entry": "/packs/admin/entry1.abc-hash.js",
  "vendor-root.js": "/packs/vendor-root.abc-hash.js",
  "vendor-customer.js": "/packs/vendor-customers.abc-hash.js",
  "vendor-admin.js": "/packs/vendor-admin.abc-hash.js",
}

f:id:robokomy:20210528183900g:plain
改善後のビルドイメージ

注意点として、splitChunksしたファイルを実際に読み込むhtmlファイルはネームスペース毎に別で用意する必要があります。
とはいえ全ユーザー種別で単一のlayoutファイルを使い回すことは稀で、それぞれ専用のlayoutファイルを用意するケースがほとんどだと思われます。manifest.jsonをhelperで読み分けるような面倒さに比べたらきっと些細なことです。

おわり

異なるユーザー種別向けの設定を別で用意しつつ、ビルド自体は統合されているようにふるまわせたいという贅沢な願いはこうして無事に叶えることができました。 Webpackを素で触れる環境はやはりよいものです。皆様も素敵なWebpackライフを。


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

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

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

■開発環境はこちら

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