広告

Azure Static Web AppsでサーバーレスSPAアプリを作ってみた

2021年9月9日

おさらいと予備知識

手順

  1. プロジェクト・Azureリソースの作成とインストール作業 
  2. 認証機能の実装
  3. ストレージの作成とAPIの実装 ←このページでやること
  4. フロントエンドアプリの実装と動作確認

ストレージ(Azure Data Lake Storage Gen2)の作成

Azureポータルへログインし、左のサイドメニューから「ストレージ」を選択します。

Azureポータル ストレージ

ストレージアカウントの画面に遷移したら、「作成」ボタンを押してストレージを新規に作成します。

ストレージ作成

任意のサブスクリプション、リソースグループ、名前、地域を設定して「作成」ボタンでストレージアカウントを作成します。

作成し終えたら、ストレージアカウントのリソースに移動し、コンテナを作成します。

任意の名前を設定して、「作成」ボタンを押します。このとき作成したコンテナ名は後で使うので覚えておいてください。

コンテナの作成

ストレージの作成は完了できたので、ストレージアカウントのリソース画面で、後々使うことになる接続文字列とキーをコピーしておきます。

ローカル環境からストレージにアクセスできるように、コピーしたストレージアカウント名とストレージキーをapiフォルダ下の local.setting.jsonに追加します。

ポイント

今回はストレージキーを環境変数で登録して利用していますが、セキュリティ向上のため、運用時はAzure Key Valueなどに保管して利用することをオススメします。

Azu;re Key Vaultのライブラリーを利用することで、クラウド上のキーの暗号化され、ローカル環境のlocal.settings.jsonに環境変数を登録しなくても済むようになります。

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "STORAGE_ACCOUNT_NAME": "[ストレージアカウント名]"
    "STORAGE_KEY": "[ストレージキー]"
  }
}

また、デプロイしたアプリケーションでも同様にストレージにアクセスできるようにします。
Azureのポータルから、作成したAzure Static Web Appsのリソースのページ内の「構成」でストレージ接続文字列とキーを設定したら「保存」を押します。

ストレージの管理機能の実装

新規にdlsg2.tsファイルをapiフォルダに作成して、ドキュメントを参考にストレージ上のファイルを管理するためのコードの実装に入ります。Azure Data Lake Storage Gen2のクライアントライブラリをインストールしたらコードを実装していきます。

npm i @azure/storage-file-datalake --prefix api

DataLakeServiceClientのインスタンス作成の際に、先ほど作成したストレージのストレージアカウント名とストレージキーが必要になります。また、Azure Data Lake Storage Gen2のコンテナにアクセスするため、先程作成したコンテナ名を引数にfileSystemClientを取得します。fileSystemClientを作成すると、コンテナへアクセスすることができます。

import {
  DataLakeFileSystemClient,
  DataLakeServiceClient,
  ListPathsOptions,
  StorageSharedKeyCredential,
} from "@azure/storage-file-datalake";
import path from "path";
import { ParsedMultipartFormData } from "/";
export default class Dlsg2 {
  private readonly dataLakeServiceClient: DataLakeServiceClient;
  private readonly directory: string;
  readonly fileSystemClient: DataLakeFileSystemClient;
  constructor(
    storageName: string,
    storageKey: string,
    directory: string = "",
    containerName = "crud"
  ) {
      this.dataLakeServiceClient = new DataLakeServiceClient(
        `https://${storageName}.dfs.core.windows.net`,
        new StorageSharedKeyCredential(storageName, storageKey)
      );
      this.fileSystemClient =
        this.dataLakeServiceClient.getFileSystemClient(containerName);
      this.directory = directory;
   }
  download = async (filename: string) => {
    const fileContents = await this.fileSystemClient
      .getFileClient(path.join(this.directory, filename))
      .read();
      async function streamToString(readableStream) {
        return new Promise((resolve, reject) => {
          const chunks = [];
          readableStream.on("data", (data) => {
            chunks.push(data.toString());
          });
          readableStream.on("end", () => {
            resolve(chunks.join(""));
          });
          readableStream.on("error", reject);
        });
      }   
    const str = await streamToString(fileContents.readableStreamBody);
    return str;
  };
  remove = async (fileName: string) => {
    const res = await this.fileSystemClient
      .getFileClient(path.join(this.directory, fileName))
      .deleteIfExists(true);
    return res.succeeded;
  };
  fetch = async () => {
    let res = [];
    const options: ListPathsOptions = { path: this.directory };
    for await (const listPath of this.fileSystemClient.listPaths(options)) {
      res.push({ name: listPath.name, size: listPath.contentLength });
    }
    return res;
  };
  upload = async (parsedData: ParsedMultipartFormData) => {
    const fileClient = this.fileSystemClient.getFileClient(
      path.join(this.directory, parsedData.filename)
    );
    await fileClient.create();
    await fileClient.append(parsedData.data, 0, parsedData.data.length);
    return await fileClient.flush(parsedData.data.length);
  };
}

ココに注意


Azure Data Lake Storage Gen2へアクセスする際は先程作成したストレージの、ストレージアカウント名とストレージキー、コンテナ名正しく設定されていないとエラーになるので注意です。

APIの実装

設定

apiフォルダ内のfunctions.jsonを編集して、DELETEメソッドを追加し、パラメータに対応させます。

  "bindings": [
    {
      "authLevel": "anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": ["get", "post", "delete"],
      "route": "storage/{filename?}"
    },
    ...

APIの実装

今回はファイルのアップロードにmultipart/form-dataを利用しているため、パースのためbusboyをインストールしてヘルパー関数を作成します。

npm i busboy --prefix api
import { AzureFunction, Context, HttpRequest } from "@azure/functions";
import Dlsg2 from "../dlsg2";
import parseMultipart from "../helper/parseMultipart";
const storage: AzureFunction = async (
  context: Context,
  req: HttpRequest
): Promise<any> => {
  const dlsg2 = new Dlsg2(
    process.env.STORAGE_ACCOUNT_NAME,
    process.env.STORAGE_KEY
  );
  if (req.method === "POST") {
    const parsedData = await parseMultipart(req);
    for await (const data of parsedData) await dlsg2.upload(data);
    context.res = { status: 201 };
  } else if (req.method === "GET") {
    if (req.params.filename) {
      const res = await dlsg2.download(req.params.filename);
      context.res = { status: 200, body: res };
    }
    const res = await dlsg2.fetch();
    context.res = { status: 200, body: res };
  } else if (req.method === "DELETE") {
    const res = await dlsg2.remove(req.params.filename);
    context.res = { status: 204, body: res };
  } else context.res = { status: 405 };
};
export default storage;
import { HttpRequest } from "@azure/functions";
import Busboy from "busboy";
export type ParsedMultipartFormData = {
  filename: string;
  data: Buffer;
};
export default function parseMultipart(
  req: HttpRequest
): Promise<ParsedMultipartFormData[]> {
  return new Promise((resolve) => {
    const files: ParsedMultipartFormData[] = [];
    const busboy = new Busboy({ headers: req.headers });
    busboy.on("file", (fieldname, file, filename) => {
      file.on("data", (data: Buffer) => {
        files.push({ filename, data });
      });
    });
    busboy.on("finish", () => {
      resolve(files);
    });
    busboy.end(req.rawBody);
  });
}

最後のページでは、フロントエンドアプリの実装と動作確認に取り掛かっていきます。

次のページへ >