AWS Bedrockを利用して、AWSの日本国内に閉じてClaude Codeを利用しよう!!
みなさん、Claude Codeは利用していますか?私はほぼ毎日使っています。
ClaudeがAWS Marketplace経由で購入できる、Claude for Enterprise Premium Seats with Claude Code Now Available in AWS Marketplaceの発表があり、個人的に盛り上がっている今日この頃です。
突然ですが、のっぴきならない事情でAWSの日本国内に閉じた形でClaude Codeを利用したいと思ったことはないでしょうか?
思ったことがある!という方にはこの記事はピッタリだと思います。
サマリ
この記事を読むと、図のように閉域でClaude Codeを利用する環境を構築できます。
前提
- Terraformがインストールされていること
- 適切なIAM権限が設定されていること
- 本記事によって生じた一切の損害は自己責任でお願いします
Let's 構築
1. Node, npm, Claude Code導入済みAMI作成
Amazon Linux 2023のAMIを利用してEC2を立ち上げ、以下コマンドを実行してNode, npm, Claude Codeを導入しましょう。導入が完了したらAMIを取得しましょう。
※AMI作成部分はごまんと作ってみた記事があると思うので、ざっくりとした手順を記載しますので不明点はお近くの生成AIにお尋ねください。
## Update all packages
sudo dnf update -y
## Install Node, npm
sudo dnf install nodejs -y
## Install Claude Code
### npmのグローバルディレクトリをユーザーホームに設定
mkdir -p ~/.npm-global
npm config set prefix '~/.npm-global'
### PATHに追加
echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
### Install Claude Code
npm install -g @anthropic-ai/claude-code
2. 閉域環境構築
以下のTerraformをapplyしましょう。
AMI IDを聞かれるので1.
で作成したAMI IDを入力しましょう。
terraform {
required_version = "= 1.13.4"
required_providers {
aws = {
source = "hashicorp/aws"
version = "= 6.16.0"
}
tls = {
source = "hashicorp/tls"
version = "= 4.1.0"
}
local = {
source = "hashicorp/local"
version = "= 2.5.3"
}
}
}
variable "region" {
type = string
description = "AWS region to deploy resources into"
default = "ap-northeast-1"
}
variable "project" {
type = string
description = "Project identifier used for tagging and naming"
default = "dev-bedrock"
}
variable "vpc_cidr" {
type = string
description = "CIDR block for the primary VPC"
default = "10.20.0.0/16"
}
variable "ec2_subnet_cidr" {
type = string
description = "CIDR block for the EC2 application subnet"
default = "10.20.1.0/24"
}
variable "endpoint_subnet_cidr" {
type = string
description = "CIDR block for the VPC endpoints subnet"
default = "10.20.2.0/24"
}
variable "subnet_az_ids" {
type = map(string)
description = "Optional overrides for subnet availability zone IDs (keys: ec2, endpoints)"
default = {}
}
variable "ami_id" {
type = string
description = "AMI ID for EC2 instance"
# default = "ami-003be359fed80aec8" ## Sample
}
provider "aws" {
region = var.region
}
data "aws_availability_zones" "available" {
state = "available"
filter {
name = "zone-type"
values = ["availability-zone"]
}
}
data "aws_caller_identity" "current" {}
locals {
tags = {
Project = var.project
ManagedBy = "Terraform"
}
interface_endpoints = {
ssm = "com.amazonaws.ap-northeast-1.ssm"
ssmmessages = "com.amazonaws.ap-northeast-1.ssmmessages"
ec2messages = "com.amazonaws.ap-northeast-1.ec2messages"
bedrock_runtime = "com.amazonaws.ap-northeast-1.bedrock-runtime"
}
resolved_subnet_az_ids = {
ec2 = lookup(var.subnet_az_ids, "ec2", element(data.aws_availability_zones.available.zone_ids, 0))
endpoints = lookup(var.subnet_az_ids, "endpoints", element(data.aws_availability_zones.available.zone_ids, 1))
}
}
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = merge(local.tags, {
Name = "${var.project}-vpc"
})
}
resource "aws_subnet" "ec2" {
vpc_id = aws_vpc.main.id
cidr_block = var.ec2_subnet_cidr
availability_zone_id = local.resolved_subnet_az_ids.ec2
map_public_ip_on_launch = false
tags = merge(local.tags, {
Name = "${var.project}-ec2-subnet"
})
}
resource "aws_subnet" "endpoints" {
vpc_id = aws_vpc.main.id
cidr_block = var.endpoint_subnet_cidr
availability_zone_id = local.resolved_subnet_az_ids.endpoints
map_public_ip_on_launch = false
tags = merge(local.tags, {
Name = "${var.project}-endpoint-subnet"
})
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
tags = merge(local.tags, {
Name = "${var.project}-private-rt"
})
}
resource "aws_route_table_association" "ec2" {
subnet_id = aws_subnet.ec2.id
route_table_id = aws_route_table.private.id
}
resource "aws_route_table_association" "endpoints" {
subnet_id = aws_subnet.endpoints.id
route_table_id = aws_route_table.private.id
}
resource "aws_security_group" "ec2" {
name = "${var.project}-ec2-sg"
description = "Restrict EC2 instance egress to HTTPS within VPC"
vpc_id = aws_vpc.main.id
egress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [aws_vpc.main.cidr_block]
}
tags = merge(local.tags, {
Name = "${var.project}-ec2-sg"
})
}
resource "aws_security_group" "endpoints" {
name = "${var.project}-endpoint-sg"
description = "Allow VPC resources to reach interface endpoints"
vpc_id = aws_vpc.main.id
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [aws_vpc.main.cidr_block]
}
tags = merge(local.tags, {
Name = "${var.project}-endpoint-sg"
})
}
data "aws_iam_policy_document" "ec2_assume" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["ec2.amazonaws.com"]
}
}
}
resource "aws_iam_role" "ec2" {
name = "${var.project}-ec2-iam-role"
assume_role_policy = data.aws_iam_policy_document.ec2_assume.json
tags = local.tags
}
resource "aws_iam_role_policy_attachment" "ssm_core" {
role = aws_iam_role.ec2.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
data "aws_iam_policy_document" "bedrock_minimal" {
statement {
effect = "Allow"
actions = [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream"
]
resources = [
"arn:aws:bedrock:ap-northeast-1:${data.aws_caller_identity.current.account_id}:inference-profile/jp.anthropic.claude-haiku-4-5-20251001-v1:0",
"arn:aws:bedrock:ap-northeast-1::foundation-model/anthropic.claude-haiku-4-5-20251001-v1:0",
"arn:aws:bedrock:ap-northeast-3::foundation-model/anthropic.claude-haiku-4-5-20251001-v1:0"
]
}
}
resource "aws_iam_policy" "bedrock_minimal" {
name = "${var.project}-bedrock-minimal-policy"
description = "Minimal policy for EC2 instance to invoke Bedrock models"
policy = data.aws_iam_policy_document.bedrock_minimal.json
tags = local.tags
}
resource "aws_iam_role_policy_attachment" "bedrock_minimal" {
role = aws_iam_role.ec2.name
policy_arn = aws_iam_policy.bedrock_minimal.arn
}
resource "aws_iam_instance_profile" "ec2" {
name = "${var.project}-ec2-instance-profile"
role = aws_iam_role.ec2.name
}
resource "tls_private_key" "ec2_ssh" {
algorithm = "RSA"
rsa_bits = 4096
}
resource "aws_key_pair" "ec2_ssh" {
key_name = "${var.project}-ec2-key"
public_key = tls_private_key.ec2_ssh.public_key_openssh
tags = local.tags
}
resource "local_file" "ssh_private_key" {
content = tls_private_key.ec2_ssh.private_key_pem
filename = "${path.module}/${var.project}-ec2-key.pem"
file_permission = "0600"
}
resource "aws_vpc_endpoint" "interface" {
for_each = local.interface_endpoints
vpc_id = aws_vpc.main.id
service_name = each.value
vpc_endpoint_type = "Interface"
private_dns_enabled = true
subnet_ids = [aws_subnet.endpoints.id]
security_group_ids = [aws_security_group.endpoints.id]
tags = merge(local.tags, {
Name = "${var.project}-${each.key}-endpoint"
})
}
resource "aws_instance" "dev" {
ami = var.ami_id
instance_type = "t3.large"
subnet_id = aws_subnet.ec2.id
vpc_security_group_ids = [aws_security_group.ec2.id]
iam_instance_profile = aws_iam_instance_profile.ec2.name
key_name = aws_key_pair.ec2_ssh.key_name
associate_public_ip_address = false
metadata_options {
http_endpoint = "enabled"
http_tokens = "required"
}
tags = merge(local.tags, {
Name = "${var.project}-dev-ec2"
})
}
output "vpc_id" {
value = aws_vpc.main.id
}
output "ec2_instance_id" {
value = aws_instance.dev.id
}
output "interface_endpoint_ids" {
value = { for name, endpoint in aws_vpc_endpoint.interface : name => endpoint.id }
}
output "ssh_private_key_path" {
description = "Path to the SSH private key file"
value = local_file.ssh_private_key.filename
}
output "ssm_connect_command" {
description = "Command to connect to EC2 instance via SSM Session Manager"
value = "aws ssm start-session --target ${aws_instance.dev.id} --region ${var.region}"
}
output "vscode_ssh_config" {
description = "SSH config entry for VS Code Remote-SSH connection via SSM"
value = <<-EOT
# Add this to your ~/.ssh/config file:
Host ${var.project}-ec2
HostName ${aws_instance.dev.id}
User ec2-user
IdentityFile ${abspath(local_file.ssh_private_key.filename)}
ProxyCommand sh -c "aws ssm start-session --target %h --document-name AWS-StartSSHSession --region ${var.region}"
EOT
}
VS CodeでEC2に接続したい方は、Terraform applyした際のoutputに記載されているconfigを~/.ssh/config
に追記しましょう。configがない場合は新規作成してください。
3. EC2接続
TerraformのOutputに記載されているssmコマンドでEC2に接続してください。
4. Claude Codeとのご対面
以下のコマンドを実行することでClaude Codeが立ち上がります。
## bashrc読み込み
source ~/.bashrc
## 環境変数設定
export CLAUDE_CODE_USE_BEDROCK=1
export ANTHROPIC_MODEL=jp.anthropic.claude-haiku-4-5-20251001-v1:0
export AWS_REGION='ap-northeast-1'
## Claude 立ち上げ
claude
/model
でmodelを確認するとjp~から始まるHaiku 4.5が利用されているのがわかると思います。
なお、許可していないOpusを利用しようとするとAPI Errorとなります。
おわり
簡単に環境構築ができたのではないでしょうか?
のっぴきならない事情がある方は色々と大変かとは思います。そんな時もテクノロジーを駆使して課題を解決していきましょう!!
この記事を読んだ方が少しでも参考になったと思ってくださったら幸いです。
Discussion