Amazon EC2 で code-server + Claude Code の開発環境を構築する

AWS

2026.7.3

Topics

はじめに

こんにちは、Paseri です。

Claude Code は CLI やローカル VSCode で使うのが一般的だと思います。
ほかにも、チーム内でエージェントや MCP 等の設定を共有、管理したい場合ブラウザからアクセスできる VSCode(code-server)を EC2 上に構築し、そこで Claude Code を動かすという選択肢もあります。

本記事では、Terraform によるインフラ構築から、Claude Code 拡張機能の導入までをご紹介します。

本記事でやること

  1. Terraform で EC2 + code-server 環境を構築
  2. Claude Code のインストールから Amazon Bedrock 経由での利用設定
  3. Claude Code 拡張機能をインストールして、エディタ上で利用可能にする

Claude Code のセットアップと VSCode の拡張機能については、以下の記事で詳しく紹介しています。

関連記事
Amazon Bedrock 経由で Claude Code を利用するまで

関連記事
VSCode で Claude Code を使用する

本記事では EC2 + code-server 固有のポイント にフォーカスしてご紹介します。

この構成のメリット・デメリット

EC2 上に code-server を立てて Claude Code を使う構成には、ローカル環境で使う場合と比較して以下のメリットがあります。

  • ブラウザだけで使える:端末を選ばずアクセスできるためローカルに VSCode や Node.js のインストールが不要
  • 環境を一元管理できる:拡張機能・設定・認証情報をサーバー側で集約、管理できるため利用者ごとの設定で差分が発生しない
  • IAM / セキュリティグループでアクセス制御:AWS のセキュリティ機構で誰がいつ使えるかを統制できる。Bedrock の API キーもサーバー側に閉じ込められる
  • Bedrock Guardrails と組み合わせやすい:サーバー側で Bedrock を呼び出すため、Guardrails による入出力制御をそのまま適用できる

一方で以下の点は考慮が必要です。

  • HTTPS が必須:Claude Code 拡張機能は Service Worker を使うため、証明書が必要
  • 拡張機能の制約:code-server は Open VSX を使用するため、Microsoft 公式 Marketplace にしかない拡張は利用できない
  • ネットワーク依存:ブラウザからのアクセスとなるため、ネットワーク環境によっては遅延を感じる場合がある

前提条件

本記事の構成を再現するには、以下が事前に必要です。

  • Terraform >= 1.0:本構成のデプロイに使用
  • AWS CLI 設定済み(SSO ログイン可能な状態):Terraform でのデプロイ時の認証に利用
  • Route 53 ホストゾーン(独自ドメイン):Let’s Encrypt での証明書取得に必要
  • Bedrock のモデルアクセス有効化:Claude モデルが利用可能な状態にしておく
  • Bedrock API キー発行済み:Claude Code の認証に使用(発行手順はこちら

アーキテクチャ

構成は以下の通りです。

今回はかなり構成を縮小していますが、用途によって適宜変更してください。

項目
OS Amazon Linux 2023
インスタンスタイプ t3.small
ストレージ 8 GiB gp3
ネットワーク 新規 VPC + パブリックサブネット
固定 IP EIP
アクセス制御 セキュリティグループ で実行元 IP のみ許可(ポート 8080)
認証 code-server パスワード認証(ランダム生成)
保守接続 SSM Session Manager
バックアップ AWS Backup(日次、保管 3 日)
自動起動/停止 EventBridge Scheduler(平日 9:00 起動 / 20:00 停止)

1. Terraform によるインフラ構築

ファイル構成

今回利用している構成は下記です。(本記事の最下部に添付しています)

terraform/
├── 01_provider.tf           # プロバイダー、backend 設定
├── 02_variable.tf           # 変数定義、パスワード生成
├── 03_vpc.tf                # VPC、サブネット、IGW、ルートテーブル、MyIP 取得
├── 04_ec2.tf                # EC2、EIP、セキュリティグループ、IAM ロール
├── 05_backup.tf             # AWS Backup(日次バックアップ)
├── 06_scheduler.tf          # EventBridge Scheduler(サーバー自動起動/停止)
├── 07_outputs.tf            # URL、パスワード、SSH コマンド等の出力
└── user_data.sh             # code-server インストールスクリプト

主要リソースの解説

※ 抜粋です。全文は Appendix を参照ください。

VPC とセキュリティグループ(03_vpc.tf / 04_ec2.tf)

terraform apply 実行時のグローバル IP を自動取得し、セキュリティグループのインバウンドルールに設定します。
これにより、意図しない外部アクセスを防止しています。

# MyIP 取得
data "http" "my_ip" {
  url = "https://checkip.amazonaws.com/"
}

locals {
  my_ip = "${chomp(data.http.my_ip.response_body)}/32"
}

# セキュリティグループ
resource "aws_security_group" "code_server" {
  name        = "${var.project_name}-vscode-server-sg"
  description = "Security group for code-server"
  vpc_id      = aws_vpc.this.id

  ingress {
    description = "code-server from my IP"
    from_port   = 8080
    to_port     = 8080
    protocol    = "tcp"
    cidr_blocks = [local.my_ip]
  }

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

User Data(user_data.sh)

User Data で code-server のインストールと初期設定を自動化します。
パスワードは random_password で生成して、terraform output で確認できるようにしています。

#!/bin/bash
set -eo pipefail
exec > >(tee /var/log/user-data.log) 2>&1

# ホスト名設定
hostnamectl set-hostname ${hostname}

# パッケージ更新 & インストール
dnf update -y
dnf install -y git tar gzip

# code-server インストール
curl -fsSL https://code-server.dev/install.sh | sh

# code-server 設定ファイル作成
mkdir -p /home/ec2-user/.config/code-server
cat > /home/ec2-user/.config/code-server/config.yaml <<EOF
bind-addr: 0.0.0.0:8080
auth: password
password: ${code_server_password}
cert: false
EOF

# 所有権設定
chown -R ec2-user:ec2-user /home/ec2-user/.config

# systemd サービス有効化・起動
systemctl enable --now code-server@ec2-user

自動起動/停止(06_scheduler.tf)

コスト削減のため、利用しない時間帯は EventBridge Scheduler で平日 9:00〜20:00 のみ起動するスケジュールを設定しています。

# 自動起動 (平日 9:00 JST)
resource "aws_scheduler_schedule" "start" {
  name                         = "${var.project_name}-vscode-server-start"
  schedule_expression          = "cron(0 9 ? * MON-FRI *)"
  schedule_expression_timezone = "Asia/Tokyo"

  flexible_time_window {
    mode = "OFF"
  }

  target {
    arn      = "arn:aws:scheduler:::aws-sdk:ec2:startInstances"
    role_arn = aws_iam_role.scheduler.arn
    input = jsonencode({
      InstanceIds = [aws_instance.code_server.id]
    })
  }
}

# 自動停止 (平日 20:00 JST)
resource "aws_scheduler_schedule" "stop" {
  name       = "${var.project_name}-vscode-server-stop"
  schedule_expression          = "cron(0 20 ? * MON-FRI *)"
  schedule_expression_timezone = "Asia/Tokyo"

  flexible_time_window {
    mode = "OFF"
  }

  target {
    arn      = "arn:aws:scheduler:::aws-sdk:ec2:stopInstances"
    role_arn = aws_iam_role.scheduler.arn

    input = jsonencode({
      InstanceIds = [aws_instance.code_server.id]
    })
  }
}

デプロイ手順

# 1. AWS SSO ログイン
aws sso login
aws sts get-caller-identity  # アカウント確認

# 2. Terraform 実行
cd terraform
terraform init
terraform plan
terraform apply

# 3. 接続情報の確認
terraform output code_server_url       # アクセス URL
terraform output -raw code_server_password  # パスワード

ブラウザで表示された URL にアクセスし、パスワードを入力すれば code-server が使えます。

2. HTTPS 化

なぜ HTTPS が必要か

Claude Code 拡張機能は Service Worker を使用しており、Service Worker は HTTPS(または localhost)でしか動作しません。
自己署名証明書でもブロックされるため、正式な証明書が必須です。

今回は、Let’s Encrypt を使って証明書の設定を行います。

DNS 設定

Route53 で A レコードを作成し、EIP に向けます。

レコード タイプ
vscode-server.example.com A <EIP>

証明書取得

SSM Session Manager で EC2 に接続し、certbot を実行します。

# certbot インストール
sudo dnf install -y certbot

# 証明書取得(standalone モード)
# ※ 一時的に EC2 のセキュリティグループでポート 80 を 0.0.0.0/0 に開放する必要があります
sudo certbot certonly --standalone -d vscode-server.example.com

※ 証明書の取得が完了したらセキュリティグループのポート 80 は戻して問題ありません。

code-server に証明書を適用

/home/ec2-user/.config/code-server/config.yaml を編集します。

bind-addr: 0.0.0.0:8080
auth: password
password: <パスワード>
cert: /etc/letsencrypt/live/vscode-server.example.com/fullchain.pem
cert-key: /etc/letsencrypt/live/vscode-server.example.com/privkey.pem

証明書ファイルの権限を設定します。

# ec2-user が読めるよう権限を付与
sudo chmod 755 /etc/letsencrypt/live
sudo chmod 755 /etc/letsencrypt/archive
sudo chgrp ec2-user /etc/letsencrypt/live/vscode-server.example.com/privkey.pem
sudo chmod 640 /etc/letsencrypt/live/vscode-server.example.com/privkey.pem

# code-server 再起動
sudo systemctl restart code-server@ec2-user

これで https://vscode-server.example.com:8080 でアクセスできるようになります。

3. Claude Code 拡張機能の導入

Open VSX からインストール

code-server は Microsoft 公式の VSCode Marketplace ではなく、Open VSX(Eclipse Foundation 運営)を使用しています。
これはライセンス上の制約によるものです。

Claude Code 拡張機能は Open VSX にも公開されているため、code-server の拡張機能画面から「Claude」で検索してインストールできます。

認証設定(環境変数方式)

code-server は systemd 経由で起動するため、.bashrc の環境変数は読み込まれません。
systemd の override ファイルで設定します。

# override ファイル作成
sudo mkdir -p /etc/systemd/system/code-server@ec2-user.service.d
sudo tee /etc/systemd/system/code-server@ec2-user.service.d/env.conf <<EOF
[Service]
Environment="AWS_BEARER_TOKEN_BEDROCK=<Bedrock API キー>"
Environment="CLAUDE_CODE_USE_BEDROCK=1"
Environment="AWS_REGION=ap-northeast-1"
EOF

# 反映
sudo systemctl daemon-reload
sudo systemctl restart code-server@ec2-user
環境変数 説明
AWS_BEARER_TOKEN_BEDROCK Bedrock の API キー
CLAUDE_CODE_USE_BEDROCK Bedrock 経由を有効化
AWS_REGION Bedrock を利用するリージョン

Claude Code の細かい設定についてはこちらの記事を参照してください。

動作確認

拡張機能のインストールと環境変数の設定が完了すると、VSCode での利用と同じようにサイドバーから Claude Code が利用可能になります。

注意事項

セキュリティグループの IP 制限

セキュリティグループは terraform apply 実行時の IP のみ許可します。
アクセス元の IP が変わる場合は都度修正をする必要があります。

証明書の更新

Let’s Encrypt の証明書は 90 日間有効です。期限が近づいたら以下の手順で更新します。

1:セキュリティグループでポート 80 を 0.0.0.0/0 に一時開放
2:SSM で接続し、以下を実行:

sudo systemctl stop code-server@ec2-user
sudo certbot renew
sudo systemctl start code-server@ec2-user

3:セキュリティグループのポート 80 ルールを削除

まとめ

今回は EC2 上で構築した、code-server に Claude Code を導入する方法をご紹介しました。
これにより、Claude Code の設定をサーバー上で統一できるメリットを受けつつ VSCode 互換エディタでの利用ができるようになりました。

要点をまとめると:

  • Terraform で一発構築 ─ VPC、EC2、バックアップ、自動起動/停止まで IaC で管理
  • HTTPS は必須 ─ Claude Code 拡張機能の Service Worker が動作するために正式な証明書が必要
  • systemd override で環境変数を設定.bashrc は systemd 起動では読まれないため

少しでも参考になれば幸いです!
最後までお読み頂きありがとうございました!

Appendix

01_provider.tf

# -----------------------
# Terraform configuration
# -----------------------
terraform {
  required_version = ">= 1.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    http = {
      source  = "hashicorp/http"
      version = "~> 3.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.0"
    }
  }

  backend "s3" {
    bucket = "your-terraform-state-bucket"
    key    = "vscode-server/terraform.tfstate"
    region = "ap-northeast-1"
  }
}

# -----------------------
# Provider
# -----------------------
provider "aws" {
  region  = var.aws_region

  default_tags {
    tags = {
      ManagedBy = "Terraform"
      Project   = var.project_name
    }
  }
}

02_variable.tf

################################################################################
# code-server パスワード生成
################################################################################
resource "random_password" "code_server" {
  length  = 16
  special = false
}

################################################################################
# 変数
################################################################################
variable "aws_region" {
  description = "AWS リージョン"
  type        = string
  default     = "ap-northeast-1"
}

variable "project_name" {
  description = "プロジェクト名"
  type        = string
  default     = "code-server"
}

03_vpc.tf

################################################################################
# MyIP 取得
################################################################################
data "http" "my_ip" {
  url = "https://checkip.amazonaws.com/"
}

locals {
  my_ip = "${chomp(data.http.my_ip.response_body)}/32"
}

################################################################################
# VPC
################################################################################
resource "aws_vpc" "this" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "${var.project_name}-vscode-server-vpc"
  }
}

################################################################################
# パブリックサブネット
################################################################################
resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.this.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "ap-northeast-1a"
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.project_name}-vscode-server-public-subnet"
  }
}

################################################################################
# インターネットゲートウェイ
################################################################################
resource "aws_internet_gateway" "this" {
  vpc_id = aws_vpc.this.id

  tags = {
    Name = "${var.project_name}-vscode-server-igw"
  }
}

################################################################################
# ルートテーブル
################################################################################
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.this.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.this.id
  }

  tags = {
    Name = "${var.project_name}-vscode-server-public-rt"
  }
}

resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

04_ec2.tf

################################################################################
# AMI (Amazon Linux 2023 最新)
################################################################################
data "aws_ami" "al2023" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-2023.*-kernel-6.1-x86_64"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

################################################################################
# IAM ロール (SSM 用)
################################################################################
resource "aws_iam_role" "ec2" {
  name = "${var.project_name}-vscode-server-ec2-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
      }
    ]
  })

  tags = {
    Name = "${var.project_name}-vscode-server-ec2-role"
  }
}

resource "aws_iam_role_policy_attachment" "ssm" {
  role       = aws_iam_role.ec2.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

resource "aws_iam_instance_profile" "ec2" {
  name = "${var.project_name}-vscode-server-ec2-profile"
  role = aws_iam_role.ec2.name
}

################################################################################
# セキュリティグループ
################################################################################
resource "aws_security_group" "code_server" {
  name        = "${var.project_name}-vscode-server-sg"
  description = "Security group for code-server"
  vpc_id      = aws_vpc.this.id

  # code-server (HTTP)
  ingress {
    description = "code-server from my IP"
    from_port   = 8080
    to_port     = 8080
    protocol    = "tcp"
    cidr_blocks = [local.my_ip]
  }

  # アウトバウンド全許可
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.project_name}-vscode-server-sg"
  }
}

################################################################################
# EC2 インスタンス
################################################################################
resource "aws_instance" "code_server" {
  ami                    = data.aws_ami.al2023.id
  instance_type          = "t3.small"
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.code_server.id]
  iam_instance_profile   = aws_iam_instance_profile.ec2.name

  root_block_device {
    volume_size = 8
    volume_type = "gp3"
    encrypted   = true
  }

  user_data = templatefile("${path.module}/user_data.sh", {
    code_server_password = random_password.code_server.result
    hostname             = "${var.project_name}-vscode-server-ec2"
  })

  tags = {
    Name = "${var.project_name}-vscode-server-ec2"
  }
}

################################################################################
# EIP
################################################################################
resource "aws_eip" "code_server" {
  instance = aws_instance.code_server.id
  domain   = "vpc"

  tags = {
    Name = "${var.project_name}-vscode-server-eip"
  }
}

05_backup.tf

################################################################################
# AWS Backup Vault
################################################################################
resource "aws_backup_vault" "this" {
  name = "${var.project_name}-vscode-server-vault"

  tags = {
    Name = "${var.project_name}-vscode-server-vault"
  }
}

################################################################################
# AWS Backup Plan (1日1回、保管3日間)
################################################################################
resource "aws_backup_plan" "this" {
  name = "${var.project_name}-vscode-server-backup-plan"

  rule {
    rule_name         = "daily-backup"
    target_vault_name = aws_backup_vault.this.name
    schedule          = "cron(0 18 * * ? *)"

    lifecycle {
      delete_after = 3
    }
  }

  tags = {
    Name = "${var.project_name}-vscode-server-backup-plan"
  }
}

################################################################################
# AWS Backup IAM ロール
################################################################################
resource "aws_iam_role" "backup" {
  name = "${var.project_name}-vscode-server-backup-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "backup.amazonaws.com"
        }
      }
    ]
  })

  tags = {
    Name = "${var.project_name}-vscode-server-backup-role"
  }
}

resource "aws_iam_role_policy_attachment" "backup" {
  role       = aws_iam_role.backup.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup"
}

################################################################################
# AWS Backup Selection (EC2 インスタンス)
################################################################################
resource "aws_backup_selection" "this" {
  name         = "${var.project_name}-vscode-server-backup-selection"
  plan_id      = aws_backup_plan.this.id
  iam_role_arn = aws_iam_role.backup.arn

  resources = [
    aws_instance.code_server.arn
  ]
}

06_scheduler.tf

################################################################################
# EC2 自動起動/停止用 IAM ロール (EventBridge Scheduler)
################################################################################
resource "aws_iam_role" "scheduler" {
  name = "${var.project_name}-vscode-server-scheduler-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "scheduler.amazonaws.com"
        }
      }
    ]
  })

  tags = {
    Name = "${var.project_name}-vscode-server-scheduler-role"
  }
}

resource "aws_iam_role_policy" "scheduler" {
  name = "${var.project_name}-vscode-server-scheduler-policy"
  role = aws_iam_role.scheduler.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "ec2:StartInstances",
          "ec2:StopInstances"
        ]
        Resource = aws_instance.code_server.arn
      }
    ]
  })
}

################################################################################
# 自動起動 (平日 9:00 JST)
################################################################################
resource "aws_scheduler_schedule" "start" {
  name       = "${var.project_name}-vscode-server-start"
  group_name = "default"

  schedule_expression          = "cron(0 9 ? * MON-FRI *)"
  schedule_expression_timezone = "Asia/Tokyo"

  flexible_time_window {
    mode = "OFF"
  }

  target {
    arn      = "arn:aws:scheduler:::aws-sdk:ec2:startInstances"
    role_arn = aws_iam_role.scheduler.arn

    input = jsonencode({
      InstanceIds = [aws_instance.code_server.id]
    })
  }
}

################################################################################
# 自動停止 (平日 20:00 JST)
################################################################################
resource "aws_scheduler_schedule" "stop" {
  name       = "${var.project_name}-vscode-server-stop"
  group_name = "default"

  schedule_expression          = "cron(0 20 ? * MON-FRI *)"
  schedule_expression_timezone = "Asia/Tokyo"

  flexible_time_window {
    mode = "OFF"
  }

  target {
    arn      = "arn:aws:scheduler:::aws-sdk:ec2:stopInstances"
    role_arn = aws_iam_role.scheduler.arn

    input = jsonencode({
      InstanceIds = [aws_instance.code_server.id]
    })
  }
}

07_outputs.tf

output "code_server_url" {
  description = "code-server アクセス URL"
  value       = "http://${aws_eip.code_server.public_ip}:8080"
}

output "code_server_password" {
  description = "code-server ログインパスワード"
  value       = random_password.code_server.result
  sensitive   = true
}

output "ssh_command" {
  description = "SSH 接続コマンド"
  value       = "ssh -i <秘密鍵パス> ec2-user@${aws_eip.code_server.public_ip}"
}

output "instance_id" {
  description = "EC2 インスタンス ID"
  value       = aws_instance.code_server.id
}

output "my_ip" {
  description = "セキュリティグループに許可された IP"
  value       = local.my_ip
}

user_data.sh

#!/bin/bash
set -eo pipefail

# ログ出力
exec > >(tee /var/log/user-data.log) 2>&1
echo "=== user_data start: $(date) ==="

# HOME を明示的に設定
export HOME=/root

# ホスト名設定
hostnamectl set-hostname ${hostname}

# パッケージ更新
dnf update -y

# 必要パッケージインストール
dnf install -y git tar gzip

# code-server インストール
curl -fsSL https://code-server.dev/install.sh | sh

# code-server 設定ファイル作成
mkdir -p /home/ec2-user/.config/code-server
cat > /home/ec2-user/.config/code-server/config.yaml <<EOF
bind-addr: 0.0.0.0:8080
auth: password
password: ${code_server_password}
cert: false
EOF

# 所有権設定
chown -R ec2-user:ec2-user /home/ec2-user/.config

# systemd サービス有効化・起動
systemctl enable --now code-server@ec2-user

echo "=== user_data complete: $(date) ==="
Paseri

2024年新卒入社。うどん好きな初心者クラウドエンジニア。

X (Twitter) をフォローする

テックブログ新着情報の他
AWSやGoogle Cloudに関する
お役立ち情報を配信中!

Recommends

こちらもおすすめ

X (Twitter) をフォローする

テックブログ新着情報の他
AWSやGoogle Cloudに関する
お役立ち情報を配信中!

Special Topics

注目記事はこちら