こんにちは、中山( @k1nakayama ) です。
先日行われていたAWS re:Invent 2024のキーノートでGitLab Duo with Amazon Qが発表されました。そこで、早速試してみようと思ったところ、現時点ではGDK(GitLab Development Kit)を使用したコントリビューター向け環境でしか使えない事がわかった上、GDKを使用してもAmazon Qとの通信が確立できず、AWSチームとGitLabチームの一部のメンバーのみが使える状態のクローズドベータな状態であることを知りました。
とはいえ、この発表の中で特に注目をしたGitLabイシューから自動的にMR(GitHubでいうところのPR)を作成してくれる機能開発機能が目玉だと思いましたので、現時点でAmazon Qエージェントを使用して、どこまでこれに近いことができるのかを検証してみました。
- 生成AIを活用して開発効率を向上させたい方
- GitLab Duo with Amazon Qが使えるようになった際のAIによる実装精度がどの程度か気になる方
- Amazon Qエージェントを使用した開発に興味がある方
GitLab Duo with Amazon Q
そもそもGitLab Duo with Amazon Qはどんな機能となるかを簡単に説明しておきます。
AWSブログ:https://aws.amazon.com/jp/blogs/news/introducing-gitlab-duo-with-amazon-q/
GitLab Duo with Amazon Qには下記の4つの機能が実装されるそうです。
- GitLabイシューに対し、
/q dev
というクイックアクションをコメントとして記載することで、イシューの内容を読み取って、必要な機能実装を自律的に行ってMRを作成してくれる機能実装機能。更に、MRのコード変更画面でコード変更内容にコメントをして、改めて/q dev
のクイックアクションを実行することで、指摘に対する実装を行います - MRに対し、
/q test
のクイックアクションを実行することで、MRのコードをテストするためのユニットテストコードを提示します - MRに対し、
/q review
のクイックアクションを実行することで、コードレビューを実施し、コード内の脆弱性や、ベストプラクティスに則っていない記述などについて指摘を行います。更に、/q fix
により、指摘箇所を自動的に適切なコードに修正することができます。 - Java 8またはJava 11で書かれたコードを、Java 17のコードへアップグレードするためのイシューを作成し、
/q transform
のクイックアクションを実行することで、レガシーコードのアップグレードを行うMRを作成します
これらの機能を活用することで、開発ライフサイクルをGitLabから離れることなく、円滑に進めることが可能になりそうです。
Amazon Qエージェントでイシューからの機能実装を試してみる
今回の実装されている機能の中で、コードレビューやテストコードの提示、レガシーコードのアップグレードは、Amazon Qエージェントの機能として実装されていました。
機能実装のアクションもAmazon Qでは/dev
のアクションを実行することでこれまでも行えていましたが、Amazon Qは現時点で英語しか使えず、Chat画面上に一文で指示を与える程度となっていたため、今回のイシューを読み取って実装するという点は、新たな試みと感じました。
そこで、今回はGitLabのクイックアクションから実行をしている裏側の実装を想定し、Amazon Qに対して、別で用意したイシュー内容を読み込ませて実装させる/dev
アクションを実行する形で、どの程度の精度で実装してもらえるかを試してみました。
準備
まずは、下記のようなイシューを想定したMarkdownファイルを用意しました。イシューの内容は日本語で記載してあります。
下記の要件に従った REST API を構築する
- API Gateway + Lambda + DynamoDB の構成の REST API を構築する
- API Gateway は REST API を使用する(HTTP API ではない)
- Lambda は Python を採用する
- Lambda は 1 つの Function ですべてのエンドポイントを対応する Lambda-lith 構成とする
- Powertools for AWS Lambda(python)を使用して実装する
- API の認証は不要
- OpenAPI Document(./openapi.yml)に従った CRUD オペレーションが可能な API を構築する
- AWS CDK(v2)でデプロイがされている
あわせて下記のようなOpenAPI Documentを用意しました
openapi: 3.0.0
info:
title: User Management API
version: 1.0.0
description: A simple API for managing user information
paths:
/users:
post:
summary: Register a new user
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
responses:
"201":
description: User created successfully
content:
application/json:
schema:
$ref: "#/components/schemas/User"
get:
summary: Get all users
responses:
"200":
description: Successful response
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/User"
/user/{id}:
put:
summary: Update user information
parameters:
- name: id
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
responses:
"200":
description: User updated successfully
content:
application/json:
schema:
$ref: "#/components/schemas/User"
"404":
description: User not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
delete:
summary: Delete a user
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"204":
description: User deleted successfully
"404":
description: User not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
components:
schemas:
User:
type: object
properties:
id:
type: string
description: ULID of the user
name:
type: string
registration_at:
type: string
format: date-time
Error:
type: object
properties:
message:
type: string
example: "User not found"
実装の指示
Amazon Q Chatの画面で、
/dev Propose code to build a REST API according to the contents of `create_api.md`
という形でイシュー内容を読み取って、実装をするように指示をしました
指示した通り、create_api.mdを読み込み、openapi.ymlを参照してコードの生成が開始され、しばらくして下記のように、4つのファイルを作成してきました
それぞれのファイルは下記のとおりです
import json
from datetime import datetime
from aws_lambda_powertools import Logger, Tracer
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
from aws_lambda_powertools.utilities.typing import LambdaContext
import boto3
from ulid import ULID
from typing import Dict, Any
logger = Logger()
tracer = Tracer()
app = APIGatewayRestResolver()
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('Users')
@app.get("/users")
@tracer.capture_method
def list_users():
try:
response = table.scan()
items = response.get('Items', [])
return {
"statusCode": 200,
"body": json.dumps(items)
}
except Exception as e:
logger.error(f"Error listing users: {str(e)}")
return {"statusCode": 500, "body": "Internal server error"}
@app.post("/users")
@tracer.capture_method
def create_user():
try:
payload = app.current_event.json_body
name = payload.get('name')
if not name:
return {"statusCode": 400, "body": "Missing required field: name"}
user_id = str(ULID())
timestamp = datetime.utcnow().isoformat()
item = {
'id': user_id,
'name': name,
'registration_at': timestamp
}
table.put_item(Item=item)
return {
"statusCode": 201,
"body": json.dumps(item)
}
except Exception as e:
logger.error(f"Error creating user: {str(e)}")
return {"statusCode": 500, "body": "Internal server error"}
@app.get("/user/{id}")
@tracer.capture_method
def get_user(id: str):
try:
response = table.get_item(
Key={
'id': id
}
)
item = response.get('Item')
if not item:
return {
"statusCode": 404,
"body": json.dumps({
"message": "User not found"
})
}
return {
"statusCode": 200,
"body": json.dumps(item)
}
except Exception as e:
logger.error(f"Error getting user: {str(e)}")
return {
"statusCode": 500,
"body": json.dumps({
"message": "Internal server error"
})
}
@app.put("/user/{id}")
@tracer.capture_method
def update_user(id: str):
try:
payload = app.current_event.json_body
name = payload.get('name')
if not name:
return {
"statusCode": 400,
"body": json.dumps({
"message": "Missing required field: name"
})
}
# Check if user exists
get_response = table.get_item(Key={'id': id})
if 'Item' not in get_response:
return {
"statusCode": 404,
"body": json.dumps({
"message": "User not found"
})
}
# Get the existing registration_at
registration_at = get_response['Item']['registration_at']
table.update_item(
Key={
'id': id
},
UpdateExpression='SET #name = :name',
ExpressionAttributeNames={
'#name': 'name'
},
ExpressionAttributeValues={
':name': name
}
)
return {
"statusCode": 200,
"body": json.dumps({
"id": id,
"name": name,
"registration_at": registration_at
})
}
except Exception as e:
logger.error(f"Error updating user: {str(e)}")
return {
"statusCode": 500,
"body": json.dumps({
"message": "Internal server error"
})
}
@app.delete("/user/{id}")
@tracer.capture_method
def delete_user(id: str):
try:
# Check if user exists
get_response = table.get_item(Key={'id': id})
if 'Item' not in get_response:
return {
"statusCode": 404,
"body": json.dumps({
"message": "User not found"
})
}
table.delete_item(
Key={
'id': id
}
)
return {
"statusCode": 204,
"body": ""
}
except Exception as e:
logger.error(f"Error deleting user: {str(e)}")
return {
"statusCode": 500,
"body": json.dumps({
"message": "Internal server error"
})
}
@logger.inject_lambda_context
@tracer.capture_lambda_handler
def lambda_handler(event: Dict[str, Any], context: LambdaContext) -> Dict[str, Any]:
return app.resolve(event, context)
まずは、Lambda Functionの本体であるlambda_functions.pyですが、イシューの指示に従い、CRUD操作を実装したLambda-lithな構造になっており、Powertools for AWS Lambdaを活用してイベントハンドラーを使用したルーティングがされており、LoggerやTracerまで適切に実装されています。また、レスポンスの内容はOpenAPI Documentに忠実に従っており、ステータスコードやレスポンス内容が適切に実装されています。
更に、UserIDはOpenAPI Documentのスキーマ定義でULIDという形で説明がされているだけでしたが、しっかり読み取ってULIDでの採番までできていました。
OpenAPI Documentでは定義されていない500エラーの実装など、エラーハンドリングも適切に実装されていて、十分ビジネスアプリケーションとしても活用できる実装内容と感じました。
from aws_cdk import (
Stack,
aws_dynamodb as dynamodb,
aws_lambda as lambda_,
aws_apigateway as apigateway,
RemovalPolicy
)
from constructs import Construct
class ApiStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
# Create DynamoDB table
table = dynamodb.Table(
self, 'UsersTable',
partition_key=dynamodb.Attribute(
name='id',
type=dynamodb.AttributeType.STRING
),
removal_policy=RemovalPolicy.DESTROY # For development - change for production
)
# Create Lambda function
handler = lambda_.Function(
self, 'ApiHandler',
runtime=lambda_.Runtime.PYTHON_3_9,
code=lambda_.Code.from_asset('app'),
handler='lambda_function.lambda_handler',
environment={
'POWERTOOLS_SERVICE_NAME': 'user-api',
'POWERTOOLS_METRICS_NAMESPACE': 'UserApi'
}
)
# Grant Lambda function permissions to access DynamoDB
table.grant_read_write_data(handler)
# Create API Gateway
api = apigateway.RestApi(
self, 'UsersApi',
rest_api_name='Users API',
description='REST API for user management'
)
# Create API Gateway integration
integration = apigateway.LambdaIntegration(handler)
# Create resources and methods
users = api.root.add_resource('users')
users.add_method('POST', integration)
users.add_method('GET', integration)
user = api.root.add_resource('user')
user_id = user.add_resource('{id}')
user_id.add_method('GET', integration)
user_id.add_method('PUT', integration)
user_id.add_method('DELETE', integration)
続いてCDKのコードです。必要なリソース定義がしっかりされており、Powertools for AWS Lambdaを活用するための環境変数の定義なども適切に行われています
1点少し残念だったのが、LambdaのランタイムバージョンがPython3.9と古めなバージョンだった点ぐらいでしょうか
aws-lambda-powertools>=2.0.0
boto3>=1.26.0
aws-cdk-lib>=2.0.0
constructs>=10.0.0
python-ulid>=1.1.0
requirements.txtもしっかり出力していて、依存関係に過不足はなさそうです
# REST API with AWS Lambda, API Gateway, and DynamoDB
This project implements a REST API using AWS serverless technologies following these key components:
- AWS API Gateway (REST API)
- AWS Lambda (Python-based Lambda-lith architecture)
- Amazon DynamoDB
- AWS CDK v2 for infrastructure deployment
- AWS Lambda Powertools for Python
## Project Structure
- `app/lambda_function.py`: Main Lambda handler with all API endpoints
- `infrastructure/app.py`: CDK infrastructure code
- `openapi.yml`: API specification
- `requirements.txt`: Python dependencies
## Setup and Deployment
1. Install dependencies:
```bash
pip install -r requirements.txt
```
2. Deploy the infrastructure:
```bash
cdk deploy
```
## API Endpoints
- POST /users - Create a new user
- GET /users/{userId} - Get user by ID
- PUT /users/{userId} - Update user information
- DELETE /users/{userId} - Delete a user
All endpoints are implemented in a single Lambda function following the Lambda-lith pattern, using AWS Lambda Powertools for improved observability and best practices.
最後にREADME.mdです。指示の中にドキュメンテーションは一切含まれていませんでしたが、勝手に十分な情報が記載されたドキュメントまで作成してくれました。
また、コード群の生成後に更なるFeedbackとして、Python3.12のランタイムを使うようにや、README.mdを日本語で記載することなどを追加で指示をしたところ、見事に指示通り修正して提示をしてくれました。
GitLab Duo with Amazon Qでは、このように作成されたMRに対して、更なる指摘をして修正を促すこともできるようなので、それらも高い精度で実現できることが分かりました。
まとめ
GitLab Duo with Amazon Qはまだクローズドベータの状態で、残念ながら使える状態にはないのですが、現時点でもAmazon Qエージェントを活用することで、今回のように高い精度で実装ができるかと思いました。
GitLabから離れることなく、このような実装ができる様になると、とても効率よく開発を進められるのではないでしょうか。
今後のGitLabにも期待をしていきたいと思いました。