7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AgentCore RuntimeをTerraformでVPCにデプロイして送信元IPアドレスを固定にする

7
Last updated at Posted at 2026-03-01

本編の前に

本手順による検証後、terraform destroyによるリソース削除を試みましたが、作成したセキュリティグループが削除できず、サブネットやVPCが削除できない状態になりました。NAT Gateway等の課金が発生するリソースは削除されたため、コスト面への影響はなさそうですが、一時的にENIが残存する状態となります。

スクリーンショット 2026-02-23 18.10.28.png

本事象はterraformのバグとしてissueが上がっていましたが、AWSのドキュメントによると、この事象は仕様の範囲と考えられるので後日確認するしかないと思われます。

ENIs are shared resources across agents that use the same subnet and security group configuration. When you delete an agent, the associated ENI may persist in your VPC for up to 8 hours before it is automatically removed.

はじめに

皆さんは作ったAIエージェントからIP制限がかかっているリソースにアクセスしたいと考えたことはありますでしょうか。通常、公開されているリモートMCPサーバーなどでIP制限がかかっていることはないと思いますが、様々な事情でIP制限がかけられているケースもあります。
通常、AgentCore Runtimeはパブリックデプロイとなるため、IPアドレスは動的となります。今回はAgentCore RuntimeをVPCにTerraformを使ってデプロイし、NAT Gateway + Elastic IPを使って送信元IPアドレスを固定化したいと思います。

なお、AgentCore RuntimeのIP固定化については、先日登壇した内容にも関連しますのでこちらもよろしければご覧ください。

構成・検証方法

今回検証をした環境になります。動作確認用のEC2はTerraformではなく、コンソールから手作業で作成しています。EC2にはnginxを設定しますが、nginxの設定方法については割愛します。
また、AgentCore Runtimeで動作させるエージェントは別途コンテナイメージを作成してECRにpushしています。

動作確認では、まずはじめにEC2のセキュリティグループのインバウンドルールを設定しない状態でAIエージェントに「(EC2のパブリックIPアドレス)にアクセスして」とプロンプトを投げます。その後、セキュリティグループにて作成したElastic IPのアドレスからのHTTPアクセスを許可します。その状態で同じプロンプトを投げてどうなるかを検証します。

せっかくなのでNAT Gatewayは昨年の11月に発表されたRegional NAT Gatewayを使ってます。

https://docs.aws.amazon.com/vpc/latest/userguide/nat-gateways-regional.html

スクリーンショット 2026-02-23 15.18.47.png

AgentCore Runtimeにデプロイしたエージェントコード

本筋ではありませんが、エージェントのコードを紹介します。Strands Agentsを利用し、http_requestツールを持たせています。このコードを別途Dockerfile作成ののち、ECRにpushしています。

agent.py
from strands import Agent
from strands.models import BedrockModel
from strands_tools import http_request
from bedrock_agentcore.runtime import BedrockAgentCoreApp

# ---- モデルの設定 ----
model = BedrockModel(
    model_id="jp.anthropic.claude-sonnet-4-5-20250929-v1:0",
    region_name="ap-northeast-1",
)

# ---- エージェントの作成 ----
agent = Agent(
    model=model,
    tools=[http_request],
    system_prompt=(
        "あなたは親切で知識豊富なAIアシスタントです。"
        "ユーザーからの質問に対して、正確でわかりやすい日本語で回答してください。"
        "わからないことは正直にわからないと答え、情報は最新のものを提供するよう努めてください。"
        "必要に応じてhttp_requestツールを使ってWeb上の最新情報を取得できます。"
    ),
)

# ---- AgentCore エントリポイント ----
app = BedrockAgentCoreApp()


@app.entrypoint
def invoke(payload, context):
    """AgentCoreから呼び出されるエントリポイント"""
    user_message = payload.get(
        "prompt", "こんにちは!何かお手伝いできることはありますか?"
    )
    result = agent(user_message)
    return {"result": result.message}


if __name__ == "__main__":
    app.run()

Terraformコード

VPC

Regional NAT Gateway以外はよく見るコードだと思います。ところどころvarで変数の値がありますが、別途variables.tfで定義しています。

# VPCの作成
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true
}

# インターネットゲートウェイの作成
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
}

# プライベートサブネットの作成(AZ-a)
resource "aws_subnet" "private_1a" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, 1)
  availability_zone = "${var.aws_region}a"
}

# プライベートサブネットの作成(AZ-c)
resource "aws_subnet" "private_1c" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, 2)
  availability_zone = "${var.aws_region}c"
}

# Regional NAT Gatewayの作成
resource "aws_nat_gateway" "regional" {
  vpc_id            = aws_vpc.main.id
  connectivity_type = "public"
  availability_mode = "regional"

  depends_on = [aws_internet_gateway.main]
}

# プライベートルートテーブルの作成
resource "aws_route_table" "private" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.regional.id
  }
}

# プライベートサブネット(1a)とルートテーブルの関連付け
resource "aws_route_table_association" "private_1a" {
  subnet_id      = aws_subnet.private_1a.id
  route_table_id = aws_route_table.private.id
}

# プライベートサブネット(1c)とルートテーブルの関連付け
resource "aws_route_table_association" "private_1c" {
  subnet_id      = aws_subnet.private_1c.id
  route_table_id = aws_route_table.private.id
}

AgentCore

最も重要な箇所はAgentCore Runtimeリソースの中の、network_configurationでVPCモードを指定するところですがそこまで複雑ではないです。

# =============================================================================
# データソース
# =============================================================================

data "aws_caller_identity" "current" {}
data "aws_region" "current" {}

# 既存のECRリポジトリを参照(agentcore-vpc-agentにある既存イメージを使用)
data "aws_ecr_repository" "agent" {
  name = var.ecr_repository_name
}

# =============================================================================
# セキュリティグループ(AgentCore Runtime用)
# =============================================================================

resource "aws_security_group" "agentcore" {
  name        = "${var.project_name}-${var.environment}-agentcore-sg"
  description = "Security group for AgentCore Runtime outbound traffic"
  vpc_id      = aws_vpc.main.id

  # AgentCore はアウトバウンドのみ(インバウンドは不要)
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Allow all outbound traffic (for Bedrock / ECR / CloudWatch access)"
  }

  tags = {
    Name        = "${var.project_name}-${var.environment}-agentcore-sg"
    Environment = var.environment
  }
}

# =============================================================================
# IAMロール(AgentCore Runtime 実行用)
# =============================================================================

resource "aws_iam_role" "agentcore_execution" {
  name = "${var.project_name}-${var.environment}-agentcore-execution-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Sid    = "AgentCoreAssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "bedrock-agentcore.amazonaws.com"
      }
      Action = "sts:AssumeRole"
      Condition = {
        # このアカウントのAgentCoreリソースからのみ AssumeRole を許可(最小権限の原則)
        StringEquals = {
          "aws:SourceAccount" = data.aws_caller_identity.current.id
        }
        ArnLike = {
          "aws:SourceArn" = "arn:aws:bedrock-agentcore:${data.aws_region.current.id}:${data.aws_caller_identity.current.id}:*"
        }
      }
    }]
  })

  tags = {
    Name        = "${var.project_name}-${var.environment}-agentcore-execution-role"
    Environment = var.environment
  }
}

# AWS管理ポリシー(BedrockAgentCore全般アクセス)をアタッチ
resource "aws_iam_role_policy_attachment" "agentcore_managed" {
  role       = aws_iam_role.agentcore_execution.name
  policy_arn = "arn:aws:iam::aws:policy/BedrockAgentCoreFullAccess"
}

# カスタムインラインポリシー(ECR・CloudWatch・Bedrock等の最小権限)
resource "aws_iam_role_policy" "agentcore_execution" {
  name = "AgentCoreExecutionPolicy"
  role = aws_iam_role.agentcore_execution.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      # ECRからコンテナイメージを取得する権限
      {
        Sid    = "ECRImageAccess"
        Effect = "Allow"
        Action = [
          "ecr:BatchGetImage",
          "ecr:GetDownloadUrlForLayer",
          "ecr:BatchCheckLayerAvailability"
        ]
        Resource = data.aws_ecr_repository.agent.arn
      },
      {
        Sid      = "ECRAuthToken"
        Effect   = "Allow"
        Action   = ["ecr:GetAuthorizationToken"]
        Resource = "*"
      },
      # CloudWatch Logsへのログ書き込み権限
      {
        Sid    = "CloudWatchLogs"
        Effect = "Allow"
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:DescribeLogGroups",
          "logs:DescribeLogStreams",
          "logs:PutLogEvents"
        ]
        Resource = "arn:aws:logs:${data.aws_region.current.id}:${data.aws_caller_identity.current.id}:log-group:/aws/bedrock-agentcore/runtimes/*"
      },
      # Bedrock モデル呼び出し権限(エージェントがLLMを使うため)
      {
        Sid    = "BedrockModelInvocation"
        Effect = "Allow"
        Action = [
          "bedrock:InvokeModel",
          "bedrock:InvokeModelWithResponseStream"
        ]
        Resource = "*"
      },
      # X-Ray トレーシング(オプション:デバッグ用途)
      {
        Sid    = "XRayTracing"
        Effect = "Allow"
        Action = [
          "xray:PutTraceSegments",
          "xray:PutTelemetryRecords",
          "xray:GetSamplingRules",
          "xray:GetSamplingTargets"
        ]
        Resource = "*"
      }
    ]
  })
}

# =============================================================================
# AgentCore Runtime(VPCモード)
# =============================================================================

resource "aws_bedrockagentcore_agent_runtime" "main" {
  agent_runtime_name = var.agent_runtime_name
  description        = "${var.project_name} Q&Aエージェント(VPC接続モード)"
  role_arn           = aws_iam_role.agentcore_execution.arn

  # コンテナイメージの指定(ECRリポジトリ: agentcore-vpc-agent)
  agent_runtime_artifact {
    container_configuration {
      container_uri = "${data.aws_ecr_repository.agent.repository_url}:${var.container_image_tag}"
    }
  }

  # VPCモードの設定
  # AgentCoreがプライベートサブネット内にENI(ネットワークインターフェース)を作成し
  # NAT Gatewayを経由してBedrockエンドポイント等に接続する
  network_configuration {
    network_mode = "VPC"
    network_mode_config {
      subnets         = [aws_subnet.private_1a.id, aws_subnet.private_1c.id]
      security_groups = [aws_security_group.agentcore.id]
    }
  }

  # エージェントコンテナに渡す環境変数
  environment_variables = {
    AWS_REGION         = var.aws_region
    AWS_DEFAULT_REGION = var.aws_region
  }

  depends_on = [
    aws_iam_role_policy.agentcore_execution,
    aws_iam_role_policy_attachment.agentcore_managed,
    aws_nat_gateway.regional,
    aws_route_table_association.private_1a,
    aws_route_table_association.private_1c,
  ]
}

動作確認

エージェントサンドボックスを利用して確認します。

IPの許可をしていない場合

まずはEC2のセキュリティグループで何も許可していない時の挙動を検証します。以下プロンプトをサンドボックス内でリクエストします。
{"prompt": "http_requestツールを使って43.207.112.106にアクセスしてください"}

スクリーンショット 2026-02-23 16.05.56.png

予想通りですが、いつまで経ってもエージェントからのレスポンスがなく、ネットワークの疎通が取れない状態であると推測できます。

この検証をしている中で気づきましたが、Strands Agentsのhttp_requestは内部でrequestsモジュールを使っていますがtimeoutの設定がなく、疎通が取れない状態だと長時間応答待ちとなってしまいます。

IP許可後

続いて、NAT Gatewayに紐づいているIPアドレスからのHTTPリクエストを許可できるようにEC2のセキュリティグループで許可します。
Regional NAT Gatewayを作成すると、作成時点では2つのEIPが割り当てられていたため、両方を許可しました。
スクリーンショット 2026-02-23 16.08.08.png

その後、同じプロンプトをリクエストしたところ今度はアクセスができ、ページの内容を取得できていることが確認できました。
スクリーンショット 2026-02-23 16.09.18.png

実施後

NAT GatewayやEC2は課金が発生するため、確実にterraform destroyをしてリソースの削除をしましょう。ただし、冒頭の説明通り、ENIが残存することがありますのですべてのリソースが完全に削除されない可能性もあります。残存するENIは課金の対象にならないので後日確認して、必要であれば削除を行いましょう。

補足(Regional NAT Gatewayについて)

今回、Regional NAT Gatewayを利用してみましたが、実際にIP固定化で運用する際はNAT Gatewayのリソース作成時にavailability_zone_addressを指定し、手動でIPアドレスを割り当てることが望ましいです。
availability_zone_addressを指定しない場合は自動でIPアドレスが割り当てされるため、スケールアウト時に接続を許可していないIPアドレスがNAT Gatewayに関連づけられる可能性があります。
パブリックサブネットの作成が不要である点など、Regional NAT Gatewayを使う利点はありますので、IPアドレスの管理だけ注意が必要です。

GitHubリポジトリ

本検証のコードは以下の通りです。

参考

Resource: aws_bedrockagentcore_agent_runtime

Resource: aws_nat_gateway

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?