🙆

SRE こそ OpenHands 使ってみな 飛ぶぞ

2025/02/28に公開

こんにちは。
ご機嫌いかがでしょうか。
"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.

https://github.com/All-Hands-AI/OpenHands

https://docs.all-hands.dev/modules/usage/getting-started

セットアップ

ローカル 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 ファイルにクレデンシャルを記述します。

.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」と入れました。文字列であれば何でも良さそうです。

alt text

GitHub Settings です。GitHub Token を入れます。

alt text

UI やチャットでの会話を日本語にすることもできそうです。
精度を求めるなら英語のほうが良さそうです。お好みで。

alt text

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 コードが格納されているリポジトリを指定します。

alt text

チャット欄には以下を入力します。

`modules` のディレクトリ配下に `aurora` というモジュールを作成し、
AWS Aurora を作成するためのコードを作成してください。

スレッドが開始します。会話してくれますね。実行を何回か繰り返しています。

alt text

2〜3分待つとコードが生成されました。それっぽいコードです。いいですね〜

クリックして展開
main.tf
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.tf
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.tf
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 も作ってくれます。

alt text


インポート用の tf ファイルも作ってもらいましょう。
自分が作ったリソースに合わせた Terraform コードにしないと使い物にはなりません。AI に対してあいまいな指示はご法度です。
ですので、確かに実際の設定値をコードへ反映させるために AWS CLI で実機の値をファイルに保存、それを読み込んでもらいます。

稼働中の Aurora クラスターとインスタンスをインポートしたいです。
`unstable` 以下にモジュールを使ったパラメータを記載してください。
稼働中の Aurora クラスターの設定値は `clioutput/db-clusters.out`、
インスタンスの設定は `clioutput/db-instances.out`を参照ください。
また、必要に応じて `modules/aurora` 以下も修正ください。

プロンプトに沿って、実行・編集・読み取りを繰り返しています。実際の設定値に合わせてくれる感じが伝わってきます。

alt text


modules/auroraunstable/aurora.tf がとりあえず完成しました。
インポート用の unstable/import.tf も作ってもらいましょう。

ありがとうございます。
インポートするには `unstable/import.tf` に既存リソースの記述が必要です。
`clioutput/db-clusters.out`、 `clioutput/db-instances.out` を読み込んで適切に記述をお願いします。

alt text

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=(以下省略)

alt text

はい、狙い通り terraform plan の出力をみて tf ファイルを修正してくれています。仲良し。

alt text


何度かのやりとりの後、ひと通りの修正が終わったようです。
変更内容を見たいので以下をお願いしてみます。

ありがとうございます。今回の変更内容のDiffを表示してください。

alt text

上は省略してますが、すべての変更内容が表示されています。
問題なさそうなのでリポジトリへ 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"
}

alt text

なるほど、修正の途中でデグレが発生したようです。人間がやっても起こりうることです。

その記述を再度追加したいので新しいモジュールに合わせた形式で `unstable/import.tf`へ追記をお願いします。

alt text

alt text

修正を Push してもらいました。
最後にねぎらいの言葉をかけて終了です。

おまけ

私の方で修正を差し込んだり、CI で Lint をしている関係でちょくちょくコンフリクトが発生します。
コンフリクトを検出して自動的に解決してくれました。

alt text

所感

すごい・・・親父が熱中するわけだ。

X で Devin や Cursor が騒がれている理由がよく分かりました。
Terraform でのインポートという比較的定型なコード生成ではありましたが、かなりの完成度だったと思います。
ゼロから書いているよりも 50〜70%の時間で完了しました。効率化の効果は大きいです。
「黙って実行すれば動く」コードにはなりませんが、人間が微修正すれば良いレベルにはなっています。

追加検証はしていませんが、今回の重要な要素は以下だと考えます。

  • 参照となる既存のコードがあった
    • スクショは貼っていないですが、既存の Terraform コードを読み込んでからコード生成を開始していました
  • パラメータとなる実際の設定を渡していた
    • インポートというタスクの特性上、設定値は不可欠
  • 可能な限り具体的なプロンプトを書いた
    • あいまいな指示だと、期待から外れた行動をしてしまいます
    • Prompting Best Practices 参考にしながらプロンプトを書くと良いかも

最後に余談ですが、私のリポジトリでは AI PR Reviewer を導入しています。
OpenHands が PR を作ると「AI が作ったコードを AI がレビューする」という近未来が垣間見えます。

Discussion

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy