<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Remix | きいちログ</title>
	<atom:link href="https://wptech.kiichiro.work/tag/remix/feed/" rel="self" type="application/rss+xml" />
	<link>https://wptech.kiichiro.work</link>
	<description>WordPressとかAWSとかPHPとか</description>
	<lastBuildDate>Wed, 15 Jan 2025 16:39:45 +0000</lastBuildDate>
	<language>ja</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>
	<item>
		<title>【Azure】Microsoft Entra IDで認可してBlob Storageを操作してみた</title>
		<link>https://wptech.kiichiro.work/14rqfd1ec6/</link>
		
		<dc:creator><![CDATA[むらおか]]></dc:creator>
		<pubDate>Wed, 15 Jan 2025 16:39:45 +0000</pubDate>
				<category><![CDATA[Tech]]></category>
		<category><![CDATA[Azure]]></category>
		<category><![CDATA[Microsoft]]></category>
		<category><![CDATA[React]]></category>
		<category><![CDATA[Remix]]></category>
		<guid isPermaLink="false">https://wptech.kiichiro.work/?p=3530</guid>

					<description><![CDATA[Azure Functions などのコンピュートリソースを用意しないで、直接 Blob Storage にファイルをアップロードする方法は無いものか、と考えていたのですが、Microsoft Entra ID で認可で [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>Azure Functions などのコンピュートリソースを用意しないで、直接 Blob Storage にファイルをアップロードする方法は無いものか、と考えていたのですが、Microsoft Entra ID で認可できればいいみたいでした。</p>



<p>別解として SAS トークンを利用した認可もありますが、どうやら Entra ID が推奨みたいです。</p>




<a rel="noopener" href="https://learn.microsoft.com/ja-jp/azure/storage/blobs/storage-blob-javascript-get-started?tabs=typescript%2Cazure-ad#authorize-access-and-connect-to-blob-storage" title="Azure Blob Storage と JavaScript または TypeScript の概要 - Azure Storage" class="blogcard-wrap external-blogcard-wrap a-wrap cf" target="_blank"><div class="blogcard external-blogcard eb-left cf"><div class="blogcard-label external-blogcard-label"><span class="fa"></span></div><figure class="blogcard-thumbnail external-blogcard-thumbnail"><img decoding="async" src="https://s.wordpress.com/mshots/v1/https%3A%2F%2Flearn.microsoft.com%2Fja-jp%2Fazure%2Fstorage%2Fblobs%2Fstorage-blob-javascript-get-started%3Ftabs%3Dtypescript%252Cazure-ad%23authorize-access-and-connect-to-blob-storage?w=160&#038;h=90" alt="" class="blogcard-thumb-image external-blogcard-thumb-image" width="160" height="90" /></figure><div class="blogcard-content external-blogcard-content"><div class="blogcard-title external-blogcard-title">Azure Blob Storage と JavaScript または TypeScript の概要 - Azure Storage</div><div class="blogcard-snippet external-blogcard-snippet">Azure Blob Storage で動作する JavaScript または TypeScript アプリケーションの開発を開始します。 この記事では、プロジェクトを設定し、Azure Blob Storage エンドポイントへのアクセス...</div></div><div class="blogcard-footer external-blogcard-footer cf"><div class="blogcard-site external-blogcard-site"><div class="blogcard-favicon external-blogcard-favicon"><img decoding="async" src="https://www.google.com/s2/favicons?domain=https://learn.microsoft.com/ja-jp/azure/storage/blobs/storage-blob-javascript-get-started?tabs=typescript%2Cazure-ad#authorize-access-and-connect-to-blob-storage" alt="" class="blogcard-favicon-image external-blogcard-favicon-image" width="16" height="16" /></div><div class="blogcard-domain external-blogcard-domain">learn.microsoft.com</div></div></div></div></a>



<p>というわけで、今回は Microsoft Entra ID で認可して Blob Storage にファイルをアップロードするところまでやってみます。</p>



<div class="wp-block-cocoon-blocks-icon-box common-icon-box block-box information-box">
<p>本記事紹介している方法は、Microsoft Entra ID やストレージアカウントの RBAC の設定が甘いと思われます。本番環境で利用する場合は十分なレビューが必要です。</p>
</div>




  <div id="toc" class="toc tnt-number toc-center tnt-number border-element"><input type="checkbox" class="toc-checkbox" id="toc-checkbox-2" checked><label class="toc-title" for="toc-checkbox-2">目次</label>
    <div class="toc-content">
    <ol class="toc-list open"><li><a href="#toc1" tabindex="0">構成</a></li><li><a href="#toc2" tabindex="0">Microsoft Entra 管理センター</a></li><li><a href="#toc3" tabindex="0">ストレージアカウント</a><ol><li><a href="#toc4" tabindex="0">Remix アプリ</a><ol><li><a href="#toc5" tabindex="0">ライブラリ</a></li><li><a href="#toc6" tabindex="0">環境変数</a></li><li><a href="#toc7" tabindex="0">認証・認可</a></li></ol></li></ol></li><li><a href="#toc8" tabindex="0">動作確認</a></li><li><a href="#toc9" tabindex="0">おわりに</a></li></ol>
    </div>
  </div>

<h2 class="wp-block-heading"><span id="toc1">構成</span></h2>



<p>Microsoft Entra ID での認証自体は Remix で作った Web アプリを経由させ、Blob Storage へのアップロードはブラウザから直接行うようにします。登場人物をまとめた図は以下です。</p>



<figure class="wp-block-image aligncenter size-full"><img fetchpriority="high" decoding="async" width="402" height="452" src="https://wptech.kiichiro.work/wp-content/uploads/2025/01/blob-upload.png" alt="ブラウザからMicrosoft Entra IDで認証してBlob Storageにファイルをアップロードするまでの構成図" class="wp-image-3540" srcset="https://wptech.kiichiro.work/wp-content/uploads/2025/01/blob-upload.png 402w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/blob-upload-267x300.png 267w" sizes="(max-width: 402px) 100vw, 402px" /></figure>



<h2 class="wp-block-heading"><span id="toc2">Microsoft Entra 管理センター</span></h2>



<p>Microsoft Entra 管理センターにて、アプリの登録を行います。アプリの登録ではアプリを一意に示す ID とアプリ自体の認証情報を作成します。</p>



<p>手順は以下を参考に。</p>




<a rel="noopener" href="https://learn.microsoft.com/ja-jp/entra/identity-platform/quickstart-web-app-nodejs-sign-in" title="クイック スタート - サンプル Web アプリでユーザーをサインインする - Microsoft identity platform" class="blogcard-wrap external-blogcard-wrap a-wrap cf" target="_blank"><div class="blogcard external-blogcard eb-left cf"><div class="blogcard-label external-blogcard-label"><span class="fa"></span></div><figure class="blogcard-thumbnail external-blogcard-thumbnail"><img loading="lazy" decoding="async" src="https://s.wordpress.com/mshots/v1/https%3A%2F%2Flearn.microsoft.com%2Fja-jp%2Fentra%2Fidentity-platform%2Fquickstart-web-app-nodejs-sign-in?w=160&#038;h=90" alt="" class="blogcard-thumb-image external-blogcard-thumb-image" width="160" height="90" /></figure><div class="blogcard-content external-blogcard-content"><div class="blogcard-title external-blogcard-title">クイック スタート - サンプル Web アプリでユーザーをサインインする - Microsoft identity platform</div><div class="blogcard-snippet external-blogcard-snippet">従業員テナントの従業員または外部テナントの顧客をサインインさせるサンプル Web アプリを構成する方法を示す Web アプリのクイック スタート</div></div><div class="blogcard-footer external-blogcard-footer cf"><div class="blogcard-site external-blogcard-site"><div class="blogcard-favicon external-blogcard-favicon"><img loading="lazy" decoding="async" src="https://www.google.com/s2/favicons?domain=https://learn.microsoft.com/ja-jp/entra/identity-platform/quickstart-web-app-nodejs-sign-in" alt="" class="blogcard-favicon-image external-blogcard-favicon-image" width="16" height="16" /></div><div class="blogcard-domain external-blogcard-domain">learn.microsoft.com</div></div></div></div></a>



<p>アプリの登録が完了すると、概要ペインからクライアント ID とテナント ID を取得できます。</p>



<figure class="wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-1 is-layout-flex wp-block-gallery-is-layout-flex">
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="302" data-id="3552" src="https://wptech.kiichiro.work/wp-content/uploads/2025/01/93bccbc237574d85974089b4572968f8-1024x302.png" alt="クライアントIDとテナントID" class="wp-image-3552" srcset="https://wptech.kiichiro.work/wp-content/uploads/2025/01/93bccbc237574d85974089b4572968f8-1024x302.png 1024w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/93bccbc237574d85974089b4572968f8-300x88.png 300w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/93bccbc237574d85974089b4572968f8-768x227.png 768w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/93bccbc237574d85974089b4572968f8-1536x453.png 1536w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/93bccbc237574d85974089b4572968f8-2048x604.png 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>
</figure>



<p>今回は Web アプリ (機密クライアント) を選択したので、クライアントシークレットを生成します。</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="209" src="https://wptech.kiichiro.work/wp-content/uploads/2025/01/d9b8ab378c3c7b947e88bb30c75b8b13-1024x209.png" alt="" class="wp-image-3557" srcset="https://wptech.kiichiro.work/wp-content/uploads/2025/01/d9b8ab378c3c7b947e88bb30c75b8b13-1024x209.png 1024w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/d9b8ab378c3c7b947e88bb30c75b8b13-300x61.png 300w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/d9b8ab378c3c7b947e88bb30c75b8b13-768x157.png 768w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/d9b8ab378c3c7b947e88bb30c75b8b13-1536x314.png 1536w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/d9b8ab378c3c7b947e88bb30c75b8b13-2048x418.png 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>




<a rel="noopener" href="https://learn.microsoft.com/ja-jp/entra/identity-platform/msal-client-applications" title="パブリックおよび機密のクライアント アプリ (MSAL) - Microsoft identity platform" class="blogcard-wrap external-blogcard-wrap a-wrap cf" target="_blank"><div class="blogcard external-blogcard eb-left cf"><div class="blogcard-label external-blogcard-label"><span class="fa"></span></div><figure class="blogcard-thumbnail external-blogcard-thumbnail"><img loading="lazy" decoding="async" src="https://s.wordpress.com/mshots/v1/https%3A%2F%2Flearn.microsoft.com%2Fja-jp%2Fentra%2Fidentity-platform%2Fmsal-client-applications?w=160&#038;h=90" alt="" class="blogcard-thumb-image external-blogcard-thumb-image" width="160" height="90" /></figure><div class="blogcard-content external-blogcard-content"><div class="blogcard-title external-blogcard-title">パブリックおよび機密のクライアント アプリ (MSAL) - Microsoft identity platform</div><div class="blogcard-snippet external-blogcard-snippet">Microsoft Authentication Library (MSAL) でのパブリック クライアント アプリケーションと機密クライアント アプリケーションについて説明します。</div></div><div class="blogcard-footer external-blogcard-footer cf"><div class="blogcard-site external-blogcard-site"><div class="blogcard-favicon external-blogcard-favicon"><img loading="lazy" decoding="async" src="https://www.google.com/s2/favicons?domain=https://learn.microsoft.com/ja-jp/entra/identity-platform/msal-client-applications" alt="" class="blogcard-favicon-image external-blogcard-favicon-image" width="16" height="16" /></div><div class="blogcard-domain external-blogcard-domain">learn.microsoft.com</div></div></div></div></a>



<p>ログイン正常完了時にトークンを送る先をリダイレクト URI として追加します。</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="398" src="https://wptech.kiichiro.work/wp-content/uploads/2025/01/cfdf6cc48710ac1722fe7cf50ae401fe-1024x398.png" alt="" class="wp-image-3553" srcset="https://wptech.kiichiro.work/wp-content/uploads/2025/01/cfdf6cc48710ac1722fe7cf50ae401fe-1024x398.png 1024w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/cfdf6cc48710ac1722fe7cf50ae401fe-300x117.png 300w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/cfdf6cc48710ac1722fe7cf50ae401fe-768x299.png 768w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/cfdf6cc48710ac1722fe7cf50ae401fe-1536x598.png 1536w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/cfdf6cc48710ac1722fe7cf50ae401fe-2048x797.png 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading"><span id="toc3">ストレージアカウント</span></h2>



<p>Microsoft Entra ID で認証されたユーザーに Blob Storage の操作を許可します。これを実現するには、ロールベースのアクセス制御である Azure RBAC を利用します。</p>



<p>まずは Azure portal から任意のストレージアカウントとコンテナーを作成します。作成できたら、アクセス制御 (IAM) ペインから 追加タブ > ロールの割り当ての追加 を選択します。</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="461" src="https://wptech.kiichiro.work/wp-content/uploads/2025/01/696c2c36a841fe44d5a74a6bdf46fff7-1024x461.png" alt="ロールの割り当ての追加" class="wp-image-3571" srcset="https://wptech.kiichiro.work/wp-content/uploads/2025/01/696c2c36a841fe44d5a74a6bdf46fff7-1024x461.png 1024w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/696c2c36a841fe44d5a74a6bdf46fff7-300x135.png 300w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/696c2c36a841fe44d5a74a6bdf46fff7-768x346.png 768w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/696c2c36a841fe44d5a74a6bdf46fff7-1536x692.png 1536w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/696c2c36a841fe44d5a74a6bdf46fff7-2048x923.png 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>データ共同作業者を選択して次へを選択します。</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="430" src="https://wptech.kiichiro.work/wp-content/uploads/2025/01/331b7821a9217c0959e15fe949e8a734-1024x430.png" alt="" class="wp-image-3573" srcset="https://wptech.kiichiro.work/wp-content/uploads/2025/01/331b7821a9217c0959e15fe949e8a734-1024x430.png 1024w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/331b7821a9217c0959e15fe949e8a734-300x126.png 300w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/331b7821a9217c0959e15fe949e8a734-768x323.png 768w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/331b7821a9217c0959e15fe949e8a734-1536x645.png 1536w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/331b7821a9217c0959e15fe949e8a734-2048x860.png 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>許可するユーザーを追加してレビューと割り当てを選択します。</p>



<h3 class="wp-block-heading"><span id="toc4">Remix アプリ</span></h3>



<p>Remix で作ったものは GitHub にアップしてます。</p>




<a rel="noopener" href="https://github.com/ShotaroMuraoka/azure-entra-auth-blob-storage" title="GitHub - ShotaroMuraoka/azure-entra-auth-blob-storage: Azure Blob Storage へのアクセスを Microsoft Entra ID で認可するアプリ" class="blogcard-wrap external-blogcard-wrap a-wrap cf" target="_blank"><div class="blogcard external-blogcard eb-left cf"><div class="blogcard-label external-blogcard-label"><span class="fa"></span></div><figure class="blogcard-thumbnail external-blogcard-thumbnail"><img loading="lazy" decoding="async" src="https://s.wordpress.com/mshots/v1/https%3A%2F%2Fgithub.com%2FShotaroMuraoka%2Fazure-entra-auth-blob-storage?w=160&#038;h=90" alt="" class="blogcard-thumb-image external-blogcard-thumb-image" width="160" height="90" /></figure><div class="blogcard-content external-blogcard-content"><div class="blogcard-title external-blogcard-title">GitHub - ShotaroMuraoka/azure-entra-auth-blob-storage: Azure Blob Storage へのアクセスを Microsoft Entra ID で認可するアプリ</div><div class="blogcard-snippet external-blogcard-snippet">Azure Blob Storage へのアクセスを Microsoft Entra ID で認可するアプリ - ShotaroMuraoka/azure-entra-auth-blob-storage</div></div><div class="blogcard-footer external-blogcard-footer cf"><div class="blogcard-site external-blogcard-site"><div class="blogcard-favicon external-blogcard-favicon"><img loading="lazy" decoding="async" src="https://www.google.com/s2/favicons?domain=https://github.com/ShotaroMuraoka/azure-entra-auth-blob-storage" alt="" class="blogcard-favicon-image external-blogcard-favicon-image" width="16" height="16" /></div><div class="blogcard-domain external-blogcard-domain">github.com</div></div></div></div></a>



<p>以降はポイントのみを説明していきます。</p>



<h4 class="wp-block-heading"><span id="toc5">ライブラリ</span></h4>



<p>Remix で Microsoft Entra ID を使った認証と Blob Storage の操作を行うのに以下のライブラリを利用しています。</p>



<ul class="wp-block-list">
<li>remix-auth: 3.7.0</li>



<li>remix-auth-microsoft: 2.0.1</li>



<li>remix-auth-oauth2: 1.11.0</li>



<li>@azure-storage-blob: 12.26.0</li>
</ul>



<h4 class="wp-block-heading"><span id="toc6">環境変数</span></h4>



<p><code>.env</code> を作成し、取得した ID とクライアントシークレット、ストレージアカウント名などを記載します。</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-plain" data-file=".env"><code>ENTRA_CLIENT_ID={クライアントID}
ENTRA_CLIENT_SECRET={クライアントシークレット}
ENTRA_REDIRECT_URI=http://localhost:5173/auth/microsoft/callback
ENTRA_TENANT_ID={テナントID}
AZURE_STORAGE_ACCCOUNT={ストレージアカウント名}
AZURE_BLOB_CONTAINER={コンテナー名}</code></pre></div>



<h4 class="wp-block-heading"><span id="toc7">認証・認可</span></h4>



<p><code>remix-auth-microsoft</code> では MicrosoftStrategy が用意されているので、それを利用した Authenticator を作っています。</p>



<p>ここでは、scope に Azure Storage のアクセストークンの要求を追加しています。</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-ts" data-file="app/services/auth.server.ts" data-lang="TypeScript" data-line="11"><code>let microsoftStrategy = new MicrosoftStrategy(
  {
    clientId: process.env.ENTRA_CLIENT_ID || &quot;&quot;,
    clientSecret: process.env.ENTRA_CLIENT_SECRET || &quot;&quot;,
    redirectUri: process.env.ENTRA_REDIRECT_URI || &quot;&quot;,
    tenantId: process.env.ENTRA_TENANT_ID || &quot;&quot;,
    scope: [
      &quot;openid&quot;,
      &quot;email&quot;,
      &quot;profile&quot;,
      &quot;https://storage.azure.com/user_impersonation&quot;,
    ], // optional
    prompt: &quot;login&quot;, // optional
  },</code></pre></div>



<p>取得したアクセストークンはセッション情報に書き出されるので、loader でフロントエンドに渡しています。</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-ts" data-file="app/routes/dashboard.tsx" data-lang="TypeScript" data-line="7"><code>export async function loader({ request }: LoaderFunctionArgs) {
  const user = await authenticator.isAuthenticated(request);
  if (!user) {
    throw new Response(&quot;Unauthorized&quot;, { status: 403 });
  }

  const accessToken = user.accessToken;
  const storageAccount = process.env.AZURE_STORAGE_ACCCOUNT;
  const blobContainer = process.env.AZURE_BLOB_CONTAINER
  return { accessToken, storageAccount, blobContainer };
}
</code></pre></div>



<p>アクセストークンは <code>@azure-storage-blob</code> が提供するクライアントの生成に利用しています。</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-ts" data-file="app/routes/dashboard.tsx" data-lang="TypeScript" data-line="2"><code>const blobServiceClient = createBlobServiceClient(
  accessToken,
  storageAccount,
);

const containerClient = blobServiceClient.getContainerClient(blobContainer);
const blockBlobClient = containerClient.getBlockBlobClient(file.name);

try {
  const response = await blockBlobClient.uploadBrowserData(file, {
    blobHTTPHeaders: { blobContentType: file.type },
  });
  console.log(&quot;upload succeeded&quot;, response.requestId);
  alert(&quot;ファイルアップロード完了&quot;);
} catch (error) {
  console.error(&quot;upload failed&quot;, error);
  alert(&quot;ファイルアップロード失敗&quot;);
}</code></pre></div>



<h2 class="wp-block-heading"><span id="toc8">動作確認</span></h2>



<p>アプリを起動します。</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-plain"><code># npm run dev </code></pre></div>



<p>ブラウザで <code>http://localhost:5173/login</code> にアクセスします。</p>



<figure class="wp-block-image aligncenter size-large is-resized"><img loading="lazy" decoding="async" width="1024" height="437" src="https://wptech.kiichiro.work/wp-content/uploads/2025/01/4ac2c4d89027686851bc700e533bd679-1024x437.png" alt="" class="wp-image-3610" style="width:499px;height:auto" srcset="https://wptech.kiichiro.work/wp-content/uploads/2025/01/4ac2c4d89027686851bc700e533bd679-1024x437.png 1024w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/4ac2c4d89027686851bc700e533bd679-300x128.png 300w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/4ac2c4d89027686851bc700e533bd679-768x327.png 768w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/4ac2c4d89027686851bc700e533bd679.png 1032w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>ログインボタンを押下すると、Microsoft のサインイン画面にリダイレクトされます。</p>



<figure class="wp-block-image aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="976" height="742" src="https://wptech.kiichiro.work/wp-content/uploads/2025/01/a210c4a89a86a2f1249080781e3bd2c1.png" alt="" class="wp-image-3612" style="width:422px;height:auto" srcset="https://wptech.kiichiro.work/wp-content/uploads/2025/01/a210c4a89a86a2f1249080781e3bd2c1.png 976w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/a210c4a89a86a2f1249080781e3bd2c1-300x228.png 300w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/a210c4a89a86a2f1249080781e3bd2c1-768x584.png 768w" sizes="(max-width: 976px) 100vw, 976px" /></figure>



<p>ストレージアカウントで許可したユーザーの認証情報を入力してログインに成功すると、ダッシュボード画面にリダイレクトします。初回ログイン時にのみ、Azure Storage へのアクセス許可を聞かれます。</p>



<figure class="wp-block-image aligncenter size-large is-resized"><img loading="lazy" decoding="async" width="1024" height="641" src="https://wptech.kiichiro.work/wp-content/uploads/2025/01/7963341bc5a5350c17c2cf5915d87052-1024x641.png" alt="" class="wp-image-3614" style="width:486px;height:auto" srcset="https://wptech.kiichiro.work/wp-content/uploads/2025/01/7963341bc5a5350c17c2cf5915d87052-1024x641.png 1024w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/7963341bc5a5350c17c2cf5915d87052-300x188.png 300w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/7963341bc5a5350c17c2cf5915d87052-768x481.png 768w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/7963341bc5a5350c17c2cf5915d87052.png 1138w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>任意のファイルを選択して Upload ボタンを押下します。ファイルのアップロードが成功するとメッセージが表示されます。</p>



<figure class="wp-block-image aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="1012" height="510" src="https://wptech.kiichiro.work/wp-content/uploads/2025/01/c52d0320f5e473f648f6bec45c03dcd3.png" alt="" class="wp-image-3617" style="width:484px;height:auto" srcset="https://wptech.kiichiro.work/wp-content/uploads/2025/01/c52d0320f5e473f648f6bec45c03dcd3.png 1012w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/c52d0320f5e473f648f6bec45c03dcd3-300x151.png 300w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/c52d0320f5e473f648f6bec45c03dcd3-768x387.png 768w" sizes="(max-width: 1012px) 100vw, 1012px" /></figure>



<p>Blob コンテナーを見てみると、ファイルがアップロードされていることが確認できます。</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="353" src="https://wptech.kiichiro.work/wp-content/uploads/2025/01/68f3872b98c096bfebd9e36277eccf5c-1024x353.png" alt="" class="wp-image-3619" srcset="https://wptech.kiichiro.work/wp-content/uploads/2025/01/68f3872b98c096bfebd9e36277eccf5c-1024x353.png 1024w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/68f3872b98c096bfebd9e36277eccf5c-300x103.png 300w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/68f3872b98c096bfebd9e36277eccf5c-768x264.png 768w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/68f3872b98c096bfebd9e36277eccf5c-1536x529.png 1536w, https://wptech.kiichiro.work/wp-content/uploads/2025/01/68f3872b98c096bfebd9e36277eccf5c.png 1766w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading"><span id="toc9">おわりに</span></h2>



<p>普段、Azure や Entra ID を利用する機会が無いので、難易度が高かったです。次は SAS トークンを試してみようと思います。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Lambda Web AdapterでRemixアプリケーションをサーバーレス化してCDKでデプロイする</title>
		<link>https://wptech.kiichiro.work/617bivwxur/</link>
		
		<dc:creator><![CDATA[むらおか]]></dc:creator>
		<pubDate>Thu, 11 Jul 2024 09:08:50 +0000</pubDate>
				<category><![CDATA[Tech]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Docker]]></category>
		<category><![CDATA[Lambda]]></category>
		<category><![CDATA[React]]></category>
		<category><![CDATA[Remix]]></category>
		<guid isPermaLink="false">https://wptech.kiichiro.work/?p=3178</guid>

					<description><![CDATA[既存の Web フレームワークを Lambda に組み込む方法として Lambda Web Adapter というのがあります。 Lambda Web Adapter でウェブアプリを (ほぼ) そのままサーバーレス化す [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>既存の Web フレームワークを Lambda に組み込む方法として Lambda Web Adapter というのがあります。</p>




<a rel="noopener" href="https://aws.amazon.com/jp/builders-flash/202301/lambda-web-adapter" title="Lambda Web Adapter でウェブアプリを (ほぼ) そのままサーバーレス化する (2025 年改訂版)  - 変化を求めるデベロッパーを応援するウェブマガジン | AWS" class="blogcard-wrap external-blogcard-wrap a-wrap cf" target="_blank"><div class="blogcard external-blogcard eb-left cf"><div class="blogcard-label external-blogcard-label"><span class="fa"></span></div><figure class="blogcard-thumbnail external-blogcard-thumbnail"><img loading="lazy" decoding="async" src="https://d1.awsstatic.com/Developer%20Marketing/jp/magazine/2025/202504/thumb_lambda-web-adapter_2025.eaa3dee4029c2885d864b4da103ab18cd6b2b4f9.jpg" alt="" class="blogcard-thumb-image external-blogcard-thumb-image" width="160" height="90" /></figure><div class="blogcard-content external-blogcard-content"><div class="blogcard-title external-blogcard-title">Lambda Web Adapter でウェブアプリを (ほぼ) そのままサーバーレス化する (2025 年改訂版)  - 変化を求めるデベロッパーを応援するウェブマガジン | AWS</div><div class="blogcard-snippet external-blogcard-snippet">VM やコンテナ用に実装されたウェブアプリを、ほとんどそのまま Lambda でも動かせる AWS Lambda Web Adapter について、新しい実装パターンを含めた使い方、仕組みや対応する Web フレームワーク、性能をご紹介しま...</div></div><div class="blogcard-footer external-blogcard-footer cf"><div class="blogcard-site external-blogcard-site"><div class="blogcard-favicon external-blogcard-favicon"><img loading="lazy" decoding="async" src="https://www.google.com/s2/favicons?domain=https://aws.amazon.com/jp/builders-flash/202301/lambda-web-adapter/" alt="" class="blogcard-favicon-image external-blogcard-favicon-image" width="16" height="16" /></div><div class="blogcard-domain external-blogcard-domain">aws.amazon.com</div></div></div></div></a>




<a rel="noopener" href="https://github.com/awslabs/aws-lambda-web-adapter" title="GitHub - awslabs/aws-lambda-web-adapter: Run web applications on AWS Lambda" class="blogcard-wrap external-blogcard-wrap a-wrap cf" target="_blank"><div class="blogcard external-blogcard eb-left cf"><div class="blogcard-label external-blogcard-label"><span class="fa"></span></div><figure class="blogcard-thumbnail external-blogcard-thumbnail"><img loading="lazy" decoding="async" src="https://opengraph.githubassets.com/d477a0d6d3e245d785d915f1b8b645c121d6d8cd3e618243358d4ae6e1b114cb/awslabs/aws-lambda-web-adapter" alt="" class="blogcard-thumb-image external-blogcard-thumb-image" width="160" height="90" /></figure><div class="blogcard-content external-blogcard-content"><div class="blogcard-title external-blogcard-title">GitHub - awslabs/aws-lambda-web-adapter: Run web applications on AWS Lambda</div><div class="blogcard-snippet external-blogcard-snippet">Run web applications on AWS Lambda. Contribute to awslabs/aws-lambda-web-adapter development by creating an account on G...</div></div><div class="blogcard-footer external-blogcard-footer cf"><div class="blogcard-site external-blogcard-site"><div class="blogcard-favicon external-blogcard-favicon"><img loading="lazy" decoding="async" src="https://www.google.com/s2/favicons?domain=https://github.com/awslabs/aws-lambda-web-adapter" alt="" class="blogcard-favicon-image external-blogcard-favicon-image" width="16" height="16" /></div><div class="blogcard-domain external-blogcard-domain">github.com</div></div></div></div></a>



<p>その仕組みですが、API Gateway などの統合先から受信したイベントをフレームワークの手前にある Lambda Web Adapter のエントリポイントが HTTP リクエストに変換して、フレームワークに渡してくれる、というイメージで良さそうです。</p>



<p>公式の実装例では、Node.js であれば Next.js と Express.js が対応していますが、HTTP であればどのようなフレームワークも動作するはずです。</p>



<p>今回はこれを使って Remix アプリをサーバーレス化してみました。既に作ったものは GitHub にあげてあります。</p>




<a rel="noopener" href="https://github.com/ShotaroMuraoka/cdk-serverless-remix" title="GitHub - ShotaroMuraoka/cdk-serverless-remix: Remix アプリケーションを Lambda Web Adapter でサーバーレス化した CDK" class="blogcard-wrap external-blogcard-wrap a-wrap cf" target="_blank"><div class="blogcard external-blogcard eb-left cf"><div class="blogcard-label external-blogcard-label"><span class="fa"></span></div><figure class="blogcard-thumbnail external-blogcard-thumbnail"><img loading="lazy" decoding="async" src="https://opengraph.githubassets.com/7e2f8909af7deb175a15b54c16dbbb4c74009bec87bc2f528dea48788888602f/ShotaroMuraoka/cdk-serverless-remix" alt="" class="blogcard-thumb-image external-blogcard-thumb-image" width="160" height="90" /></figure><div class="blogcard-content external-blogcard-content"><div class="blogcard-title external-blogcard-title">GitHub - ShotaroMuraoka/cdk-serverless-remix: Remix アプリケーションを Lambda Web Adapter でサーバーレス化した CDK</div><div class="blogcard-snippet external-blogcard-snippet">Remix アプリケーションを Lambda Web Adapter でサーバーレス化した CDK. Contribute to ShotaroMuraoka/cdk-serverless-remix development by crea...</div></div><div class="blogcard-footer external-blogcard-footer cf"><div class="blogcard-site external-blogcard-site"><div class="blogcard-favicon external-blogcard-favicon"><img loading="lazy" decoding="async" src="https://www.google.com/s2/favicons?domain=https://github.com/ShotaroMuraoka/cdk-serverless-remix" alt="" class="blogcard-favicon-image external-blogcard-favicon-image" width="16" height="16" /></div><div class="blogcard-domain external-blogcard-domain">github.com</div></div></div></div></a>




  <div id="toc" class="toc tnt-number toc-center tnt-number border-element"><input type="checkbox" class="toc-checkbox" id="toc-checkbox-4" checked><label class="toc-title" for="toc-checkbox-4">目次</label>
    <div class="toc-content">
    <ol class="toc-list open"><li><a href="#toc1" tabindex="0">CDKの準備</a></li><li><a href="#toc2" tabindex="0">Remixアプリケーションの作成</a></li><li><a href="#toc3" tabindex="0">デプロイ</a></li><li><a href="#toc4" tabindex="0">参考資料</a></li></ol>
    </div>
  </div>

<h2 class="wp-block-heading"><span id="toc1">CDKの準備</span></h2>



<p>デプロイの方法は CDK にしました。init で新しいプロジェクトを作成しておきます。</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-plain"><code>$ cdk --version
2.148.0 (build e5740c0)

$ cdk init app --language typescript</code></pre></div>



<p>今回は HTTP リクエストを受け付けるので、API Gateway と統合します。Stack は以下からお借りしました。</p>



<figure class="wp-block-embed is-type-rich is-provider-hatena-blog wp-block-embed-hatena-blog"><div class="wp-block-embed__wrapper">
<iframe title="AWS Lambda Web AdapterでServerless Next.jsを実現する  - Activ8 Tech Blog" src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fsynamon.hatenablog.com%2Fentry%2F2023%2F07%2F18%2F080000" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe>
</div></figure>



<div class="hcb_wrap"><pre class="prism line-numbers lang-ts" data-file="my-lambda-stack.ts" data-lang="TypeScript"><code>import * as cdk from &#39;aws-cdk-lib&#39;;
import { Construct } from &#39;constructs&#39;;
import * as lambda from &#39;aws-cdk-lib/aws-lambda&#39;;
import { Platform } from &#39;aws-cdk-lib/aws-ecr-assets&#39;;
import * as apigw from &#39;aws-cdk-lib/aws-apigatewayv2&#39;;
import { HttpLambdaIntegration } from &#39;aws-cdk-lib/aws-apigatewayv2-integrations&#39;;

export class MyLambdaStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    const handler = new lambda.DockerImageFunction(this, &#39;Handler&#39;, {
      code: lambda.DockerImageCode.fromImageAsset(&#39;./my-remix&#39;, { // Remix アプリケーションのディレクトリ名とする
        platform: Platform.LINUX_AMD64,
      }),
      memorySize: 256,
      timeout: cdk.Duration.seconds(30),
    });

    new apigw.HttpApi(this, &#39;Api&#39;, {
      apiName: &#39;MyLambdaRemix&#39;,
      defaultIntegration: new HttpLambdaIntegration(&#39;Integration&#39;, handler),
    });
  }
}
</code></pre></div>



<h2 class="wp-block-heading"><span id="toc2">Remixアプリケーションの作成</span></h2>



<p>CDK のプロジェクトルートで Remix アプリを作成します。</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-plain"><code>$ npx create-remix@latest</code></pre></div>



<p>Remix アプリのルートに Dockerfile を作成します。Remix に対応した Dockerfile を作成したことがなかったので、以下を参考にそれっぽく書いてみました。</p>




<a rel="noopener" href="https://github.com/clintonwoo/hackernews-remix-react/blob/main/Dockerfile" title="hackernews-remix-react/Dockerfile at main · clintonwoo/hackernews-remix-react" class="blogcard-wrap external-blogcard-wrap a-wrap cf" target="_blank"><div class="blogcard external-blogcard eb-left cf"><div class="blogcard-label external-blogcard-label"><span class="fa"></span></div><figure class="blogcard-thumbnail external-blogcard-thumbnail"><img loading="lazy" decoding="async" src="https://opengraph.githubassets.com/9f4b585a4145808d862d74acfde120c24aa19eb58f0d39f34c2549a48ecc5e13/clintonwoo/hackernews-remix-react" alt="" class="blogcard-thumb-image external-blogcard-thumb-image" width="160" height="90" /></figure><div class="blogcard-content external-blogcard-content"><div class="blogcard-title external-blogcard-title">hackernews-remix-react/Dockerfile at main · clintonwoo/hackernews-remix-react</div><div class="blogcard-snippet external-blogcard-snippet">Hacker News clone written with universal TypeScript, using React and Remix. - clintonwoo/hackernews-remix-react</div></div><div class="blogcard-footer external-blogcard-footer cf"><div class="blogcard-site external-blogcard-site"><div class="blogcard-favicon external-blogcard-favicon"><img loading="lazy" decoding="async" src="https://www.google.com/s2/favicons?domain=https://github.com/clintonwoo/hackernews-remix-react/blob/main/Dockerfile" alt="" class="blogcard-favicon-image external-blogcard-favicon-image" width="16" height="16" /></div><div class="blogcard-domain external-blogcard-domain">github.com</div></div></div></div></a>



<div class="hcb_wrap"><pre class="prism line-numbers lang-bash" data-lang="Bash" data-line="23, 40"><code>FROM node:22-bookworm-slim AS base

FROM base AS deps
RUN mkdir /app
WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci

FROM base AS builder

RUN mkdir /app
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

ENV NODE_ENV production
RUN npm run build

FROM base AS runner

# lambda-web-adapter
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.5.0 /lambda-adapter /opt/extensions/lambda-adapter

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 remix

RUN mkdir /app
WORKDIR /app

RUN chown remix:nodejs ./
USER remix

COPY --from=builder --chown=remix:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=remix:nodejs /app/build ./build
COPY --from=builder --chown=remix:nodejs /app/public ./public
COPY --from=builder --chown=remix:nodejs /app/package.json /app/package-lock.json ./

ENV NODE_ENV production
ENV PORT 3000
EXPOSE 3000

CMD [&quot;npm&quot;, &quot;run&quot;, &quot;start&quot;]</code></pre></div>



<p>ここでは Lambda Web Adapter のために難しい記述をする必要はありません。ローカルでも ECS でも動くコンテナイメージであれば、Lambda Web Adapter で動作するようです。</p>



<p>追記が必要なのは Lambda の extension を追加するというところのみです。</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-bash" data-lang="Bash"><code>COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.5.0 /lambda-adapter /opt/extensions/lambda-adapter</code></pre></div>



<p>また、デフォルトでは 8080 でポートを Listen しているので、変更する場合は PORT を指定します。</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-bash" data-lang="Bash"><code>ENV PORT 3000</code></pre></div>



<h2 class="wp-block-heading"><span id="toc3">デプロイ</span></h2>



<p>CDK のルートディレクトリでデプロイを実行します。</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-plain"><code>$ cdk deploy</code></pre></div>



<p>管理コンソールから API Gateway の画面を確認すると、作成した API Gateway のエンドポイントが作成されています。</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="384" src="https://wptech.kiichiro.work/wp-content/uploads/2024/07/24ba3ff1ffab27bfd8a591fc1c9770c3-1024x384.png" alt="AWS の管理コンソールで API Gateway &gt; API &gt; MyLambdaRemixの画面を開いている。エンドポイントが有効になっており、リンクが表示されている" class="wp-image-3225" srcset="https://wptech.kiichiro.work/wp-content/uploads/2024/07/24ba3ff1ffab27bfd8a591fc1c9770c3-1024x384.png 1024w, https://wptech.kiichiro.work/wp-content/uploads/2024/07/24ba3ff1ffab27bfd8a591fc1c9770c3-300x112.png 300w, https://wptech.kiichiro.work/wp-content/uploads/2024/07/24ba3ff1ffab27bfd8a591fc1c9770c3-768x288.png 768w, https://wptech.kiichiro.work/wp-content/uploads/2024/07/24ba3ff1ffab27bfd8a591fc1c9770c3-1536x576.png 1536w, https://wptech.kiichiro.work/wp-content/uploads/2024/07/24ba3ff1ffab27bfd8a591fc1c9770c3-2048x768.png 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>デフォルトのエンドポイントの URL をクリックして Remix アプリの画面が表示されていれば OK です。</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="952" height="502" src="https://wptech.kiichiro.work/wp-content/uploads/2024/07/5a7c608aaf71c8c8671576d79db1798d.png" alt="Remix の初期の画面で、Welcome to Remix と表示されている" class="wp-image-3228" srcset="https://wptech.kiichiro.work/wp-content/uploads/2024/07/5a7c608aaf71c8c8671576d79db1798d.png 952w, https://wptech.kiichiro.work/wp-content/uploads/2024/07/5a7c608aaf71c8c8671576d79db1798d-300x158.png 300w, https://wptech.kiichiro.work/wp-content/uploads/2024/07/5a7c608aaf71c8c8671576d79db1798d-768x405.png 768w" sizes="(max-width: 952px) 100vw, 952px" /></figure>



<h2 class="wp-block-heading"><span id="toc4">参考資料</span></h2>




<a rel="noopener" href="https://serverless.co.jp/blog/g30vzpio0ww" title="Dockerを使わない、Remix / Next.js 14 など最新ウェブフレームワークのAWS完全サーバーレス構成と環境構築方法 | ブログ | Serverless Operations" class="blogcard-wrap external-blogcard-wrap a-wrap cf" target="_blank"><div class="blogcard external-blogcard eb-left cf"><div class="blogcard-label external-blogcard-label"><span class="fa"></span></div><figure class="blogcard-thumbnail external-blogcard-thumbnail"><img loading="lazy" decoding="async" src="https://images.microcms-assets.io/assets/7c0b324145eb4ee6bd26d44022795cf4/219c94fc1d5b45038289ecdb40b09463/overview.png?w=1200&#038;fm=webp&#038;q=80" alt="" class="blogcard-thumb-image external-blogcard-thumb-image" width="160" height="90" /></figure><div class="blogcard-content external-blogcard-content"><div class="blogcard-title external-blogcard-title">Dockerを使わない、Remix / Next.js 14 など最新ウェブフレームワークのAWS完全サーバーレス構成と環境構築方法 | ブログ | Serverless Operations</div><div class="blogcard-snippet external-blogcard-snippet">（※ 2025/01/31 追記）この記事の後続編として、React Router v7 と Next.js 15 を利用する構成の詳細な構築手順について、こちらの記事（</div></div><div class="blogcard-footer external-blogcard-footer cf"><div class="blogcard-site external-blogcard-site"><div class="blogcard-favicon external-blogcard-favicon"><img loading="lazy" decoding="async" src="https://www.google.com/s2/favicons?domain=https://serverless.co.jp/blog/g30vzpio0ww" alt="" class="blogcard-favicon-image external-blogcard-favicon-image" width="16" height="16" /></div><div class="blogcard-domain external-blogcard-domain">serverless.co.jp</div></div></div></div></a>



<p></p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
