BEMAロゴ

エンジニアの
成長を支援する
技術メディア

社内ナレッジをClaude CodeでTerraform IaC化してみた ― AWS Client VPN 相互認証の構築

はじめに

こんにちは、株式会社メンバーズの池田です。

「普段はアプリケーション開発がメインだけど、急遽インフラ環境の構築も任された」という経験はないでしょうか。今回私自身がその状況になり、社外のインターン生向けにAWS環境へのアクセスを付与するため、AWS Client VPNを構築する機会がありました。幸いにも、前年の担当者が残してくれたAWSコンソールの手順書があったため、構築の道筋は立っている状態でした。

前回の作業後は、コストおよびセキュリティの観点から、手順書だけを残して主要なリソースは削除する運用となっておりました。そして今回、2年連続で同じ作業が発生したことで、来年以降も繰り返す可能性が高まりました。

そこで、既存のナレッジを最大限に活かしつつ、今後の対応が少しでも楽になるかと思い、TerraformによるIaC(Infrastructure as Code)化を進めることにしました。

私自身、インフラ専任のエンジニアという立場ではありません。しかし、「Claude Codeを相棒にすれば、コンソール手順しかなくても一気にコード化できるのでは?」という挑戦的なモチベーションもありました。

この記事では、既存のコンソール手順書を参照しながら、Claude CodeでTerraformコードを生成・構築した流れと、その過程で気づいたことを紹介します。AWS Client VPN(相互認証)をコンソールで構築する際のリファレンスとしても参照できるよう、コンソールでの構築手順も合わせて掲載しています。

相互認証の利点と利用シーン

AWS Client VPNで相互認証を有効にすることで、次のセキュリティ上のメリットがあります。

利点

  1. ユーザー識別の高度化

    • サーバー認証だけでなく、クライアントも証明書を提示することで認証の強度が高まる

    • 第三者によるなりすましや不正アクセスのリスクを軽減

  2. VPN接続者の個別管理

    • 各クライアント証明書をユーザーごとに発行することで、個別の接続ログ管理やアクセス制御が可能

    • 部署や役割に応じてネットワークアクセスを制限できる

  3. 接続ログによる監査とユーザー識別

    • クライアント証明書のCN(Common Name)をユーザー識別に活用

    • 誰が・いつ・どのIPで接続したかをCloudWatch Logsで正確にトラッキング可能
      ※なお、CloudWatchへのログ出力には20〜30分程度のタイムラグがある。

こんな時に使う

  • 社外パートナーやインターン生など、一時的なアクセスを安全に許可したい

  • AD連携やSAML認証が導入できない軽量な環境でセキュアな認証を行いたい

  • ユーザーごとのネットワークアクセス権限を明確に分けたい

  • リソースへのネットワークアクセスをVPN経由に限定したい

コンソールでの構築手順

まずは元ナレッジに基づいたコンソール手順を紹介します。次章のIaC化もこの手順をベースにしているので、「コンソールで何をやっているのか」を把握しておくとこの後のTerraform化の流れが理解しやすくなります。※本手順は前年(2025年)時点のコンソール手順をベースにしています。AWSコンソールのUIは変更される場合があるため、画面構成が異なる場合はご注意ください。

前提条件

  • 少なくとも1つのサブネットとインターネットゲートウェイを持つVPCがあること

    • サブネットに関連付けられているルートテーブルには、インターネットゲートウェイへのルートが必要

  • VPNの接続ログを出力する場合、CloudWatchのロググループを1つ作成しておくこと

EasyRSAで証明書ペアを作成

相互認証では、サーバーとクライアントそれぞれの証明書が必要です。ここではEasyRSAを使って作成します。

  1. EasyRSAをダウンロード/インストール

$ git clone https://github.com/OpenVPN/easy-rsa.git
$ cd easy-rsa/easyrsa3
  1. ルート認証局(CA)を作成

$ ./easyrsa init-pki
$ ./easyrsa build-ca nopass
  1. サーバー証明書の作成

$ ./easyrsa --san=DNS:server.example.com build-server-full server.example.com nopass
  • server.example.comの部分は適当な名前をつける(存在しないドメインで良いが、FQDN形式にすること)

  1. クライアント証明書の作成

$ ./easyrsa build-client-full client1.example.com nopass
  • client1.example.com の部分は適当な名前をつける(存在しないドメインで良いが、FQDN形式にすること)

  • 複数のユーザーに個別の証明書を割り当てる場合には人数分作成する

  • 指定したCNはCloudWatchの接続ログにそのまま出力されるため、実名ではなく client1.example.com のような連番を使い、氏名との対応は管理台帳で管理する運用を推奨する

  1. 証明書とキーを作業用フォルダーにコピー

mkdir ~/custom_folder/
cp pki/ca.crt ~/custom_folder/
cp pki/issued/server.example.com.crt ~/custom_folder/
cp pki/private/server.example.com.key ~/custom_folder/
cp pki/issued/client1.example.com.crt ~/custom_folder/    # 人数分
cp pki/private/client1.example.com.key ~/custom_folder/   # 人数分
cd ~/custom_folder/

ACMに証明書をアップロード

作成した証明書をAWS Certificate Manager(ACM)にアップロードします。

# サーバー証明書
aws acm import-certificate 
  --certificate fileb://server.example.com.crt 
  --private-key fileb://server.example.com.key 
  --certificate-chain fileb://ca.crt

# クライアント証明書(どれか1つでOK)
aws acm import-certificate \
  --certificate fileb://client1.example.com.crt \
  --private-key fileb://client1.example.com.key \
  --certificate-chain fileb://ca.crt

※コマンド実行時に出力されるCertificateArnは、後述するTerraformでのIaC化を行う場合はterraform.tfvarsに記載してTerraformに渡すため、メモしておいてください。

Client VPNエンドポイントを作成

  1. AWSの VPC コンソールOpen in new tab を開く

  2. ナビゲーションペインで「クライアント VPN エンドポイント」を選択し、「クライアント VPN エンドポイントを作成」をクリック

  3. 「名前タグ」、「説明」、「クライアント IPv4 CIDR」を入力する

  • 「クライアントIPv4 CIDR」にはVPCやサブネットの「IPv4 CIDR」と被らない範囲を指定すること

  1. 認証情報を入力する

  • 「サーバー証明書 ARN」には前述のACMでアップロードしたサーバー証明書を選択する

  • 相互認証を使用のチェックをONにする

  • 「クライアント証明書 ARN」には前述のACMでアップロードしたクライアント証明書を選択する

  1. ログ出力を設定する

  • 「クライアント接続のログ詳細を有効化」をONにする

  • 事前に用意しておいたロググループを選択する

  • ログストリーム名は空欄でOK

  1. DNSとVPCを設定する

  • DNSサーバーは8.8.8.8などを入力する

  • VPC ID に事前に用意したVPCを選択する

  1. 残りはデフォルト設定のまま「クライアント VPN エンドポイント」を作成をクリック

  • 作成直後は状態がpending-associateになる

ターゲットネットワークを関連付ける

  1. 作成したクライアントVPNエンドポイントを開く

  2. 「ターゲットネットワークの関連付け」→「ターゲットネットワークを関連付ける」をクリック

  1. 事前に用意したVPCとサブネットを選択してターゲットネットワークを関連付けるをクリック

  • 「VPCと関連付けるサブネットを選択」で事前に用意したVPCとサブネットを選択する

  • 「ターゲットネットワークを関連付ける」をクリックする

    • クライアントVPNエンドポイントの状態がavailableになる

VPCの承認ルールを追加する

  1. AWSのVPCコンソールで前手順で作成したクライアントVPNエンドポイントを開く

  2. 「承認ルール」→「認証ルールを追加」をクリック

  1. 認証ルールを設定する

  • 「アクセスを有効にする送信先ネットワーク」に許可するネットワークのCIDRを入力

    • 例えば、VPC全体へのアクセスを許可する場合はVPCのIPv4 CIDRブロックを指定する

  1. 「認証ルールを追加」をクリックする

インターネットへのアクセスを提供する

  1. AWSのVPCコンソールで前手順で作成したクライアントVPNエンドポイントを開く

  2. 「ルートテーブル」→「ルートを作成」をクリック

  1. ルートを設定する

  • ルート送信先に「0.0.0.0/0」を入力する

  • 「ターゲットネットワーク関連付けのサブネットID」でルーティングするサブネットのIDを選択する

  1. 前述のVPCの承認ルールを追加すると同様の手順で送信先が「0.0.0.0/0」の承認ルールを追加する

セキュリティグループの要件を確認する

クライアントVPNエンドポイントに関連付けられているセキュリティグループにインターネットへのアウトバウンドトラフィックが許可されていることを確認する

  • 許可されていない場合は「0.0.0.0/0」への全てのトラフィックを許可するアウトバウンドルールを追加する

  • VPCコンソールでセキュリティグループを開き、「アウトバウンドルール」を選択して「アウトバウンドルールの編集」をクリックする

  • タイプにすべてのトラフィックを選択し、送信先に「0.0.0.0/0」を追加する

  • 「ルールを保存」をクリックする

クライアントVPNエンドポイント設定ファイルを作成する

最後に、VPNクライアントが使う.ovpn形式の設定ファイルを作成してユーザーに配布します。

  1. AWSのVPCコンソールで前手順で作成したクライアントVPNエンドポイントを開く

  2. 右上の「クライアント設定をダウンロード」をクリックして設定ファイルをダウンロードする

  3. 任意のテキストエディタで設定ファイルを開き、以下の内容を追加する

<cert>
クライアント証明書(.crt ファイル)の内容
</cert>

<key>
クライアントのプライベートキー(.key ファイル)の内容
</key>
  1. 必要なクライアントの数分、証明書とプライベートキーの内容を差し替えて作成する

  2. 作成した設定ファイルを利用者に個別に配布する

注意:.ovpnファイルには秘密鍵が埋め込まれており、ファイルを持っている人がそのままVPNに接続できる認証情報です。配布経路(メール・チャットツール等)と保管場所には注意してください。

Claude CodeでTerraform IaC化

ここからが今回の本題です。上記のコンソール手順を、Claude Codeを使ってTerraformに変換した流れを紹介します。

構築したもの

本記事のコードはTerraform AWS Provider v5.xを使用しています。バージョンによって挙動が異なる場合があるためご注意ください。

最終的なTerraformの構成は以下のとおりです。

terraform/
├── main.tf
├── variables.tf
├── outputs.tf
├── vpc.tf          # VPC・サブネット・IGW・ルートテーブル
├── vpn.tf          # Client VPN エンドポイント・関連設定
├── logging/        # CloudWatch ログ(独立した state で管理)
│   ├── main.tf
│   ├── cloudwatch.tf
│   └── outputs.tf
└── verification/   # 疎通確認用 EC2(一時的)
    ├── main.tf
    ├── variables.tf
    ├── outputs.tf
    └── ec2.tf

logging/ はVPNエンドポイントとは別のTerraform Stateで管理しています。terraform destroyでエンドポイントを削除しても監査ログが消えないようにするためです。なお、今回はstateをローカルで管理していますが、チームで運用する場合はS3などのリモートバックエンドで管理する必要があります。

CLAUDE.mdでルールを宣言

Claude Codeに作業を任せる上でまず考えたのが、AIの行動をどう制御下に置くかという点です。口頭で指示を出しながら進める中で、Claude Codeが意図せずAWS CLIコマンドを直接実行してリソースを作ってしまうと、何が作られたのか追跡できなくなります。それを防ぐため、リポジトリのルートに CLAUDE.mdを置き、以下の一文を書きました。

# 作業ルール
- インフラの操作・構築は IaC(Terraform 等)で行う。AWS CLI コマンドの直接実行は原則禁止。

これにより、Claude Codeが操作をする際はTerraformを通すことになります。Terraformで管理することで、構成がコードとして残り、再現性の担保と変更履歴の追跡が可能になります。CLAUDE.mdはあくまで指示ベースの制御であり、技術的な強制ではありません。Claude CodeにはHooksという仕組みを使ってより堅牢なブロックも実現できますが、今回の用途ではへCLAUDE.md の記述で十分でした。

元ナレッジを渡してTerraform化を指示

コンソール手順書を参照させた上で「これをTerraformで書いてください」と指示するだけで、基本的なコードはほぼ生成してくれました。なお、「せっかくなら前提となるリソースも含めて一元管理しよう」というモチベーションもあり、既存環境には依存せず、手順書で「前提条件」としていた VPC や CloudWatch ロググループも含め、新規で一式まとめてTerraformで構築するように指示しています。

コンソール手順の各ステップがTerraformリソースに対応しているため、変換精度は想定以上に高く、VPC・サブネット・エンドポイント・承認ルール・ルートテーブルといったリソースがひととおり出力されました。

なお、証明書の作成(EasyRSA)とACMへのアップロードはTerraformでは管理せず手動で行っています。証明書の作成はローカルでEasyRSAを実行し、ACMへのアップロードは前半の手順にある通りAWS CLIを用いています。秘密鍵などの機密情報を含む作業のため、安全側に倒した判断です。

手順書という「人間向けに書かれたドキュメント」をClaude Codeに渡すだけでここまでできるのは、素直に便利だと感じました。ただし、「コンソールが裏でやってくれていた設定」はTerraformコードには現れないため、そこは自分で補う必要がありました。

詰まったポイント

IaC化する過程で、コンソール手順書には書かれていなかった落とし穴がいくつか見つかりました。

  1. CloudWatchのログが出ない―Claude Codeの誤診と原因調査
    TerraformでapplyしてもVPNの接続ログがCloudWatchに出力されない、という問題に直面しました。

    Claude Codeが「ログストリーム名が空欄なのが原因ではないか」と判断したため、それに従って明示的に指定しました。元ナレッジには「空欄でOK」と書いてあったものの、ログが出ていなかったため、その提案を採用した形です。

connection_log_options {
  enabled               = true
  cloudwatch_log_group  = "/client-vpn/example/2026"
  cloudwatch_log_stream = "connections"  # 明示的に指定してみた
}

同時に、Claude Codeの提案を受けて、delivery.logs.amazonaws.com からのログ書き込みを許可するリソースポリシーも追加しました。コンソールでは 自動で付与されているOpen in new tab ようですが、Terraformでは明示的に書く必要があるようです。

resource "aws_cloudwatch_log_resource_policy" "client_vpn" {
  policy_name = "client-vpn-log-policy"
  policy_document = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "delivery.logs.amazonaws.com" }
      Action    = ["logs:CreateLogStream", "logs:PutLogEvents"]
      Resource  = "${aws_cloudwatch_log_group.client_vpn.arn}:*"
      Condition = {
        StringEquals = { "aws:SourceAccount" = data.aws_caller_identity.current.account_id }
      }
    }]
  })
}

これらの設定を同時に行った結果、無事ログが出力されるようになりました。

当時は「どちらが決定打だったのか、あるいは単に出力までのタイムラグ(20〜30分)だったのか」が完全には切り分けられなかったのですが、後日AWSの ドキュメント Open in new tabを改めて確認したところ、ログストリームは省略してもAWS側が自動生成してくれるため、必須ではないとのこと。つまり、コンソール構築時にAWSが裏で自動付与してくれていた リソースポリシー Open in new tabが、Terraformでの構築時に不足していたことが原因だった可能性が高い、という結論に至りました。

AIの提案をきっかけに、コンソールが裏でやってくれていたことの正体に気づかされた出来事でした。

  1. フルトンネル構成(split_tunnel=false)でインターネットを使う場合はDNSサーバーが必須
    VPN接続後にインターネットが使えなくなり、原因を調べたところDNSサーバーが未設定だったことが分かりました。元ナレッジには「必要な場合入力する」とあり、任意設定だと思っていましたが、フルトンネル構成でインターネットを使う場合は必須でした。そのため、vpn.tf 内のエンドポイントリソース(aws_ec2_client_vpn_endpoint)に以下の設定を追記しました。

resource "aws_ec2_client_vpn_endpoint" "example" {
  # (その他の設定は省略)
  dns_servers = ["8.8.8.8", "8.8.4.4"]
}

まとめ

振り返ってみると、今回はインフラの細部まで完璧に理解していなくても、「要件を満たす環境」が作れてしまいました。Claude Codeに手順書を渡してコードを書いてもらい、あとは「VPNに繋がるか」「ログは出るか」「EC2と通信できるか」をとにかくテストする。内部の細かい仕様を完全に把握しきれていない部分もありましたが、「結果として要件を満たしているか」を丁寧に検証しながら進めることで、これだけの環境が構築できることには、AIツールの可能性を実感しています。

たしかに、Claude Codeの提案を活用しながら進めた結果、求めていた結果は得られました。ただ、構築時点では、ログが出なかった問題の原因に関して何が本当の原因だったかを自分自身では把握しきれていませんでした。結果を得ることと、理解することは別物だと改めて気づかされました。

この経験を踏まえたからこそ、インフラ構築におけるセキュリティや構成の根本的な理解は必須だと改めて感じています。「AI エージェントに任せればいい」で済む話ではなく、何を作っているかの基本的な把握は引き続き欠かせません。AI が得意なのは判断を速く実装に落とし込むことであって、判断そのものの代替にはならない、と考えています。

今回の前提にあったのは、前年の担当者がコンソール手順をきちんとナレッジとして残してくれていたことです。自社の要件を満たす「拠り所」がなければ、これほどのスピードと確度では進められなかったはずです。前年の担当者にレビューも依頼しており、人とナレッジの積み重ねがあってこその構築でした。改めて感謝しています。

AIエージェント自体の進化に加え、それを取り巻くツールや仕組みも急速に整いつつあります。インフラ構築の敷居はこれからさらに下がっていくと思いますが、それは「理解しなくていい」ことではなく、「意図を理解している人が、圧倒的なスピードで形にできる」環境になっていくことだと自分は思っています。今回の経験を糧に、次は AI の手を借りつつも、インフラの裏側まで深く語れるエンジニアを目指していきたいです。

参考

この記事が役に立ったと思ったら、
ぜひ「いいね」とシェアをお願いします!

リンクをコピーXでシェアするfacebookでシェアする

この記事を書いた人

池田 直史
池田 直史
2019年にメンバーズに中途で入社。入社後から現在までSaaSクライアントのWebアプリケーション開発に従事。趣味は動物園、水族館巡り。アカアシドゥクラングールが好きです。
詳しく見る
ページトップへ戻る