SRE こそ OpenHands 使ってみな 飛ぶぞ
こんにちは。
ご機嫌いかがでしょうか。
"No human labor is no human error" が大好きな吉井 亮です。
Software Development AI Agent 流行していますね。SDAA と略す人はいなさそうですが、SDAA という略称が広まってほしいものです。
そのような流れに乗るべく、SRE とされている私も SDAA を使いこなしたいと思います。
Devin が有名ですが、スモールスタートしたかったので OSS の OpenHands を使ってみます。
OpenHands が何をしてくれるかは公式リポジトリから引用します。
OpenHands agents can do anything a human developer can: modify code, run commands, browse the web, call APIs, and yes—even copy code snippets from StackOverflow.
Important
Using OpenHands for work? We'd love to chat! Fill out this short form to join our Design Partner program, where you'll get early access to commercial features and the opportunity to provide input on our product roadmap.
セットアップ
ローカル PC で OpenHands を実行するためのセットアップを行います。
モデル
モデルは Bedrock の anthropic.claude-3-5-sonnet-20241022-v2:0
を使います。
モデルを有効にしておきます。手順は Add or remove access to Amazon Bedrock foundation models に従います。
Docker Compose
公式の手順では Docker コマンドが案内されていますが、個人的な好みで Docker Compose を使います。
ディレクトリは以下のようにしました。
.
├── compose.yaml
├── data
└── openhands-state
compose.yaml は以下のようになります。
WORKSPACE_MOUNT_PATH はフルパス指定が必要でした。
AWS_REGION_NAME はお使いのリージョンです。
services:
openhands:
image: docker.all-hands.dev/all-hands-ai/openhands:0.26
env_file:
- .env
ports:
- "3000:3000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./openhands-state:/.openhands-state
- ./data:/workspace
environment:
- SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.26-nikolaik
- LOG_ALL_EVENTS=true
- WORKSPACE_MOUNT_PATH=/openhands/data
- AWS_REGION_NAME=us-west-2
.env ファイルにクレデンシャルを記述します。
AWS_SECRET_ACCESS_KEY=your_secret_access_key
AWS_ACCESS_KEY_ID=your_access_key_id
GitHub Token
OpenHands に GitHub Token を登録しておくと、GitHub のリポジトリを操作できるようになります。
OpenHands が生成したコードを Push することもできます。PR も作ってくれます。
Developer Settings から Token を取得します。
詳しくは GitHub Token Setup を参照ください。
起動
起動しましょう。
docker-compose up -d
http://localhost:3000
にアクセスすると GUI が表示されます。
初回は AI Provider を設定する画面が出ますが、Bedrock はここで設定できないのでスキップします。
左下に歯車アイコンがあるのでそこをクリックします。
LLM Settings に Bedrock のモデルを設定します。画像のように設定してください。
注意点があります。API Key
に何かの文字列を入れてください。空欄だとうまくモデルを呼び出してくれません。私は「AWS」と入れました。文字列であれば何でも良さそうです。
GitHub Settings です。GitHub Token を入れます。
UI やチャットでの会話を日本語にすることもできそうです。
精度を求めるなら英語のほうが良さそうです。お好みで。
Terraform で使ってみた
前提
OpenHands の準備が終わったので使っていきます。
個人で稼働しているシステムがありまして、そのリソースが Terraform 化の途中だったのでお手伝いしてもらいます。
単純な ELB - ECS - Aurora の構成だと考えてください。ELB と ECS は Terraform 化していたのですが、Aurora は未着手な状態です。
OpenHands に Terraform コードを書いてもらい、インポートの一歩手前まで準備してもらいます。
Terraform ディレクトリ構造は以下のようになっています。
.
├── unstable
│ ├── import.tf
│ ├── provider.tf
│ ├── variables.tf
│ ├── elb.tf
│ ├── ecs.tf
│ └── aurora.tf # OpenHands で作成してもらう
└── modules
└── elb
├── main.tf
├── output.tf
└── variable.tf
└── ecs
├── main.tf
├── output.tf
└── variable.tf
└── aurora # OpenHands で作成してもらう
├── main.tf # OpenHands で作成してもらう
├── output.tf # OpenHands で作成してもらう
└── variable.tf # OpenHands で作成してもらう
OpenHands やってみた
OpenHands UI のホーム画面です。
リポジトリを開く
で既存の Terraform コードが格納されているリポジトリを指定します。
チャット欄には以下を入力します。
`modules` のディレクトリ配下に `aurora` というモジュールを作成し、
AWS Aurora を作成するためのコードを作成してください。
スレッドが開始します。会話してくれますね。実行を何回か繰り返しています。
2〜3分待つとコードが生成されました。それっぽいコードです。いいですね〜
クリックして展開
resource "aws_rds_cluster" "aurora_cluster" {
cluster_identifier = var.cluster_identifier
engine = var.engine
engine_version = var.engine_version
availability_zones = data.aws_availability_zones.available.names
database_name = var.database_name
master_username = var.master_username
master_password = var.master_password
backup_retention_period = var.backup_retention_period
preferred_backup_window = var.preferred_backup_window
vpc_security_group_ids = [aws_security_group.aurora_sg.id]
db_subnet_group_name = aws_db_subnet_group.aurora_subnet_group.name
skip_final_snapshot = var.skip_final_snapshot
tags = var.tags
}
resource "aws_rds_cluster_instance" "aurora_instances" {
count = var.instance_count
identifier = "${var.cluster_identifier}-${count.index + 1}"
cluster_identifier = aws_rds_cluster.aurora_cluster.id
instance_class = var.instance_class
engine = aws_rds_cluster.aurora_cluster.engine
engine_version = aws_rds_cluster.aurora_cluster.engine_version
tags = var.tags
}
resource "aws_db_subnet_group" "aurora_subnet_group" {
name = "${var.cluster_identifier}-subnet-group"
subnet_ids = var.subnet_ids
tags = var.tags
}
resource "aws_security_group" "aurora_sg" {
name_prefix = "${var.cluster_identifier}-sg"
vpc_id = var.vpc_id
ingress {
from_port = 3306
to_port = 3306
protocol = "tcp"
cidr_blocks = var.allowed_cidr_blocks
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = var.tags
}
data "aws_availability_zones" "available" {
state = "available"
}
variable "cluster_identifier" {
description = "The cluster identifier for the Aurora cluster"
type = string
}
variable "engine" {
description = "The name of the database engine to be used for this DB cluster"
type = string
default = "aurora-mysql"
}
variable "engine_version" {
description = "The database engine version"
type = string
}
variable "instance_class" {
description = "The instance class to use for the Aurora instances"
type = string
}
variable "instance_count" {
description = "The number of instances to create in the cluster"
type = number
default = 2
}
variable "database_name" {
description = "Name for an automatically created database on cluster creation"
type = string
}
variable "master_username" {
description = "Username for the master DB user"
type = string
}
variable "master_password" {
description = "Password for the master DB user"
type = string
}
variable "vpc_id" {
description = "VPC ID to create the cluster in"
type = string
}
variable "subnet_ids" {
description = "A list of VPC subnet IDs to create the cluster in"
type = list(string)
}
variable "allowed_cidr_blocks" {
description = "A list of CIDR blocks that are allowed to access the database"
type = list(string)
default = []
}
variable "backup_retention_period" {
description = "The days to retain backups for"
type = number
default = 7
}
variable "preferred_backup_window" {
description = "The daily time range during which automated backups are created"
type = string
default = "02:00-03:00"
}
variable "skip_final_snapshot" {
description = "Determines whether a final DB snapshot is created before the DB cluster is deleted"
type = bool
default = false
}
variable "tags" {
description = "A map of tags to add to all resources"
type = map(string)
default = {}
}
output "cluster_endpoint" {
description = "The cluster endpoint"
value = aws_rds_cluster.aurora_cluster.endpoint
}
output "cluster_reader_endpoint" {
description = "The cluster reader endpoint"
value = aws_rds_cluster.aurora_cluster.reader_endpoint
}
output "cluster_id" {
description = "The ID of the cluster"
value = aws_rds_cluster.aurora_cluster.id
}
output "cluster_resource_id" {
description = "The Resource ID of the cluster"
value = aws_rds_cluster.aurora_cluster.cluster_resource_id
}
output "cluster_instances" {
description = "A list of all cluster instance IDs"
value = aws_rds_cluster_instance.aurora_instances[*].id
}
output "security_group_id" {
description = "The ID of the security group created for the Aurora cluster"
value = aws_security_group.aurora_sg.id
}
output "db_subnet_group_name" {
description = "The name of the DB subnet group created for the Aurora cluster"
value = aws_db_subnet_group.aurora_subnet_group.name
}
編集が終わると Push ボタンが表示されます。PullRequest も作ってくれます。
インポート用の tf ファイルも作ってもらいましょう。
自分が作ったリソースに合わせた Terraform コードにしないと使い物にはなりません。AI に対してあいまいな指示はご法度です。
ですので、確かに実際の設定値をコードへ反映させるために AWS CLI で実機の値をファイルに保存、それを読み込んでもらいます。
稼働中の Aurora クラスターとインスタンスをインポートしたいです。
`unstable` 以下にモジュールを使ったパラメータを記載してください。
稼働中の Aurora クラスターの設定値は `clioutput/db-clusters.out`、
インスタンスの設定は `clioutput/db-instances.out`を参照ください。
また、必要に応じて `modules/aurora` 以下も修正ください。
プロンプトに沿って、実行・編集・読み取りを繰り返しています。実際の設定値に合わせてくれる感じが伝わってきます。
modules/aurora
と unstable/aurora.tf
がとりあえず完成しました。
インポート用の unstable/import.tf
も作ってもらいましょう。
ありがとうございます。
インポートするには `unstable/import.tf` に既存リソースの記述が必要です。
`clioutput/db-clusters.out`、 `clioutput/db-instances.out` を読み込んで適切に記述をお願いします。
Terraform plan
Terraform import をした経験がある方は解ってくれると思いますが、インポートは苦行です笑。
既存リソースに合わせてコードを書くストレスは表現しようがありません。独り言が増えます。
それが OpenHands に任せるとここまで10分強です。ストレスもありません。なんてことでしょう!
それではついにきました terraform plan
を手動実行してみます。
$ terraform plan
╷
│ Error: Unsupported argument
│
│ on aurora.tf line 7, in module "aurora":
│ 7: availability_zones = ["ap-northeast-1c", "ap-northeast-1a", "ap-northeast-1d"]
│
│ An argument named "availability_zones" is not expected here.
╵
╷
│ Error: Unsupported argument
│
│ on aurora.tf line 9, in module "aurora":
│ 9: instances = 1
│
│ An argument named "instances" is not expected here.
╵
(以下省略)
ズコーーー
まあ予想通りです。いきなりすべてが完璧に完了するわけがありません。
エラーの解消は terraform plan
を流しながら修正してもらいしょう。
プロンプトで AWS クレデンシャルを渡し terraform plan
を実行してもらいます。
次のクレデンシャルを使って `terraform plan`を試してください
AWS_REGION=ap-northeast-1
AWS_SECRET_ACCESS_KEY=(以下省略)
はい、狙い通り terraform plan
の出力をみて tf ファイルを修正してくれています。仲良し。
何度かのやりとりの後、ひと通りの修正が終わったようです。
変更内容を見たいので以下をお願いしてみます。
ありがとうございます。今回の変更内容のDiffを表示してください。
上は省略してますが、すべての変更内容が表示されています。
問題なさそうなのでリポジトリへ Push してもらいます。
Push してもらったのですが、いまいち反映されていないファイルがありました。
記述が不足している箇所を調査してもらいます。
もう1度確認させてください。
以下の記述が含まれるファイル名をフルパスで教えてください。
# Aurora DB Cluster
import {
to = module.aurora.aws_rds_cluster.this
id = "name"
}
# Aurora DB Instance
import {
to = module.aurora.aws_rds_cluster_instance.this[0]
id = "name-instance"
}
なるほど、修正の途中でデグレが発生したようです。人間がやっても起こりうることです。
その記述を再度追加したいので新しいモジュールに合わせた形式で `unstable/import.tf`へ追記をお願いします。
修正を Push してもらいました。
最後にねぎらいの言葉をかけて終了です。
おまけ
私の方で修正を差し込んだり、CI で Lint をしている関係でちょくちょくコンフリクトが発生します。
コンフリクトを検出して自動的に解決してくれました。
所感
すごい・・・親父が熱中するわけだ。
X で Devin や Cursor が騒がれている理由がよく分かりました。
Terraform でのインポートという比較的定型なコード生成ではありましたが、かなりの完成度だったと思います。
ゼロから書いているよりも 50〜70%の時間で完了しました。効率化の効果は大きいです。
「黙って実行すれば動く」コードにはなりませんが、人間が微修正すれば良いレベルにはなっています。
追加検証はしていませんが、今回の重要な要素は以下だと考えます。
- 参照となる既存のコードがあった
- スクショは貼っていないですが、既存の Terraform コードを読み込んでからコード生成を開始していました
- パラメータとなる実際の設定を渡していた
- インポートというタスクの特性上、設定値は不可欠
- 可能な限り具体的なプロンプトを書いた
- あいまいな指示だと、期待から外れた行動をしてしまいます
- Prompting Best Practices 参考にしながらプロンプトを書くと良いかも
最後に余談ですが、私のリポジトリでは AI PR Reviewer を導入しています。
OpenHands が PR を作ると「AI が作ったコードを AI がレビューする」という近未来が垣間見えます。
Discussion