Claude でhashicorp/agent-skillsを使って Terraform のモノリシック構成をモジュール化してみた

Claude でhashicorp/agent-skillsを使って Terraform のモノリシック構成をモジュール化してみた

2026.02.23

はじめに

クラウド事業本部、あきやまです。

2/2頃、hashicorpよりhashicorp agent skillsが発表されました。
今回は機能の一つであるrefactor-moduleを使用して既存のモノシリック構成をmodules構成にできるか試してみました。既存環境をmodules構成にしてみたいけど、煩雑で困っているという方の参考になればと思います。

https://github.com/hashicorp/agent-skills

HashiCorp Agent Skills とは

HashiCorp Agent Skills は terraform と packer の2つサービスに対してプラグインを提供しています。各プラグインには1~4つのスキルが設定されています。

スクリーンショット 2026-02-22 16.47.15

プラグイン スキル 概要
terraform-code-generation terraform-style-guide HashiCorp スタイルガイド準拠の HCL 生成
terraform-test .tftest.hcl テスト作成
azure-verified-modules Azure Verified Modules 準拠コード生成
terraform-module-generation refactor-module モノリシック構成 → モジュール化リファクタリング
terraform-stacks Terraform Stacks(マルチ環境オーケストレーション)
terraform-provider-development new-terraform-provider プロバイダースキャフォールディング
provider-resources リソース・データソース実装
provider-actions ライフサイクル操作実装
run-acceptance-tests 受け入れテスト実行・デバッグ
packer-builders aws-ami-builder amazon-ebs ビルダーで Amazon マシンイメージ (AMI) を構築する
azure-image-builder Azure マネージド イメージと Azure コンピューティング ギャラリー イメージを構築する
windows-builder WinRM と PowerShell を使用したプラットフォームに依存しない Windows イメージ パターン
packer-hcp push-to-registry hcp_packer_registry を設定して、ビルド メタデータを HCP Packer にプッシュします。

環境

項目 バージョン
OS macOS Tahoe 126.3
Claude Code 2.1.50
Terraform 1.14.3

準備

以下がインストール済みであることを前提とします。

  • Claude Code: AI アシスタント本体
  • Terraform CLI: モジュール化後の terraform plan 実行用

やってみた

Step 1: プラグインのインストール

まず Claude Code のプラグインマーケットプレイスから HashiCorp Agent Skills を追加します。

claude plugin marketplace add hashicorp/agent-skills

続いて、terraform-module-generation プラグインをインストールします。

claude plugin install terraform-module-generation@hashicorp

Step 2: 検証用のモノリシック構成を用意する

リファクタリング対象として、VPC・サブネット・セキュリティグループ・EC2 の典型的なモノリシック構成を用意します。

refactor-module/
∟ main.tf          # VPC + Subnet + SG + EC2 が全部入り

main.tfの内容は以下です。

main.tf
# main.tf - モノリシック構成(リファクタリング前)

terraform {
  required_version = ">= 1.1.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.region
}

# --- VPC ---
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name        = "${var.project}-vpc"
    Environment = var.environment
  }
}

# --- Subnet ---
resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = var.public_subnet_cidr
  availability_zone       = "${var.region}a"
  map_public_ip_on_launch = true

  tags = {
    Name        = "${var.project}-public-subnet"
    Environment = var.environment
  }
}

# --- Security Group ---
resource "aws_security_group" "web" {
  name_prefix = "${var.project}-web-"
  vpc_id      = aws_vpc.main.id

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

  ingress {
    from_port   = 443
    to_port     = 443
    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        = "${var.project}-web-sg"
    Environment = var.environment
  }
}

# --- EC2 Instance ---
resource "aws_instance" "web" {
  ami                    = var.ami_id
  instance_type          = var.instance_type
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.web.id]

  tags = {
    Name        = "${var.project}-web"
    Environment = var.environment
  }
}

# variables.tf

variable "region" {
  description = "AWS region"
  type        = string
  default     = "ap-northeast-1"
}

variable "project" {
  description = "Project name"
  type        = string
  default     = "demo"
}

variable "environment" {
  description = "Environment name"
  type        = string
  default     = "dev"
}

variable "vpc_cidr" {
  description = "VPC CIDR block"
  type        = string
  default     = "10.0.0.0/16"
}

variable "public_subnet_cidr" {
  description = "Public subnet CIDR block"
  type        = string
  default     = "10.0.1.0/24"
}

variable "ami_id" {
  description = "AMI ID for EC2 instance"
  type        = string
  default     = "ami-088103e734f7e0529" # /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-x86_64
}

variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t3.micro"
}

この構成の問題点を整理すると、次のとおりです。

  • 全リソースが 1 ファイルに集約されていて見通しが悪い
  • VPC 周りの構成を他プロジェクトで再利用できない
  • 環境ごとの差分管理(dev/stg/prod)が変数の切り替えだけでは限界がある
  • テストがない

Step 3: refactor-module でモジュール化する

Claude Code 上で、このモノリシック構成のディレクトリを開いた状態でrefactor-moduleと指示します。何度か実施したところ、どの程度、module化するのかなどの質問がClaudeから行われました。

Claude code 出力例
❯ refactor-module                                                                                    

⏺ Skill(terraform-module-generation:refactor-module)                                                 
  ⎿  Successfully loaded skill                                                                       

⏺ まず、既存のTerraform構成を分析します。                                                            

⏺ Explore(Explore Terraform codebase structure) Haiku 4.5                                            
  ⎿  Done (6 tool uses · 29.8k tokens · 33s)                                                         
  (ctrl+o to expand)                                                                                 

⏺ 分析が完了しました。現在の構成を整理します。                                                       

  現状分析                                                                                           

  項目: ファイル構成                                                                                 
  内容: main.tf 1ファイルにすべて集約(モノリシック)                                                
  ────────────────────────────────────────                                                           
  項目: リソース                                                                                     
  内容: aws_vpc, aws_subnet, aws_security_group, aws_instance (4リソース)                            
  ────────────────────────────────────────                                                           
  項目: 変数
  内容: 7個(region, project, environment, vpc_cidr, public_subnet_cidr, ami_id, instance_type)
  ────────────────────────────────────────
  項目: Output
  内容: なし
  ────────────────────────────────────────
  項目: State
  内容: 空(管理リソースなし) → State移行不要
  ────────────────────────────────────────
  項目: Provider
  内容: AWS v5.100.0 / Terraform >= 1.1.0

  依存関係

  aws_vpc.main
    ├── aws_subnet.public
    ├── aws_security_group.web
    │       └── aws_instance.web ← aws_subnet.public も参照

  リファクタリングの方針を確認させてください。                                                       

⏺ User answered Claude's questions:                                                                  
  ⎿  · モジュール分割の粒度をどうしますか? → 2モジュール構成 (Recommended)
     · 抽象化レベルをどうしますか? → intermediate (Recommended)                                     

⏺ 了解しました。2モジュール構成 + intermediate 抽象化レベルで進めます。                              

  リファクタリング計画                                                                               

  【Before】                     【After】                                                           
  main.tf (全部入り)      →     modules/                                                             
                                  network/    # VPC + Subnet + SG                                    
                                    main.tf / variables.tf / outputs.tf                              
                                  compute/    # EC2                                                  
                                    main.tf / variables.tf / outputs.tf                              
                                main.tf       # Root module呼び出し                                  
                                variables.tf  # Root変数                                             
                                outputs.tf    # Root出力                                             
                                versions.tf   # Provider制約                                         

  並列で3エージェントに実装を委託します。                                                            

⏺ 3 Task agents finished (ctrl+o to expand)                                                          
   ├─ Implement network module · 4 tool uses · 25.9k tokens                                          
   │  ⎿  Done                                                                                        
   ├─ Implement compute module · 7 tool uses · 25.3k tokens                                          
   │  ⎿  Done                                                                                        
   └─ Implement root configuration · 5 tool uses · 26.8k tokens                                      
      ⎿  Done                                                                                        
  ⎿  Found 5 new diagnostic issues in 1 file (ctrl+o to expand)                                      

⏺ 全ファイルの生成が完了しました。診断エラーはモジュール初期化前のため発生しています。生成されたコー 
  ドを検証します。                                                                                   

⏺ Explore(Validate all generated files) Haiku 4.5                                                    
  ⎿  Done (10 tool uses · 30.7k tokens · 20s)                                                        
  (ctrl+o to expand)                                                                                 

⏺ コードレビューは全項目 PASS です。terraform init と terraform validate で実行検証します。          

⏺ Bash(terraform init -upgrade 2>&1)                                                                 
  ⎿  Initializing the backend...                                                                     
     Upgrading modules...                                                                            
     - compute in modules/compute                                                                    
     … +14 lines (ctrl+o to expand)                                                                  

⏺ Bash(terraform validate 2>&1)                                                                      
  ⎿  Success! The configuration is valid.                                                            

⏺ Bash(terraform fmt -check -recursive -diff 2>&1)                                                   
  ⎿  (No output)                                                                                   

⏺ すべて通りました。リファクタリング完了です。                                                       

  結果サマリ                                                                                         

  ┌──────────────────────┬────────────────────────────┐                                              
  │       検証項目       │            結果            │                                            
  ├──────────────────────┼────────────────────────────┤         
  │ terraform init       │ OK                         │         
  ├──────────────────────┼────────────────────────────┤                                              
  │ terraform validate   │ OK                         │                                              
  ├──────────────────────┼────────────────────────────┤                                              
  │ terraform fmt -check │ OK(フォーマット差分なし) │                                              
  └──────────────────────┴────────────────────────────┘                                              

  生成ファイル構成

  refactor-module/
  ├── versions.tf          # Provider/Terraform版制約
  ├── variables.tf         # Root変数(7個、元のデフォルト値を保持)
  ├── main.tf              # Provider + module "network" / "compute" 呼び出し
  ├── outputs.tf           # 5つのoutput(vpc_id, subnet_id, sg_id, instance_id, public_ip)
  └── modules/
      ├── network/         # VPC + Subnet + Security Group
      │   ├── main.tf      # 3リソース(dynamic ingress block付き)
      │   ├── variables.tf # 7変数(CIDR validation付き)
      │   └── outputs.tf   # 4 output
      └── compute/         # EC2 Instance
          ├── main.tf      # 1リソース
          ├── variables.tf # 6変数
          └── outputs.tf   # 3 output

  主な改善点

  - 分離: ネットワーク層とコンピュート層を独立モジュール化
  - 再利用性: 各モジュールを別プロジェクトでも使い回し可能
  - 柔軟性: ingress_rules を dynamic block で可変化、tags は merge パターン
  - 安全性: CIDR 変数に validation ブロックを追加   

Step 4: 生成されたモジュール構成を確認する

refactor-module が生成する典型的な出力を確認していきます。

ディレクトリ構成

  refactor-module/
  ├── versions.tf          
  ├── variables.tf         
  ├── main.tf
  ├── moved.tf    
  ├── outputs.tf           
  └── modules/
      ├── network/         
      │   ├── main.tf     
      │   ├── variables.tf 
      │   └── outputs.tf   
      └── compute/         
          ├── main.tf      
          ├── variables.tf 
          └── outputs.tf   

ルートモジュール(main.tf)

main.tf
main.tf
provider "aws" {
  region = var.region
}

module "network" {
  source = "./modules/network"

  name              = "${var.project}-${var.environment}"
  vpc_cidr          = var.vpc_cidr
  subnet_cidr       = var.public_subnet_cidr
  availability_zone = "${var.region}a"

  tags = {
    Project     = var.project
    Environment = var.environment
  }
}

module "compute" {
  source = "./modules/compute"

  name                   = "${var.project}-${var.environment}-web"
  ami_id                 = var.ami_id
  instance_type          = var.instance_type
  subnet_id              = module.network.subnet_id
  vpc_security_group_ids = [module.network.security_group_id]

  tags = {
    Project     = var.project
    Environment = var.environment
  }
}

modules/network/main.tf

main.tf
resource "aws_vpc" "this" {
  cidr_block           = var.vpc_cidr
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = merge(var.tags, {
    Name = "${var.name}-vpc"
  })
}

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.this.id
  cidr_block              = var.subnet_cidr
  availability_zone       = var.availability_zone
  map_public_ip_on_launch = var.map_public_ip_on_launch

  tags = merge(var.tags, {
    Name = "${var.name}-public-subnet"
  })
}

resource "aws_security_group" "this" {
  name_prefix = "${var.name}-web-"
  vpc_id      = aws_vpc.this.id

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

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

  tags = merge(var.tags, {
    Name = "${var.name}-web-sg"
  })
}

modules/network/variables.tf

variables.tf
variable "name" {
  description = "Name prefix for all resources created by this module."
  type        = string
}

variable "vpc_cidr" {
  description = "CIDR block for the VPC."
  type        = string

  validation {
    condition     = can(cidrhost(var.vpc_cidr, 0))
    error_message = "vpc_cidr must be a valid IPv4 CIDR block (e.g. \"10.0.0.0/16\")."
  }
}

variable "subnet_cidr" {
  description = "CIDR block for the public subnet."
  type        = string

  validation {
    condition     = can(cidrhost(var.subnet_cidr, 0))
    error_message = "subnet_cidr must be a valid IPv4 CIDR block (e.g. \"10.0.1.0/24\")."
  }
}

variable "availability_zone" {
  description = "Availability zone in which the public subnet will be created."
  type        = string
}

variable "map_public_ip_on_launch" {
  description = "Whether instances launched into the public subnet receive a public IP address automatically."
  type        = bool
  default     = true
}

variable "ingress_rules" {
  description = "List of ingress rules to apply to the web security group."
  type = list(object({
    from_port   = number
    to_port     = number
    protocol    = string
    cidr_blocks = list(string)
  }))
  default = [
    {
      from_port   = 80
      to_port     = 80
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    },
    {
      from_port   = 443
      to_port     = 443
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    },
  ]
}

variable "tags" {
  description = "Map of tags to apply to all resources. Resource-specific Name tags are merged on top."
  type        = map(string)
  default     = {}
}

modules/network/outputs.tf

outputs.tf
output "vpc_id" {
  description = "The ID of the VPC."
  value       = aws_vpc.this.id
}

output "vpc_cidr_block" {
  description = "The primary CIDR block of the VPC."
  value       = aws_vpc.this.cidr_block
}

output "subnet_id" {
  description = "The ID of the public subnet."
  value       = aws_subnet.public.id
}

output "security_group_id" {
  description = "The ID of the web security group."
  value       = aws_security_group.this.id
}

modules/compute/main.tf

main.tf
# modules/compute/main.tf
resource "aws_instance" "this" {
  ami                    = var.ami_id
  instance_type          = var.instance_type
  subnet_id              = var.subnet_id
  vpc_security_group_ids = var.vpc_security_group_ids

  tags = merge(var.tags, {
    Name = var.name
  })
}

modules/compute/variables.tf

variables.tf
variable "name" {
  description = "Name prefix for the instance"
  type        = string
}

variable "ami_id" {
  description = "AMI ID for the EC2 instance"
  type        = string
}

variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t3.micro"
}

variable "subnet_id" {
  description = "Subnet ID to launch the instance in"
  type        = string
}

variable "vpc_security_group_ids" {
  description = "List of security group IDs"
  type        = list(string)
}

variable "tags" {
  description = "Tags to apply to the instance"
  type        = map(string)
  default     = {}
}

modules/compute/outputs.tf

outputs.tf
output "instance_id" {
  description = "The ID of the EC2 instance"
  value       = aws_instance.this.id
}

output "instance_public_ip" {
  description = "The public IP address of the EC2 instance"
  value       = aws_instance.this.public_ip != null ? aws_instance.this.public_ip : ""
}

output "instance_private_ip" {
  description = "The private IP address of the EC2 instance"
  value       = aws_instance.this.private_ip
}

moved.tf(ステート移行)

モジュール化の際に見落としがちなのがステート移行です。refactor-module はここも自動で面倒を見てくれました。

moved.tf
# moved.tf - 既存ステートの移行

moved {
  from = aws_vpc.main
  to   = module.networking.aws_vpc.main
}

moved {
  from = aws_subnet.public
  to   = module.networking.aws_subnet.public
}

moved {
  from = aws_security_group.web
  to   = module.networking.aws_security_group.web
}

moved {
  from = aws_instance.web
  to   = module.compute.aws_instance.web
}

moved ブロックは Terraform 1.1 以降で利用できる機能です。
terraform state mv コマンドを使わずに、コード内でステート移行を
宣言的に記述できます。terraform plan を実行すると、リソースの
再作成なしに移行が行われることを事前確認できます。
その後、Planを実施し無事、差分がないことが確認できました。

まとめ

今回はそもそものリソース数も少なかったため、比較的スムーズに
modules化を行うことができました。何度か検証したところAIからの
質問には(もちろん)若干の差異がありました。しかしmodulesの粒度
についての質問は全件ありましたので、ご興味ある方はぜひお試しください。

注意点: Skills はあくまで知識提供であり、生成されたコードの
正しさは必ずレビューが必要です。また、検証時点では Claude Code の
プラグイン機能がプレビュー段階の場合があります。最新の対応状況は
公式ドキュメントをご確認ください。

関連リンク

この記事をシェアする

FacebookHatena blogX

関連記事