😇

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を利用する環境を構築できます。

image

前提

  • 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に接続してください。

image

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

image

/modelでmodelを確認するとjp~から始まるHaiku 4.5が利用されているのがわかると思います。

image

なお、許可していないOpusを利用しようとするとAPI Errorとなります。
image

おわり

簡単に環境構築ができたのではないでしょうか?

のっぴきならない事情がある方は色々と大変かとは思います。そんな時もテクノロジーを駆使して課題を解決していきましょう!!

この記事を読んだ方が少しでも参考になったと思ってくださったら幸いです。

Discussion