13
4

はじめに

この記事でやること

GenUのソースコードをベースにAWS CDKでのインフラ構築方法について理解していきたいと思います。
といいつつほぼAWS CDKのチュートリアルをなぞっただけになってしまいましたが、GenUのカスタマイズの第一歩として、ざっくりどんな構成になっているか理解することをゴールにしたいと思います。

GenUとは

AWS公式がOSSで提供している、生成AIのWEBアプリがわずか数コマンドで簡単に構築できるデモアプリです。

アプリインフラは全てAWSのサービスで完結しており、生成AIサービスはAmazon Bedrockを利用します。
AWSアカウントを持っており、Nodejsを使えるPCがあれば10分とかからずにこんなリッチなUIのWEBアプリが作れます。

image.png

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のソースでも探してみます。

image.png

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

ここで諸々ベースの構成ファイルが出来上がりました。

image.png

続いてCDK実行のための仮想環境に入り、依存関係をインストール

source .venv/bin/activate # On Windows, run `.\venv\Scripts\activate` instead
python -m pip install -r requirements.txt

プロジェクトディレクトリ直下にあるapp.pyの中身がこちら

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の中身

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を以下のように変更します。

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_lambdaFucntionクラスでLambda関数を定義するようです。
AWS SAMのtemplate.yamlでいうこの辺の設定

template.yaml
Function:
    Type: AWS::Serverless::Functio
    Properties:
      FunctionName: ***
      Role: ***
      CodeUri: ***
      Handler: ***

codeはチュートリアルだとインラインで書いているので、後で別ファイルを参照できるように変えたいと思います。ランタイムもpythonに変えたい。とりあえずこのまま進みます。

ステップ5: Lambda関数のURLを定義する

hello_cdk_stack.py
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ブロック

teplate.yaml
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にアクセスします。

image.png

おおーちゃんとデプロイされていますね。

アプリの変更

チュートリアルで作成したLambda関数はCDKのコード内にインラインで書かれていました。
実運用ではそんなことありえないので、別ファイルを参照するように変更します。
ランタイムもPythonにします。

ランタイムの指定はこちらから調べました。

まずはhello_cdk_stack.pyと同じディレクトリにhelloフォルダを作成、その中にlambda-handler.pyを作成します。

app.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にアクセス

image.png

元気なハローが返ってきました。

cdk.jsonはなに?

CDKのプロジェクトの設定をしているファイルですね。package.json的なやつでしょう。
プロジェクトのエントリーファイルはappブロックで定義するみたいです。

cdk.json
{
  "app": "python3 app.py",
}

GenUのコードを見てみる

さてさて、AWS CDKのチュートリアルを実践したことでCDKのプロジェクト構成が理解できました。
本題のGenUのソースコードを追ってみます。

cdk.json./packages/cdk下にあるので、ここがCDKプロジェクトのルートディレクトリですね。
まずGenUはTypescriptでCDKのコードが定義されているので、cdk.jsonのappブロックは以下のような記述です。

cdk.json
{
    "app": "npx ts-node --prefer-ts-exts bin/generative-ai-use-cases.ts"
}

またチュートリアルとは違いcontextブロックに次のような定義があり、CDKコード内で使われているのではと推測。

"ragKnowledgeBaseEnabled": false

探してみる

bin/generative-ai-use-cases.ts
// 省略
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を使用していますが、ifforで作成するリソースを柔軟に定義できるのは良いですね。
Terraformでも同じようなことはできますが、他のリソースから参照する場合などにコードが煩雑になってしまうのがモヤポイントでした。
また、LambdaのデプロイにローカルマシンでDockerを使用しないで良い点も嬉しいです。
社内LANがProxyを使用している関係で、環境構築に手間取るケースが多く。。。

システムの規模によって使い分けていこうと思います。

13
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
4