はじめに
この記事でやること
GenUのソースコードをベースにAWS CDKでのインフラ構築方法について理解していきたいと思います。
といいつつほぼAWS CDKのチュートリアルをなぞっただけになってしまいましたが、GenUのカスタマイズの第一歩として、ざっくりどんな構成になっているか理解することをゴールにしたいと思います。
GenUとは
AWS公式がOSSで提供している、生成AIのWEBアプリがわずか数コマンドで簡単に構築できるデモアプリです。
アプリインフラは全てAWSのサービスで完結しており、生成AIサービスはAmazon Bedrockを利用します。
AWSアカウントを持っており、Nodejsを使えるPCがあれば10分とかからずにこんなリッチなUIのWEBアプリが作れます。
AWS CDKとは
AWS CDK (Cloud Development Kit)とは、TypescriptやPythonなどのプログラミング言語を使ってCloudFormationのテンプレートが作成できる、AWS公式のIaCツールです。
プログラミング言語なのでif文やfor文が使え、より柔軟にインフラの定義ができます。
IaCツールとしてよく使われるTerraformとの比較はこちらの記事がとてもわかりやすかったです。
とりあえずアプリのデプロイ
とりあえずGenUのアプリをデプロイしてみます
前提として以下の準備が必要です。
- AWSアカウント
- AWS CLIのインストール
- Nodejsのインストール
GenU公式リポジトリの手順通りにコマンドを叩くだけでアプリが立ち上がります。
詳しくデプロイの手順が見たい場合はワークショップも用意されています。
AWS CDKのコードはどこ?
このリポジトリにはAWSインフラ用のCDKのコードと、WEBアプリなのでWEB画面用のReactコードが入っています。
CDKナニモワカラナイの状態なのでまずはCDKのコードがどこにあるか探します。
ということでAWS CDKがどのような構成になっているのか理解するために、公式チュートリアルをなぞってみることにしました。
CDKの実行環境準備
チュートリアル通りに進めます。
ステップ1〜3は実施済みの前提で本記事では飛ばします。
ステップ4: Node.jsとプログラミング言語の前提条件をインストールする
言語は業務で一番使い慣れているPythonにしてみました(GenUはTypescript)。
$ node --version
v22.4.1
$ python --version
Python 3.12.4
ステップ5: AWS CDK CLIをインストールする
AWS CDK CLIをインストールします。
npm install -g aws-cdk
ステップ6: CDK CLIのセキュリティ認証情報を設定する
AWSの認証情報はAWS_DEFAULT_PROFILE環境変数に設定します。
export AWS_DEFAULT_PROFILE=<プロファイル名>
ステップ7: AWS環境をブートストラップする
さて、単純にcdk bootstrap
コマンドを実行すれば対話形式で進むのかと思いきや、実行すると次のエラーが出ました。
Specify an environment name like 'aws://123456789012/us-east-1', or run in a directory with 'cdk.json'.
ふむふむアカウント名とリージョンからなる環境名を指定するか、cdk.json
があるディレクトリで実行しろとのことです。
cdk.json
とやらがCDKの設定ファイルっぽいので、GenUのソースでも探してみます。
generative-ai-use-cases-jp/packages/cdk/cdk.json
にありました。
このcdk
フォルダにおそらくGenUのCDK周りのソースがあるのでしょう。
中身を見ても今のところわからないので、とりあえず公式チュートリアルの環境をブートストラップする方法を見ます。
環境名を指定する方法でブートストラップします。
# リージョンは任意で
cdk bootstrap aws://<アカウント名>/ap-northeast-1
実行結果
✅ Environment aws://<アカウント名>/ap-northeast-1 bootstrapped.
ここでcdk.json
が出来上がるのかと思いきや、なにも生成されませんでした。
とりあえず次の手順に行きます。
CDKアプリを作成する
Hello World!
を返すLambdaを作るサンプルアプリを作っていきます。
ステップ1: CDKプロジェクトを作成する
ディレクトリ作成
mkdir hello-cdk && cd hello-cdk
CDKプロジェクトの初期化
cdk init app --language python
ここで諸々ベースの構成ファイルが出来上がりました。
続いてCDK実行のための仮想環境に入り、依存関係をインストール
source .venv/bin/activate # On Windows, run `.\venv\Scripts\activate` instead
python -m pip install -r requirements.txt
プロジェクトディレクトリ直下にあるapp.py
の中身がこちら
#!/usr/bin/env python3
import os
import aws_cdk as cdk
from hello_cdk.hello_cdk_stack import HelloCdkStack
app = cdk.App()
HelloCdkStack(app, "HelloCdkStack",
# If you don't specify 'env', this stack will be environment-agnostic.
# Account/Region-dependent features and context lookups will not work,
# but a single synthesized template can be deployed anywhere.
# Uncomment the next line to specialize this stack for the AWS Account
# and Region that are implied by the current CLI configuration.
#env=cdk.Environment(account=os.getenv('CDK_DEFAULT_ACCOUNT'), region=os.getenv('CDK_DEFAULT_REGION')),
# Uncomment the next line if you know exactly what Account and Region you
# want to deploy the stack to. */
#env=cdk.Environment(account='123456789012', region='us-east-1'),
# For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html
)
app.synth()
hello_cdk/hello_cdk_stack.py
の中身
from aws_cdk import (
# Duration,
Stack,
# aws_sqs as sqs,
)
from constructs import Construct
class HelloCdkStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
# The code that defines your stack goes here
# example resource
# queue = sqs.Queue(
# self, "HelloCdkQueue",
# visibility_timeout=Duration.seconds(300),
# )
hello_cdk_stack.py
> class HelloCdkStack(Stack)
クラスとしてCloudFormationのスタックを定義するようです。
ステップ4: Lambda関数を定義する
hello_cdk_stack.py
を以下のように変更します。
from aws_cdk import (
Stack,
aws_lambda as _lambda, # Import the Lambda module
)
from constructs import Construct
class HelloCdkStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
# Define the Lambda function resource
my_function = _lambda.Function(
self, "HelloWorldFunction",
runtime = _lambda.Runtime.NODEJS_20_X, # Provide any supported Node.js runtime
handler = "index.handler",
code = _lambda.Code.from_inline(
"""
exports.handler = async function(event) {
return {
statusCode: 200,
body: JSON.stringify('Hello World!'),
};
};
"""
),
)
ふむふむaws_lambda
のFucntion
クラスでLambda関数を定義するようです。
AWS SAMのtemplate.yaml
でいうこの辺の設定
Function:
Type: AWS::Serverless::Functio
Properties:
FunctionName: ***
Role: ***
CodeUri: ***
Handler: ***
code
はチュートリアルだとインラインで書いているので、後で別ファイルを参照できるように変えたいと思います。ランタイムもpythonに変えたい。とりあえずこのまま進みます。
ステップ5: Lambda関数のURLを定義する
from aws_cdk import CfnOutput, Stack
from aws_cdk import aws_lambda as _lambda
from constructs import Construct
class HelloCdkStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
# Define the Lambda function resource
my_function = _lambda.Function(
self,
"HelloWorldFunction",
runtime=_lambda.Runtime.NODEJS_20_X, # Provide any supported Node.js runtime
handler="index.handler",
code=_lambda.Code.from_inline(
"""
exports.handler = async function(event) {
return {
statusCode: 200,
body: JSON.stringify('Hello World!'),
};
};
"""
),
)
# Define the Lambda function URL resource
my_function_url = my_function.add_function_url(
auth_type=_lambda.FunctionUrlAuthType.NONE,
)
# Define a CloudFormation output for your URL
CfnOutput(self, "myFunctionUrlOutput", value=my_function_url.url)
Lambda呼び出し用のURLの定義のようです。
CfnOutput
の部分がおそらくteplate.yaml
でいうOutputs
ブロック
Outputs:
ApiUrl:
Description: "API Gateway endpoint URL"
Value: !Sub "https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/${StageParameter}/"
ステップ6: CloudFormationテンプレートを合成する
これまでに作成したPythonコードからCloudFormationテンプレートを作成します。
cdk synth
実行するとコンソール上にCloudFormationテンプレートが出力されました(長くなるので中身は省略。チュートリアルの出力例を見てください)。
ファイルに出力して欲しいのでオプションを探す
$ cdk synth --help
-o, --output Emits the synthesized cloud assembly into a directory
(default: cdk.out) [string]
ありました
cdk synth --output <任意のフォルダ名>
jsonで作成予定のスタックの設定ファイルが出力されました。
ステップ7: (オプション) 環境をブートストラップする
デフォルトは実行環境の準備でブートストラップした環境にデプロイされるのですが、コード内でアカウントやリージョンを指定することもできる模様。今回はやりません。
ステップ8: CDKスタックをデプロイする
いよいよデプロイします。
cdk deploy
実行結果
✅ HelloCdkStack
✨ Deployment time: 38.05s
Outputs:
HelloCdkStack.myFunctionUrlOutput = ***
Stack ARN:***
✨ Total time: 45.5s
Lambda関数1つだけなので1分も経たずに出来上がりました。
出力されたURLにアクセスします。
おおーちゃんとデプロイされていますね。
アプリの変更
チュートリアルで作成したLambda関数はCDKのコード内にインラインで書かれていました。
実運用ではそんなことありえないので、別ファイルを参照するように変更します。
ランタイムもPythonにします。
ランタイムの指定はこちらから調べました。
まずはhello_cdk_stack.py
と同じディレクトリにhello
フォルダを作成、その中にlambda-handler.py
を作成します。
import json
def handler(event, context):
return {"statusCode": 200, "body": json.dumps("Hello!!!!!")}
元気なハローを返すだけの関数です。
変更の差分を確認します。
$ cdk diff
Stack HelloCdkStack
Hold on while we create a read-only change set to get a diff with accurate replacement information (use --no-change-set to use a less accurate but faster template-only diff)
Resources
[~] AWS::Lambda::Function HelloWorldFunction HelloWorldFunctionB2AB6E79
├─ [~] Code
│ ├─ [+] Added: .S3Bucket
│ ├─ [+] Added: .S3Key
│ └─ [-] Removed: .ZipFile
├─ [~] Handler
│ ├─ [-] index.handler
│ └─ [+] lambda-handler.handler
└─ [~] Runtime
├─ [-] nodejs20.x
└─ [+] python3.12
変更をデプロイします。
cdk deploy
デプロイが終わったら出力されるURLにアクセス
元気なハローが返ってきました。
cdk.json
はなに?
CDKのプロジェクトの設定をしているファイルですね。package.json
的なやつでしょう。
プロジェクトのエントリーファイルはappブロックで定義するみたいです。
{
"app": "python3 app.py",
}
GenUのコードを見てみる
さてさて、AWS CDKのチュートリアルを実践したことでCDKのプロジェクト構成が理解できました。
本題のGenUのソースコードを追ってみます。
cdk.json
が./packages/cdk
下にあるので、ここがCDKプロジェクトのルートディレクトリですね。
まずGenUはTypescriptでCDKのコードが定義されているので、cdk.json
のappブロックは以下のような記述です。
{
"app": "npx ts-node --prefer-ts-exts bin/generative-ai-use-cases.ts"
}
またチュートリアルとは違いcontext
ブロックに次のような定義があり、CDKコード内で使われているのではと推測。
"ragKnowledgeBaseEnabled": false
探してみる
// 省略
const ragKnowledgeBaseEnabled =
app.node.tryGetContext('ragKnowledgeBaseEnabled') || false;
const ragKnowledgeBaseStack = ragKnowledgeBaseEnabled
? new RagKnowledgeBaseStack(app, 'RagKnowledgeBaseStack', {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: modelRegion,
},
crossRegionReferences: true,
})
: null;
// 省略
ありました。tryGetContext
でコンテキストを参照し、値によってスタックを作成するか分岐しています。柔軟ですね。
Lambda以外のリソースについてもaws-cdk
に定義されているクラスを使用して定義していけば良さそうです。
コードの詳細までは追えてませんが、ざっとどんな感じでGenUが構成されているか理解できました。
おわりに
普段TerraformやAWS SAMを使用していますが、if
やfor
で作成するリソースを柔軟に定義できるのは良いですね。
Terraformでも同じようなことはできますが、他のリソースから参照する場合などにコードが煩雑になってしまうのがモヤポイントでした。
また、LambdaのデプロイにローカルマシンでDockerを使用しないで良い点も嬉しいです。
社内LANがProxyを使用している関係で、環境構築に手間取るケースが多く。。。
システムの規模によって使い分けていこうと思います。