この記事はApplibot Advent Calendar 2024 の11日目の記事です。前回の記事はこちらです。
はじめに
GitHub Copilotが正式リリースされてから約2年半が経ちました。
コードを書いている途中にエディタ上でコードの提案をしてくれたり、Copilotに指示をすればコードを全部書いてくれたりと、エンジニアには必要不可欠なツールになっていると思います。
私自身、インフラエンジニアとしてTerraformのコードを書いているのですが、GitHub Copilotが正式リリースされると聞いたときは、非常に楽しみにしておりました。
ですが、リリース当初はGoogle CloudのTerraformのコードを主に書いていたこともあるのですが、GitHub Copilotからの提案でterraform planが通ることは数えるほどしかなく、Google CloudのTerraformのコードを書くときはGitHub Copilotを使わずに開発をしてきました。
最近になり、またAWSのTerraformのコードを書くことになったので、改めてGitHub CopilotにAWSとGoogle CloudのTerraformのコードを書かせてみて、実用的な提案がされるのかを検証してみました。
Terraformの初期設定
まずはGitHub CopilotにTerraformの初期設定ファイルの提案をしてもらいました。
AWS
## 入力: AWSのterraformの管理をしたいが、何からすればいい?
# まずは、providerを設定する必要がある。
# 以下のようにprovider.tfを作成し、providerを設定する。
# provider "aws" {
# region = "ap-northeast-1"
# }
# AWSのproviderを設定する
# 1. region: AWSのリージョンを指定する
# このようにproviderを設定することで、terraformでAWSを操作することができるようになる。
# このprovider.tfを作成したら、terraform initを実行して、providerを初期化する。
# terraform init
# これで、AWSのterraformの管理ができるようになる。
## 入力: providerの設定値を変数にしたいが、どうすればいい?
# providerの設定値を変数にしたい場合は、以下のように変数を定義し、providerの設定値に変数を指定する。
# variable "region" {
# description = "The region in which AWS resources will be created."
# }
# provider "aws" {
# region = var.region
# }
# このように変数を定義し、providerの設定値に変数を指定することで、providerの設定値を変数にすることができる。
# このprovider.tfを作成したら、terraform initを実行して、providerを初期化する。
# terraform init
# これで、AWSのterraformの管理ができるようになる。
## 入力: 変数の値を秘匿(Git管理の対象外)としたいが、どうすればいい?
# 変数の値を秘匿(Git管理の対象外)としたい場合は、以下のように.tfvarsファイルを作成し、変数の値を記述する。
# terraform.tfvars
# region = "ap-northeast-1"
# このように.tfvarsファイルを作成し、変数の値を記述することで、変数の値を秘匿(Git管理の対象外)とすることができる。
# このterraform.tfvarsを作成したら、terraform initを実行して、providerを初期化する。
# terraform init
# これで、AWSのterraformの管理ができるようになる。
## 入力: tfstateファイルをS3に保存したいが、どうすればいい?
# tfstateファイルをS3に保存する場合は、以下のようにbackendを設定する。
# backend "s3" {
# bucket = "my-tfstate-bucket"
# key = "terraform.tfstate"
# region = "ap-northeast-1"
# }
# このようにbackendを設定することで、tfstateファイルをS3に保存することができる。
# このbackend.tfを作成したら、terraform initを実行して、backendを初期化する。
# terraform init
# これで、tfstateファイルをS3に保存することができるようになる。
## 入力: backendという設定がないって言われた。。。
# backendという設定がない場合は、以下のようにbackendを設定する。
# terraform {
# backend "s3" {
# bucket = "my-tfstate-bucket"
# key = "terraform.tfstate"
# region = "ap-northeast-1"
# }
# }
# このようにbackendを設定することで、tfstateファイルをS3に保存することができる。
# このmain.tfを作成したら、terraform initを実行して、backendを初期化する。
# terraform init
# これで、tfstateファイルをS3に保存することができるようになる。
このように質問を繰り返し、バケット名等の修正はした上で、各種ファイルを作成しました。
variable "region" {
description = "ap-northeast-1"
}
provider "aws" {
region = var.region
}
region = "ap-northeast-1"
terraform {
backend "s3" {
bucket = "test-jaramon-tfstate-bucket"
key = "terraform.tfstate"
region = "ap-northeast-1"
}
}
これで terraform init
をしてみると、ちゃんと通りました。
Google Cloud
## 入力: Google Cloudのterraformの管理をしたいが、何からすればいい?
# まずは、providerを設定する必要がある。
# 以下のようにprovider.tfを作成し、providerを設定する。
# provider "google" {
# credentials = file("account.json")
# project = "my-project-id"
# region = "asia-northeast1"
# zone = "asia-northeast1-a"
# }
# Google Cloudのproviderを設定する
# 1. credentials: Google Cloudのサービスアカウントのjsonファイルを指定する
# 2. project: Google CloudのプロジェクトIDを指定する
# 3. region: Google Cloudのリージョンを指定する
# 4. zone: Google Cloudのゾーンを指定する
# このようにproviderを設定することで、terraformでGoogle Cloudを操作することができるようになる。
# このprovider.tfを作成したら、terraform initを実行して、providerを初期化する。
# terraform init
# これで、Google Cloudのterraformの管理ができるようになる。
## 入力: credentialsを使わないようにしたいが、どうすればいい?
# credentialsを使わない場合は、以下のようにproviderを設定する。
# provider "google" {
# project = "my-project-id"
# region = "asia-northeast1"
# zone = "asia-northeast1-a"
# }
# このようにcredentialsを指定しない場合は、Google Cloudのサービスアカウントのjsonファイルを使わずに、
# Google CloudのプロジェクトID、リージョン、ゾーンを指定してproviderを設定することができる。
# このprovider.tfを作成したら、terraform initを実行して、providerを初期化する。
# terraform init
# これで、Google Cloudのterraformの管理ができるようになる。
## 入力: providerの設定値を変数にしたいが、どうすればいい?
# providerの設定値を変数にしたい場合は、以下のように変数を定義し、providerの設定値に変数を指定する。
# variable "project" {
# description = "The project in which Google Cloud resources will be created."
# default = "my-project-id"
# }
# variable "region" {
# description = "The region in which Google Cloud resources will be created."
# default = "asia-northeast1"
# }
# variable "zone" {
# description = "The zone in which Google Cloud resources will be created."
# default = "asia-northeast1-a"
# }
# provider "google" {
# project = var.project
# region = var.region
# zone = var.zone
# }
# このように変数を定義し、providerの設定値に変数を指定することで、providerの設定値を変数にすることができる。
# このprovider.tfを作成したら、terraform initを実行して、providerを初期化する。
# terraform init
# これで、Google Cloudのterraformの管理ができるようになる。
## 入力: 変数の値を秘匿(Git管理の対象外)としたいが、どうすればいい?
# 変数の値を秘匿(Git管理の対象外)としたい場合は、以下のように.tfvarsファイルを作成し、変数の値を記述する。
# # terraform.tfvars
# project = "my-project-id"
# region = "asia-northeast1"
# zone = "asia-northeast1-a"
# このように.tfvarsファイルを作成し、変数の値を記述することで、変数の値を秘匿(Git管理の対象外)とすることができる。
# この.tfvarsファイルを作成したら、terraform initを実行して、providerを初期化する。
# terraform init
# これで、Google Cloudのterraformの管理ができるようになる。
## 入力: tfstateファイルをGCSに保存したいが、どうすればいい?
# tfstateファイルをGCSに保存する場合は、以下のようにbackendを設定する。
# backend "gcs" {
# bucket = "my-tfstate-bucket"
# prefix = "terraform.tfstate"
# }
# このようにbackendを設定することで、tfstateファイルをGCSに保存することができる。
# このbackend.tfを作成したら、terraform initを実行して、backendを初期化する。
# terraform init
# これで、Google Cloudのterraformの管理ができるようになる。
## 入力: backendという設定がないって言われた。。。
# backendという設定がない場合は、以下のようにbackendを設定する。
# terraform {
# backend "gcs" {
# bucket = "my-tfstate-bucket"
# prefix = "terraform.tfstate"
# }
# }
# このようにbackendを設定することで、tfstateファイルをGCSに保存することができる。
# このmain.tfを作成したら、terraform initを実行して、backendを初期化する。
# terraform init
# これで、tfstateファイルをGCSに保存することができるようになる。
こちらも同様に、バケット名等の修正はした上で、各種ファイルを作成しました。
variable "project" {
description = "The project in which Google Cloud resources will be created."
default = "my-project-id"
}
variable "region" {
description = "The region in which Google Cloud resources will be created."
default = "asia-northeast1"
}
variable "zone" {
description = "The zone in which Google Cloud resources will be created."
default = "asia-northeast1-a"
}
provider "google" {
project = var.project
region = var.region
zone = var.zone
}
project = "XXXXXXXX"
region = "asia-northeast1"
zone = "asia-northeast1-a"
terraform {
backend "gcs" {
bucket = "test-jaramon-tfstate-bucket"
prefix = "terraform.tfstate"
}
}
これで terraform init
をしてみると、こちらもちゃんと通りました。
Webアプリケーションの追加
次にWebアプリケーションとしてNGINXのコンテナをデプロイしていきます。
AWSとGoogle Cloudでは色々と異なりますが、AWSではAmazon ECS on Fargate、Google CloudではCloud Runを利用していこうかと思います。
AWS
## VPCを作成し、パブリックサブネットにIP制限をした上でのロードバランサ、
## プライベートサブネットにECS、タスクにNGINXのコンテナを作成して、
## アクセスできるようにしたいが、どうすればいい?
## ただし、プライベートサブネットのECSには、ロードバランサからのみアクセスできるようにしたい。
# VPCを作成し、パブリックサブネットにIP制限をした上でのロードバランサ、プライベートサブネットにECS、タスクにNGINXのコンテナを作成して、アクセスできるようにする場合は、以下のようにmain.tfを作成する。
# main.tf
# provider "aws" {
# region = "ap-northeast-1"
# }
# resource "aws_vpc" "my_vpc" {
# cidr_block = "10.0.0.0/16"
#
# tags = {
# Name = "my-vpc"
# }
# }
# resource "aws_subnet" "public_subnet" {
# vpc_id = aws_vpc.my_vpc.id
# cidr_block = "10.0.1.0/24"
# availability_zone = "ap-northeast-1a"
#
# tags = {
# Name = "public-subnet"
# }
# }
# resource "aws_security_group" "public_sg" {
# vpc_id = aws_vpc.my_vpc.id
#
# ingress {
# from_port = 80
# to_port = 80
# protocol = "tcp"
# cidr_blocks = [""] # IP制限をしたい場合は、IPアドレスを指定する
# }
#
# egress {
# from_port = 0
# to_port = 0
# protocol = "-1"
# cidr_blocks = ["0.0.0.0/0"]
# }
# }
# resource "aws_lb" "my_lb" {
# name = "my-lb"
# internal = false
# load_balancer_type = "application"
# security_groups = [aws_security_group.public_sg.id]
# subnets = [aws_subnet.public_subnet.id]
#
# enable_deletion_protection = false
# }
# resource "aws_subnet" "private_subnet" {
# vpc_id = aws_vpc.my_vpc.id
# cidr_block = "
# availability_zone = "ap-northeast-1c"
#
# tags = {
# Name = "private-subnet"
# }
# }
# resource "aws_security_group" "private_sg" {
# vpc_id = aws_vpc.my_vpc.id
#
# ingress {
# from_port = 80
# to_port = 80
# protocol = "tcp"
# security_groups = [aws_lb.my_lb.id]
# }
#
# egress {
# from_port = 0
# to_port = 0
# protocol = "-1"
# cidr_blocks = ["0.0.0.0/0"]
# }
# }
# resource "aws_ecs_cluster" "my_cluster" {
# name = "my-cluster"
# }
# resource "aws_ecs_task_definition" "my_task" {
# family = "my-task"
# network_mode = "awsvpc"
# requires_compatibilities = ["FARGATE"]
# cpu = "256"
# memory = "512"
#
# container_definitions = jsonencode([
# {
# name = "nginx"
# image = "nginx:latest"
# cpu = 0
# memory = 128
# essential = true
# portMappings = [
# {
# containerPort = 80
# hostPort = 80
# }
# ]
# }
# ])
# }
# resource "aws_ecs_service" "my_service" {
# name = "my-service"
# cluster = aws_ecs_cluster.my_cluster.id
# task_definition = aws_ecs_task_definition.my_task.arn
# desired_count = 1
# launch_type = "FARGATE"
# network_configuration {
# subnets = [aws_subnet.private_subnet.id]
# security_groups = [aws_security_group.private_sg.id]
# assign_public_ip = false
# }
# }
初回にしてはなんとなくそれっぽいものができ、ちゃんと terraform plan
も通りました。
ではこれを terraform apply
してみます。
以下、apply結果の抜粋です。
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
aws_ecs_cluster.my_cluster: Creating...
aws_vpc.my_vpc: Creating...
...
╷
│ Error: creating ELBv2 application Load Balancer (my-lb): operation error Elastic Load Balancing v2: CreateLoadBalancer, https response error StatusCode: 400, RequestID: 4940d1a6-913d-4dad-b2e7-bb68586ec5b9, api error ValidationError: At least two subnets in two different Availability Zones must be specified
│
│ with aws_lb.my_lb,
│ on main.tf line 45, in resource "aws_lb" "my_lb":
│ 45: resource "aws_lb" "my_lb" {
ロードバランサの作成時にエラーが出ました。
Availability Zoneが複数必要だったみたいです。
修正をお願いしてみましょう。(毎回全量書いてくれるので、差分だけ記載します。)
## 入力: api error ValidationError: At least two subnets in two different Availability Zones must be specified
## のエラーが出たので、パブリックサブネットを追加して修正して欲しい
# パブリックサブネットを追加して修正する場合は、以下のようにmain.tfを修正する。
...
# resource "aws_subnet" "public_subnet2" {
# vpc_id = aws_vpc.my_vpc.id
# cidr_block = "
# availability_zone = "ap-northeast-1c"
# tags = {
# Name = "public-subnet2"
# }
# }
# resource "aws_lb" "my_lb" {
# name = "my-lb"
# internal = false
# load_balancer_type = "application"
# security_groups = [aws_security_group.public_sg.id]
# subnets = [aws_subnet.public_subnet.id, aws_subnet.public_subnet2.id]
# enable_deletion_protection = false
# }
...
# このようにmain.tfを修正することで、パブリックサブネットを追加して修正することができる。
# このmain.tfを修正したら、terraform planを実行して、変更内容を確認する。
# terraform plan
# これで、パブリックサブネットを追加して修正することができるようになる。
再度applyしてみます。
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
aws_ecs_cluster.my_cluster: Creating...
aws_vpc.my_vpc: Creating...
...
╷
│ Error: creating ELBv2 application Load Balancer (my-lb): operation error Elastic Load Balancing v2: CreateLoadBalancer, https response error StatusCode: 400, RequestID: e5f036db-c24c-4383-9ff0-76b3d02672d0, InvalidSubnet: VPC vpc-XXXXXXXXXXXX has no internet gateway
│
│ with aws_lb.my_lb,
│ on main.tf line 55, in resource "aws_lb" "my_lb":
│ 55: resource "aws_lb" "my_lb" {
今度はInternet Gatewayがないと言われてしまいました。
このようにエラーが出るたびに解決していき、最終的には以下のファイルになりました。
terraform {
backend "s3" {
bucket = "test-jaramon-tfstate-bucket"
key = "terraform.tfstate"
region = "ap-northeast-1"
}
}
resource "aws_vpc" "my_vpc" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "my-vpc"
}
}
resource "aws_subnet" "public_subnet" {
vpc_id = aws_vpc.my_vpc.id
cidr_block = "10.0.1.0/24"
availability_zone = "ap-northeast-1a"
tags = {
Name = "public-subnet"
}
}
resource "aws_subnet" "public_subnet2" {
vpc_id = aws_vpc.my_vpc.id
cidr_block = "10.0.2.0/24"
availability_zone = "ap-northeast-1c"
tags = {
Name = "public-subnet2"
}
}
resource "aws_internet_gateway" "my_igw" {
vpc_id = aws_vpc.my_vpc.id
tags = {
Name = "my-igw"
}
}
resource "aws_route_table" "my_rt" {
vpc_id = aws_vpc.my_vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.my_igw.id
}
tags = {
Name = "my-rt"
}
}
resource "aws_route_table_association" "public_subnet_association" {
subnet_id = aws_subnet.public_subnet.id
route_table_id = aws_route_table.my_rt.id
}
resource "aws_route_table_association" "public_subnet2_association" {
subnet_id = aws_subnet.public_subnet2.id
route_table_id = aws_route_table.my_rt.id
}
resource "aws_security_group" "public_sg" {
vpc_id = aws_vpc.my_vpc.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["XXX.XXX.XXX.XXX/32"] # IP制限をしたい場合は、IPアドレスを指定する
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_lb" "my_lb" {
name = "my-lb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.public_sg.id]
subnets = [aws_subnet.public_subnet.id, aws_subnet.public_subnet2.id]
enable_deletion_protection = false
}
resource "aws_subnet" "private_subnet" {
vpc_id = aws_vpc.my_vpc.id
cidr_block = "10.0.3.0/24"
availability_zone = "ap-northeast-1c"
tags = {
Name = "private-subnet"
}
}
resource "aws_security_group" "private_sg" {
vpc_id = aws_vpc.my_vpc.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.public_sg.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_ecs_cluster" "my_cluster" {
name = "my-cluster"
}
resource "aws_ecs_task_definition" "my_task" {
family = "my-task"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = "256"
memory = "512"
container_definitions = jsonencode([
{
name = "nginx"
image = "nginx:latest"
cpu = 0
memory = 128
essential = true
portMappings = [
{
containerPort = 80
hostPort = 80
}
]
}
])
}
resource "aws_ecs_service" "my_service" {
name = "my-service"
cluster = aws_ecs_cluster.my_cluster.id
task_definition = aws_ecs_task_definition.my_task.arn
desired_count = 1
launch_type = "FARGATE"
network_configuration {
subnets = [aws_subnet.private_subnet.id]
security_groups = [aws_security_group.private_sg.id]
assign_public_ip = false
}
}
こちらでapplyを実施してみます。
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
aws_ecs_cluster.my_cluster: Creating...
aws_vpc.my_vpc: Creating...
...
Apply complete! Resources: 14 added, 0 changed, 0 destroyed.
ちゃんと通りました。
ですが、コンソールのAmazon ECSを見に行くと、NGINXのイメージをpullすることができず、タスクが起動していませんでした。
原因はプライベートサブネットでタスクを動かそうとしていたため、NATゲートウェイかECRのVPCエンドポイントが必要そうでした。
とはいえ、applyが通るところまでは書いてもらえたのと、コードの内容もそれほど悪くはなかったため、十分すぎる成果かと思い、いったんここまでにしました。
Google Cloud
## 入力: Cloud ArmorでIP制限をして、それをロードバランサに適用し、
## ロードバランサのバックエンドはCloud RunでNGINXのコンテナを作成して、
## アクセスできるようにするterraformのコードを書いて欲しい
# Cloud ArmorでIP制限をして、それをロードバランサに適用し、ロードバランサのバックエンドはCloud RunでNGINXのコンテナを作成して、アクセスできるようにするterraformのコードを以下に示す。
# provider "google" {
# project = var.project
# region = var.region
# zone = var.zone
# }
# resource "google_compute_global_address" "lb_ip" {
# name = "lb-ip"
# purpose = "GLOBAL"
# address_type = "EXTERNAL"
# }
# resource "google_compute_firewall" "public" {
# name = "public-allow"
# network = "default"
# allow {
# protocol = "tcp"
# ports = ["80"]
# }
# source_ranges = [""] # IP制限をしたい場合は、IPアドレスを指定する
# }
# resource "google_cloud_run_service" "nginx" {
# name = "nginx"
# location = "asia-northeast1"
# template {
# spec {
# containers {
# image = "nginx:latest"
# }
# }
# }
# }
# resource "google_compute_backend_service" "nginx" {
# name = "nginx"
# protocol = "HTTP"
# timeout_sec = 10
# backend {
# group = google_cloud_run_service.nginx.status[0].url
# }
# }
# resource "google_compute_url_map" "url_map" {
# name = "url-map"
# default_service = google_compute_backend_service.nginx.self_link
# }
# resource "google_compute_target_http_proxy" "http_proxy" {
# name = "http-proxy"
# url_map = google_compute_url_map.url_map.self_link
# }
# resource "google_compute_global_forwarding_rule" "forwarding_rule" {
# name = "forwarding-rule"
# target = google_compute_target_http_proxy.http_proxy.self_link
# port_range = "80"
# }
# resource "google_compute_security_policy" "security_policy" {
# name = "security-policy"
# rule {
# action = "allow"
# priority = 1000
# match {
# config {
# src_ip_ranges = [""] # IP制限をしたい場合は、IPアドレスを指定する
# }
# }
# }
# }
# resource "google_compute_security_policy_association" "security_policy_association" {
# name = "security-policy-association"
# security_policy = google_compute_security_policy.security_policy.self_link
# target = google_compute_target_http_proxy.http_proxy.self_link
# }
# このterraformのコードでは、Cloud ArmorでIP制限をして、それをロードバランサに適用し、
# ロードバランサのバックエンドはCloud RunでNGINXのコンテナを作成して、アクセスできるようにしている。
# このterraformのコードを実行することで、Cloud ArmorでIP制限をして、それをロードバランサに適用し、
# ロードバランサのバックエンドはCloud RunでNGINXのコンテナを作成して、アクセスできるようにすることができる。
こちらもなんとなくそれっぽいものができたのですが、色々と足りないような気もします。
これを terraform plan
してみます。
╷
│ Error: Invalid resource type
│
│ on main.tf line 79, in resource "google_compute_security_policy_association" "security_policy_association":
│ 79: resource "google_compute_security_policy_association" "security_policy_association" {
│
│ The provider hashicorp/google does not support resource type "google_compute_security_policy_association".
╵
security_policy_association というresourceは存在しないと言われ、planすら通りません。
その後、何度も挑戦したのですが、ロードバランサに必要な構成要素が提案されることはありませんでした。
Google Cloudのロードバランサの構成要素自体が多いのもあるとは思いますが、ロードバランサを作って欲しい と伝えても、できることはありませんでした。
まとめ
GitHub Copilotは非常に便利なツールではあり、AWSのTerraformのコードはかなりいい提案をしてくれます。
しかし、Google CloudのTerraformのコードについては、曖昧な指示ではplanすら通らないものが提案されてきてしまうため、まだまだ参考程度にした方が良さそうです。
ですが、ChatGPT(GPT-4o)であれば精度の良い提案もされているので、GitHub Copilotの提案だけで書ける未来は近いと思っています。
(ただ、VS CodeのGitHub CopilotがGPT-4oベースになったと聞いていたのですが、なぜこの違いが生まれているのかまでは分かりませんでした)