AWS CloudFormation 実践 第5回 ChatGPTでコードレビュー

GitHub ActionsのMarketplaceを見ていたところ、気になるものがありました。

なんと「AIがコードレビューしてくれる」です。

今回はこちらについて、CI/CDを絡めた実装方法の手順説明と、実施結果の評価をやっていきたいと思います。

Cloud Formationから、やや脱線気味ですが・・・w

前提条件

構造としては、Gitでプルリクに対してChatGPTにレビューを行ってもらい、結果をレビューコメントとして、受け取ります(図の上部)これにより、メインブランチへのマージ前に、コード誤り・改善の気づきを得ることができます。

  • 補足
    • このActionsでは、ChatGPTのエンジンとして「ChatGPT 3.5」「4」「4 Turbo」等を指定できます。
    • このレビュー機能は3.5でも使用可能ですが、上位バージョンを使用することで、レビュー精度は高くなります。
    • 現在、4以降を使用するには、有償クレジットの購入(最低 5$)等が必要となります。

実装方法解説

GitHub Actionsワークフローに関する実装要素の細かいところなどは、前回の記事を参照ください。

camelrush.hatenablog.com

GitHub Actionsのstepで、「anc95/ChatGPT-CodeReview@main」を使用します。

1. ChatGPTのAPI KEYを取得

  • API KEYの取得手順は割愛します、こちらのサイト等を参考にしてみてください。
    qiita.com

  • 高度なレビューを行いたい場合は、Credit(最低5$)を購入することで、使用可能となります。注意点を以下に挙げます。

    • ここで紹介するレビューは、無償版の「gpt-3.5-turbo」でも可能です。
    • 「ChatGPT」と「ChatGPT API」は別製品です。「Chat GPT Plus」で有償サブスクリプション(20$/月)を払っていても、ChatGPT APIの高度AIが使用できるわけではありません。別途、Chat GPT APIのCreditを購入する必要があります。
    • Credit購入前のAPI KEYを使用すると、ChatGPT4が使用できないことがあるようです。購入後、改めてAPI KEYを払い出しし、そちらを使用してください。

2. GitHubレポジトリにAPI KEYを設定

  • GitHubで、レビューするコードのレポジトリを開き、サインインします。
  • 上部の「Settings」メニューを開き、サイドメニューから「Secrets and variables」-「Actions」を選択します。
  • 「New repository secret」ボタンをクリックします。
  • 「Name」に「OPENAI_API_KEY」、「Secret」に取得したAPI KEYを設定して「Add secret」ボタンをクリックします。

3. Actionsワークフローを作成

  • 対象のレポジトリに対し、ルート下にワークフローファイルを追加します(ファイルの作り方は、前回のコラムを参考にしてください)
  • 作成するワークフローyamlは、次のとおりです(ほぼ、公式のサンプルのままです)
name: Code Review

permissions:
  contents: read
  pull-requests: write

on:
  pull_request:
    types: [opened, reopened, synchronize]

jobs:
  review:
    # if: ${{ contains(github.event.*.labels.*.name, 'gpt review') }} # Optional; to run only when a label is attached
    runs-on: ubuntu-latest
    steps:
      - uses: anc95/ChatGPT-CodeReview@main
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          # Optional
          LANGUAGE: Japanese
          OPENAI_API_ENDPOINT: https://api.openai.com/v1
          MODEL: gpt-3.5-turbo # https://platform.openai.com/docs/models
          PROMPT: # example: Please check if there are any confusions or irregularities in the following code diff:
          top_p: 1 # https://platform.openai.com/docs/api-reference/chat/create#chat/create-top_p
          temperature: 1 # https://platform.openai.com/docs/api-reference/chat/create#chat/create-temperature
          max_tokens: 4096
          MAX_PATCH_LENGTH: 10000 # if the patch/diff length is large than MAX_PATCH_LENGTH, will be ignored and won't review. By default, with no MAX_PATCH_LENGTH set, there is also no limit for the patch/diff length.
  • on: pull_requestとあるとおり、このワークフローは、プルリクエストの操作時に実行されます。
  • MODEL: gpt-3.5-turboの部分が、レビューエンジンの指定です。高度なAIを使用されたい方は、gpt-4-turboに変更してください(ただし、こちらを使用するには、前述のとおり、最低 5$のCredit購入が必要です)
  • max_tokens: 4096は、無償版を想定した場合の最大値です。有償版であればもっと大きい値が指定可能です。
  • 作成したワークフローファイルを、Master(Main)ブランチにCommit、Pushします。

4. レビュー依頼(Pull Request)を作成

  • レビューして欲しいコードを、ワークブランチにPushします。
    • レビューは、Pull Requestが示す変更の差分に対してだけ行われます。そのため、ソース全部をレビューさせるには、一旦ソースファイル自体をレポジトリから削除して、再度ファイルをPushしなおした方がよいです。
  • GitHubのPull Requestメニューから、「New pull request」ボタンをクリックします。
  • 上部でMaster(main)ブランチへのマージを指定して、「Create pull request」ボタンを押します。
     
  • 次画面で「Create pull request」ボタンを押すと、ワークフローが開始されます。

コードレビューが終わると、プルリクへのコメントとしてレビュー結果が応答されます(メールでも配信されてきました)

以上が実施手順となります。

それではやってみましょう!

レビューのお題

レビュー対象は、前回までのCloud Formationテンプレートファイルとしました。

内容を、以下に再掲します。

▼(テンプレート全文はここをクリックしてください)

AWSTemplateFormatVersion: 2010-09-09

Parameters:
  # My IP Address CIDR(Required)
  cftMyIpAddressCidr:
    Type: String
    Description: (Required)Mapping to Inbound rule on ec2'security group.

  # Environment variable
  cftEnv:
    Type: String
    Default: Dev
    AllowedValues:
    - Prd
    - Stg
    - Dev
    Description: Enter Environment Prd, Stg or Dev. Default is Dev.

Mappings:
  # Environment Mapping
  cftEnvMap:
    Prd:
      Suffix: prd
      InstanceType: t3.small
      VpcCidr: 10.2.0.0/21
      PublicSubnetCidr: 10.2.1.0/24
      PrivateSubnetCidr: 10.2.2.0/24
    Stg:
      Suffix: stg
      InstanceType: t3.small
      VpcCidr: 10.1.0.0/21
      PublicSubnetCidr: 10.1.1.0/24
      PrivateSubnetCidr: 10.1.2.0/24
    Dev:
      Suffix: dev
      InstanceType: t3.micro
      VpcCidr: 10.0.0.0/21
      PublicSubnetCidr: 10.0.1.0/24
      PrivateSubnetCidr: 10.0.2.0/24

Resources:
  # VPC
  cftVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !FindInMap [cftEnvMap, !Ref cftEnv, VpcCidr]
      EnableDnsSupport: true
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-vpc-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  # Internet Gateway
  cftIgw:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-igw-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix]}
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref cftVpc
      InternetGatewayId: !Ref cftIgw

  # Subnet (public)  
  cftPublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: true
      VpcId: !Ref cftVpc
      CidrBlock: !FindInMap [cftEnvMap, !Ref cftEnv, PublicSubnetCidr]
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-public-subnet-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  cftPublicRtb:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref cftVpc
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-public-rtb-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  cftPublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref cftPublicRtb
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref cftIgw

  cftPublicRtAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref cftPublicSubnet
      RouteTableId: !Ref cftPublicRtb

  # Subnet (private) 
  cftPrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: false
      VpcId: !Ref cftVpc
      CidrBlock: !FindInMap [cftEnvMap, !Ref cftEnv, PrivateSubnetCidr]
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-private-subnet-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  cftPrivateRtb:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref cftVpc
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-private-rtb-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  cftPrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref cftPrivateRtb
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref cftNatgw

  cftPrivateRtAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref cftPrivateSubnet
      RouteTableId: !Ref cftPrivateRtb

  # NAT Gateway  
  cftNatgw:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt cftNatgwEip.AllocationId
      SubnetId: !Ref cftPublicSubnet
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-natgw-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  cftNatgwEip:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

  # EC2(public)
  cftPublicEc2:
    Type: AWS::EC2::Instance
    Properties:
      KeyName: cf-tutorial-ec2-key
      DisableApiTermination: false
      ImageId: ami-020283e959651b381
      InstanceType: !FindInMap [cftEnvMap, !Ref cftEnv, InstanceType]
      SubnetId: !Ref cftPublicSubnet
      Monitoring: false
      SecurityGroupIds:
      - !Ref cftPublicEc2Sg
      UserData: !Base64 |
        #!/bin/bash -ex
        # put your script here
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-public-ec2-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  # SecurityGroup(public EC2)
  cftPublicEc2Sg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub
      - cf-tutorial-public-ec2-sg-${env}
      - {env: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix]}
      GroupDescription: Allow SSH Access.
      VpcId: !Ref cftVpc
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 22
        ToPort: 22
        CidrIp: !Sub
        - ${MyIpCidr}
        - {MyIpCidr: !Ref cftMyIpAddressCidr}
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-public-ec2-sg-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  # ElasicIp for public EC2
  cftPublicEc2Eip:
    Type: AWS::EC2::EIP
    Properties:
      InstanceId: !Ref cftPublicEc2

  # EC2(private)
  cftPrivateEc2:
    Type: AWS::EC2::Instance
    Properties:
      KeyName: cf-tutorial-ec2-key
      DisableApiTermination: false
      ImageId: ami-020283e959651b381
      InstanceType: !FindInMap [cftEnvMap, !Ref cftEnv, InstanceType]
      SubnetId: !Ref cftPrivateSubnet
      Monitoring: false
      SecurityGroupIds:
      - !Ref cftPrivateEc2Sg
      UserData: !Base64 |
        #!/bin/bash -ex
        # put your script here
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-private-ec2-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

  # SecurityGroup(private EC2)
  cftPrivateEc2Sg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub
      - cf-tutorial-private-ec2-sg-${env}
      - {env: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix]}
      GroupDescription: Allow SSH Access from Public EC2 Only.
      VpcId: !Ref cftVpc
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 22
        ToPort: 22
        CidrIp: !Sub
        - ${privateIp}/32
        - {privateIp: !GetAtt cftPublicEc2.PrivateIp}
      Tags:
      - Key: Name
        Value: !Sub
        - cf-tutorial-private-ec2-sg-${suffix}
        - {suffix: !FindInMap [cftEnvMap, !Ref cftEnv, Suffix],}

# ======================
#  Outputs Statement
# ======================
# IP Address Allocated to Public EC2
Outputs:
  cftPublicEc2Eip:
    Value: !GetAtt cftPublicEc2Eip.PublicIp

実行結果

上記をAIでレビューしてもらった結果、次のとおり回答されました。

ChatGPTらしく、すごい精度で回答が出てきました。

いくつか、ピックアップしてみます。

  • 「ポジティブな点」で、いくつかほめてくれててうれしい^^。「3. リソースタグ付け」は「タグ付けしているからわかりやすいね」だ。ふむふむ。
  • 「2. EIPリソースの属性」は「宣言が上下逆だ」と言っている。問題はないので、不要な指摘っぽい。
  • 「3. パラメータの検証」は「CIDR形式かどうかチェックが必要だ」と言っている。さらに、「セキュリティリスクになる可能性がある」と警告しており、Ingress記述と併せて、この指定がネットワークのエンドポイントになっていることが理解できているのがすごい!
  • 「4. 可用性ゾーンのハードコーディング」は「AZを固定しているからParameterやMappingで指定するようにしては?」と言ってる。ごもっとも。

まとめ

今回、NGの指摘ポイントを作り忘れたので、明確なNG指摘はありませんでしたが、それでもこれだけ改善提案を出してくれるのはありがたいです。実際にレビューアに提出する前に指摘を上げてくれるので、本人の気づきにもなりますし、開発の運用効率も上がりますね。