MSのblogにコメントが掲載された話

この記事は SmartHRのカレンダー | Advent Calendar 2023 - Qiita シリーズ1の 9日目の記事です

2023/08に publishされた MSのblogにコメントが掲載されました。 techcommunity.microsoft.com

まだ正式版としてはリリースされていないですが、この機能の開発に当たって起きた出来事などを振りかえってみます。

ことのはじまり

時は流れ

  • 2022/03 Oktaと HR Driven Provisioningの連携を開始
  • 2022/10 Microsoft Ignite 2022 のとあるセッションの中でInbound Provisioning APIの Launch Partnerとして紹介

開発中におきたこと

  • SCIM WGへのお誘い
  • APIの開発遅延からの実装スタートの遅延
  • Teamsで直接開発チームとやりとりできるストレスフリーさ
    • 仕様の確認や、追加で欲しい機能などの要望を直接あげられる
  • Office Hourでの英語がなきゃだめだ感.. (Teamsの auto transcriptでギリギリなんとか)

所管

  • HRシステムの認証周りを担当していたら Microsoftのblogにコメントと名前が載りました
  • こんなこともおきるのですねえ

B2B SaaS におけるプロビジョニング

これは、Calendar for SmartHRの村だよ | Advent Calendar 2021 - Qiita 17日目の記事です。

B2B SaaS のアカウント管理にもとめられるもの

  • 必要なタイミングで必要とする従業員へ付与
  • 不要となったタイミングで剥奪

アカウント発行/SSO

  • SaaS毎に認証・アカウント発行をすると付与漏れ、剥奪漏れが起きる
  • IdPを中心にアカウントの発行・剥奪が行われる
  • 利用者は IdPでのSSOで対象のSaaSを利用する

プロビジョニング

  • アカウント発行・剥奪の自動化のためのプロビジョニング
  • 通常のSaaSにおいては、
    • IdPにアカウントがある前提
    • IdPにアカウントがあり新規発行されるアカウントは各SaaSにプロビジョニングされアカウントが作られる
    • 権限付与は対象のユーザーの属性によって管理されたりする

SCIM

OIDF-J の WGによる資料

従業員の情報の源泉

  • 新しく社員が入社し、その対象の従業員の情報を得るタイミングはどこになるか
  • 人事チームなどが握っている情報
    • 人事チームが管理するHRシステムなどにまず情報が登録される
  • IdPにどのタイミングで付与されるのか
    • 人事チームから情シスチームなどに情報が伝搬され IdPのアカウントや社用のメアドなどが発行される
  • IdPが アカウント管理の中心にはいることとになるが、その源泉たる従業員の情報は HRシステムが最初に持つことは多いと思われる

入社時のフロー概念

入社時のフロー概念
入社時のフロー概念

退職時のフロー概念

退職時のフロー概念
退職時のフロー概念

入社情報と退職情報を適切に捉える必要がある

  • データの源泉となるところを起点とする方がよさそう
  • HR Driven Provisioning と呼ばれる概念
  • 従業員の入退社のイベントを捉えてprovisioningをする

SmartHRの例

  • SmartHR のアカウントは入社前に発行されて退職後も使って欲しいという思想で出来ている
    • 入社時に情報を入力・確認してもらう
    • 退職後も在職期間中に発行された情報を閲覧する
  • IdPとの連携は SAML/SSO機能として 2019年にリリースしているが、SSOのところのみをサポート
    • プロビジョニングに関しては、要望をもらっていたものの、IdPとどちらのアカウントが先に発行されるべきなのかの結論が出ずpend状態
  • Okta社からの提案で Provisioningの featureの実装を再開 (2021/07)
    • HR Driven Provisioning でデータの源泉となるものを SmartHRから Oktaへインポートする
    • Oktaでアカウントが発行されると SmartHRへ Okta経由で SSOできるようになる
    • closed beta扱いで SmartHR社でのテストを実施予定

まとめ

  • B2B SaaS における ProvisioningとSSOは正義
  • HR Driven Provisioningによって従業員情報のデータの源泉を抑えると良いことがあるかもしれない
  • おわり

ISUCON11 予選にでた #isucon

今年も ISUCONに参加した。 去年に引き続き一人で参加。

www.youtube.com

やったこと

  • ruby 実装へ変更
  • oj の導入
  • newrelicの導入
  • indexの追加
  • dbサーバを別インスタンス
  • mariadbだったのでmysql8もしくはpostgresql へのいれかえ実施を試みる
    • うまくできず放置

失敗

  • service 再起動処理がtypoしていて実はstopがうまく行っておらず読み込めてない事案
    • しばらく経ってから気づいて、そうそうに仕込んだ変更でbugが混入していてあとから500に気づく事案
    • 原因がわからなくてしばらく悩んだ..
  • RDBMSの入れ替え作業
  • ここ2年ぐらい実務ではpostgresqlだけなので mysqlのコマンドを忘れまくり

bench結果
bench結果

まとめ

  • 今年も一人で出てみたけど、やっぱ一人じゃできない課題の大きさだった
  • 運営の皆様ありがとうございました!
  • 公式テーマソンググッと来るものがありました。

www.youtube.com

Pull Request に ラベルを付けたい話

これは SmartHR Advent Calendar 2020 - Qiita 7日目の記事です。 日々の開発時に Pull Request を作ったり、またそのレビューをしたり、レビューが終われば リリースしたりすると思います。 その際に、データパッチの実行を忘れないようにしたり、migration が含まれてるリリースになっているかを ひと目でわかりやすくするためのものを改て実装したので紹介です。

TL;DR

  • もともと 2017年につくった AWS lambda (nodejs 6.10) の実装があった
  • ↑の実行環境が EOL
  • ruby で実装し直したので紹介

やりたいこと

  • 特定のファイルが更新されたらその旨をラベルにつけたい
    • db/migrate/* のファイルが更新されたら db:migrate ラベルを付けたい
    • etc
  • base branch が特定の条件であればラベルをつけたい
    • 大きめの機能開発をする際に features/*** ブランチをきって開発しているので、そのさいに features/any-branch-name のラベルをつけたい

他の方法

実装的な課題と解決

  • GitHub REST API は Pull Request にふくまれる change files が300までしか取得できない
  • lambda の実装時は、 REST API を利用していたので、稀に対象の PR が大きいものになっていると change files が全て取得できない問題があった
  • GitHub GraphQL API では300以上取得できる
  • ruby 実装の際に、 change files は GraphQL API、 ラベルをつける実装は REST API の実装のハイブリッド構成にした
  • また、社内の複数のレポジトリでも動くようにかつ、それぞれの設定ができるようにする

実行時のフロー

  • Pull Request の Webhook を受け取る
  • payload の event の状態を見る
  • 対象の event であれば、 GraphQL API を call して、 change files を全て取得する
  • 設定に応じて、ファイルをチェックし、ラベルのリストを作る
  • REST API をつかって、 Pull Request にラベルを設定する

実行フローのサンプル

class Githubs::PrLabelerController < ApplicationController

  def create
    event = request.env['HTTP_X_GITHUB_EVENT']
    unless event == 'pull_request'
      msg = { message: 'event is not pull_request.' }
      render json: msg
      return
    end

    # webhook signature
    github_webhook_secret = ENV['GITHUB_WEBHOOK_SECRET']
    if github_webhook_secret
      http_x_hub_signature = request.env['HTTP_X_HUB_SIGNATURE']
      payload_body = request.body.read
      signature = GithubWebhookSignature.new(secret: github_webhook_secret,
                                             http_x_hub_signature: http_x_hub_signature,
                                             payload_body: payload_body)
      if signature.invalid?
        msg = { message: 'webhook signature is invalid.' }
        render json: msg
        return
      end
    end

    payload = JSON.parse(params['payload'])

    if payload['action'] == 'opened'
      pr_number = payload['number']
      full_repo = payload['repository']['full_name']
      owner, repo = full_repo.split('/')

      change_files = GithubQueryHelper.call(pr_number: pr_number, owner: owner, repo: repo)
      ::Rails.logger.info("💡 #{change_files}")

      # extract changes files.
      file_labels = config.labels[repo.to_sym][:files]
      targets = {}
      if file_labels.present?
        file_labels.each_key do |label|
          regex = Regexp.new(file_labels[label])
          targets[label.to_s] = change_files.select do |v|
            v unless v.match(regex).nil?
          end
        end
      end
      
      # add labels
      add_labels = targets.select { |_k, v| v.present? }.keys.uniq

      # extract base branch
      base_branch = payload['pull_request']['base']['ref']
      branch_labels = config.labels[repo.to_sym][:branch]
      if branch_labels.present?
        branch_labels.each_key do |label|
          regex = Regexp.new(branch_labels[label])
          next if base_branch.match(regex).nil?

          add_labels << if label.to_s == 'base-branch'
                          base_branch
                        else
                          label.to_s
                        end
        end
      end

      octokit = Octokit::Client.new(access_token: ENV['GITHUB_ACCESS_TOKEN'])

      octokit.add_labels_to_an_issue(full_repo, pr_number, add_labels) if add_labels.present?

      msg = { message: 'ok' }
    else
      msg = { message: 'action is not opened.' }
    end

    render json: msg
  end
end

GraphQL API をつかった change files 取得サンプル

class GithubQueryHelper
  attr_reader :pr_number, :owner, :repo, :github_client

  def self.call(pr_number:, owner:, repo:)
    new(pr_number: pr_number, owner: owner, repo: repo).call
  end

  def initialize(pr_number:, owner:, repo:)
    @pr_number = pr_number
    @owner = owner
    @repo = repo
    @github_client ||= GithubApi::V4::Client.new(ENV['GITHUB_ACCESS_TOKEN'])
  end

  def call
    retrieve_files_from_pr
  end

  private

  def retrieve_files_from_pr
    per_page = 100
    options = {
      pr_number: pr_number,
      per_page: per_page,
      owner: owner,
      repo: repo
    }

    files = []

    total = github_client.graphql(query: init_query(options))

    total_count = total['data']['repository']['pullRequest']['files']['totalCount']
    end_cursor = total['data']['repository']['pullRequest']['files']['pageInfo']['endCursor']
    page = (total_count / per_page.to_f).ceil

    files << total['data']['repository']['pullRequest']['files']['edges'][0]['node']['path']

    i = 0
    while i < page
      options[:endCursor] = end_cursor
      end_cursor, filelist = exec_query(list_query(options))
      files.concat(filelist)
      i += 1
    end

    files
  end

  def list_query(options)
    <<~QUERY
      {
        repository(owner: "#{options[:owner]}", name: "#{options[:repo]}") {
          pullRequest(number: #{options[:pr_number]}) {
            files(first: #{options[:per_page]}, after: "#{options[:endCursor]}") {
              pageInfo {
                endCursor
                startCursor
              }
              edges {
                node {
                  path
                }
              }
            }
          }
        }
      }
    QUERY
  end

  def init_query(options)
    <<~TOTAL_QUERY
      {
        repository(owner: "#{options[:owner]}", name: "#{options[:repo]}") {
          pullRequest(number: #{options[:pr_number]}) {
            files(first: 1) {
              totalCount
              pageInfo {
                endCursor
                startCursor
              }
              edges {
                node {
                  path
                }
              }
            }
          }
        }
      }
    TOTAL_QUERY
  end

  def exec_query(query)
    data = github_client.graphql(query: query)

    end_cursor = data['data']['repository']['pullRequest']['files']['pageInfo']['endCursor']

    files = []

    data['data']['repository']['pullRequest']['files']['edges'].each do |edge|
      files << edge['node']['path']
    end
    [end_cursor, files]
  end
end

おわり

おわり

ISUCON10 予選敗退した #isucon

今年も ISUCON の予選にでた。

今年は、一人で参加した。あと、個人スポンサーにもなってみた。


やったことメモ

  • mysql をapp の instance とは別 instance へ
  • app改善してみたけど、最終的にはほぼ revert
  • db の index追加
  • appのログ出力
  • unicornunix socket 通信化
  • nginx のログを ltsv にして alp でざっとログを確認
  • newrelic agent を突っ込んで把握できるように
  • unicorn を -E production で起動

最終的には、アプリの改善はほぼできず。。

f:id:tknzk:20200914223818p:plain


起きたこと

  • 103の instance をmysqlようにしていたら 17:30過ぎに突如 instance の応答が失われる..
  • しばらく放置して運営にチケット投げてみる
  • チケット投げたら、突如復旧...

感想

  • やっぱり、一人じゃ厳しいので来年はチームででるぞ..!
  • 運営のみなさま今年もありがとうございました!!

isucon.net

B2B SaaS (MTWA) でのアカウントモデルの検討事項

この記事は、SmartHR Advent Calendar 2019 の 9日目の記事です。

SmartHR に入社してから、その殆どの時間で、アカウント周りの改修やらなんやらを行っているのですが、 B2B SaaS におけるアカウントモデルの検討事項を洗い出してみました。

検討する上でのユースケース

  • B2B
    • 担当者が扱うアカウントで良い場合
  • B2E
    • 従業員のアカウントが必要な場合

一つのテナントに紐づくアカウントでよいか

  • サービス全体でアカウントを共有するモデル

    • GitHub
      • EMAIL などで一意のアカウントが発行される
      • org には招待などで紐づけ
  • テナントごとにアカウントを作成するモデル

    • Slack
      • テナントごとにアカウントが発行される
      • テナントの切り替え = アカウントの切り替え
      • テナントの切り替えが簡単にできるUIをつくるだけで良いかも
  • 複数のテナントにまたがってログインができる機能は必要か

    • サービスの特性によって、必要な場合もある

アカウントを作成するのは誰か

  • 招待の概念が必要か
    • サービスの特性による
      • 従業員も使うサービスであるならば、従業員アカウントを管理者権限のアカウントが招待をする必要があるかもしれない
      • もしくは、社員番号などと連動した従業員アカウントの発行などが必要かもしれない

従業員それぞれのアカウントが必要なのか

  • 従業員がサービスにアクセスする必要があるならば、必要
    • 勤怠管理システムなど

権限設定が必要か

  • 任意の権限を設定できる必要があるか
  • 従業員がアクセスできるサービスの場合は必須
    • 見えてはいけないものを見えないように

認証は独自でやるのか IdP と連携するのか

  • 認証基盤を運用するのは大変
    • できれば IdP と連携するものをリリース当初から持っておいたほうが楽
      • そもそも IdP に投げる仕組みにしてあるほうが好ましいかも
    • 退職したら、アクセスできないようにする必要があるかも

セッションの管理はテナントごとになっている必要があるか

  • 複数のテナントにまたがったアカウントがなければ不要かも
  • MTWA のアーキテクチャとしてテナントの切替方法がどの様になっているか
    • サブドメインベースでテナントがきりかわるのか
    • 同一ドメインで PATH にテナント情報が付与されているのか
    • Slack の web 版が最近、サブドメから PATH 方式に切り替わっていてなぜなのかが気になるところです。

おわりに

サービスの成長や、方向性などで、無限に考えることが多くなりがちですが、 つどつど見極めつつ、検討するのがいいかと思います。

SmartHR でのアカウント周りについて、悲喜こもごももあるのですが、 書けない話も多いので、気になる方は、どこかで直接あったときにでも。

ISUCON9 予選敗退した #isucon

今年も ISUCON の予選にでた。

今年は、去年のチームメンバーの都合が合わずに新規で、社内で募集してチームを作って参加した。
メンバーは @ykarakita@tak_wak_dev でチーム名は ウデムシlab 。
二人は初参加だったけど、事前練習をすることができずに、ぶっつけ本番となってしまった。


アプリの改善は二人に任せつつ、下回りをザクッと対応する方針で進めた。

やったことメモ

  • ruby 実装に変更
  • nginx のログを ltsv 化
  • alp に食わせて必要なタイミングで共有
  • nginx -> ruby 間を unixsocket に変更
  • mysql 用の instance に変更
  • mysql slow log の確認

アプリの改善の方は

という感じで、 N+1 を潰す作業に追われてしまった様子だった。

あと、途中で オフィスに誰か来たなと思ったら LINEからきた人だった


最終スコアは、 3910イスコイン で惨敗だった。。

f:id:tknzk:20190909001620p:plain


初参加の二人に ISUCON の楽しさをしってもらえたので、来年も、がんばるぞ..!
運営の皆様、ありがとうございました!

isucon.net