先週1週間、リフレッシュ休暇(当社の年次休暇)を頂き、宮古島旅行に行ってきました。
天気にも恵まれ、海もとてもキレイだったので、テンションが上がりっぱなしだった菅野です。
最近、AWSのリソースを作成する際に、CDKを利用することが増えてきました。
AWSのリソース定義としては、CloudFormationがありますが、CDKは、以下のようなメリットがあります。
今回は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を用いることでデプロイすることができます。
作成する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コード
- /api配下にリソースを追加するようにメソッド定義を変更
- s3のディストリビューション設定はCloudFrontディストリビューション作成後に実施するように移動
- CloudFrontのディストリビューション定義を修正に変更し、オリジン定義等を手動で行うように変更
- CloudFrontのAPI側のキャッシュ設定を変更してキャッシュしないように修正
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では、キャリア採用を行っています。少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。 www.wantedly.com
- ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
- Elasticsearch等を使ったデータ収集/分析/可視化
- マイクロサービス、DevOps、最新のOSSやクラウドサービスを利用する開発プロジェクト
- 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長