🐡

GitHub Copilotのコードレビュー機能はTerraformでも使えるのか?パブリックプレビューを徹底検証してみた

に公開

こんにちはトンヌラです。
先日GitHub Copilotコードレビューが一般提供されましたね。

当時界隈でもかなり盛り上がった印象で、さまざまな記事が投稿されました。
https://zenn.dev/dotdtech_blog/articles/b00a950566affd
https://zenn.dev/rescuenow/articles/55ea72023527d1

私自身、個人でAWS環境をTerraformでコード管理していることもあり、

「これってTerraformコードにもCopilotのレビュー機能使えるのでは?」

と思い、早速調べてみました。

https://docs.github.com/ja/enterprise-cloud@latest/copilot/using-github-copilot/code-review/using-copilot-code-review#language-support

Copilotコードレビューの言語サポート状況

GitHub公式ドキュメントには、以下のように記載されています:

「GitHub Web サイト上の Copilot コードレビュー では、すべての言語がサポートされています。」

おおお!これはすごい!

…と思いきや、続きにはこんな文が。

「次の言語のサポートは一般提供されています。

C
C#
C++
Go
Java
JavaScript
Kotlin
Markdown
Python
Ruby
Swift
TypeScript
他のすべての言語は、パブリック プレビュー としてサポートされています。」

……ん? 結局ちゃんと対応してるの?それともベータ扱いなの?

というわけで、

「それなら自分でTerraformで検証してみようじゃないか!」

という気持ちになり、実際に以下の観点でCopilotコードレビューの精度を試してみました。

検証したこと

  1. Terraformコードがコード規約に違反していた場合に指摘できるか?

  2. Terraformのベストプラクティスに反するコードを指摘できるか?

  3. 過去のコードとスタイルが異なる場合に一貫性の欠如を指摘できるか?

使ったコード

今回実装した(cursorで実装してもらった)コードは以下です。

ディレクトリ構成
terraform/
 ┝main.tf
 ┝variables.tf
 ┝locals.tf
 └modules/
   ┝main.tf
   ┝variables.tf
   └output.tf
main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}
# VPCの作成
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = merge(
    {
      Name = "test-project-vpc"
    },
    local.common_tags
  )
}

# パブリックサブネットの作成
resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "ap-northeast-1a"
  map_public_ip_on_launch = true

  tags = merge(
    {
      Name = "test-project-public-subnet"
    },
    local.common_tags
  )
}

# インターネットゲートウェイの作成
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = merge(
    {
      Name = "test-project-igw"
    },
    local.common_tags
  )
}

# ルートテーブルの作成
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }

  tags = merge(
    {
      Name = "test-project-public-rt"
    },
    local.common_tags
  )
}

# ルートテーブルの関連付け
resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

# EC2モジュールの使用
module "ec2" {
  source = "./modules"
  for_each = local.ec2_instances
  ami_id                    = each.value.ami_id
  instance_type            = each.value.instance_type
  subnet_id                = aws_subnet.public.id
  vpc_id                   = aws_vpc.main.id
  project_name             = var.project_name
  environment              = var.environment
  instance_name            = each.value.name
  security_group_description = local.security_groups[each.key].description
  ingress_rules            = local.security_groups[each.key].ingress_rules
  tags                     = local.common_tags
} 
locals.tf
locals {
  # 共通の設定
  common_tags = {
    Environment = var.environment
    Project     = var.project_name
    ManagedBy   = "terraform"
  }

  # EC2インスタンスの設定
  ec2_instances = {
    web_server = {
      name          = "web-server"
      instance_type = "t2.micro"
      ami_id        = "ami-0d52744d6551d851e"  # Amazon Linux 2023 AMI
      description   = "Web server EC2 instance"
    }
    app_server = {
      name          = "app-server"
      instance_type = "t2.small"
      ami_id        = "ami-0d52744d6551d851e"  # Amazon Linux 2023 AMI
      description   = "Application server EC2 instance"
    }
  }

  # セキュリティグループの設定
  security_groups = {
    web_server = {
      name        = "web-server-sg"
      description = "Security group for web server"
      ingress_rules = [
        {
          port        = 80
          protocol    = "tcp"
          cidr_blocks = ["0.0.0.0/0"]
          description = "HTTP"
        },
        {
          port        = 22
          protocol    = "tcp"
          cidr_blocks = ["0.0.0.0/0"]
          description = "SSH"
        }
      ]
    }
    app_server = {
      name        = "app-server-sg"
      description = "Security group for application server"
      ingress_rules = [
        {
          port        = 8080
          protocol    = "tcp"
          cidr_blocks = ["0.0.0.0/0"]
          description = "Application"
        },
        {
          port        = 22
          protocol    = "tcp"
          cidr_blocks = ["0.0.0.0/0"]
          description = "SSH"
        }
      ]
    }
  }
} 
main.tf(modules配下)
# EC2インスタンスの作成
resource "aws_instance" "web" {
  ami                    = var.ami_id
  instance_type          = var.instance_type
  subnet_id              = var.subnet_id
  vpc_security_group_ids = [aws_security_group.web.id]

  tags = merge(
    {
      Name = "${var.project_name}-${var.instance_name}"
    },
    var.tags
  )
}

# セキュリティグループの作成
resource "aws_security_group" "web" {
  name        = "${var.project_name}-${var.instance_name}-sg"
  description = var.security_group_description
  vpc_id      = var.vpc_id

  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
      description = ingress.value.description
    }
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = merge(
    {
      Name = "${var.project_name}-${var.instance_name}-sg"
    },
    var.tags
  )
} 
variables.tf(modules配下)
variable "ami_id" {
  description = "EC2インスタンスのAMI ID"
  type        = string
}

variable "instance_type" {
  description = "EC2インスタンスタイプ"
  type        = string
}

variable "subnet_id" {
  description = "EC2インスタンスを配置するサブネットID"
  type        = string
}

variable "vpc_id" {
  description = "VPCのID"
  type        = string
}

variable "project_name" {
  description = "プロジェクト名"
  type        = string
}

variable "instance_name" {
  description = "インスタンス名"
  type        = string
}

variable "security_group_description" {
  description = "セキュリティグループの説明"
  type        = string
}

variable "ingress_rules" {
  description = "セキュリティグループのインバウンドルール"
  type = list(object({
    port        = number
    protocol    = string
    cidr_blocks = list(string)
    description = string
  }))
}

variable "tags" {
  description = "リソースに付与するタグ"
  type        = map(string)
  default     = {}
}

variable "environment" {
  description = "環境名"
  type        = string
} 
outputs.tf(modules配下)
output "instance_id" {
  description = "作成されたEC2インスタンスのID"
  value       = aws_instance.web.id
}

output "public_ip" {
  description = "EC2インスタンスのパブリックIPアドレス"
  value       = aws_instance.web.public_ip
}

output "private_ip" {
  description = "EC2インスタンスのプライベートIPアドレス"
  value       = aws_instance.web.private_ip
}

output "security_group_id" {
  description = "作成されたセキュリティグループのID"
  value       = aws_security_group.web.id
} 

Terraformコードがコード規約に違反していた場合に指摘できるか?

コード規約

ファイル構成

  • 各リソースは適切なディレクトリに分割して配置する
  • 変数は variables.tf に定義する
  • ローカル変数は locals.tf に定義する
  • モジュール化を活用し、再利用可能なコードを作成する

命名規則

  • リソース名は小文字とハイフンを使用(例:aws_instance.web_server
  • 変数名は小文字とアンダースコアを使用(例:instance_type
  • タグ名は小文字とハイフンを使用(例:environment-name
  • モジュール名は目的を表す名詞を使用(例:ec2vpc

フォーマット

  • インデントは2スペースを使用
  • リソースブロックの開始と終了の間に空行を入れる
  • 長い行は適切に改行する
  • 変数やパラメータの代入時は、=の前後にスペースを入れる
  • 複数のパラメータを設定する場合、=の位置を揃える
    # 良い例
    resource "aws_instance" "example" {
      ami           = var.ami_id
      instance_type = var.instance_type
      subnet_id     = var.subnet_id
    }
    
    # 悪い例
    resource "aws_instance" "example" {
      ami = var.ami_id
      instance_type=var.instance_type
      subnet_id= var.subnet_id
    }
    
  • モジュールの呼び出し時も同様に=の位置を揃える
    # 良い例
    module "ec2" {
      source = "./modules/ec2"
      ami_id = var.ami_id
      name   = var.instance_name
    }
    

コメント

  • 複雑なロジックには適切なコメントを付ける
  • 各リソースの目的を説明するコメントを付ける
  • モジュールの使用方法をコメントで説明する

規約違反箇所をコードに混入

EC2を追加するシナリオで、コード規約に違反させたコードを実装します。

main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}
# VPCの作成
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = merge(
    {
      Name = "test-project-vpc"
    },
    local.common_tags
  )
}

# パブリックサブネットの作成
resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "ap-northeast-1a"
  map_public_ip_on_launch = true

  tags = merge(
    {
      Name = "test-project-public-subnet"
    },
    local.common_tags
  )
}

# インターネットゲートウェイの作成
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = merge(
    {
      Name = "test-project-igw"
    },
    local.common_tags
  )
}

# ルートテーブルの作成
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }

  tags = merge(
    {
      Name = "test-project-public-rt"
    },
    local.common_tags
  )
}

# ルートテーブルの関連付け
resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

# セキュリティグループの作成(命名規則違反)
resource "aws_security_group" "WEB_SERVER_SG" {
  name        = "WEB_SERVER_SG"
  description = "Web server security group"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name="WEB_SERVER"
    Environment="DEV"
  }
}

# EC2インスタンスの作成(フォーマット違反)
resource "aws_instance" "WEB_SERVER" {
ami="ami-0d52744d6551d851e"
instance_type="t2.micro"
subnet_id=aws_subnet.public.id
vpc_security_group_ids=[aws_security_group.WEB_SERVER_SG.id]
tags={
  Name="WEB_SERVER"
  Environment="DEV"
}
}

# アプリケーションサーバー用セキュリティグループ(コメント違反)
resource "aws_security_group" "APP_SERVER_SG" {
  # セキュリティグループを作る
  name        = "APP_SERVER_SG"
  description = "App server security group"
  vpc_id      = aws_vpc.main.id

  # ポートを開ける
  ingress {
    from_port   = 8080
    to_port     = 8080
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  # 全部のポートを開ける
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name="APP_SERVER"
    Environment="DEV"
  }
}

# アプリケーションサーバー(インデント違反)
resource "aws_instance" "APP_SERVER" {
    ami="ami-0d52744d6551d851e"
    instance_type="t2.small"
    subnet_id=aws_subnet.public.id
    vpc_security_group_ids=[aws_security_group.APP_SERVER_SG.id]
    tags={
        Name="APP_SERVER"
        Environment="DEV"
    }
}

# EC2モジュールの使用
module "ec2" {
  source = "./modules"
  for_each = local.ec2_instances
  ami_id                    = each.value.ami_id
  instance_type            = each.value.instance_type
  subnet_id                = aws_subnet.public.id
  vpc_id                   = aws_vpc.main.id
  project_name             = var.project_name
  environment              = var.environment
  instance_name            = each.value.name
  security_group_description = local.security_groups[each.key].description
  ingress_rules            = local.security_groups[each.key].ingress_rules
  tags                     = local.common_tags
} 

いよいよGitHub Copilotコードレビューにレビューをリクエスト!

早速プルリクエストを作成します。
コメントにはgithub copilotコードレビューに対して日本語でレビューして欲しい旨と、コード規約についても記載します。

プルリクエストを作成後、右側のrequestボタンを押します。

レビュー結果が出力されました!確認して見ましょう。

結果としては以下です。

規約 指摘有無
ファイル構成 ✖️
命名規則
フォーマット
コメント ✖️

まずまずといったところでしょうか。
ハック的な使い方をしている点、プロンプトが足跡である点を改善すればもう少し改良される気もします。

TerraformやAWSのベストプラクティスに反するコードを指摘できるか?

実装したアンチパターン

以下のアンチパターンを実装してみました。

  1. モジュールの構成が不適切:
    単一のmain.tfファイルのみ(variables.tfやoutput.tfをmain.tfに集約)

  2. プロバイダーの設定が不適切:
    チャイルドモジュール内にproviderブロックを記述

  3. セキュリティ設定が不適切:
    IMDSv1を許可(http_tokens = "optional")

実装したコード

main.tf(modules配下)
# プロバイダーブロックをチャイルドモジュール内に記述(アンチパターン)
provider "aws" {
  region = "ap-northeast-1"
}

# EC2インスタンスの作成
resource "aws_instance" "web" {
  ami                    = var.ami_id
  instance_type          = var.instance_type
  subnet_id              = var.subnet_id
  vpc_security_group_ids = [aws_security_group.web.id]

  # IMDSv1を許可(アンチパターン)
  metadata_options {
    http_endpoint               = "enabled"
    http_tokens                 = "optional"  # IMDSv1を許可
    http_put_response_hop_limit = 1
  }

  tags = merge(
    {
      Name = "${var.project_name}-${var.instance_name}"
    },
    var.tags
  )
}

# セキュリティグループの作成
resource "aws_security_group" "web" {
  name        = "${var.project_name}-${var.instance_name}-sg"
  description = var.security_group_description
  vpc_id      = var.vpc_id

  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
      description = ingress.value.description
    }
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = merge(
    {
      Name = "${var.project_name}-${var.instance_name}-sg"
    },
    var.tags
  )
} 

# variableなどもチャイルドモジュール内に記述(アンチパターン)
variable "ami_id" {
  description = "EC2インスタンスのAMI ID"
  type        = string
}

variable "instance_type" {
  description = "EC2インスタンスタイプ"
  type        = string
}

variable "subnet_id" {
  description = "EC2インスタンスを配置するサブネットID"
  type        = string
}

variable "vpc_id" {
  description = "VPCのID"
  type        = string
}

variable "project_name" {
  description = "プロジェクト名"
  type        = string
}

variable "instance_name" {
  description = "インスタンス名"
  type        = string
}

variable "security_group_description" {
  description = "セキュリティグループの説明"
  type        = string
}

variable "ingress_rules" {
  description = "セキュリティグループのインバウンドルール"
  type = list(object({
    port        = number
    protocol    = string
    cidr_blocks = list(string)
    description = string
  }))
}

variable "tags" {
  description = "リソースに付与するタグ"
  type        = map(string)
  default     = {}
}

variable "environment" {
  description = "環境名"
  type        = string
} 

output "instance_id" {
  description = "作成されたEC2インスタンスのID"
  value       = aws_instance.web.id
}

output "public_ip" {
  description = "EC2インスタンスのパブリックIPアドレス"
  value       = aws_instance.web.public_ip
}

output "private_ip" {
  description = "EC2インスタンスのプライベートIPアドレス"
  value       = aws_instance.web.private_ip
}

output "security_group_id" {
  description = "作成されたセキュリティグループのID"
  value       = aws_security_group.web.id
} 

指摘結果は以下の通りです。
注入したアンチパターンを全て指摘してくれました!!

過去のコードとスタイルが異なる場合に一貫性の欠如を指摘できるか?

実装した一貫性不足な記載

過去のコードスタイルと異なる以下の点を含めてS3バケットのコードを実装しました。

  1. インデントの不一致
    既存のコードは2スペース
    新規追加のS3バケット関連コードは4スペース
  2. タグの記述方法の違い
    既存のコード: merge() 関数を使用
    新規コード: 直接マップを指定
  3. リソース名の命名規則の違い
    既存のコード: project-name-resource-type 形式
    新規コード: data_storage のようなシンプルな形式
  4. コメントスタイルの違い
    既存のコード: シンプルな日本語コメント
    新規コード: より詳細な日本語コメント

実装したコード

main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}
# VPCの作成
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = merge(
    {
      Name = "test-project-vpc"
    },
    local.common_tags
  )
}

# パブリックサブネットの作成
resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "ap-northeast-1a"
  map_public_ip_on_launch = true

  tags = merge(
    {
      Name = "test-project-public-subnet"
    },
    local.common_tags
  )
}

# インターネットゲートウェイの作成
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = merge(
    {
      Name = "test-project-igw"
    },
    local.common_tags
  )
}

# ルートテーブルの作成
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }

  tags = merge(
    {
      Name = "test-project-public-rt"
    },
    local.common_tags
  )
}

# ルートテーブルの関連付け
resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

# EC2モジュールの使用
module "ec2" {
  source = "./modules"
  for_each = local.ec2_instances
  ami_id                    = each.value.ami_id
  instance_type            = each.value.instance_type
  subnet_id                = aws_subnet.public.id
  vpc_id                   = aws_vpc.main.id
  project_name             = var.project_name
  environment              = var.environment
  instance_name            = each.value.name
  security_group_description = local.security_groups[each.key].description
  ingress_rules            = local.security_groups[each.key].ingress_rules
  tags                     = local.common_tags
}

# S3バケットの作成(スタイルの不一致を含む)
resource "aws_s3_bucket" "data_storage" {
    bucket = "my-test-project-data-storage"
    
    # バージョニングを有効化
    versioning {
        enabled = true
    }
    
    # サーバーサイド暗号化を設定
    server_side_encryption_configuration {
        rule {
            apply_server_side_encryption_by_default {
                sse_algorithm = "AES256"
            }
        }
    }
    
    # タグを直接指定(mergeを使用していない)
    tags = {
        Name = "data-storage"
        Environment = var.environment
        Project = var.project_name
        ManagedBy = "terraform"
    }
}

# バケットポリシーの設定(インデントが4スペース)
resource "aws_s3_bucket_policy" "data_storage_policy" {
    bucket = aws_s3_bucket.data_storage.id
    
    policy = jsonencode({
        Version = "2012-10-17"
        Statement = [
            {
                Effect = "Allow"
                Principal = {
                    AWS = "arn:aws:iam::123456789012:root"
                }
                Action = "s3:*"
                Resource = [
                    aws_s3_bucket.data_storage.arn,
                    "${aws_s3_bucket.data_storage.arn}/*"
                ]
            }
        ]
    })
} 

コードレビューの結果は以下の通りです。

こちらは流石に無理でしたね。。
まあ、サボらずにコード規約をしっかり定義する事は必須なようです。

まとめ

GitHub Copilotのコードレビュー機能は、「Terraformのような構成言語でもどこまで活用できるのか?」という観点で検証してみました。

結果としては――

-コード規約違反の一部(命名規則やフォーマット)にはしっかり反応

-ベストプラクティスに反する典型的なアンチパターンにも強く反応

-コードスタイルの一貫性までは、まだ難しい

という印象でした。

特にベストプラクティスへの感度は予想以上で、**TerraformでもCopilotが十分に“使える場面がある”**と実感しました。

一方で、スタイルの一貫性やプロジェクト固有の規約違反の検出については、現状まだ人間の超有識者には敵わない側面があるというのが正直なところです。

今後に向けて

Terraformを本番で扱う現場でも、Copilotを一次レビューの支援ツールとして導入することで、レビューの効率化や教育コストの削減に一役買う可能性もあるかもしれません。
本記事が導入を検討している方のお力になれば幸いです。

Discussion