every Tech Blog

株式会社エブリーのTech Blogです。

Vue Fes Japan 2024 Pre LT Party で登壇しました

はじめに

エブリーの羽馬(https://twitter.com/naoki_haba)です。

2024年10月17日に開催された Vue Fes Japan 2024 Pre LT Party にて「unplugin-vue-routerで実現するNuxt風ファイルベースルーティング」というテーマで登壇させていただきました。

optim.connpass.com

この記事では、unplugin-vue-router の魅力と発表で伝えたかったポイントについて共有します。

イベント概要

Vue Fes Japan 2024に先立って開催された事前LTイベントでは、Vue.js に関する様々なトピックについて、短時間で濃密な情報共有が行われました。

発表のハイライト

発表では、Vue.js プロジェクトでよく直面する以下のような課題に対する解決策として、unplugin-vue-router を紹介させていただきました:

www.docswell.com

  • 😓 route.js(ts) の肥大化による管理の複雑化
  • 🔁 ページ追加時の煩わしい反復作業
  • 🤔 Nuxt を使わずにファイルベースルーティングを実現したいニーズ

主要な説明ポイント

  1. 型安全性の実現

    • ルート名とパスの自動補完
    • パラメータの型チェック
    • 存在しないルートの即時検出
  2. ファイルベースルーティングの利点

    • ファイル構造による直感的なルート管理
    • ネストされたレイアウトの自然なサポート
    • 動的ルートの簡単な定義
  3. データローダーの可能性

    • ルート単位でのデータプリフェッチ
    • コンポーネントとデータ取得ロジックの分離

導入のメリット

unplugin-vue-router の導入により、以下のような効果が期待できます:

  1. 📈 開発効率の向上

    • ルーティング設定の自動化
    • 手動設定の手間を大幅に削減
  2. 🧠 認知負荷の軽減

    • ファイル構造に集中するだけでOK
    • 複雑なルーティングロジックから解放
  3. 🔧 柔軟性の向上

    • Vue.js プロジェクトでファイルベースルーティングを実現
    • Nuxt ライクな機能を単体のVue.jsアプリケーションで実現

注意点

発表では、以下の注意点についても触れさせていただきました:

  • 安定性と実験的機能

    • 型付きルーティングとファイルベースルーティングは安定
    • データローダーなどの実験的APIは将来変更の可能性あり
  • SSRサポート

    • 現時点でSSR(サーバーサイドレンダリング)はサポートされていない

まとめ

Vue Fes Japan 2024 Pre LT Partyでの発表を通じて、unplugin-vue-routerの主要な機能と活用方法について共有させていただきました。Vue.jsプロジェクトの開発効率を向上させるための選択肢として、ぜひ検討いただければ幸いです。

また、10月30日のアフターイベントでも登壇させていただきますので、そちらもぜひご覧ください。

studist.connpass.com

amazon-quicksight-embedding-sdkを利用したQuickSightの埋め込み

エブリーでデータエンジニアを担当している塚田です。

QuickSightを活用したビジュアライズを進めていますが、そのビジュアルの埋め込みで外部のサイトで表示する部分を検証しています。

今回はその検証過程で利用したamazon-quicksight-embedding-sdkについて、使用方法などを紹介します。

Amazon QuickSightとは

Amazon QuickSightは、AWSが提供するビジネスインテリジェンス (BI) サービスです。AWSとの連携が容易で、比較的簡単に利用を開始することができます。

QuickSightの大きな特徴として、SPICEと呼ばれるインメモリエンジンがあります。SPICEはカラムナフォーマットでデータを保存することで、高速なクエリ処理を実現しています。これにより、大量のデータを高速に分析し、ダッシュボードに表示することができます。

QuickSightダッシュボードをアプリケーションに埋め込むことで、自社サービスの一機能として活用できます。

例えば:

  • 社内ポータルサイトに組み込んで、従業員が必要なデータに簡単にアクセス
  • 顧客向けポータルサイトに統合して、利用状況の分析機能を提供
  • 既存のWebアプリケーションにシームレスに統合してデータビジュアライゼーション機能を追加

埋め込みの実装手順

今回は、認証済みユーザーでQuickSightダッシュボードにアクセスする実装例を紹介します。

なお、QuickSightでは以下のような他の埋め込みオプションも用意されています。

  • 匿名ユーザーによるアクセス
  • ダッシュボード以外のビジュアルの埋め込み

環境

  • npmとNext.js、Reactがインストールされていること
  • 必要なパッケージがインストールされていること
    • @aws-sdk/client-quicksight
    • amazon-quicksight-embedding-sdk

埋め込みQuickSightのURL取得

QuickSightダッシュボードの埋め込みURLを取得します。 AWS CLIやAWS SDKを使用して生成できます。

一例としてAWS SDK for JavaScriptを使った場合のURL取得サンプルを示します UserArnはListUsersCommandなどを利用することで確認ができます。

import { QuickSightClient, GenerateEmbedUrlForRegisteredUserCommand, GenerateEmbedUrlForRegisteredUserCommandInput } from '@aws-sdk/client-quicksight';

// QuickSight クライアントの作成
const quicksightClient = new QuickSightClient({
  region: "ap-northeast-1" // QuickSightを利用しているリージョンを指定
});

// 埋め込み用 URL の取得
const params: GenerateEmbedUrlForRegisteredUserCommandInput = {
  AwsAccountId: "123456789012", // QuickSightを利用しているAWSアカウントIDを指定
  SessionLifetimeInMinutes: 600,
  UserArn: "arn:aws:quicksight:ap-northeast-1:123456789012:user/default/xxxxxxxxxx", // 利用するQuickSightのユーザーARNを指定
  ExperienceConfiguration: {
    Dashboard: {
      InitialDashboardId: "xxxxxxxx-1111-xxxx-1111-xxxxxxxxxxxx", // 利用したいダッシュボードIDを指定
      FeatureConfigurations: {
        StatePersistence: { Enabled: false },
        SharedView: { Enabled: false },
        Bookmarks: { Enabled: false },
      }
    }
  },
};
const command = new GenerateEmbedUrlForRegisteredUserCommand(params);
try {
  const data = await quicksightClient.send(command)
  const embedUrl = data.EmbedUrl || ''
  console.log(embedUrl)
} catch (error) {
  console.error('Error generating embed URL:', error);
}

amazon-quicksight-embedding-sdkを利用した埋め込み処理

先のURLへそのままアクセスすることで埋め込み用のダッシュボードにアクセスすることが可能です。 ただ、システム内で利用する際にはそのURLを伝えて見てもらうような運用は考えられないので、Webページに埋め込んでアクセスできるようにしたいと思います。

すでにNext.jsなどを利用してサーバーが起動しており、ブラウザからアクセス可能な状態を前提にします。

注意事項

  • QuickSightの管理画面でドメイン許可の設定が必要です
  • 埋め込み先のサーバーはHTTPSである必要があります
    • 開発環境ではnext dev --experimental-httpsを使用できます
"use client";
import { useEffect, useRef, useState } from 'react';
import { createEmbeddingContext } from 'amazon-quicksight-embedding-sdk';

export default function QuickSightDashboard() {
  const containerRef = useRef<HTMLDivElement>(null);
  const [embeddingContext, setEmbeddingContext] = useState<any>(null);
  const [embeddedDashboard, setEmbeddedDashboard] = useState<any>(null);
  const [dashboardParameter, setDashboardParameter] = useState<string | null>(null);

  useEffect(() => {
    const fetchEmbeddingContext = async () => {
      const context = await createEmbeddingContext();
      setEmbeddingContext(context);
    };
    fetchEmbeddingContext();
  }, []);

  useEffect(() => {
    if (embeddingContext) {
      embed();
    }
  }, [embeddingContext]);

  useEffect(() => {
    if (embeddedDashboard && dashboardParameter) {
      embeddedDashboard.setParameters(dashboardParameter);
    }
  }, [embeddedDashboard, dashboardParameter]);

  const embed = async () => {
    const frameOptions = {
      url: "https://xxxxxxxxxxxx", // 前の手順で作成された埋め込み用URLを指定
      container: containerRef.current,
      width: "100%",
      height: "100%",
      resizeHeightOnSizeChangedEvent: true,
      onChange: (changeEvent: any) => {
        switch (changeEvent.eventName) {
          case 'FRAME_MOUNTED': {
            console.log("Do something when the experience frame is mounted.");
            break;
          }
        }
      },
    };

    const contentOptions = {
      parameters: dashboardParameter,
      locale: "ja-JP",
      sheetOptions: {
        singleSheet: false
      },
      toolbarOptions: {
        export: true,
        undoRedo: false,
        reset: true
      },
      attributionOptions: {
        overlayContent: false,
      },
      themeOptions: {
        "themeOverride": {
          "UIColorPalette": {
            SecondaryBackground: '#FFFFFF',
            SecondaryForeground: '#000000'
          },
          "DataColorPalette": {
            "Colors": [
              "#E6194B",
              "#3CB44B",
              "#FFE119",
              "#4363D8",
              "#F58231"
            ]
          },
          "Typography": {
            "FontFamilies": [
              {"FontFamily":"Comic Neue"}
            ]
          }
        }
      },

      onMessage: async (messageEvent: any) => {
        switch (messageEvent.eventName) {
          case 'CONTENT_LOADED': {
            console.log("コンテンツのロードが完了しました:", messageEvent.message.title);
            break;
          }
          case 'PARAMETERS_CHANGED': {
            console.log("パラメータが変更されました:", messageEvent.message.changedParameters);
            break;
          }
        }
      },
    };

    const embedDashboard = await embeddingContext.embedDashboard(frameOptions, contentOptions);
    setEmbeddedDashboard(embedDashboard);
  };

  return (
    <div ref={containerRef}></div>
  );
};

今回はサンプルなのでframeOptionscontentOptionsは設定できるものの中からよく使いそうなものを指定しています。

他にもREADMEなどで指定できる内容が記載されているので、利用用途に合わせて変更することで目的にあった表現に近づくと思います。

実際に利用することを見据えた対応

  • 埋め込み用URL生成ロジックをAPI化し、アクセス時にURL発行->embedUrlの設定を行うことで柔軟に表示できるようにする
  • 今回は用意しかしていませんがdashboardParameterを変化させることで、QuickSight外からのパラメータの入れ込みができるようになるのでデザインの自由度が上がる

まとめ

マネージドなBIの良さを生かしながらプロダクトに組み込むにはという視点で今回は埋め込み処理の方法を取り上げました。

埋め込むことによって表現できる幅の広がりやデザインの統一感も生まれると思うので、必要に応じてこういった機能の利用をしていくべきと感ました。

Flutter2から3に上げた際のNull Safety対応

Flutter2から3に上げた際のNull Safety対応

はじめに

リテールハブ開発部のネットスーパー事業向き合いで開発を行っている野口です。 今回は、弊社で開発しているFlutterアプリのバージョンを2.10.5から3.24.3に上げた際にNull Safety対応を行ったのでそれについて書いていきます。

Null Safetyとは

Null Safetyは、変数が null を持つことによって発生するバグを防ぐための仕組みです。 Flutter2以前では変数はnullableになっているため、nullを考慮したコードが必要でした。 Flutter3以降では、変数がnon-nullableになるため、より堅牢なコードが書けるようになります。詳しくは以下をご覧ください。

https://dart.dev/null-safety/understanding-null-safety

Null Safety対応の手順

実際にFlutter 3.24.3にアップグレードした際、800件近いエラーが発生しました。(このプロジェクトの総Dart行数は20430行です) これらを効率的に解決するため、段階的に対応しました。 まず、データの基盤であるモデル層から対応を開始し、次にリポジトリ層とユースケース層を経由して、最終的にUI層に至る順で対応しました。こうすることで、データが正しく上流から下流に流れることを確認しつつ、エラーを段階的に解消することができました。

具体的な手順としては、まずモデル層で null の許容や必須を明確にし、次にリポジトリ層やユースケース層でデータ取得やビジネスロジックに対する null 処理を適切に行い、最後にUI層で画面表示の際に null を考慮した処理を実装していくという流れです。

このように各層で順を追って対応することで、エラーの混乱を最小限に抑えることができました。

パターンごとのアプローチ

ここからは実際に発生したエラーの具体例とそれに対する対応方法を紹介します。

パターン1 : 初期値のエラー

DateTime date;

エラー内容:

Non-nullable instance field 'date' must be initialized. Try adding an initializer expression, or a generative constructor that initializes it, or mark it 'late'.

対応方法

初期値が定義されていないことで起きています。 対応方法としては以下の3つがあります。

  1. nullableにする
DateTime? date;

?を付けて変数をnullableにすると、nullを許容するようになります。これによって、変数に初期値を定義しなくてもエラーは発生しません。 ただし、?を使用する際にはnullかどうかを考慮したコードを書く必要があります。

2.lateをつける

late DateTime date;

date = DateTime.now(); // どこかで代入する必要がある

lateを使うことでnon-nullableにすることができます。 変数が後で代入されることが確実であれば、lateを使用した方がnullableを使用した場合のようにnullを考慮したコードを書かなくて良くなります。 しかし、変数が後で代入されなければnullエラーが出るので確実に代入される場面で使用しましょう。

3.初期値を設定する

DateTime date = DateTime.now();

特定のデフォルト値(日付など)が決まっている場合には、初期値を設定します。 デフォルト値入れていいか判断しずらい場合は思わぬバグを起こさないために、デフォルト値を安易に入れないほうが良いかなと思います。

パターン2: モデルのエラー

class User {
  final String id;
  final String name;
   
  User({this.id, this.name});
}

エラー内容:

The parameter 'id' can't have a value of 'null' because of its type, but the implicit default value is 'null'.

対応方法

変数のidやnameはnon-nullableとして定義されているが、デフォルトでnullが入るようになっているためエラーが出ています。 対応方法としては以下の2つがあります。

  1. idのように必須の値はrequiredキーワードを付けて、必須パラメータにします。
  2. nameのようにnullになる可能性があるフィールドには、?を付けてnullableにします。
class User {
  final String id;    // Nullを許容しない
  final String? name; // Nullを許容する
   
  User({required this.id, this.name});
}

パターン3: nullを返す可能性がある

static String getUrl() {
  switch (environment) {
    case "production":
      return "production.example.com";
    case "staging":
      return "staging.example.com";
    case "development":
      return "development.example.com";
    default:
      return null;
  }
}

エラー内容:

A value of type 'Null' can't be returned from the method 'getUrl' because it has a return type of 'String'

対応方法

返却値がStringと定義されているがnullを返却する可能性があるためエラーが出ています。 対応方法としては以下の2つがあります。

1.デフォルト値を設定する null を返す代わりにデフォルト値を設定します。

static String getUrl() {
  switch (environment) {
    case "production":
      return "production.example.com";
    case "staging":
      return "staging.example.com";
    case "development":
      return "development.example.com";
    default:
      return "development.example.com";
  }
}

2.受け取り側で null チェックを行う デフォルト値を設定できない場合は、呼び出し元で null チェックを行います。

String? url = getUrl();
if (url == null) {
  throw Exception("Invalid environment");
}

パターン4:FutureBuildersnapshotの受け取り

FutureBuilder<ResultSampleData>(
    future: fetchData(),
    builder: (BuildContext context, AsyncSnapshot<ResultSampleData> snapshot) {
      if (snapshot.hasData) {
        List<String> dataList = snapshot.data.dataList; // エラー箇所
      }
    },
);

エラー内容:

The property 'dataList' can't be unconditionally accessed because the receiver can be 'null'.

対応方法

snapshot.dataがnullの可能性があるためエラーが出ています。 FutureBuilderhasDatadataのnullチェックをしているため、data!を使ってnullを除外します。

if (snapshot.hasData) {
  List<String> dataList = snapshot.data!.dataList;
}

パターン5:ModalRouteでの引数の受け取り

final String args = ModalRoute.of(context).settings.arguments;

エラー内容:

A value of type 'Object?' can't be assigned to a variable of type 'String'.

対応方法

ModalRoute.of(context).settings.argumentsObject?型であり、それが実際にStringであるかどうかが保証されないため、型不一致のエラーが出ています。

as Stringを使ってキャストし、この値はString型として扱って良いことを明示してあげることでコンパイラが型を正しく認証でき、エラーが解消できます。

final String args =
        ModalRoute.of(context)?.settings.arguments as String;

Null Safety対応を行って感じたこと

膨大なエラー数であったが、モデル、ポジトリ層、ユースケース層、UI層で段階分けすることと、エラーのパターン分けをすることで、混乱を最小限に抑えて作業できた点が良かったです。

既存のコードにはnullを適切に処理している部分もありましたが、ほとんどの箇所でnull処理が不十分であり、全体的にnullが入りやすい設計になっていたことを再認識しました。

おそらく、Flutter2でもnullチェックを意識してコードを書くことは大切だと思うので、そもそも既存のコードの書き方にも問題がありそうだなと感じました。

まとめ

今回はFlutter3でのNull Safety対応についてまとめました。 初めての移行作業ではありましたが、段階分けとエラーパターンの分類を行うことで、効率的かつ統一感のある対応ができました。 個々のエラーは一見シンプルではあるものの、パターン化して整理することで、どのように対応すべきか迷うことが少なくなり、結果的に作業がスムーズに進んだと感じています。 Null Safetyの対応は、手間がかかるものの、コードの信頼性や堅牢性が向上し、バグの予防に大きく寄与します。今回の記事が、Null Safety対応やFlutterのバージョンアップを迷っている方にとって、少しでも参考になれば幸いです。

ご覧いただきありがとうございました。

Vue Fes Japan 2024 参加レポート

エブリーは2024年10月19日(土)に開催された Vue Fes Japan 2024 にゴールドスポンサーとして協賛させていただきました。

今回は参加レポートとして、会場の様子やセッションの感想についてお届けします。

イベント概要

Vue Fes Japanは、日本最大のVue.jsカンファレンスです。今年も多くの開発者が集まり、最新のVue.js関連技術や事例について学び、交流する機会となりました。

セッションの感想

1. キーノート

Evan You氏によるキーノートセッションでは、Vue.jsエコシステムの最新動向と将来の展望について、深い洞察が共有されました。

主な注目ポイントは以下の通りです:

  1. Vueフレームワークの最新進展
  2. Nuxt DevToolsの将来像
  3. Viteビルドツールの現状と今後の方向性
    • 現行のesbuild・Rollup・SWC構成から、RolldownとOxcへの移行戦略
    • OxcコンパイラとRolldownバンドラーの性能評価

これらのトピックを中心に、多岐にわたる内容が網羅されました。

個人的に、Evan You 氏が最近設立した Void Zero Inc. に非常に注目しています。

JavaScriptエコシステム全体のために、オープンソースかつ高性能な統合開発ツールチェーンの構築の実現によって、Vue.jsはもとより、JavaScript開発全般にもたらす可能性に大きな期待を寄せています。

2. Vue.js / Nuxt ハンズオン

Vue Fes Japan では、毎年恒例のハンズオン企画として、Vue.js を学び始めたい方向けの教材を提供しています。今年は特別な取り組みがありました。

Nuxt の公式チュートリアル「Nuxt Tutorial」の作者である Anthony Fu 氏と Vue Fes Japan のコラボレーションにより、この公式チュートリアルの日本語版が先行公開されました。このチュートリアルがハンズオン企画の題材として使用されました。

内容は Vue.js の基礎(リアクティビティ、Composition API など)から始まり、Nuxt のコアなコンセプトまでが網羅されていました。

これから Vue.js・Nuxt を学び始めたい方には、このチュートリアルを通じて、より深い理解を得ることができると思います。

learn-nuxt.vuejs-jp.org

3. 次世代フロントエンドクロストーク

次世代フロントエンドクロストークセッションでは、JavaScriptエコシステムの最新動向と課題について活発な議論が展開されました。

主な注目ポイントは以下の通りです:

  1. フロントエンドビルドツールの進化

    • Viteが Vue や React SPA のデファクトスタンダードとして定着
    • Rust製ツール(Oxcコンパイラ、Rolldownバンドラーなど)の台頭
  2. JavaScriptエコシステムのRust化の加速

  3. AIによる大規模コード生成の可能性と課題

これらのトピックを通じて、フロントエンド開発の未来像について多角的な視点が提示されました。

特にRustの重要性が強調されたことで、私自身もRust学習への意欲が大いに刺激されました。このセッションを通じて、フロントエンド開発の将来がより鮮明に見えてきたと感じています。

スポンサーブース紹介

エブリーでは DELISH KITCHEN Web や DELISH KITCHEN チラシ などで Vue.js を採用しています。
いつも Vue コミュニティの恩恵を受けている我々もコミュニティのさらなる盛り上がりに貢献してくべく、スポンサーとして協賛させていただき、ブースも出展させていただきました!

ブース

エブリーでは、今回も弊社が提供するDELISH KITCHENのサービスをイメージしたブースの雰囲気を作りました。多くの方からDELISH KITCHENをを使っていますとの声をかけていただき、とても嬉しかったです。

ノベルティ

今回もDELISH KITCHENにちなんだノベルティを用意させていただきました。

  • ステッカー
  • DELISH KITCHENグッズ
  • CTOブレンドのコーヒーバッグ

DELISH KITCHENグッズに関してはXフォローでの抽選プレゼントキャンペーンを行いました。DELISH KITCHENグッズに関してはたくさんの商品があるのですが、その中でも人気のある商品を中心に5つ準備させていただきました。参加者の方々にも好評で多くの方に参加していただけました!

アンケート

今回、アンケートでは「Vue について教えて! 」と題して、「Vue の好きなところ」、「Vue の苦労したところ」について回答してもらいました。今回のアンケートでは付箋に自由に記述っしてもらう形式を取り、多くの方から様々な意見をいただくことができました。
回答いただいた多くの皆様、ありがとうございました!

各社スポンサーブースの様子

会場の1階にはスポンサーブースが展開され、各社の趣向を凝らしたブースに多くの人が足を止めていました。
どのブースも、それぞれの会社の特徴を生かした面白い展示が行われており、飽きることなく見て回ることができました。

GMO インターネットグループさんのブースでは、3種類の生成 AI を使って天秤.AI by GMO の Web 画面を Vue.js で出力させた実装と実際の画面を展示して、好きな出力結果のアンケートを行っていました。 生成 AI の利用はとてもホットなトピックなので興味深かったです。筆者の好みは GPT-4 の出力結果でした!

MedPeer さんのブースでは、「握力で技術的負債を粉砕しよう!」と題して、握力測定をすることでノベルティをもらえるという企画を行っていました。握力測定ができるブースは初めて見たのでとても新鮮でした。ちなみに、筆者は 43.6 kg という結果で、無事に学校で体力測定をしていた頃の過去の自分を超えることができました!

まとめ

Vue Fes Japan 2024 にゴールドスポンサーとして協賛できたことを光栄に思います。このイベントを通じて、Vue.js コミュニティの発展に寄与できたことは、私たちにとって大きな喜びです。

多くの方々にエブリーのブースにお立ち寄りいただき、Vue.js の最新トレンドやエブリーのサービスについて活発な議論を交わせたことに、心から感謝申し上げます。皆様との対話は、私たちにとっても大変刺激的で有意義な経験となりました。

今回のイベントでの経験を糧に、エブリーは今後も Vue.js コミュニティのさらなる発展に貢献していく所存です。Vue.js の最新情報やベストプラクティス、そしてエブリーのサービスを通じた実践的な知見を、継続的に発信してまいります。

Amazon Cognitoのユーザーの移行

はじめに

エブリーでソフトウェアエンジニアをしている本丸です。
最近、Amazon Cognitoのユーザープールから別のユーザープールにユーザーを移行する方法について調査する機会がありました。
Amazon Cognitoに関しては色々な箇所で使われていると思いますが、ユーザーの移行について触れる機会はそれほど多くないかと思いますので紹介しようかと思います。

Amazon Cognitoとは

Amazon Cognito(以降Cognitoと表記します)は、AWSが提供するウェブアプリとモバイルアプリ用のアイデンティティプラットフォームです。ユーザーの認証・承認を行うユーザープールとユーザーにAWSリソースへのアクセスを許可するアイデンティティプールを持っています。

DELISH KITCHENでは、ユーザーのメールアドレスの管理とメールアドレスを用いたサインインにCognitoのユーザープールを利用しています。

Lambdaトリガー

CognitoにはLambdaトリガーという機能があり、ユーザープールに対してサインインなどのイベントが発生した時に、それをトリガーとしてLambdaを呼び出すことができます。

公式ドキュメントからの引用ですが、Lambdaトリガーとして設定できるイベントには下記のようなものがあります。

トリガーの種類 説明
認証前の Lambda トリガー サインインリクエストを承認または拒否するカスタム検証
サインアップ前の Lambda トリガー サインアップリクエストを承認または拒否するカスタム検証を実行する
ユーザー移行の Lambda トリガー 既存のユーザーディレクトリからユーザープールにユーザーを移行する
カスタムメッセージの Lambda トリガー メッセージの高度なカスタマイズとローカライズを実行する

Cognitoのユーザープールへのインポート

ユーザープールへのインポート・移行方法は2つ用意されています。CSVを用いた方法とLambdaトリガーを用いた方法です。

CSVを用いたインポート

CSVを用いたインポートでは、指定されたフォーマットのCSVファイルを使用して一括でユーザーのインポートを行います。公式ドキュメントでは低労力で低コストなオプションとして紹介されていました。
こちらの方法では、セキュリティの観点からパスワードのインポートができないようになっています。そのため、移行の際にユーザー側でパスワードの変更が必要になります。

Lambdaトリガーを用いたインポート

Lambdaトリガーを利用したインポートでは、前述したLambdaトリガーを起点にユーザーの移行を行います(上述の表のユーザー移行の Lambda トリガーが今回説明するトリガーです)。このトリガーは、ユーザーがサインインする時とパスワードのリセットを行うときに発火します。
こちらの方法では、パスワードも連携されるのですが認証フローにUSER_PASSWORD_AUTHまたはADMIN_USER_PASSWORD_AUTHを指定し、ユーザー名とパスワードによる認証を行わなければならない点に注意です。 少しイメージしにくいかと思うので、次でもう少し詳細に説明します。

Lambdaトリガーを用いたユーザーの移行の実装

Lambdaトリガーを用いたユーザー移行の流れはおおよそ図のようになります。

ユーザーを移行したい先のアプリケーションでサインインもしくはパスワードリセットが呼び出されたのをトリガーにしてユーザー移行のLambda(図のuser migration lambda)が呼び出されます。
ユーザー移行のLambdaの実装は次のようになります。

import { 
  CognitoIdentityProviderClient,
  AdminInitiateAuthCommand,
  AdminGetUserCommand,
  CognitoIdentityProviderClientConfig,
  UserNotConfirmedException
} from "@aws-sdk/client-cognito-identity-provider";
import { UserMigrationTriggerHandler } from "aws-lambda";

const userMigration: UserMigrationTriggerHandler = async (event) => {
    const config: CognitoIdentityProviderClientConfig = {
        region: 'ap-northeast-1',
    };
    const client = new CognitoIdentityProviderClient(config);

    if(event.triggerSource == "UserMigration_Authentication") {
        const adminInitiateAuthCommand = new AdminInitiateAuthCommand({
            UserPoolId: ${USER_POOL_ID}, 
            ClientId: ${CLIENT_ID},
            AuthFlow: "ADMIN_USER_PASSWORD_AUTH", 
            AuthParameters: {
                "USERNAME": event.userName,
                "PASSWORD": event.request.password,
            },
        });
        // 認証できるかチェック
        try {
            await client.send(adminInitiateAuthCommand)
        } catch (e) {
            console.log(`user auth failed: ${e.message}`);
            throw e;
        }

        // cognitoに登録するユーザー情報構築
        const adminGetUserCommand = new AdminGetUserCommand({
            UserPoolId: process.env.DK_USER_POOL_ID,
            Username: event.userName,
        });

        try {
            const response = await client.send(adminGetUserCommand);
            // 移行先のユーザーに持たせたい情報を詰め込む
            event.response.userAttributes = {
                "email": response.UserAttributes.find((attr) => attr.Name === "email")?.Value ?? "",
                "email_verified": response.UserAttributes.find((attr) => attr.Name === "email_verified")?.Value ?? "false",
            };
            // 検証メールを送信しないため、下記を指定する
            event.response.messageAction = "SUPPRESS";
            event.response.finalUserStatus = "CONFIRMED";
            return event;
        } catch (e) {
            console.log(`get user failed: ${e.message}`);
            throw e;
        }
    }
    return event;
};

このユーザー移行のLambdaの中で、移行元となるCognitoでユーザーの認証を行い、認証が成功した場合にユーザーの情報を取得します。その情報を移行先のCognitoに返すことでユーザーの移行を行います。 event.responseに渡すデータを変更することで移行先のユーザーに持たせたい情報を変更したり、ユーザーがそのメールアドレスの正当な所有者であるかを確認するメールを送信するかなどを操作することができます。

まとめ

Cognitoのユーザーの移行方法を調査して、ユーザーの移行を行うためにAWS公式で便利な機能が用意されていることを知ることができました。
2つの方法にそれぞれメリット・デメリットがあるかと思うので適切に使うようにしていきたいと思います。

参考資料