移転しました。

約3秒後に自動的にリダイレクトします。

Yarnを使うとAzure FunctionsのGitHub ActionsからのZipデプロイが檄遅になるのを回避する

Viibarのエンジニアの id:mstssk です。

先日リリースしたVideoTouchではAzureのマネージドサービスを組み合わせて開発を行っています。

videotouch.jp

Azure Functionsでのデプロイで「そんなところが影響するの!?」と罠を踏んだところがあったのを記事にしてみます。


※この記事はZennにも投稿しています。


npmとYarnでファイルのタイムスタンプの扱いが違うとかとんでもない罠だ。

問題

Azure FunctionsにJavaScript(TypeScript)のコードをGitHub ActionsでAzure/functions-actionアクションを使ってZipデプロイを行なっていました。 プロジェクトは func init --typescript で生成した構成ほぼそのまま。 パッケージマネージャーをnpmからYarnに変えたところ、数分で済んでいたデプロイが数十分かかるようになってしまいました。

原因

Azure/functions-actionアクションを使ってデプロイする場合、node_modulesディレクトリごとZipで固めてデプロイ先の環境に展開しなおします。 ですが、毎回全ファイルをコピーしているわけではなく、タイムスタンプが更新されていなければスキップするようになっています。

Efficient file copy: Files will only be copied if their timestamps don't match what is already deployed. Generating a zip using a build process that caches outputs can result in faster deployments.

https://github.com/projectkudu/kudu/wiki/Deploying-from-a-zip-file-or-url#comparison-with-zip-api

ここで、npmとYarnの動作の違いがネックになってきます。

npm

npmはnode_modulesの下に展開されるファイルのタイムスタンプを全て 1985-10-26T08:15:00.000Z に固定しています。 これは、タイムスタンプの違いによってファイルのハッシュ値が異なってしまい発生する問題を回避するためだそうです。 https://github.com/npm/npm/commit/58d2aa58d5f9c4db49f57a5f33952b3106778669

Yarn

一方、Yarnではnode_modulesの下のファイルのタイムスタンプは、単純にインストールを行なった日時、またはローカルキャッシュされた日時になります。 つまり、GitHub Actions(またはその他のCI)でYarnのキャッシュを持ち回すようにしていない場合、常にCIが走った日時のタイムスタンプでZipデプロイが行われることになります。

npmでも一番最初のデプロイでは全ファイルのコピーが行われていましたが、当時は依存パッケージも少なく気にならなかったので気付いていなかっただけのようです。

解決策

npmに戻してしまうのも手段の一つです。 しかし、やんごとなき理由でパッケージマネージャーをYarnにせざるを得ないこともあるので、Yarnを使う前提の回避方法が必要です。1

キャッシュによって問題を回避する方法と、抜本的にAzure Functionsの使い方を変えてしまう方法の2種類があります。

キャッシュする

Yarnではnode_modulesの下のファイルのタイムスタンプはキャッシュに依存するので、素直にGitHub Actions上でYarnのキャッシュを保持するようにしましょう。

公式のcacheアクションのドキュメントにYarnの場合のサンプルが載っているので参考にします。

https://github.com/actions/cache/blob/main/examples.md#node---yarn

GitHub Actionsの設定ファイルは、例えば次のような形になるはずです。

# .github/workflows/deploy.yml の関係するstepsだけ抜粋
- name: Get yarn cache directory path
  id: yarn-cache-dir-path
  run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
  id: yarn-cache
  with:
    path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
    key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
    restore-keys: |
      ${{ runner.os }}-yarn-
- run: yarn install --frozen-lockfile
- run: yarn run build
- run: yarn install --frozen-lockfile --production
- name: Deploy
  uses: Azure/functions-action@v1
  id: fa-foobar
  with:
    app-name: func-foobar
    publish-profile: ${{ secrets.FOOBAR }}

Run From Package

Zipパッケージを環境上に展開するためファイルコピーに関連する問題が発生してしまっています。 そこで、Zipパッケージをそのまま実行ファイルにする方法が用意されています。

https://docs.microsoft.com/ja-jp/azure/azure-functions/run-functions-from-deployment-package

むしろ今Azure Functionsを使うならRun From Packageを使うのが安牌なのですが、手が追いついてませんでした😓 既にZipデプロイをしているなら、よほど特殊な事をしていない限りはRun From Packageへの移行はすんなりいきます。


  1. 🍏はnpmでいいじゃん派です。

jest.config.jsや.eslintrc.jsで入力補完する

Viibarのエンジニアの id:mstssk です。

つい先日、ここ1年ほど作ってきた新サービスVideoTouchを公開しました。

videotouch.jp

TypeScriptを使っている部分ではJestでテストしたり、ESLintを使っています。 それらのコンフィグファイルを書いていて、もどかしかった部分を簡易的に改善する方法を知ったので記事にしてみます。


※この記事はZennにも投稿しています。

jest.config.jsや.eslintrc.jsとかを書くときに入力補完したいなー、でもそのためだけにVisual Studio Codeの拡張入れたり、ts-nodeを導入してjest.config.tsにするのはやりすぎな気がするなー、と思っていました。

調べていたら、JSDocでTypeScriptの型定義を使えるとわかったのでメモ。

サンプル

// jest.config.js
/** @type {import("@jest/types").Config.InitialOptions} */
const config = {
  preset: "foobar",
};
module.exports = config;
// .eslintrc.js
/** @type {import("@typescript-eslint/experimental-utils").TSESLint.Linter.Config} */
const config = {
  extends: ["foobar"],
};
module.exports = config;

@typescript-eslint/experimental-utilsからインポートするのは微妙だけど他に良い感じのを見つけられなかった。

注意

module.exports = {} と直接exportする書き方にすると余計なものもサジェストされてしまいます。 objectではなくコードのブロックと見なされてしまうので無関係なglobalスコープのクラス名とかまで入力候補に出てきてしまいます。 一度変数に代入しましょう。

/** @type {import("@jest/types").Config.InitialOptions} */
module.exports = {
  // 👎 objectではなくコードのブロックと見なされてしまうので関係ないものもサジェストされてしまう。
};

スクリーンショット

Visual Studio Code 1.55.0 で動作確認。

入力補完中のスクリーンショット

参考

qiita.com

jsdoc.app

VAddyを使用して脆弱性診断を実施した話

こんにちは。エンジニアの@daikichiです。 今回は、弊社サービスにセルフ脆弱性診断ツールVAddyを導入した時の話をしたいと思います。

脆弱性診断の診断方法

大きく2パターンあります。

  • ツール診断
    • メリット: ⾃動で効率的に膨⼤なパターンをチェックできる
    • デメリット: 複雑なシステムで検知率が上げづらい
  • ⼿動診断
    • メリット: セキュリティの専⾨家が実施し、ツールでは⾒つけ づらい脆弱性に対象可能
    • デメリット:専⾨的な知識が必要となるため、価格が⾼く時間 がかかりやすい

導入予定のサービスでは今後の開発を見込んで、継続的に診断できる方法を検討していました。 その中でも、現在の環境でもっとも効率が良いと感じたのがツール診断にあたるVAddyでした。

VAddyとは

クラウド型Webアプリケーション脆弱性診断ツールでサーバのOS、ミドルウェア、開発言語に関わらず検査可能です。 ツール診断を主としていますが、手動診断にも対応しています。

VAddyを選んだ理由

  • セキュリティに精通していないエンジニアを対象としている為操作が容易
    • クローリングからスキャン、結果確認まで全て管理画面で実施可能
    • 検査項目は必要最低限
    • 脆弱性実施までのフローがシンプル
  • API連携可能(CI連携可能)
  • サポートのレスポンスが速い

特に驚いたのがサポートのレスポンスの速さです。もちろんその時の状況にもよると思いますが、私が何を聞いても営業担当、エンジニア担当の方からの迅速、的確なレスポンスで安心でした。 これは他ツールで検証した中でも断トツで、長く利用する事を前提に考えていたので大変心強かったです。

診断項目

検査項目はプランによりますが、最大9つの検査項目が用意されています。 導入サービスでは初めて脆弱性診断を実施するという事もあり、初月一番上のEnterpriseプランを契約する事にしました。 9つでは少なく感じるかもしれませんが、これでも現実の攻撃の約90%(※クラウド型WAF「Scutum」の観測から算出)をカバーしているとの事です。

「()」内はどのOWASP TOP 10 に対応しているかを記載

  • [Enterprise][Professional][Strater] SQLインジェクション(A1 インジェクション)
  • [Enterprise][Professional][Starter] クロスサイトスクリプティング (A1 インジェクション)
  • [Enterprise][Professional]    リモートファイルインクルージョン(A1 インジェクション)
  • [Enterprise][Professional]    コマンドインジェクション (A1 インジェクション)
  • [Enterprise][Professional]    ディレクトリトラバーサル (A3 機微な情報の露出)
  • [Enterprise][Professional]    ブラインドSQLインジェクション (A1 インジェクション)
  • [Enterprise]          安全でないデシリアイゼーション (A8. 安全でないデシリアライゼーション)
  • [Enterprise]          XML外部実体攻撃 (A4 XML外部エンティティ参照)
  • [Enterprise]          HTTPヘッダインジェクション (A1.インジェクション)

脆弱性診断実施のステップ

基本的な使い方だと4ステップで簡単に脆弱性診断ができます。 スタートアップガイドに沿ってやれば問題なく実行できました。

  • 【STEP 1】プロジェクトの作成
  • 【STEP 2】クロール(シナリオ作成)の設定と実行
  • 【STEP 3】スキャンの実行
  • 【STEP 4】レポート確認

クロール時のTips

  • クロールするブラウザはFireFoxがよい
    • → VAddyプロキシを通す必要があるので、設定が必要になりますが、Google Chorome、Edgeはブラウザだけで設定できなくて、OSのネットワーク設定でやらないとできない
  • 外部サイトからライブラリを読み込んでいるアプリケーションはプロキシの除外設定が必要
    • → VAddyのProxyサーバーは管理画面で登録されたFQDNとの通信だけを許可している為

実施所感

初期設定から1時間もかからずに診断の実施ができました。レポートの画面はシンプルで見やすいです。リクエスト、レスポンスの検査データ情報が確認できるので、あとはコードと照らし合わせて実際に脆弱性の問題があるかの確認をする流れです。

本来ならクロール(シナリオ作成)まで自動化できればよいのですが、それはまた別の機会に。

まとめ

  • VAddyはセキュリティに精通していない開発エンジニアでも使えるようにシンプルに設計されていて、良い
  • セルフVAddyの利用で100%安心というわけではなく、規模、フェーズによっては手動診断を実施するのがよい

AzureのApplicationGatewayにAppServiceの証明書を使う方法

あけおめ、年末は積みゲー消化のために、対馬を救いながら、稲作していたら、終わってしまいました、id:gaoohです。 近未来でサイボーグで戦いたいのに、まだ手が回らない。

本題です。 年末、AzureのApplicationGatewayにAppServiceの証明書を適用するのではまったのでブログにまとめます。

手順

前提としてAppServiceの証明書は発行して利用できる状態であるとします。

まずApplicationGatewayがKeyVaultの証明書を取得できるように、証明書の参照権限をもった User Assigned Managed Identity IDを発行し紐づけます。 その上でApplicationGatewayで利用する証明書を登録という手順が必要です。

1. ApplicationGateway に User Assigned Managed Identity ID を発行

f:id:gaooh:20210104221343p:plain

2. 発行した User Assigned Managed Identity ID に 証明書の参照権限を付与

f:id:gaooh:20210104221237p:plain

3. ApplicationGatewayで利用する証明書を登録

これは 2020/12 月の段階ではWebUIは用意されていないのでコマンドで

az network application-gateway ssl-cert create --gateway-name "#{gateway-name}" \
  --resource-group "#{resource-group-name}" -n #{sslname} 
  --key-vault-secret-id "https://#{key-vaultname}.vault.azure.net/secrets/****"

PowerShellがお好みだったらこっちを参考 docs.microsoft.com

以上を行うとリスナー作成時の証明書の選択肢に該当の証明書が選べるようになります。

ハマったポイント

以前はこのコマンドもなかったのか、証明書を一度エクスポートしてApplicationGatewayに登録するという方法が取られていて、ぐぐるとその方法が出てくる。 なんならWebコンソールだけ見ているとそれしか方法がないように思える。

f:id:gaooh:20210104221241p:plain

「そうかエクスポートするのか」と思い、うっかりこの方法で行ってしまった。

ちなみに一応できたんのだが、PowerShellで証明書をエクスポートすると中間証明書がないものがエクスポートされるので、うっかりそれを設定してしまったのです。 中間証明書ってなくてもブラウザで確認するだけだと気がつかなくて、気がつくのが遅れたという二重の罠にもハマりました。

この方法だと証明書の更新の際に作業が必要なので、ApplicationGatewayの利用をしたいだけなら今は利用すべきじゃないので、当初書いた手順が正しい。

いじょ。

Chrome拡張機能のminimum_chrome_versionを安易に設定してはいけない

最近Chrome拡張機能を触ってる id:mstssk です。

つい先日、Chrome拡張機能の審査でハマったのでのでそのまとめです。

3行まとめ

  • manifest.jsonminimum_chrome_versionフィールドには、ちゃんと機能上必要なAPIのサポートバージョンに準じた値を設定しよう。またはいっそ設定しない。
  • 2020年11月発表のポリシーの改定の際に、おそらく審査システムも変更され、minimum_chrome_versionが最近すぎると即審査が不承認にされるようになった。
  • 安易に「最新のChrome 87でだけ動作確認したから"87"にしておこう。古いChromeで動かされても困るし」とかやってはいけない。

何があったか

ここしばらくChrome拡張機能を仕事でいじっています。 その中でminimum_chrome_versionを安易に指定していたためにハマりました。

minimum_chrome_versionとは

Chrome拡張機能のメタ情報を管理するmanifest.jsonファイルにはminimum_chrome_versionというフィールドがあります。 このフィールドは必須ではないですが、バージョン番号を設定しておくと「このバージョン未満では動作しないのでインストール不可」という制御が行なえます。

developer.chrome.com developer.chrome.com

そこで「動作確認してない古いChromeで動かされても困るから、とりあえず今の最新verにしておこう」と考えて、開発当時のChromeが83だったので"minimum_chrome_version": "83" とmanifest.jsonに設定しました。

しばらくは、それでChromeウェブストアの審査は何事もなく通り、ちょくちょくアップデートを上げる際も問題なく審査が通っていました。

突然の不承認の嵐

11月中旬に少し大きめの変更を審査に出したときのことです。 「説明に表記されている機能を提供していない。」という理由で初めて不承認となりました。

そんなばかな!? 大きめの変更とはいえUIの更新が主で機能は変わってないぞ!?

アプリストアの類で審査に落ちる事自体はよくある話なのですが、この時は変更内容と指摘事項が乖離していて混乱しました。
たまたま厳しいレビュアーにあたってしまったか、ログイン必須なものなので公の機能を提供できていないと判断されたか…など考えたものの、不承認となったのはしょうがないので怪しいところを直して再提出をしました。
UIの動線を変えたり、実装上の都合で付けていたpermissionを外して何とか代替実装にしたり、といった事をしましたが、その後3連続で不承認となりました。

サポート問い合わせと拍子抜け

提出物を詳しく調べたところ、ポリシーに準拠していることがわかりました。

サポートに異議申し立てを送信したところ、そんな内容の返信がすんなり返ってきました。
なんじゃそりゃと思いつつ、返信に従って同じものを再度審査に送信し直したら、無事審査を通過しました。 *1

たぶん審査システムが変更された?

その後もサポートとやり取りしたところ*2、審査側で「システムが"This extension requires Google Chrome version 83 or greater. Could not load manifest."というエラーで不承認にしたがその後詳細に調べたところChrome 83以降で動作することを確認した」といった返信が来ました。

そうしてやっと合点がいきました。
ちょうど不承認をくらったあたりで、Chromeウェブストアのポリシー改定が発表されており、ストアのダッシュボード上の入力項目が増えたりしていました。

blog.chromium.org

あくまで推測ですが、ポリシー改定のタイミングで審査システムも変更が入ったのではないでしょうか。 それが審査システムの不具合なのか、それともある程度古いバージョンもサポートすることを念頭に置いたものだから新しめのバージョンでは不承認となってしまのか、詳しいことはわかりません。

しかし、この後minimum_chrome_versionフィールドの値を機能上必要なAPIが実装されたバージョンまで落としてから審査に出した際は、すんなり審査が通りました。

参考

*1:実際は、再送信したらまたそれがrejectされて、また異議申し立てしたら"今度はこっちで通過させておきます"的な返事が来てやっと通ったりした。

*2:その次のリリースでまた不承認になってまた異議申し立てしたの意

WebでHTMLをいい感じにコピーさせる

こんにちは id:mstssk です。

Webサービスを作っていると、何かしらをユーザーにコピーして使ってもらうというシチュエーションが出てきます。 WebページをシェアするためにURLのコピーボタンが置いてある、なんていうのはよく見かけますね。

しかし、ある程度複雑なコンテンツはHTML情報としてコピーさせたい事が稀にあります。 画像やリンクを含んだHTMLです。

そこで、もう少し欲張って、こういうことが出来ないか調べてみました。

3行まとめ

  • ただのテキストをコピーするだけならクリップボードAPIを使えばOK。
  • HTMLをWYSIWYGに貼り付ける状態でコピーさせるには、現在非推奨な実装方法を使いつつ一工夫必要になるけど可能。
  • 一行余った
動作サンプル

stackblitz.com

以下、解説。

Webブラウザクリップボード API

developer.mozilla.org

最近のWebブラウザにはPCのクリップボードを操作するAPIが標準搭載されており、JavaScriptを数行書くだけで任意のテキストをコピーさせられます。

// 「ほげほげ」というテキストをコピーする場合
copyButton.addEventListener("click", () => {
  navigator.clipboard.writeText("ほげほげ")
    .then(()=> console.log("copied!"));
});

好き勝手にクリップボードを操作できるわけではなく制約があります。 ユーザーがボタンをクリックしたりとか何かしら操作を行った時だけクリップボードへのアクセスが許可されています。

このクリップボード APIはどのWebブラウザにもある機能ですが、2020年5月現在ではテキストまたは画像ファイルしかコピーさせられません

2020/07/10 追記

Safari 13.1.1ではクリップボード APIでtext/htmlのコピーが行えるようです。

クリップボードAPIの制限については、執筆時点のものであり、またブラウザごとに対応状況も違っています。 この記事を参考にする前にクリップボードAPIの最新の実装状況を確認するのをおすすめします。

HTMLコピーを無理やり実現するやり方

Hackyなやり方ですが、Chrome, Safari, Firefoxで動く実装はあります。

developer.mozilla.org

document.execCommand("copy"); でコピー動作を直接呼び出す方法は、クリップボードAPIが登場する以前はずっと使われてきましたが、現在では非推奨とされています。 しかし、今回はクリップボードAPIで未対応のデータ(HTML)を扱うため致し方ありません。

実際の実装は以下の通り。 行っているのは、ユーザーがマウスでWebページ内を範囲選択してコピーしているのと同じ事をJavaScriptからやっているだけ。 ただし、プレーンテキストの情報も持たせるために一工夫しています。

const html = `<a href="https://example.com/">
                <img src="https://github.com/viibar.png">
              </a>`;
copyButton.addEventListener("click", () => {
  // コピー対象にするダミー要素
  const elem = document.createElement("span");
  elem.style.height = "0px";
  elem.style.width = "0px";
  elem.innerHTML = html; // ダミー要素の中身が無いとSafariで動作しないので。
  document.body.appendChild(elem);

  // ダミー要素を選択状態にする
  const range = document.createRange();
  const selection = document.getSelection();
  selection.removeAllRanges();
  range.selectNodeContents(elem);
  selection.addRange(range);

  // コピーが行われたとき、コピーデータをすげ替える。ここが一工夫。
  document.addEventListener("copy", function listener(event) {
    event.preventDefault();
    event.clipboardData.setData("text/html", html); // for WYSIWYG
    event.clipboardData.setData("text/plain", html);
    document.removeEventListener("copy", listener);
    console.log("copied!", event);
  });

  // コピー実行
  document.execCommand("copy");

  // 選択状態解除とダミー要素削除
  selection.removeAllRanges();
  document.body.removeChild(elem);
});
実際に貼り付けてみた場合のスクリーンショット

WYSIWYGエディタやMS Wordなんかは、HTMLを解釈してそういうコンテンツとして貼り付けられます。

Gmailのメール作成画面(WYSIWYG) MS Word
Gmailのメール作成画面 Wordの画面

WYSIWYGエディタやWordにただのテキストとして貼り付けたいときは、右クリックメニューにオプションがあるはずです。

ただのテキストエディタでは、HTMLのソースコードをテキストとして貼り付けられます。

Visual Studio Code
Visual Studio Code

GitHubのOrganizationを2FA必須にしようとしたらちょっと大変でした

Viibar開発部の id:mstssk です。

もうしばらく前になるのですが、Viibarで使っているGitHubのOrganizationを2要素認証(以下、2FA)を必須にしました。

Organizationに所属するアカウントを2FA必須にするのは、設定画面でチェックボックスを切り替えるだけで簡単に行なえます。

Orgの2FAの必須設定

しかし、設定を有効にする時に既にOrganizationに2FAを有効にしていないアカウントがあると、そのアカウントは強制的にOrganizationから外されてしまいます。 事前に所属アカウントの2FAをすべて有効にしておかなければいけません。

help.github.com

よっしゃやったろ!と思って手を付けたら、思いのほか手間がかかってしまいました。

既存アカウントを2FAに

まずはOrganizationに所属しているアカウントのうちどれが2FAになっているかを確認していきます。

Organizationの管理権限をもっていれば、所属するアカウントの一覧でそれぞれ2FAになっているかどうかを確認できます。

help.github.com

既に社内のエンジニアのアカウントはすべて2FAになっていました。

他方、Viibarではコーポレートサイトの管理もGitHubリポジトリにしており、エンジニアではない広報担当者のアカウントが2FAになっていませんでした。 その時、近くに座っていらっしゃったので、早速社内で声をかけて2FAを有効にしてもらいました。

謎の共有アカウントを削除

他の2FAになっていないアカウントを見ていくと、 viibar-admin というアカウントがありました。 なにかの管理用アカウントっぽい名前ですが、アクティビティを見ても何年もなにもしていません。

社内で聞いてみたところ、むかーしに共有して使っていたアカウントとのこと。 完全に使っていないので、とりあえずOrganizationから外しました。

アカウント自体は残しておこうかと少し迷いましたが、削除することにしました。

ただ名前の確保のためにアカウントを取得しておくことはGitHubのポリシーに反します。 かつ、使われていないアカウントは思いの外カジュアルに名前を他の希望者に移したりされたりします。 将来的に活用したいというのも現時点では無く、素直にアカウントを消しておきました。

GitHub はアカウント名を不正に占拠することを禁止しており、現時点で使用しないアカウント名を将来の使用のために保持することはできません。 使用されていないアカウントは、GitHub スタッフの裁量で名前を変更または削除される場合があります。

GitHub ユーザ名ポリシー - GitHub ヘルプ https://help.github.com/ja/github/site-policy/github-username-policy

いざアカウントを削除しようとした際に、あれ?パスワードどこ?というひと悶着があったのはまた別のお話。

Machine Userの2FA化

GitHubは様々なタスクの自動化のために専用アカウントを作る事を許容していて、それは Machine User と呼ばれます。

ViibarでもBeaverくんと呼んでいるアカウントを一つ使っています。

さてビーバーくんのアカウントを2FA化しようとして一つ壁にぶつかりました。 アカウントを開発部の中で共同管理出来ている状態を維持しつつ、どうやって2要素目の認証を設定するのか。

GitHubの2FAにはTOTPアプリまたはSMS認証が必要です。 つまり、特定のスマートフォンに依存するのが普通です。

1Password の有料プランであればワンタイムパスワードを共同管理できますが、弊社で使っているのは LastPass です。

どうせ特定のデバイスに依存せざるを得ないなら、セキュリティトークンを社内で管理すればよいのでは?とも思いました。 しかし、セキュリティトークンはTOTPまたはSMS認証を設定した上でのバックアップ的にしか使えません。

最終的には、開発部の共用スマートフォンにTOTPアプリを入れてビーバーくんを2FA化しました。

終わりに

思ったより時間がかかってしまいましたが、これでGitHubのOrganizationを2FA必須にできました。 以後、Organizationに追加するメンバーにも常に2FAを要求します。 いちいち「セキュリティのために2FAにしてね」などとアナウンスしなくても済むようになりました。

セキュリティはもちろん大事ですが、常にセキュリティを意識するのは大変です。 Organizationの設定ひとつで所属メンバーに常に2FAを必須にできるのは楽ちんなので、この記事をご覧になった方もお忘れでしたら設定しましょう。 もし問題が起きたら、この記事が参考になれば幸いです。