【AI Shift Advent Calendar 2023】Container Apps + API Managementで構築するAPI Gateway

こんにちは、AIShift バックエンドエンジニアの石井(@sugar235711)です。
本記事はAIShift Advent Calendar 2023の 10日目の記事となります。

今回の記事では、Container Apps + API ManagementでAPI Gatewayを構築する手順をご紹介します。

GCP版はこちら

はじめに

今回は Azure上でセキュアなAPI開発環境を作成するための構成を紹介します。

本記事で作る構成

  • 構成1

Alt text

  • 構成2(実践編)

Alt text

※今回は開発環境の準備のため作成するリソースは基本的にDeveloper Planを利用します。
本番環境での利用は業務要件等を考慮してご検討ください。

使用するAzureリソース

API Management

API Managementは、Web APIを統合管理するためのプラットフォームです。Azure上で構築されたAPIやオンプレミスのAPIを統合管理することが可能で、認証・認可、スロットリングなどの制御も行うことができます。

API Management

今回のデモでは、Container Apps上に構築したREST APIを利用しますが、gRPCなどの他のプロトコルでも利用可能です。

Azure Container Apps

Azure Container Appsは、Kubernetes上に構築されたマネージドサービスです。AKSとは異なり、クラスター構成や管理の手間が不要で、アプリケーション開発に焦点を当てたサービスとなっています。

2022年に一般提供(GA)が開始されたこのサービスは、後発ゆえに高機能で使いやすいサービスとなっています。

主な特徴としては以下のようなものがあります:

  • CNCF OSS(Envoy、KEDA、Dapr)が組み込まれています。
  • ユーザ認証機能が提供されています。
  • カナリアリリース、ABテスト、ブルーグリーンデプロイメントなどのデプロイ戦略をサポートしています。
  • Kubernetesクラスタの管理が不要です。
  • Service Connectorを通じて他のAzureリソースとの連携が容易です。

詳細については、以下のリンクを参照してください:
https://learn.microsoft.com/ja-jp/azure/container-apps/overview

デモ環境の構築

まず、適当なリソースグループを作成し、その中にAzure Virtual Network(VNet)を作成します。Container Apps、API Management用にそれぞれサブネットを作成し、その中にリソースを配置します。

Container Appsの作成

Container Appsを作成するには、Containerリソースを管理するContainer Apps Environmentが必要です。
このEnvironmentに対してサブネットを割り当てることで、Container Appsが利用するネットワークを制御することができます。
なお、ワークロード環境では最低でも/27のサブネットを割り当て、Microsoft.App/environmentsに対して委任する必要があります。

https://learn.microsoft.com/ja-jp/azure/container-apps/networking?tabs=workload-profiles-env%2Cazure-cli#subnet

作成したサブネットをContainer Apps Environmentに割り当てます。
infrasturcture subnetに割り当て、Virtual IPをInternalに設定します。
これによって、Container Appsに割り当てられたIPアドレスはVNet内からのみアクセス可能となります。

Container Apps Environment

次にこのEnvironmentに対してContainer Appsを作成します。
ここではサンプルのイメージを利用してContainer Appsを作成します。Internalに設定することで、VNet内からのみアクセス可能な環境となります。

Container Apps Creation

デプロイが完了したら、Application Urlを確認します。
Internalに設定したため、VNet内からのみアクセス可能なので、このURLにアクセスするとアクセスが拒否されます。

Container Settings

No Access

次に、VNet内でContainer Appsの名前解決を行うために、プライベートDNSゾーンを作成します。作成するプライベートDNSゾーンの「name」には、コンテナーアプリの「Application Url」のドメイン部分を指定します。

Private DNS Instance Detail

DNS Zoneが作成されたら、レコードを作成します。
レコード名には、Container Appsのドメイン、 IPアドレスにはContainer Apps EnvironmentのStatic IPを指定します。

Record Creation

最後にVirtual NetWork Linkを作成し、VNetとプライベートDNSゾーンを紐づけます。
これでContainer Appsの名前解決が可能になりました。

Network Link

API Managementの作成

今回はAPI ManagementにPublicIPを割り当て、API Gatewayとして利用します。
その際、インターネットからのアクセスを制御するために、Network Security Group(NSG) を割り当てたサブネットに紐づけます。

Network Security Group(NSG)
基本的には、API ManagementのVNet使用設定に従ってinbound/outboundの設定を行います。
https://learn.microsoft.com/ja-jp/azure/api-management/api-management-using-with-vnet?tabs=stv2#configure-nsg-rules

サブネットに対してのNSGの適用します。
その際にサブネットの委任をしないように注意してください。
Subnet API Management

次に、API Management用のPublicIPを取得します。(SKUがStandardでないとAPI Managementに割り当てられません)
取得したら、設定からDNS名のラベル付けを行います。

IP DNS Name

以上の準備ができたら、API Managementを作成します。
その際に、先ほど作成したサブネットおよびIPアドレスを指定します。
外部に公開するためにConnectivity TypeをExternalに設定します。

API Management Creation

リソースを作成したら、API ManagementにContainer Appsをインポートします。

API Management Container Import

インポートした際に設定したsuffixとgatewayのURLを合わせてアクセスすると、コンテナーアプリにアクセスすることができます(デフォルトでサブスクリプションが要求されるので一時的に解除しています)。

API Access

https://<resource-name>.azure-api.net/にアクセスすると、Container Appsにアクセスすることができます。
API Access Result

ここまでで、API Management経由でContainer Appsにアクセスすることができました。

IP制限や流量管理はAPI Managementの機能を利用することで制御することができます。
例えば、API Management経由の全てのリクエストにIP制限をかけたい場合は、以下のようにフィルターを設定します。

IP Filter

設定以外のIPからアクセスした場合、アクセスが拒否されます。

Access Denied

ここまでで、冒頭で紹介した構成の実現ができました。

実践編

今回は簡易的な実験のためにサンプルイメージを利用しましたが、より実運用に近い構成も紹介します。
Alt text

デモ用のイメージを用意する

今回は、BunとTypescriptで構成された簡易的なREST APIを用意します。

repository

今回は、Azure RegistryにイメージをPushし、そこからContainer Appsにデプロイします。

linux/amd64のイメージを用意します。
※Container AppsはArmのイメージには対応していません。

$ docker build --pull --platform linux/amd64 -t bun-hello-world .
$ docker tag bun-hello-world .azurecr.io/dev
$ docker push .azurecr.io/dev

その後、先ほどの手順でサンプルイメージを利用する部分を変更し、Registry経由でイメージを取得してコンテナ化します。

DBサーバーの用意

Azure Database for PostgreSQLを用意します。
Developer Planを利用したいので、Azure Database for PostgreSQLのフレキシブルサーバーを作成します。

https://learn.microsoft.com/ja-jp/azure/postgresql/flexible-server/overview

DB用のサブネットを準備します。
このサブネットはMicrosoft.DBforPostgreSQL/flexibleServersに委任する必要があります。

サブネットの準備

DBサーバーもVNet以外からのアクセスを防ぐために、Private access (VNet Integration)を選択します。

https://learn.microsoft.com/ja-jp/azure/postgresql/flexible-server/concepts-networking-private#virtual-network-concepts

Private accessの選択

こちらもContainer Appsと同様にPrivate DNSゾーンを作成します。Flexible Serverの作成時に同時に自動作成することも可能です。

Private DNSゾーンの作成

Container AppsとAzure Database for PostgreSQLを繋ぐ

このDBサーバーとContainer Appsを連携させるために、Service Connectorを利用します。

Service ConnectorはAzure上のリソースとの連携を容易にするための機能です。これを利用すると、環境変数の情報などを自動でContainer Appsに設定することができます。

上記で作成したDBサーバーを選択し、Container Appsと連携させます。

Container Apps側で使用するランタイムの環境に合わせて接続情報を設定します。

Service Connectorの設定

ここまで行うと、ContainerからDBサーバーにアクセスすることができます。

実際にコンテナからアクセスしてみます。MonitoringのConsoleからコンテナを確認できます。Postgresのサーバー名を指定してnslookupしてみます。

nslookupの結果

正常に解決できているようです。

次に、手元から直接DBサーバーにアクセスしてみます。これはPublicに公開していないのでアクセスできないはずです。
Alt text

アクセスできませんでした。正しくVNet内でのみアクセスが制御できているようです。

次に、API ManagementからContainer Appsにアクセスし、Container Appsに構築したアプリケーションからDBサーバーにアクセスできるか確認します。

使用するソースは以下の通りです。DBにログインしているユーザーの情報を取得するAPIです。

import { Hono } from "hono";
import { Pool } from "pg";
import fs from "fs";
const app = new Hono();

const sslConfig = Bun.env.AZURE_POSTGRESQL_SSL
  ? {
      ca: fs.readFileSync("./DigiCertGlobalRootCA.crt.pem"),
    }
  : false;

const pool = new Pool({
  user: Bun.env.AZURE_POSTGRESQL_USER,
  host: Bun.env.AZURE_POSTGRESQL_HOST,
  database: Bun.env.AZURE_POSTGRESQL_DATABASE,
  password: Bun.env.AZURE_POSTGRESQL_PASSWORD,
  port: parseInt(Bun.env.AZURE_POSTGRESQL_PORT!, 10),
  ssl: sslConfig,
});

app.get("/", async (c) => {
  try {
    console.log("Connecting to database");
    const client = await pool.connect();
    try {
      const result = await client.query("SELECT current_user;");
      const currentUser = result.rows.at(0).current_user;
      return c.json({ message: Hello, ${currentUser} }, 200);
    } finally {
      client.release();
    }
  } catch (err) {
    if (err instanceof Error) {
      console.error(err);
      return c.json({ error: err.message }, 500);
    }
    console.error("An unknown error occurred");
    return c.json({ error: "An unknown error occurred" }, 500);
  }
});

export default app;
# use the official Bun image
FROM oven/bun:1 as base
WORKDIR /usr/src/app

# Install common network troubleshooting tools
RUN apt-get update && apt-get install -y \
    curl \
    telnet \
    netcat \
    dnsutils

# install dependencies into temp directory
# this will cache them and speed up future builds
FROM base AS install
RUN mkdir -p /temp/dev
COPY package.json bun.lockb /temp/dev/
RUN cd /temp/dev && bun install --frozen-lockfile

# install with --production (exclude devDependencies)
RUN mkdir -p /temp/prod
COPY package.json bun.lockb /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile --production

# copy node_modules from temp directory
# then copy all (non-ignored) project files into the image
FROM base AS prerelease
COPY --from=install /temp/dev/node_modules node_modules
COPY . .

# Copy the DigiCertGlobalRootCA.crt.pem file
COPY DigiCertGlobalRootCA.crt.pem ./

# [optional] tests & build
ENV NODE_ENV=production
RUN bun test
RUN bun run build

# copy production dependencies and source code into final image
FROM base AS release
COPY --from=install /temp/prod/node_modules node_modules
COPY --from=prerelease /usr/src/app/index.ts .
COPY --from=prerelease /usr/src/app/package.json .

# Copy the DigiCertGlobalRootCA.crt.pem file to final image
COPY --from=prerelease /usr/src/app/DigiCertGlobalRootCA.crt.pem ./

# run the app
USER bun
EXPOSE 3000/tcp
ENTRYPOINT [ "bun", "run", "index.ts" ]

TLSの設定を行うために、DBサーバーの証明書をダウンロードしています。ダウンロードはPortalから行うことができます。
※注意: 今回は一時的なデモアプリのためイメージに証明書を含めていますが、本番環境ではAzure Key Vaultなどに証明書を保存して利用することをおすすめします。

動作確認
Alt text

以上のアプリをContainer Appsにデプロイし、API Management経由でアクセスすることでDBまでセキュアに接続していることが確認できました。

おわりに

今回は Azure Container Apps + API ManagementでAPI Gatewayを構築する手順をご紹介しました。

AI Shiftではエンジニアの採用に力を入れています!
少しでも興味を持っていただけましたら、カジュアル面談でお話しませんか?
(オンライン・19時以降の面談も可能です!)
面談フォームはこちら

明日のAdvent Calendar 11日目の記事は、開発チームの福島による記事の予定です。こちらもよろしくお願いいたします。

PICK UP

TAG