はじめに

Nablarchには、2020年9月29日にリリースされた5u18でクラウドネイティブ対応というものが入っています。 これは、NablarchをAWSやAzureなどのクラウド環境で利用しやすくするためのアップデートです。 詳細は、Nablarch 5u18をリリースしました!を参照してください。

このクラウドネイティブ対応によって、NablarchはAzureでも利用できるようになっています。 実際にAzure上でNablarchを使ったアプリケーションを動かしてみたので、本記事ではその手順について解説します。

なお、Nablarchを使ったアプリケーションには、サービス開発リファレンスで公開されているチャットアプリの ver 1.1(以下example-chat)を使用しました。 サービス開発リファレンスについては、SPA + REST API構成のサービス開発リファレンスを参照してください。

システム構成

システムの構成は、上図のようにしました。

コンテナを実行するサービス

example-chatは、以下3つのコンポーネントで構成されています。

  • frontend
    • Reactで構築されたSPA画面
  • backend
    • RESTful Web APIを提供するJavaアプリケーション
  • notifier
    • WebSocketでチャットのメッセージをサーバーからプッシュするJavaアプリケーション

これらのコンポーネントは、Dockerコンテナ化してApp ServiceWeb App for Containersで動かします。

AzureにはDockerコンテナを動かすサービスとしてContainer Instancesというサービスもありますが、今回はApp Serviceを選択しました。 サービスの選択には、こちらのチャートを利用しました。 このチャートによると、マイクロサービスでないWebアプリにはApp Serviceが向いているそうです。

DockerコンテナのイメージはContainer Registryにあらかじめプッシュしておき、App Serviceからプルして使用します。

ストレージサービス

アプリケーションが使用するストレージには、以下のサービスを使用しています。

  • Azure Database for PostgreSQL
    • example-chatが使用する様々なデータを永続化します
  • Azure Cache for Redis
    • セッションなどの一時情報の保存に使用したり、backendからnotifierにメッセージを送るためのキューとして使用します
  • Azure Blob Storage
    • アップロードされた画像ファイルなどを保存するオブジェクトストレージとして使用します

メール送信機能は使用しない

example-chatは、サインアップのときにメールを送信して本人確認を行います。

メールの送信には、AWSであればAmazon Simple Email Service (SES)というメール送信のためのサービスがありますが、Azureには独自のメール送信サービスはありません。 代わりに、SendGridのようなサードパーティのメール送信サービスを使用することが推奨されています。 詳しくは、以下のページを参照してください。

本検証の目的は、NablarchのアプリケーションがAzure上で動作するかどうかを確認することです。 SendGridを使用する選択肢もありましたが、検証を簡単に進めるため、メールの送信機能は利用しないことにしました。

メール送信機能を利用しない場合、新規のサインアップはできなくなります。 しかし、テストデータを投入すれば事前定義されたユーザでのログインは可能なので、動作検証はできます。

セキュリティについて

セキュリティを考慮するとWAFプライベートリンクなどの利用が必要になってきます。

しかし、今回はNablarchのアプリケーションがAzure上で動かせるかどうかを確認することが目的です。 このため、セキュリティに関するサービスは導入せず、簡単に構築できるシンプルなシステム構成にしています。

準備作業・前提条件

今回の検証で必要な準備作業や、前提条件などを説明します。

ローカルでの作業は、Windows 10上で行いました。 CLIの操作は、コマンドプロンプトかGit Bashで行っています。

また、以下のツールが必要になるので、あらかじめローカルにインストールしておいてください(インストール手順については割愛します)。 括弧内は、検証時の各ツールのバージョンです。

また、Azureの環境を構築するために、サブスクリプションを用意してください。 Azureのアカウントが無い場合は、アカウントを作成してください。

なお、以下の手順では、全てのAzureリソースを「東日本(japaneast)」リージョンに作成しています。

アプリケーションのDockerイメージを作成する

まずは、example-chatの各コンポーネントのDockerイメージを作成します。

Gitのリポジトリをローカルにクローンして、ver 1.1のタグに切り替えます。 Gitコマンドを実行できるCLIを使い、以下のコマンドを実行してください(以下はGit Bashから実行した例です)。

$ git clone git@github.com:Fintan-contents/example-chat.git

$ cd example-chat

$ git checkout 1.1

notifierをビルドする

最初はnotifierをビルドします。

コマンドラインでexample-chatのルートフォルダに移動し、以下のようにコマンドを実行します。

> cd notifier

> mvn package jib:dockerBuild -DskipTests

notifierがビルドされ、ローカルのDockerレジストリにexample-chat-notifierという名前のイメージが追加されます。

> docker images
REPOSITORY             TAG       IMAGE ID       CREATED        SIZE
...
example-chat-notifier  latest    4a59725ccc08   51 years ago   451MB
...

notifierのビルドは以上です。

backendをビルドする

次に、backendをビルドします。 ただし、backendはAzureで動かすために、若干の修正が必要です。

Azure Blob Storageに対応させる

example-chatのver 1.1は、画像などを保存するオブジェクトストレージにAmazon S3を使用する実装が使われています。

AzureではオブジェクトストレージにAzure Blob Storageを使うので、このままではAzure上で画像のアップロード機能などが利用できません。

このAwsS3ClientPublishableFileStorageというインタフェースを実装しています。 このインタフェースは、オブジェクトストレージの具体的なサービスを隠蔽し、example-chatが要求する抽象的なAPIのみを定義しています。

つまり、Azure Blob Storage用のPublishableFileStorage実装クラスを作って差し替えれば、Azure上でもオブジェクトストレージが使用できるようになります。

ここでは、実装の修正方法だけを説明します。 Azure Blob StorageのJava SDKの詳しい使い方については、公式のガイドを参照してください。

まず、pom.xmlにAzure Blob StorageのSDKを依存関係として追加します。

    <dependency>
      <groupId>com.azure</groupId>
      <artifactId>azure-storage-blob</artifactId>
      <version>12.10.0</version>
    </dependency>

次に、PublishableFileStorageのAzure Blob Storage用実装クラス、AzureBlobStorageClientを作成します。

AzureBlobStorageClient のソースコードは、こちらを開くと確認できます
package com.example.infrastructure.service;

import com.azure.storage.blob.BlobClient;
import com.azure.storage.blob.BlobContainerClient;
import com.azure.storage.blob.BlobServiceClient;
import com.azure.storage.blob.BlobServiceClientBuilder;
import com.azure.storage.blob.specialized.BlobInputStream;
import com.example.application.service.message.PublishableFileStorage;
import com.example.domain.model.channel.ChannelId;
import com.example.domain.model.message.ImageData;
import com.example.domain.model.message.ImageFile;
import com.example.domain.model.message.ImageKey;
import com.example.domain.model.message.MessageHistoryData;
import com.example.domain.model.message.MessageHistoryFile;
import com.example.domain.model.message.MessageHistoryKey;
import nablarch.core.repository.di.config.externalize.annotation.ConfigValue;
import nablarch.core.repository.di.config.externalize.annotation.SystemRepositoryComponent;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.function.BiFunction;

@SystemRepositoryComponent
public class AzureBlobStorageClient implements PublishableFileStorage {

    private final String connectionString;
    private final String containerName;

    public AzureBlobStorageClient(
           @ConfigValue("${azure.blobStorage.connectionString}") String connectionString,
           @ConfigValue("${azure.blobStorage.containerName}") String containerName) {
        this.connectionString = connectionString;
        this.containerName = containerName;
    }

    @Override
    public ImageKey save(ChannelId channelId, ImageFile imageFile) {
        String key = uploadFile(channelId, imageFile.path(), imageFile.contentType());
        return new ImageKey(key);
    }

    @Override
    public MessageHistoryKey save(ChannelId channelId, MessageHistoryFile messageHistoryFile) {
        String key = uploadFile(channelId, messageHistoryFile.path(), "text/csv");
        return new MessageHistoryKey(key);
    }

    private String uploadFile(ChannelId channelId, Path path, String contentType) {
        BlobContainerClient blobContainerClient = createBlobContainerClient();
        if (!blobContainerClient.exists()) {
            blobContainerClient.create();
        }

        String key = UUID.randomUUID().toString();
        BlobClient blobClient = blobContainerClient.getBlobClient(key);

        blobClient.uploadFromFile(path.toString());
        blobClient.setMetadata(Map.of("channelId", channelId.value().toString(), "contentType", contentType));

        return key;
    }

    @Override
    public ImageData findImageData(ChannelId channelId, ImageKey imageKey) {
        return findFile(channelId, imageKey.value(), ImageData::new);
    }

    @Override
    public MessageHistoryData findMessageHistoryData(ChannelId channelId, MessageHistoryKey messageHistoryKey) {
        return findFile(channelId, messageHistoryKey.value(), MessageHistoryData::new);
    }

    private <T> T findFile(ChannelId channelId, String key, BiFunction<byte[], String, T> resultCreator) {
        BlobContainerClient blobContainerClient = createBlobContainerClient();
        BlobClient blobClient = blobContainerClient.getBlobClient(key);

        try (BlobInputStream blobInputStream = blobClient.openInputStream()) {
            Map<String, String> metadata = blobInputStream.getProperties().getMetadata();
            if (!Objects.equals(metadata.get("channelId"), channelId.value().toString())) {
                throw new RuntimeException();
            }

            byte[] bytes = blobInputStream.readAllBytes();
            String contentType = metadata.get("contentType");

            return resultCreator.apply(bytes, contentType);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private BlobContainerClient createBlobContainerClient() {
        BlobServiceClient blobServiceClient = new BlobServiceClientBuilder().connectionString(connectionString).buildClient();
        return blobServiceClient.getBlobContainerClient(containerName);
    }
}

次に、AwsS3Clientをシステムリポジトリの管理対象から外すため、AwsS3Client.javaを削除します。

最後に、AzureBlobStorageClientに渡す環境設定値の定義をsrc/main/resources/env.configに追加します。

...
azure.blobStorage.connectionString=changeme
azure.blobStorage.containerName=example-chat
...

以上で、Azureで動かすための修正は完了です。

テストデータが登録されるようにする

アプリケーション起動時に、テストデータがデータベースへ登録されるようにします。

以下の要領で、テストデータのSQLファイルをコピーしてください。

  • コピー元のファイル
    • backend/src/test/resources/db/testdata/V9999__testdata.sql
  • コピー先のフォルダ
    • backend/src/main/resources/db/migration

コピー後のフォルダの中は、以下のようになります。

> dir /b
V1_1__prepare_accounts.sql
V1__create_table.sql
V9999__testdata.sql

これで、アプリケーション起動時にV9999__testdata.sqlが実行されて、テストデータが登録されるようになります。

ビルドする

ビルドのコマンドは、notifierのときと同じです。 コマンドラインでexample-chatのルートフォルダに移動し、以下のコマンドを実行してください。

> cd backend

> mvn package jib:dockerBuild -DskipTests

backendがビルドされ、ローカルのDockerレジストリにexample-chat-backendという名前のイメージが追加されます。

> docker images
REPOSITORY             TAG      IMAGE ID       CREATED        SIZE
...
example-chat-backend   latest   d7cfaf840c7e   51 years ago   471MB
...

backendのビルドは以上です。

frontendをビルドする

frontendも、ビルドの前に少しだけ手を入れる必要があります。

backendのURLを設定する

frontendからbackendのAPIを実行するために、frontendはあらかじめbackendのURLを知っておく必要があります。

example-chatでは、backendのURLはCreate React AppのEnvironment Variablesを使って設定します。

frontendフォルダの直下に.env.localという名前のファイルを作成します。 ファイルの中身は、次のようにしてください。

REACT_APP_BACKEND_BASE_URL=https://<backendのWebアプリ名>.azurewebsites.net

<backendのWebアプリ名>には、後で作成するbackendのWebアプリの名前を記述します。 任意の名前を考えて設定してください(例:example-chat-backend)。 ただし、この名前はAzure全体で一意である必要があります。

.env.localが作成できたら、コマンドラインでexample-chatのルートフォルダに移動し、以下のコマンドを実行してください。

> cd frontend

> npm install

> npm run build

> docker build -t example-chat-frontend .

frontendがビルドされ、ローカルのDockerレジストリにexample-chat-frontendという名前のイメージが追加されます。

> docker images
REPOSITORY             TAG       IMAGE ID       CREATED         SIZE
...
example-chat-frontend  latest    eb98c2d881e0   15 minutes ago  137MB
...

frontendのビルドは以上です。

DockerイメージをAzure Container Registryにアップロードする

frontend, backend, notifierの3つのDockerイメージが作成できたら、これらをAzure Container Registryにアップロード(プッシュ)します。

まず、Azure Container Registryにレジストリを作成します。 作成手順については、クイック スタート:Azure portal を使用して Azure コンテナー レジストリを作成するを参照してください(レジストリの名前は任意です)。

レジストリが作成できたら、Azure CLIを使ってレジストリにログインします。 ただし、その前にAzure CLIのサインインを済ませておく必要があります。 サインインは、az loginコマンドで行います。 詳細はSign in with Azure CLIを参照してください。

サインインが完了したら、以下のコマンドでレジストリにログインします。

> az acr login --name <レジストリ名>

<レジストリ名>には、作成したレジストリの名前を指定してください。 作成したレジストリの名前がmyregistryの場合は、以下のようになります。

> az acr login --name myregistry
Login Succeeded

レジストリへのログインが完了したら、ローカルのDockerイメージにタグを設定します。

> docker tag example-chat-frontend:latest <レジストリ名>.azurecr.io/example-chat-frontend:latest

> docker tag example-chat-backend:latest <レジストリ名>.azurecr.io/example-chat-backend:latest

> docker tag example-chat-notifier:latest <レジストリ名>.azurecr.io/example-chat-notifier:latest

最後に、イメージをAzure上のレジストリにプッシュします。

> docker push <レジストリ名>.azurecr.io/example-chat-frontend:latest

> docker push <レジストリ名>.azurecr.io/example-chat-backend:latest

> docker push <レジストリ名>.azurecr.io/example-chat-notifier:latest

Azureポータルでレジストリを開き、メニューの「サービス」→「リポジトリ」でアップロードされたイメージの一覧が確認できます。 3つのイメージがプッシュできていることを確認してください。

以上で、イメージのアップロードは完了です。

各種ストレージサービスを作成する

続いて、各種ストレージサービスを作成していきます。

Azure Blob Storage

Azure Blob Storageを使い始めるためには、先にストレージアカウントを作成する必要があります。 ストレージアカウントの作成方法については、ストレージ アカウントを作成するを参照してください(ストレージアカウントの名前は任意です)。

ストレージアカウントが作成できたら、次にコンテナーを作成します。 コンテナーを作成する手順については、コンテナーを作成するを参照してください。 このとき、作成するコンテナーの名前はexample-chatとしてください。

Azure Blob Storageの作成手順は以上です。

Azure Database for PostgreSQL

Azure Database for PostgreSQLの単一サーバーを作成します。 作成手順については、クイック スタート:Azure portal を使用して Azure Database for PostgreSQL サーバーを作成するを参照してください。

ただし、作成時に指定するパラメータは、以下のように設定してください(特に記述がないパラメータは任意の値を設定してください)。

パラメータ
バージョン 11
管理者ユーザー名 postgres

作成直後のデータベースは外部から接続できません。 Azure内からの接続を許可するために、設定を変更します。 手順はAzure からの接続を参照してください。

Azure Database for PostgreSQLの作成手順は以上です。

Azure Cache for Redis

Azure Cache for Redisを作成します。 作成手順については、クイックスタート: オープンソースの Redis キャッシュを作成するを参照してください。

ただし、作成時に指定するパラメータは、以下のように設定してください(特に記述がないパラメータは任意の値を設定してください)。

パラメータ
接続方法 パブリックエンドポイント
Redisバージョン 4

Azure Cache for Redisの作成手順は以上です。

App Serviceでコンテナインスタンスを作成する

最後に、App Serviceを作成してexample-chatの3つのコンテナを実行します。

App Serviceプランを作成する

App Serviceを使い始めるには、まずApp Serviceプランを作成する必要があります。

Azure Marketplaceで「App Serviceプラン」を検索してリソースを作成します(App Service Planで検索すると出てきます)。

「オペレーティングシステム」にLinuxを選択して作成してください。名前やサイズは任意のもので構いません。

各Webアプリを作成する

作成したApp Serviceプランに、3つの「Webアプリ」を追加します。

Azure Marketplaceで「Webアプリ」を検索してリソースを作成します(Web Appで検索すると出てきます)。

作成時に指定するパラメータは、以下のように設定してください(特に記述がないパラメータは任意の値を設定してください)。

パラメータ
名前 任意(ただし、backendの名前はfrontendの.env.localで指定したものと同じ名前にする)
公開 Dockerコンテナー
オペレーティングシステム Linux
地域 先ほど作成したApp Serivceプランと同じリージョン
Linuxプラン 先ほど作成したApp Serviceプラン
イメージソース Azure Container Registry
レジストリ 先の手順で作成したAzure Container Registry
イメージ example-chat-frontend, example-chat-backend, example-chat-notifier
タグ latest

環境変数を設定する

「Webアプリ」が作成できたら、次に環境変数を設定します。

Nablarchには、OS環境変数で設定値を上書きする機能が用意されています。 example-chatは、この機能を利用してデータベースの接続先など環境に依存する設定を環境変数で上書きできるようにしています。

コンテナに渡す環境変数を設定する方法については、アプリケーションの設定の構成を参照してください。

以下に、各「Webアプリ」に設定する環境変数を記載します。

アプリケーションの設定が完了したら変更内容を保存し、コンテナを再起動してください。

backend

名前
NABLARCH_DB_URL jdbc:postgresql://<DBのサーバー名>:5432/postgres?sslmode=require
NABLARCH_DB_USER postgres@<DBのサーバー名>
NABLARCH_DB_PASSWORD DB作成時に指定したパスワード
NABLARCH_LETTUCE_SIMPLE_URI rediss://<Redisのパスワード>@<Redisのホスト名>:6380
WEBSOCKET_URI wss://<notifierのWebアプリ名>.azurewebsites.net:443/notification
CORS_ORIGINS https://<frontendのWebアプリ名>.azurewebsites.net
BACKEND_BASE_URL https://<backendのWebアプリ名>.azurewebsites.net
APPLICATION_EXTERNAL_URL https://<frontendのWebアプリ名>.azurewebsites.net
NABLARCH_SESSIONSTOREHANDLER_COOKIESECURE true
AZURE_BLOBSTORAGE_CONNECTIONSTRING 作成したストレージアカウントの接続文字列
WEBSITES_PORT 8080

値に埋め込む<DBのサーバー名>などは、いずれもAzureポータルで対象のリソースを開くことで確認できます。 以下で、それぞれの値の参照方法を説明します。

次の2つは、各リソースの「概要」ページで確認できます。

  • <DBのサーバー名>
  • <Redisのホスト名>

次の2つは、各リソースの「アクセス キー」ページで確認できます。

  • <Redisのパスワード>
  • 作成したストレージアカウントの接続文字列

notifier

名前
NABLARCH_LETTUCE_SIMPLE_URI backendと同じ
WEBSITES_PORT 8080

frontend

名前
WEBSITES_PORT 80

すべての「Webアプリ」に共通で設定しているWEBSITES_PORTは、App Serviceがアプリのポートを特定するための設定です。 詳しくは ポート番号を構成する を参照してください。

動作確認

お疲れ様です、以上でexample-chatがAzure上で動くようになりました。

ブラウザで、https://<frontendのWebアプリ名>.azurewebsites.net にアクセスしてみてください。 以下のような画面が表示されれば成功です。

テスト用のユーザーを利用して、ログインしてみましょう。

画像のアップロードも問題なく動いているようです。

おわりに

クラウドネイティブ対応でコンテナ化や環境変数による設定値の上書きが可能となったことで、Azure上でも問題なくNablarchアプリケーションを動かすことができました。

本記事が、AzureでのNablarchを用いたシステム開発を検討している方の参考になれば幸いです。