Alternative Architecture DOJO

オルターブースのクラウドネイティブ特化型ブログです。

Svelte on Azure Static Web Apps with Azure Cosmos DB

オルターブース エンジニアのみっつーです!
この記事はオルターブース Advent Calendar 2021の16日目の記事です。

adventar.org

昨日は松村さんによるStatiqをAzure Static Web Appsにデプロイする話でした!
こちらにはGitHub Actionsのカスタマイズ話が含まれていて、今日の私の投稿では全く触れないので、併せて読むと理解が広がって面白いと思います。

aadojo.alterbooth.com

今日は、Svelteアプリケーションの開発と、開発したアプリのAzureデプロイ方法を紹介します。
また、Azure Static Web Apps内包のAzure Functionsを使ってAzure Cosmos DBデータ操作を実現する方法も紹介します。
(Svelte以外のJSフレームワークもデプロイ手順自体はほぼ同じため、Azure Static Web Appsを使ったSPA開発の汎用的なヒントにして頂ければと思います!)

今回実装する構成図

目次

  1. 必要なツール、アカウント
  2. Azure Static Web Apps作成
  3. Azure Functions追加
  4. Azure Cosmos DB作成、Azure Functions実装
  5. Todoアプリ実装
  6. 参考

1. 必要なツール、アカウント

2. Azure Static Web Apps作成

まずはサンプルアプリを取ってきて、Azure Static Web Appsへデプロイしてみましょう。

2-1. GitHubリポジトリ作成、ローカルへクローン

↓にアクセスします。
https://github.com/staticwebdev/svelte-basic/generate
必要な情報入力後「Create repository from template」を押下します。
自分のアカウントにリポジトリが作成されるので、そのリポジトリを手元にクローンします。

2-2. VSCodeからデプロイ

先程クローンしたソースコードをVSCodeで開きます。プロジェクトルートディレクトリで開いてください。
VSCodeのAzure拡張機能のアイコンを選択し、Azure拡張機能メニューを開きます。
STATIC WEB APPS横の + ボタンを押下します。
(VSCodeにAzure Static Web Apps拡張機能が入っていなければ追加してください。)

設定項目について聞かれるので、↓の入力内容で進めます。

  • Select subscription: 使用するAzureサブスクリプションを選択
  • (1/5) Enter a name for the new static web app.: 任意
  • (2/5) Select a region for Azure Functions~: 任意の近い地域
  • (3/5) Choose build preset to configure default project structure: Svelte
  • (4/5) Enter the location of your application code: /
  • (5/5) Enter the location of your build output~: public

入力後、yamlファイル /.github/workflows/azure-static-web-apps-~.yml が生成されていることを確認します。
(デプロイ設定用ファイルです。先程設定したソースコードディレクトリ・成果物ディレクトリの設定も含まれます。シークレット系の情報はGitHubリポジトリのSecrets設定側で持っています。)

続いて実際にデプロイ状況を確認していきましょう。

2-3. デプロイ状況確認

GitHub Actions実行状況確認画面を確認します。
先程のyamlファイルにより、mainの変更をトリガーとし、デプロイ処理が実行されます。
実行状況はこの画面からいつでも確認できます。

デプロイ完了後、Webアプリの動作をブラウザから確認します。
ポータルからURLが確認できるので、そのURLに任意のブラウザからアクセスします。

下記画像の通り画面が表示されればOKです。

3. Azure Functions追加

サンプルプロジェクトにAzure Functionsのコードを追加していきます。

3-1. Azure Functionsの追加と各所コード更新

VSCodeのコマンドパレット(F1で起動)から Azure Static Web Apps: Create HTTP Function... を実行します。
言語はJavaScriptとし、Function名は TodosRead と入力します。

/api/TodosRead/index.js を下記内容に更新します。
JavaScriptのAzure Functionsのプロジェクト構成は、ディレクトリ名が関数名、index.jsが実行コードとなります。(関数名はプロジェクト内で一意である必要があります。)

module.exports = async function (context, req) {
    context.res.json({
        text: "Hello from the API"
    });
};

/api/TodosRead/function.json を編集します。
function.jsonでは、ルーティングやクエリパラメータ、バインディングの設定が可能です。
このステップではルーティング設定 "route": "todos" を加え、 /api/todos となるようにします。(ルーティング設定を行わない場合はデフォルトで /api/関数名 となります。)
また、 "methods": ["get"] とし、GETリクエストのみTodosReadへルーティングするようにします。

{
  "bindings": [
    {
      "authLevel": "anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "get"
      ],
      "route": "todos"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ]
}

/src/App.svelte を下記内容に更新します。
Functionsと疎通できているか簡易的に確認する内容です。

<script>
    (async function() {
        const { text } = await( await fetch(`/api/todos`)).json();
        document.querySelector('#name').textContent = text;
    }())
</script>

<div id="name">...</div>

3-2. ローカルデバッグ

プロジェクトルートを作業ディレクトリとした状態で、下記コマンドを順に実行します。

npm install

# Svelteプロジェクトのビルド
npm run build

# Azure Static Web Appsとしてデバッグ起動
swa start public --api-location api

ブラウザから実行URL http://localhost:4280 にアクセスし、 Hello from the API が表示されればOKです。
確認後、 Ctrl + c 等でデバッグを止めます。

3-3. 再デプロイ

ここまでの変更をgit pushします。(mainブランチがトリガーとなるので、もし作業ブランチを切っている場合はmainへマージしてください。)
push後、2-3同様の手順で、GitHub Actionsでデプロイ状況確認を行います。

デプロイ完了後、ブラウザからアプリURLにアクセスし、反映されていることを確認します。

4. Azure Cosmos DB作成、Azure Functions実装

Azure Cosmos DBを作って、Azure FunctionsにCRUD処理を実装していきます。

4-1. Azure Cosmos DB EmulatorにDatabaseとContainer作成

まずはローカル上にデバッグで使用する環境を作ります。
Azure Cosmos DB Emulatorを立ち上げ、localhostのAzure Cosmos DB Explorer https://localhost:8081/_explorer/index.html へアクセスします。
下記の通りDatabaseとContainerを作成します。

  • Database id: SvelteTest
  • Container id: Todos
  • Pertition key: pk

手動でダミーデータを作る際は、下記形式で追加すればOKです。(doneはtrue/false、descriptionは任意の文字列)

{
  "done": false,
  "description": "test",
  "pk": "todos"
}

4-2. TodosReadを更新

ソースコードを変更するので、VSCodeに戻ります。
/api/TodosRead/function.json に下記の通り入力バインド設定を追加します。
ここにCosmos DBクエリを書いておくと、コード側ではCosmos DBからデータを取ってきた状態からスタートできます。(このクエリにリクエストのパラメータを含めることもできますが今回は割愛。入力バインドのドキュメントを本投稿最後に貼ってますのでご参考ください)

{
  "bindings": [
    {
      "authLevel": "anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "get"
      ],
      "route": "todos"
    },
    {
      "name": "documents",
      "type": "cosmosDB",
      "direction": "in",
      "databaseName": "SvelteTest",
      "collectionName": "Todos",
      "sqlQuery": "SELECT * from c where c.pk = 'todos'",
      "connectionStringSetting": "CosmosDBConnection"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ]
}

/api/TodosRead/index.js を下記内容に更新します。めっちゃ短い。超便利。

module.exports = async function (context, req) {
    context.res.json(context.bindings.documents);
}

4-3. TodosCreateを作成

TodosReadを作ったときと同じ手順(コマンドパレットから Azure Static Web Apps: Create HTTP Function... 実行)で TodosCreate を作成します。

/api/TodosCreate/function.json にルーティング設定、出力バインド設定を追加します。methodsはpostのみにしておきます。
この出力バインド設定を入れておくと、Cosmos DBにデータを入れる処理はFunctionsのプラットフォーム機能側で吸収されるので、こちらで実装するコードではオブジェクトを渡すだけでOKです。

{
  "bindings": [
    {
      "authLevel": "anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "post"
      ],
      "route": "todos"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    },
    {
      "name": "todoDocument",
      "type": "cosmosDB",
      "databaseName": "SvelteTest",
      "collectionName": "Todos",
      "createIfNotExists": true,
      "connectionStringSetting": "CosmosDBConnection",
      "direction": "out"
    }
  ]
}

/api/TodosCreate/index.js を下記内容に更新します。
今回は明示的にid採番するためuuidパッケージを使用します(npm installは後ほどまとめて実行します)。
※id無しでもCosmos DBインサートは成功してidが自動で払い出されますが、今回は簡単にidを画面に渡すため予め採番の方式を取っています

const { v4: uuidv4 } = require('uuid');

module.exports = async function (context, req) {
    // 簡易バリデーション
    if (!req.body.description || Object.prototype.toString.call(req.body.done) !== '[object Boolean]') {
        context.res = { status: 400 };
        context.done(); // 明示的に終了
    }

    // レスポンスに含めたいのでここで採番
    req.body.id = uuidv4();

    // 出力バインドにリクエストBodyを渡す
    context.bindings.todoDocument = req.body;
    
    // idを返す
    context.res = {
        status: 200,
        body: {id: req.body.id}
    };
};

4-4. TodosUpdateを作成

TodosUpdate を作成します。( Azure Static Web Apps: Create HTTP Function... 実行)

/api/TodosUpdate/function.json にルーティング設定、出力バインド設定を追加します。methodsはputにします。
内容はおおよそTodosCreateと同じです。

{
  "bindings": [
    {
      "authLevel": "anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "put"
      ],
      "route": "todos"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    },
    {
      "name": "todoDocument",
      "type": "cosmosDB",
      "databaseName": "SvelteTest",
      "collectionName": "Todos",
      "createIfNotExists": true,
      "connectionStringSetting": "CosmosDBConnection",
      "direction": "out"
    }
  ]
}

/api/TodosUpdate/index.js を下記内容に更新します。

module.exports = async function (context, req) {
    // 簡易バリデーション(Update想定なのでidもチェック)
    if (!req.body.id || !req.body.description || Object.prototype.toString.call(req.body.done) !== '[object Boolean]') {
        context.res = { status: 400 };
        context.done(); // 明示的に終了
    }

    // 出力バインドにリクエストBodyを渡す
    context.bindings.todoDocument = req.body;

    context.res = { status: 204 };
};

4-5. TodosDeleteを作成

TodosDelete を作成します。( Azure Static Web Apps: Create HTTP Function... 実行)

/api/TodosDelete/function.json にルーティング設定を追加します。methodsはdeleteにします。
{id} を含むことで、index.js中で const id = context.bindingData.id; のように取ってくることが可能になります。

{
  "bindings": [
    {
      "authLevel": "anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "delete"
      ],
      "route": "todos/{id}"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ]
}

/api/TodosDelete/index.js を下記内容に更新します。
今回の中では唯一CosmosClientを使う内容です。

const CosmosClient = require('@azure/cosmos').CosmosClient;

module.exports = async function (context, req) {
    const id = context.bindingData.id; // パスからid取ってくる

    const client = new CosmosClient(process.env['CosmosDBConnection']);
    const database = client.database('SvelteTest');
    const container = database.container('Todos');
    const partitionKey = 'todos'; // 今回は決め打ち

    await container.item(id, partitionKey).delete();

    context.res = { status: 204 };
}

4-6. Functionsローカルデバッグ

/api/local.settings.json を下記内容で作成します(既にあれば内容更新)。
local.settings.jsonはAzure Functionsのローカルデバッグ時に読み込まれる環境変数を定義するjsonファイルです。(ので、gitignore対象です。)

"NODE_TLS_REJECT_UNAUTHORIZED": "0" はAzure Cosmos DB Emulatorに接続する際必要な設定です。
https://docs.microsoft.com/ja-jp/azure/cosmos-db/sql/sql-api-nodejs-get-started#connect-to-the-azure-cosmos-account

また、以降Azure Cosmos DB Emulatorが起動していることを確認してから動作確認を行うようご注意ください。

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "NODE_TLS_REJECT_UNAUTHORIZED": "0",
    "CosmosDBConnection": "AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
  }
}

プロジェクトルートを作業ディレクトリとした状態で、下記コマンドを順に実行します。

# apiディレクトリに移動
cd api

# パッケージ追加(Functions側のpackage.jsonに適用するため、必ずapiディレクトリで実行するよう注意)
npm install uuid
npm install @azure/cosmos

# Functionsデバッグ起動(実行PCにAzure Functions Core Toolsが必要です)
func start

Postmanから各種APIコールを行います。
下記画像は、POSTする際の例です。

4-7. Azure Cosmos DBリソース作成

Azure Cosmos DBのリソースを新規作成します。
https://azuremarketplace.microsoft.com/en-us/marketplace/apps/Microsoft.DocumentDB?tab=Overview

作成時パラメータの参考ポイントは↓です。残りも画面の指示に従ってか注意書きを読みつつ設定します。

  • 最初の「API オプションの選択」選択画面では「コア (SQL) - 推奨」を選択
  • 「場所」は特に理由が無ければ「東日本」でOK
  • 「容量モード」は「プロビジョニングされたスループット」
    • (Free適用しないのであればServerlessでも可)
  • Free レベル割引の適用
    • オンにすると最初の1000RU/sと25GBストレージ使用分が無料になります
    • 1サブスクリプションにつき1Azure Cosmos DBリソースのみ適用可能な設定です

作成後、「キー」メニューから「プライマリ接続文字列」をメモします。

また、同じく左メニューの「データ エクスプローラー」にアクセスし、ローカルに作成したのと同じ内容でDatabase( SvelteTest )、Container( Todos )を作成します。

4-8. Azure Functionsアプリケーション設定追加

Azure Static Web Appsの「構成」画面を開きます。
「追加」ボタンを押し、下記内容で追加後、「保存」を押下します。

  • 名前: CosmosDBConnection
  • 値: 先程メモしたAzure Cosmos DB接続文字列( AccountEndpoint=https://<リソース名>.documents.azure.com:443/;AccountKey=<アカウントキー>; )

4-9. 再デプロイ

3-3同様git pushを行い、GitHub Actionsでのデプロイが成功することを確認します。
また、Postmanから実行URLを <Static Web AppsのURL>/api/todos としての動作確認も可能です。
この場合、データの参照・更新先は4-7で作成したAzure上のCosmos DBになることに注意してください。

5. Todoアプリ実装

実装したFunctionsを活用するよう、Svelteアプリ側の実装を行います。

5-1. App.svelte実装

/src/App.svelte を下記ページサンプルのApp.svelteの内容で全て上書きします。
https://svelte.dev/tutorial/animate
以降、このApp.svelteをベースに変更を加えていきます。

画面ロード時処理(23~32行)をTodosReadコール処理に書き換えます。

// uid変数は使わないため削除、todosは空配列とする
let todos = [];

(async function() {
    // TodosReadをコール
    todos = await (await fetch(`/api/todos`)).json();
}())

以降、全て async function に変わるので、メソッドの中身だけコピペしないようご注意ください。

addメソッドを下記内容に更新します。(TodosCreateコール処理を追加など)

async function add(input) {
    // idはサーバーサイドで採番するため削除
    const todo = {
        done: false,
        description: input.value,
        pk: 'todos'
    };

    // TodosCreateをコール
    const req = {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(todo)
    };
    const { id } = await (await fetch(`/api/todos`, req)).json();
    todo.id = id; // 採番されたidをフロント側のパラメータにも反映

    todos = [...todos, todo];
    input.value = '';
}

removeメソッドを下記内容に更新します。(Deleteコール処理を追加)

async function remove(todo) {
    // TodosDeleteをコール
    await fetch(`/api/todos/${todo.id}`, { method: 'DELETE' });
    
    todos = todos.filter(t => t !== todo);
}

markメソッドを下記内容に更新します。(TodosUpdateコール処理を追加など)

async function mark(todo, done) {
    todo.done = done;

    // TodosUpdateをコール
    const req = {
        method: 'PUT',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(todo)
    };
    await fetch(`/api/todos`, req);

    // removeでTodosDeleteをコールするようにしたため、removeは使わずtodos.filterだけ持ってきた
    todos = todos.filter(t => t !== todo);
    todos = todos.concat(todo);
}

5-2. ローカルデバッグ

Azure Cosmos DB Emulatorが起動していることを再確認してから動作確認を行うようご注意ください。

再度プロジェクトルートを作業ディレクトリとした状態で下記コマンドにてローカルデバッグ起動します。(先程のFunctionsのデバッグでapiディレクトリにいる場合はご注意ください。)

npm run build
swa start public --api-location api

5-3. 再デプロイ

3-3同様git pushを行い、GitHub Actionsでのデプロイ状況確認とブラウザからの動作確認を行います。

以上でSvelteアプリとAzure Cosmos DBデータ操作を行うAzure FunctionsのAzure Static Web Appsデプロイが完了しました!
今回は最終形をGitHubにアップしましたので上手くいかない場合などはこちらご参考ください😉

github.com

6. 参考


Azure Static Web Appsはフリーから使い始められる且つ手軽にGitHub ActionsでのCI/CD設定が可能なので、まずはフロントのコードだけでもデプロイする形で是非使ってみてください!
そしてバックエンドの実装もしたい!という方もAzure Functionsで気軽に開発スタートできるので、本投稿を少しでも開発に役立てていただければ幸いです。
また、今回のアプリは画面アクセスした人全員に同じデータが表示されちゃうので、ユーザーIDの仕組みを設けるなどの応用をきかせてみるのも良いかと思います!

ここまで読んで頂きありがとうございました。Svelte初めて触りましたが使いやすくて助かりました笑
明日以降のオルターブースアドベントカレンダーもお楽しみに🌟

www.alterbooth.com

www.alterbooth.com

cloudpointer.tech