Amazon Q Developerエージェントを使って、イシューに対する機能実装を擬似的に再現してみた

AWS

こんにちは、中山( @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にも期待をしていきたいと思いました。

無料相談実施中
AWSを使用したサーバーレスアプリケーションの構築
サーバーレス開発内製化、AWSへの移行等
様々な課題について無料でご相談お受けいたします。
最新情報をチェックしよう!