Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

ChatGPT/Bing Chat Enterprise で AWS CDK v2 のコードを作成する

先週1週間、リフレッシュ休暇(当社の年次休暇)を頂き、宮古島旅行に行ってきました。
天気にも恵まれ、海もとてもキレイだったので、テンションが上がりっぱなしだった菅野です。

最近、AWSのリソースを作成する際に、CDKを利用することが増えてきました。
AWSのリソース定義としては、CloudFormationがありますが、CDKは、以下のようなメリットがあります。

  • TypeScriptやPython等、慣れたプログラミング言語AWSのリソースを定義できる。
  • CloudFormationと比べると、大幅に記述量が減り、分かりやすくなる。

今回はAWS CDKに不慣れな私が生成AIを利用してどこまで簡単に作成できるか試してみます。
最初に、ChatGPTを利用して、CDKのコード出力を行おうとしたのですが、CDKの最新バージョン(v2)は2021年12月にリリースされており、2021年9月までの情報を学習しているChatGPTのGPT-4ではCDKv2のコードを生成できません。

そこで今回は、Bing Chat Enterprise を利用して、CDKv2のコード生成を行ってみました。

AWS CDKとは

AWS Cloud Development Kit の略です。
Pyton,TypeScript等様々な言語でAWSのリソースをソースコードとして管理できるフレームワークです。 ソースコードで作成したリソースはAWS CDK Cliを用いることでデプロイすることができます。

docs.aws.amazon.com

作成するAWSリソースの説明

本記事ではPetShopのペットを管理する簡単なWebアプリケーションを例として以下のようなリソースを作成してもらいます。

  • S3の静的ホスティングでWebクライアントを作成。
  • APIGateway、Lambda、DynamoDBを用いたAPIを作成。
  • 上記Webクライアント、API双方の前段にCloudFrontを配置する。

Bing Chat EnterpriseでCDKのリソースを作成

では早速Bing Chat EnterpriseにCDKコードを作成してもらいましょう。
(中略)

それぞれ以下のコードを出力してくれました。
CDKコード

from aws_cdk import core
from aws_cdk import (
    aws_s3 as s3,
    aws_s3_deployment as s3_deploy,
    aws_lambda as _lambda,
    aws_apigateway as api_gw,
    aws_dynamodb as ddb,
    aws_cloudfront as cf
)

class PetShopCdkStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        # S3 bucket for web hosting
        website_bucket = s3.Bucket(self, "WebsiteBucket",
            website_index_document="index.html",
            removal_policy=core.RemovalPolicy.DESTROY
        )

        # DynamoDB table for pets
        pet_table = ddb.Table(self, "PetTable",
            partition_key=ddb.Attribute(name="id", type=ddb.AttributeType.STRING),
            removal_policy=core.RemovalPolicy.DESTROY
        )

        # Lambda function for CRUD operations
        pet_lambda = _lambda.Function(self, "PetLambda",
            runtime=_lambda.Runtime.PYTHON_3_9,
            handler="handler.handler",
            code=_lambda.Code.asset("lambda"),
            environment={
                "TABLE_NAME": pet_table.table_name
            }
        )

        # Grant the Lambda function permissions to read/write the DynamoDB table
        pet_table.grant_read_write_data(pet_lambda)

        # API Gateway for the Lambda function
        pet_api = api_gw.LambdaRestApi(self, "PetApi",
            handler=pet_lambda,
            default_cors_preflight_options=api_gw.CorsOptions(
                allow_origins=["*"]
            )
        )

        # CloudFront Distribution
        distribution = cf.CloudFrontWebDistribution(self, "Distribution",
            origin_configs=[
                cf.SourceConfiguration(
                    s3_origin_source=cf.S3OriginConfig(
                        s3_bucket_source=website_bucket
                    ),
                    behaviors=[cf.Behavior(is_default_behavior=True)]
                ),
                cf.SourceConfiguration(
                    custom_origin_source=cf.CustomOriginConfig(
                        domain_name=pet_api.domain_name,
                        origin_protocol_policy=cf.OriginProtocolPolicy.HTTPS_ONLY
                    ),
                    behaviors=[cf.Behavior(path_pattern="/api/*")]
                )
            ]
        )

静的ホスティングクライアント

<!DOCTYPE html>
<html>
<head>
    <title>Pet Shop</title>
</head>
<body>
    <h1>Welcome to the Pet Shop!</h1>
    <button onclick="fetchData()">Fetch Data</button>
    <div id="data"></div>

    <script>
    function fetchData() {
        fetch('/api')
            .then(response => response.json())
            .then(data => document.getElementById('data').textContent = JSON.stringify(data));
    }
    </script>
</body>
</html>

ちょっとWebUIがシンプルすぎるのと、S3、APIGatewayはCloudFront側からのみのリクエストを受け付けるようになっていないため、変更を加えてもらいます。
また、Lambda関数も出力してもらえなかったので追加で出力するよう依頼していきます。


(中略)

(中略)

(中略)
(中略)

実装が中途半端なままファイル出力したり、変更点のみの部分的な出力であったので、何度か応答を繰り返して更新していってもらいました。

最終的な結果は以下のようになりました。

CDKコード

from constructs import Construct
from aws_cdk.core import Stack, RemovalPolicy, CfnOutput
from aws_cdk.aws_s3 import Bucket, BlockPublicAccess
from aws_cdk.aws_s3_deployment import BucketDeployment, Source
from aws_cdk.aws_lambda import Function, Runtime, Code
from aws_cdk.aws_apigateway import LambdaRestApi, CorsOptions, MethodOptions, RestApi
from aws_cdk.aws_dynamodb import Table, Attribute, AttributeType
from aws_cdk.aws_cloudfront import Distribution, SourceConfiguration, S3OriginConfig, Behavior, CustomOriginConfig, OriginAccessIdentity

class PetshopCdkStack(Stack):

    def __init__(self, scope: Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        # DynamoDB Table
        pet_table = Table(self, "PetTable",
            partition_key=Attribute(name="id", type=AttributeType.STRING),
            removal_policy=RemovalPolicy.DESTROY
        )

        # Lambda function
        pet_lambda = Function(self, "PetHandler",
            runtime=Runtime.PYTHON_3_8,
            handler="handler.handler",
            code=Code.from_asset("lambda"),
            environment={
                'PET_TABLE_NAME': pet_table.table_name
            }
        )

        pet_table.grant_read_write_data(pet_lambda)

        # API Gateway
        pet_api = RestApi(self, "Endpoint",
            default_cors_preflight_options=CorsOptions(
                allow_origins=["*"],
                allow_methods=["GET", "POST", "PUT", "DELETE"]
            ),
            handler=pet_lambda,
            proxy=False
        )

        pet_resource = pet_api.root.add_resource('api')

        # S3 bucket for web hosting
        website_bucket = Bucket(self, "WebsiteBucket",
            website_index_document="index.html",
            auto_delete_objects=True,
            removal_policy=RemovalPolicy.DESTROY,
            block_public_access=BlockPublicAccess.BLOCK_ALL
        )

        BucketDeployment(self, "DeployWebsite",
            sources=[Source.asset('./website')],
            destination_bucket=website_bucket,
            distribution_paths=["/*"]
        )

        # CloudFront Distribution
        oai = OriginAccessIdentity(self, "OAI")
        
        website_bucket.grant_read(oai)

        distribution = Distribution(self, "Distribution",
            default_behavior_options={
                "origin": S3Origin(website_bucket),
                "origin_access_identity": oai,
            },
            additional_behaviors={
                "/api/*": {
                    "origin": HttpOrigin(f"{pet_api.rest_api_id}.execute-api.{self.region}.amazonaws.com"),
                    "origin_protocol_policy": OriginProtocolPolicy.HTTPS_ONLY,
                }
            }
        )

        CfnOutput(self, "CloudFrontURL",
            value=distribution.domain_name,
            description="The CloudFront distribution URL"
        )


OAIを用いてアクセス制限をかけるようにしてくれています。

静的ホスティングWebクライアント

<!DOCTYPE html>
<html>

<head>
    <title>Pet Shop</title>
    <script>
        const API_ENDPOINT = "/api";

        async function createPet() {
            const name = document.getElementById('name').value;
            const type = document.getElementById('type').value;
            const data = { id: Date.now().toString(), name, type };
            await fetch(API_ENDPOINT, {
                method: 'POST',
                body: JSON.stringify(data),
                headers: { 'Content-Type': 'application/json' }
            });
            loadPets();
        }

        async function updatePet() {
            const id = document.getElementById('updateId').value;
            const name = document.getElementById('updateName').value;
            const type = document.getElementById('updateType').value;
            const data = { id, name, type };
            await fetch(API_ENDPOINT, {
                method: 'PUT',
                body: JSON.stringify(data),
                headers: { 'Content-Type': 'application/json' }
            });
            loadPets();
        }

        async function deletePet() {
            const id = document.getElementById('deleteId').value;
            await fetch(`${API_ENDPOINT}?id=${id}`, {
                method: 'DELETE'
            });
            loadPets();
        }

        async function loadPets() {
            const petsList = document.getElementById('petsList');
            petsList.innerHTML = '';
            const pets = await getAllPets();
            pets.forEach(pet => {
                const petItem = document.createElement('li');
                petItem.textContent = `${pet.name} (${pet.type}) - ID: ${pet.id}`;
                petsList.appendChild(petItem);
            });
        }

        async function getAllPets() {
            const response = await fetch(API_ENDPOINT);
            return response.json();
        }

        window.onload = () => {
            loadPets();
        };
    </script>
</head>

<body>
    <h1>Welcome to Pet Shop</h1>

    <h2>Add a new pet</h2>
    <div>
        <label>Name: <input type="text" id="name"></label>
        <label>Type: <input type="text" id="type"></label>
        <button onclick="createPet()">Add Pet</button>
    </div>

    <h2>Update a pet</h2>
    <div>
        <label>ID: <input type="text" id="updateId"></label>
        <label>Name: <input type="text" id="updateName"></label>
        <label>Type: <input type="text" id="updateType"></label>
        <button onclick="updatePet()">Update Pet</button>
    </div>

    <h2>Delete a pet</h2>
    <div>
        <label>ID: <input type="text" id="deleteId"></label>
        <button onclick="deletePet()">Delete Pet</button>
    </div>

    <h2>List of pets</h2>
    <ul id="petsList"></ul>
</body>

</html>

CRUD処理も各フォームから出力するようになっています。

Lambdaハンドラ

import json
import boto3
import os

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['TABLE_NAME'])

def handler(event, context):
    try:
        if event['httpMethod'] == 'GET':
            # Retrieve pet by ID or list all
            pet_id = event['queryStringParameters'].get('id')
            if pet_id:
                response = table.get_item(Key={'id': pet_id})
                return {
                    'statusCode': 200,
                    'body': json.dumps(response.get('Item'))
                }
            else:
                response = table.scan()
                return {
                    'statusCode': 200,
                    'body': json.dumps(response['Items'])
                }

        elif event['httpMethod'] == 'POST':
            # Create a new pet
            body = json.loads(event['body'])
            table.put_item(Item=body)
            return {
                'statusCode': 200,
                'body': json.dumps({'message': 'Pet created successfully!'})
            }

        elif event['httpMethod'] == 'PUT':
            # Update a pet
            body = json.loads(event['body'])
            table.update_item(Key={'id': body['id']}, UpdateExpression="set info=:r", ExpressionAttributeValues={':r': body['info']})
            return {
                'statusCode': 200,
                'body': json.dumps({'message': 'Pet updated successfully!'})
            }

        elif event['httpMethod'] == 'DELETE':
            # Delete a pet
            pet_id = event['queryStringParameters'].get('id')
            if pet_id:
                table.delete_item(Key={'id': pet_id})
                return {
                    'statusCode': 200,
                    'body': json.dumps({'message': 'Pet deleted successfully!'})
                }
            else:
                return {
                    'statusCode': 400,
                    'body': json.dumps({'message': 'Pet ID is required!'})
                }

        else:
            return {
                'statusCode': 400,
                'body': json.dumps({'message': 'Invalid request method!'})
            }
    except Exception as e:
        return {
            'statusCode': 500,
            'body': json.dumps({'message': str(e)})
        }

手動での修正

Bing Chat Enterprise で生成したのみで作成したリソースファイルでは一部エラーが出てデプロイできない部分が出たり、疎通できない部分があったので手動で修正をかけてあげましょう。

CDKコード

from constructs import Construct
from aws_cdk import Stack, RemovalPolicy, CfnOutput, Duration
from aws_cdk.aws_s3 import Bucket, BlockPublicAccess
from aws_cdk.aws_s3_deployment import BucketDeployment, Source
from aws_cdk.aws_lambda import Function, Runtime, Code
from aws_cdk.aws_apigateway import LambdaRestApi
from aws_cdk.aws_dynamodb import Table, Attribute, AttributeType
from aws_cdk.aws_cloudfront import CloudFrontWebDistribution, SourceConfiguration, S3OriginConfig, Behavior, CustomOriginConfig, OriginAccessIdentity, CloudFrontAllowedMethods, OriginProtocolPolicy


class PetshopCdkStack(Stack):

    def __init__(self, scope: Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        # DynamoDB Table
        pet_table = Table(self, "PetTable",
            partition_key=Attribute(name="id", type=AttributeType.STRING),
            removal_policy=RemovalPolicy.DESTROY
        )

        # Lambda function
        pet_lambda = Function(self, "PetHandler",
            runtime=Runtime.PYTHON_3_8,
            handler="handler.handler",
            code=Code.from_asset("lambda"),
            environment={
                'PET_TABLE_NAME': pet_table.table_name
            }
        )

        pet_table.grant_read_write_data(pet_lambda)

        # API Gateway
        pet_api = LambdaRestApi(self, "Endpoint",
            handler=pet_lambda,
            proxy=False
        )

        api_resource = pet_api.root.add_resource("api")
        pet_resource = api_resource.add_resource("pet")
        pet_id_resource = pet_resource.add_resource("{id}")

        pet_resource.add_method('GET')    # Read
        pet_resource.add_method('POST')   # Create
        pet_resource.add_method('PUT')    # Update
        pet_resource.add_method('DELETE') # Delete

        pet_id_resource.add_method('GET')

        # S3 bucket for web hosting
        website_bucket = Bucket(self, "WebsiteBucket",
            website_index_document="index.html",
            auto_delete_objects=True,
            removal_policy=RemovalPolicy.DESTROY,
            block_public_access=BlockPublicAccess.BLOCK_ALL
        )


        # CloudFront Distribution
        oai = OriginAccessIdentity(self, "OAI")
        
        website_bucket.grant_read(oai)

        distribution = CloudFrontWebDistribution(self, "Distribution",
            origin_configs=[
                SourceConfiguration(
                    s3_origin_source=S3OriginConfig(
                        s3_bucket_source=website_bucket,
                        origin_access_identity=oai
                    ),
                    behaviors=[Behavior(is_default_behavior=True)]
                ),
                SourceConfiguration(
                    custom_origin_source=CustomOriginConfig(
                        origin_path='/prod',
                        domain_name=f"{pet_api.rest_api_id}.execute-api.{self.region}.amazonaws.com",
                        origin_protocol_policy=OriginProtocolPolicy.HTTPS_ONLY
                    ),
                    behaviors=[Behavior(path_pattern="api/*", allowed_methods=CloudFrontAllowedMethods.ALL,default_ttl=Duration.seconds(0),max_ttl=Duration.seconds(0),min_ttl=Duration.seconds(0))]
                )
            ]
        )

        BucketDeployment(self, "DeployWebsite",
            sources=[Source.asset('./website')],
            destination_bucket=website_bucket,
            distribution_paths=["/*"],
            distribution=distribution
        )

        CfnOutput(self, "CloudFrontURL",
            value=distribution.distribution_domain_name,
            description="The CloudFront distribution URL"
        )

Lambdaハンドラ

  • GetのメソッドについてQueryStrinParameterではなくPathParameterを利用するように修正
  • 判定条件を修正してPathParameterが定義されていない際に参照エラーが出ないように変更
import json
import boto3
import os

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['PET_TABLE_NAME'])

def handler(event, context):
    try:
        if event['httpMethod'] == 'GET':
            # Retrieve pet by ID or list all
            if event['pathParameters'] and event['pathParameters'].get('id'):
                response = table.get_item(Key={'id': event['pathParameters'].get('id')})
                return {
                    'statusCode': 200,
                    'body': json.dumps(response.get('Item'))
                }
            else:
                response = table.scan()
                return {
                    'statusCode': 200,
                    'body': json.dumps(response['Items'])
                }

        elif event['httpMethod'] == 'POST':
            # Create a new pet
            body = json.loads(event['body'])
            table.put_item(Item=body)
            return {
                'statusCode': 200,
                'body': json.dumps({'message': 'Pet created successfully!'})
            }

        elif event['httpMethod'] == 'PUT':
            # Update a pet
            body = json.loads(event['body'])
            table.update_item(Key={'id': body['id']}, UpdateExpression="set info=:r", ExpressionAttributeValues={':r': body['info']})
            return {
                'statusCode': 200,
                'body': json.dumps({'message': 'Pet updated successfully!'})
            }

        elif event['httpMethod'] == 'DELETE':
            # Delete a pet
            pet_id = event['queryStringParameters'].get('id')
            if pet_id:
                table.delete_item(Key={'id': pet_id})
                return {
                    'statusCode': 200,
                    'body': json.dumps({'message': 'Pet deleted successfully!'})
                }
            else:
                return {
                    'statusCode': 400,
                    'body': json.dumps({'message': 'Pet ID is required!'})
                }

        else:
            return {
                'statusCode': 400,
                'body': json.dumps({'message': 'Invalid request method!'})
            }
    except Exception as e:
        return {
            'statusCode': 500,
            'body': json.dumps({'message': str(e)})
        }

デプロイ、Webページへのアクセス

以下のコマンドでデプロイしましょう。

cdk bootstrap
cdk deploy

以下のようにコンソール出力されたらデプロイ成功です。

PetshopCdkStack.CloudFrontURLに表示されているURLへアクセスしてみましょう。


必要情報を入力して、Add Petボタンを押すと 無事追加したペットが表示されました。

まとめ

Bing Chat Enterpriseを活用してAWS CDKのコードを作成、デプロイしてAWSのリソースを作成しました。
他のPythonコードとは異なり、1回のプロンプト入力で動作するコードを出力することは難しく、手動での修正や細かい調整が必要になりそうですが、ある程度の形は作成できるのでCDKに不慣れなエンジニアでも作業時間は大幅に短縮できそうです。
今後もChatGPTの可能性に迫っていこうと思います。

Acroquest Technologyでは、キャリア採用を行っています。
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSクラウドサービスを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長
  少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。 www.wantedly.com