Amazon Web Services ブログ

Next.js の API Routes から Amazon Virtual Private Cloud (Amazon VPC) 内のリソースにアクセスする方法

開発者はしばしば、AWS Amplify Hosting にデプロイされた Next.js アプリケーションから、Amazon Virtual Private Cloud (Amazon VPC) 内にデプロイされたリソースにアクセスする必要があります。

Amazon VPC を使用すると、お客様は隔離された仮想ネットワークでリソースを起動できます。しかし、開発者は、複雑なネットワークアクセス制御とセキュリティグループのために、Amazon VPC 内で API とデータベースを呼び出すためにフロントエンドアプリケーションを接続することが困難であると感じるかもしれません。

この投稿では、AWS Amplify Hosting 上で動作する Next.js サーバーサイドレンダリング (SSR) アプリケーションから、Amazon Relational Database Service (Amazon RDS)AWS Lambda などのリソースや VPC 内のリソースにアクセスするためのソリューションを実装します。

ソリューションの概要

まず、AWS Cloud Development Kit (AWS CDK) を使って、Amazon VPC 内に Lambda 関数をビルドしてデプロイします。 次に、Pages Router を使用した Next.js API Routes を通じて Amazon VPC 内のデータにアクセスする Next.js アプリを作成し、AWS Amplify Hosting 上でホストされた React UI にアクセスします。 AWS Systems Manager Parameter Store を利用した API キーやその他の設定データのデモを行います。

その結果、エンドユーザーが Amazon VPC 内からデータを表示するためにアクセスできる Next.js アプリが作成されます。

前提条件

このチュートリアルでは以下のものが必要です。

VPC スタックでの Lambda 関数の作成

Next.js アプリケーションがアクセスできる保護された Amazon VPC リソースを説明するために、Lambda 関数が動作する Amazon VPC を作成します。

まず、AWS CDK をインストールします。前提条件の詳細は AWS CDK の開始方法を参照してください。

$ npm install -g aws-cdk

次に、アプリ用の新しいディレクトリを作成します。

$ mkdir lambda-in-a-vpc
$cd lambda-in-a-vpc

次に、以下のコマンドを実行して AWS CDK アプリケーションを作成します。

$ cdk init app —language=typescript

生成したら、lib/lambda-in-a-vpc-stack.ts の内容を以下のコードに置き換えます。

AWS CDK Stack は、パブリック、プライベート、隔離されたサブネットを持つ Amazon VPC、セキュリティグループ、Amazon VPC の隔離されたサブネットに配置される Lambda 関数 (Node.js) を作成します。

Lambda 関数をセキュリティグループ付きのプライベートサブネットに配置することで、Amazon VPC 内で関数を分離します。 これにより、パブリックインターネットから分離された Lambda 関数のセキュアなネットワーク環境が提供されますが、プライベートサブネット内のデータベースのような Amazon VPC 内のリソースにアクセスすることができます。

// lib/lambda-in-a-vpc-stack.ts

import {
  CfnOutput,
  Duration,
  Stack,
  StackProps,
  aws_ec2 as ec2,
  aws_lambda as lambda,
} from "aws-cdk-lib";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import { Construct } from "constructs";
import path = require("path");

export class LambdaInAVpcStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const vpc = new ec2.Vpc(this, "LambdaVpc", {
      subnetConfiguration: [
        {
          name: "Isolated",
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
        },
        {
          name: "Private",
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
        },
        {
          name: "Public",
          subnetType: ec2.SubnetType.PUBLIC,
        },
      ],
    });

    // Create a security group to be used on the lambda functions
    const lambdaSecurityGroup = new ec2.SecurityGroup(
      this,
      "Lambda Security Group",
      {
        vpc,
      }
    );

    const getDataLambda: NodejsFunction = new NodejsFunction(
      this,
      id + "-getDataLambda",
      {
        memorySize: 1024,
        timeout: Duration.seconds(5),
        runtime: lambda.Runtime.NODEJS_18_X,
        handler: "handler",
        entry: path.join(__dirname, "../lambda/getData.ts"),
        vpc: vpc,
        vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
        securityGroups: [lambdaSecurityGroup],
      }
    );

    new CfnOutput(this, "getDataLambdaArn", {
      value: getDataLambda.functionArn,
      exportName: "getDataLambdaArn",
    });
  }
}

Lambda 関数 (Node.js) の内部では、Amazon RDS インスタンスような Amazon VPC 内のリソースや、Amazon S3 バケット、その他の保護されたリソース、または外部のサードパーティ API など任意のリソースからデータを取得することができます。

次に、lambda ディレクトリを作成し、その下に getData.ts を作成します。 説明のためにデータはハードコードされていますが、この Lambda 関数は Amazon RDS やその他のデータソースから地理データを取得し、それを返す前に検証や変換を行うことができます。

// lambda/getData.ts

import { APIGatewayProxyResultV2 } from "aws-lambda";

const geoData = [
  {
    name: "United States",
    states: [
      "Alabama",
      "Alaska",
      "Arizona",
      //...
    ],
  },
  {
    name: "Canada",
    states: [
      "Alberta",
      "British Columbia",
      "Manitoba",
      // ...
    ],
  },
  {
    name: "Mexico",
    states: [
      "Jalisco",
      "Mexico City",
      "Oaxaca",
      // ...
    ],
  },
];

exports.handler = async function (): Promise<APIGatewayProxyResultV2> {
  try {
    return {
      statusCode: 200,
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(geoData, null, 2),
    };
  } catch (error) {
    console.error("Unable to return data:", error);
    return {
      statusCode: 500,
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(error),
    };
  }
};

cdk deploy を実行して AWS CDK スタックをデプロイし、次のセクションで Next.js アプリケーションで使用するために返されるアウトプットをメモします。

$ cdk deploy
[+] Building 92.4s (14/14) FINISHED
                                                                                                                                   8.4s
WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested
  
  asset-output/index.js  831b 
  
⚡ Done in 102ms

✨  Synthesis time: 97.21s

LambdaInAVpcStack: deploying... [1/1]
LambdaInAVpcStack: creating CloudFormation changeset...

 ✅  LambdaInAVpcStack
 
✨  Deployment time: 33.99s

Outputs:
LambdaInAVpcStack.getDataLambdaArn = arn:aws:lambda:us-east-1:074128318641:function:LambdaInAVpcStack-LambdaInAVpcStackgetDataLambda1E-33sG563OFj2H
Stack ARN:
arn:aws:cloudformation:us-east-1:074128318641:stack/LambdaInAVpcStack/1fbc2790-2a57-11ee-9757-0ecf5ea19ac5

✨  Total time: 131.2s

LambdaInAVpcStack.getDataLambdaArn の末尾にある Lambda 関数名 (この場合は LambdaInAVpcStack-LambdaInAVpcStackgetDataLambda1E-33sG563OFj2H) に注意してください。

Next.js Amplify アプリの作成

次に、Pages Router を使って Next.js アプリケーションを作成します。

$ npx create-next-app@latest geo-web-app 
✔ Would you like to use TypeScript? … No / `_Yes_` 
✔ Would you like to use ESLint? … `_No_` / Yes 
✔ Would you like to use Tailwind CSS? … No / `_Yes_` 
✔ Would you like to use `src/` directory? … No / `_Yes_` 
✔ Would you like to use App Router? (recommended) … `_No_` / Yes 
✔ Would you like to customize the default import alias (@/*)? … `_No_` / Yes`

AWS Amplify JavaScript、AWS Amplify UI ライブラリをインストールします。 これらの依存関係はオプションですが、本記事では Next.js アプリケーションの UI を構築するために使用します。

$ npm i aws-amplify @aws-amplify/ui-react

以下のインポートで pages/_app.tsx を更新して、Amplify UI のスタイルを設定します。

// Import Amplify UI styles
import "@aws-amplify/ui-react/styles.css";
import type { AppProps } from "next/app";

export default function App({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}

更新を Git にコミットし、Git プロバイダにプッシュします。

Next.js アプリの Amplify へのデプロイ

アプリを Git プロバイダにプッシュしたら、Amplify Hosting にデプロイする準備ができました。

まず Amplify コンソールにアクセスしてください。Amplify アプリを作成したことがない場合は、ページを一番下までスクロールし、Amplify Hosting > Host your web app > Get started を選択します。アプリを作成したことがある場合は、New app > Host web app を選択します。

Git リポジトリのホスティングプロバイダを選択し、Continue を選択します。

Git プロバイダーによっては、Amplify Hosting にリポジトリへのアクセスを許可するようプロンプトが表示されます。認証に成功したら、Recently updated repositories リストからこのアプリのリポジトリを選択し、Next を選択します。

Build settings ページで、Amplify は自動的に正しいビルド設定を検出するので、設定を変更する必要はありません。デフォルトのまま Next を選択します。

Review ページで、Save and deploy を選択します。

アプリが作成され、Amplify Hosting コンソールのアプリのページに移動します。Amplify Hosting は、プロジェクト用に分離されたビルドとホスティング環境をプロビジョニングし、デプロイします。このプロセスには 2 ~ 3 分かかります。以下のように、ProvisionBuild、または Deploy リンクを選択することで、進行状況を確認できます。

パラメータストアでシークレットを手動で作成

Next.js の API Routes では、AWS SDK が Amazon VPC 内の Lambda 関数を呼び出すために、シークレットにアクセスする必要があります。

シークレットは、設定データ管理とシークレット管理のためのセキュアで階層的なストレージを提供する Parameter Store に格納します。

Amplify Hosting コンソールで、App Settings: General に移動し、App ARN を取得します。 最後のスラッシュ (/) の後の値が App ID で、Parameter Store にキーを保存するときに使用されます。

VPC スタックの CDK アウトプットから取得した VPC_AWS_REGIONVPC_LAMBDA_FUNCTION_NAME のためのシークレットを作成する必要があります。

Next.js の API Routes と Amazon VPC の AWS Lambda の間の統合ポイントは、VPC Lambda 関数を呼び出すアクセス権を持つ IAM ユーザーまたはロールによって実現されます。 このユーザーまたはロールには、AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY、および VPC Lambda 関数を呼び出す権限が必要です。 IAM ユーザーの作成IAM ユーザーのアクセスキーの作成、および共有責任モデルを使用したアクセススコープのベストプラクティスを参照してください。

これらのシークレットは、AWS コンソールの Parameter Store に移動して手動で設定できます。 Amplify Hostingドキュメントの環境変数のページの指示に従って、パラメータ名は以下の形式に従ってください。 今回のケースでは Amplify バックエンドを持っていないので、ブランチ名は main にします。

/amplify/{your_app_id}/{your_backend_environment_name}/{your_parameter_name}

完了すると、Parameter Store で以下のように表示されます。

パラメータストアでのシークレット作成の自動化

オプションとして、以下の .env.local テンプレートと Bash スクリプト sync-ssm-params.sh を利用して、プロジェクト内の .env.local ファイルから Parameter Store に直接シークレットを保存することもできます。 このスクリプトは、ローカル開発環境に AWS CLIjq が Amplify アプリ ID と一緒にインストールされている必要があります。

.env.localVPC_AWS_REGIONVPC_AWS_REGIONVPC_LAMBDA_FUNCTION_NAME を設定し、AWS_PROFILEAWS CLI で設定した使用したいプロファイルを設定します。 AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY は、AWS CLI を使用してスクリプトが取得します。

# .env.local

AWS_APP_ID=<Copy from Amplify Hosting Console>
AWS_PROFILE=default
VPC_AWS_REGION=
VPC_LAMBDA_FUNCTION_NAME=
#!/bin/bash
# sync-ssm-params.sh
# Allow list of parameters

allowlist=(
  AWS_ACCESS_KEY_ID
  AWS_SECRET_ACCESS_KEY
  VPC_AWS_REGION
  VPC_LAMBDA_FUNCTION_NAME
)

# Get the name of the current branch
APP_BRANCH=$(git rev-parse --abbrev-ref HEAD)

# Load .env into environment
export $(cat .env.local | grep -v '^#' | xargs) 

# Get AWS access keys from AWS CLI profile
AWS_ACCESS_KEY_ID=$(aws configure get aws_access_key_id --profile $AWS_PROFILE)
AWS_SECRET_ACCESS_KEY=$(aws configure get aws_secret_access_key --profile $AWS_PROFILE)
for key in "${allowlist[@]}"; do
  aws ssm put-parameter --name "/amplify/$AWS_APP_ID/$APP_BRANCH/$key" --value "${!key}" --type SecureString --overwrite
done

カスタムポリシーで Amplify Hosting のサービスロールの更新

Parameter Store に保存されたシークレットにアクセスする前に、アプリケーションのデプロイ時に Amplify Hosting が作成した Service Role を更新して、このアプリケーションの Parameter Store からの読み取り権限を持たせる必要があります。

Amplify Hosting コンソールに移動し、アプリケーションに移動して、App Settings: General に移動し、Service Role に注目してください。

AWS コンソール の IAM に移動し、Service Role を検索します。

Add permissions > Attach policy を選択して、インラインポリシー AllowAmplifySSMCalls をロールに追加します。 以下のポリシーを Amplify アプリ ID で更新し、JSON エディタに貼り付けます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowAmplifySSMCalls",
            "Effect": "Allow",
            "Action": [
                "ssm:GetParametersByPath",
                "ssm:GetParameters",
                "ssm:GetParameter"
            ],
            "Resource": [
                "arn:aws:ssm:*:*:parameter/amplify/<AMPLIFY_APP_ID>/*"
            ]
        }
    ]
}

ポリシーが保存されると、Permissions policies の下に他のポリシーと一緒に表示されます。

パラメータストアからシークレットをロードするための Amplify ビルドのカスタマイズ

最後に、Amplify CI のビルド設定ファイル amplify.yml を更新して、Parameter Store から Next.js がビルド処理中に参照する環境ファイル (.env) にシークレットをロードする必要があります。

プロジェクトに amplify.yml ファイルを追加して、jq ユーティリティ ($secretsの値を解析するのに必要) をインストールし、ビルド中に Parameter Store からシークレットを .env にロードするための以下のコマンドを追加します。 jq の使用はオプションであり、環境変数をサーバー側のランタイムからアクセスできるようにするためのガイダンスに従って grep や他のユーティリティを使用しても構いません。

echo $secrets | jq -r 'to_entries|map("\(.key)=\(.value)")|.[]' >> .env

以下は、完全な amplify.yml ファイルです。

version: 1

frontend:
  phases:
    preBuild:
      commands:
        - yum -y install jq
        - jq --version
        - npm ci
    build:
      commands:
        - echo $secrets | jq -r 'to_entries|map("\(.key)=\(.value)")|.[]' >> .env
        - npm run build
  artifacts:
    baseDirectory: .next
    files:
      - "**/*"
  cache:
    paths:
      - node_modules/**/*

ファイルを Git にコミットし、Git プロバイダーにプッシュしてデプロイを開始します。

Amazon VPC Lambda 関数のデータで Next.js アプリを更新

シークレットを Parameter Store に格納したら、Next.js アプリから Amazon VPC 内のデータにアクセスできるようになります。

AWS SDK に依存するので、次のコマンドでインストールします。

$ npm install aws-sdk

page/api の下に Next.js API Routes getGeoData.ts を作成し、AWS SDK を初期化して Amazon VPC Lambda 関数を呼び出す次のコードを記述します。

// pages/api/getGeoData.ts

import { Lambda } from "aws-sdk";
import { NextApiRequest, NextApiResponse } from "next";

const lambda = new Lambda({
  region: process.env.VPC_AWS_REGION,
  accessKeyId: process.env.AWS_ACCESS_KEY_ID,
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
});

export default async (req: NextApiRequest, res: NextApiResponse) => {
  lambda.invoke(
    {
      FunctionName: process.env.VPC_LAMBDA_FUNCTION_NAME!,
      Payload: JSON.stringify({}),
    },
    (err, data) => {
      if (err) {
        console.log(err);
        res.status(500).json({ error: err });
      } else {
        res.status(200).json({ data });
      }
    }
  );
};

次に、データにアクセスするフロントエンドのコードを記述します。 pages/index.ts を、pages/api/getGeoData への API コールを行い、AWS Amplify UI を使用して結果を表に表示する以下のコードに置き換えます。

// pages/index.tsx

import {
  Heading,
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableRow,
  View,
} from "@aws-amplify/ui-react";
import { useEffect, useState } from "react";

type Country = {
  name: string;
  states: string[];
};

export default function Home() {
  const [geoData, setGeoData] = useState<Country[]>([]);

  useEffect(() => {
    fetch("/api/getGeoData")
      .then((res) => res.json())
      .then((data) => {
        const payload = JSON.parse(data.data.Payload);
        const body = JSON.parse(payload.body);
        setGeoData(body);
      });
  }, []);
  
  return (
    <View padding="1rem">
      <Heading level={2} marginBottom={25}>
        Countries and States
      </Heading>
      <br />

      {geoData.length === 0 && <div>Loading...</div>}
      {geoData.length > 0 && (
        <Table width={500}>
          <TableHead>
            <TableRow>
              <TableCell as="th">Country</TableCell>
              <TableCell as="th">States</TableCell>
            </TableRow>
          </TableHead>

          <TableBody>
            {geoData.map((country) => (
              <TableRow key={country.name}>
                <TableCell>{country.name}</TableCell>
                <TableCell>
                  {country.states.map((state) => (
                    <div key={state}>{state}</div>
                  ))}
                </TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      )}
    </View>
  );
}

ファイルを Git にコミットし、Git プロバイダーにプッシュして最終的なデプロイを開始します。

デプロイ後、プロジェクトの URL に移動すると、Amazon VPC の Lambda 関数からデータがロードされます。

クリーンアップ

AWS CDK スタックのリソースを削除するには、lambda-in-a-vpc CDK プロジェクトのルートから cdk destroy を実行します。

Next.js Amplify アプリを削除するには、Amplify Hosting でアプリに移動し、Actions > Delete app を選択します。

まとめ

本記事では、AWS CDK を使用して Amazon VPC 内で関数を分離するために、セキュリティグループ付きのプライベートサブネットに Lambda 関数を構築してデプロイしました。

次に Next.js アプリを作成し、Next.js の API Routes を通して Amazon VPC 内のデータにアクセスし、AWS Amplify Hosting にホストされデプロイされた React UI にアクセスしました。 パラメータストアを使用して APIキーやその他の設定データを安全に保存し、アクセスするためのベストプラクティスを紹介しました。

カスタムドメインの設定や、プルリクエスト用の Web プレビュー機能ブランチ (feature branch) など、Amplify Hosting の機能についての詳細は、AWS Amplify Hosting ドキュメントをご覧ください。

本記事は「Accessing resources in a Amazon Virtual Private Cloud (Amazon VPC) from Next.js API Routes」を翻訳したものです。

翻訳者について

稲田 大陸

AWS Japan で働く筋トレが趣味のソリューションアーキテクト。普段は製造業のお客様を中心に技術支援を行っています。好きな AWS サービスは Amazon Location Service と AWS Amplify で、日本のお客様向けに Amazon Location Service の解説ブログなどを執筆しています。