【AWS】Interface 型 VPC エンドポイント経由で EC2 から Secrets Manager にプライベート接続する環境の構築手順(Terraform)

プライベートサブネット内の EC2 インスタンスが、Internet Gateway / NAT Gateway を使わずに、Interface 型 VPC エンドポイント経由で AWS サービスへ接続する構成です。

今回は例として Secrets Manager に接続する構成にします。Secrets Manager、SSM、ECR、CloudWatch Logs、KMS、STS などは Interface 型 VPC エンドポイントでよく使います。

Gateway 型 VPC エンドポイントについては以下で詳しく解説しています。

Interface 型 VPC エンドポイントとは

Interface 型 VPC エンドポイントは、ざっくり言うと、VPC 内に AWS サービス接続用の ENI を作り、その ENI のプライベート IP アドレス宛てに通信する仕組みです。

Interface 型 VPC エンドポイントは PrivateLink を利用して AWS サービスなどに接続する仕組みです。

Gateway 型 VPC エンドポイントは、ルートテーブルに

S3宛て → Gateway Endpoint

のようなルートを追加する方式でした。

一方、Interface 型はルートテーブルではなく、サブネット内に

エンドポイントENI

が作成されます。

イメージとしてはこうです。

EC2
 ↓
Secrets Managerの通常DNS名
例: secretsmanager.ap-northeast-1.amazonaws.com
 ↓
VPC内ではエンドポイントENIのプライベートIPに名前解決される
 ↓
Interface型VPCエンドポイント
 ↓
AWS PrivateLink
 ↓
Secrets Manager

EC2 から見ると普通に AWS サービスへアクセスしているように見えますが、実際の通信は VPC 内のエンドポイント ENI を経由します。

Interface 型 VPC エンドポイントと Gateway 型 VPC エンドポイント比較表

項目Interface型Gateway型
代表サービスSecrets Manager、SSM、ECR、CloudWatch Logs、KMS、STSなどS3、DynamoDB
実体サブネット内のENIルートテーブルのターゲット
通信先DNSでエンドポイントENIへ向けるルートテーブルで制御
セキュリティグループありなし
AWS PrivateLink使う使わない
料金時間課金+データ処理量課金ありS3/DynamoDB Gateway Endpointは追加料金なし
オンプレミスからの利用Direct Connect / VPN経由で利用可能なケースあり基本不可

Gateway 型 VPC エンドポイントは PrivateLink を使わず、S3 と DynamoDB 向けの仕組みです。また、S3 の Gateway 型 VPC エンドポイント追加料金なしですが、Interface 型 VPC エンドポイントは追加コストがあります。

構成

今回は以下のような構成です。

AWS
└── VPC 10.0.0.0/16
    └── Availability Zone A
        └── Private Subnet 10.0.1.0/24
            ├── EC2
            │   └── Secrets Managerへアクセス
            │
            └── Interface型VPCエンドポイント
                ├── Endpoint ENI
                ├── Private IP: 10.0.1.x
                └── Security Group付き

AWS PrivateLink
└── AWS Secrets Manager

ポイントは、EC2 と Interface 型エンドポイントが同じプライベートサブネット内にあることです。

Interface 型エンドポイントは指定したサブネットごとに ENI を作成します。高可用性にしたい場合は、複数 AZ の プライベートサブネットにエンドポイントを作成します。

構成図

通信の流れ

Secrets Manager へアクセスする例で考えます。EC2 上のアプリケーションや AWS CLI が、以下の Secrets Manager のエンドポイントへアクセスします。

https://secretsmanager.ap-northeast-1.amazonaws.com

このとき、Interface 型 VPC エンドポイントで private_dns_enabled = true にしていると、VPC 内ではこの DNS 名がエンドポイント ENI のプライベート IP に解決されます。

private_dns_enabled ( Optional[bool] ) – 指定された VPC にプライベート ホスト ゾーンを関連付けるかどうか。これにより、デフォルトの DNS ホスト名を使用してサービスにリクエストを送信できます。デフォルト:IInterfaceVpcEndpointService インスタンスによって設定されます。IInterfaceVpcEndpointService インスタンスによって定義されていない場合は true です。

つまり、EC2から見ると、

secretsmanager.ap-northeast-1.amazonaws.com

へアクセスしているつもりですが、実際には

10.0.1.x

のような VPC 内のプライベート IP に通信しています。

その通信が Interface 型 VPC エンドポイントを通り、AWS PrivateLink 経由で Secrets Manager へ届きます。

PrivateLink とは

PrivateLink は、Interface 型 VPC エンドポイントの裏側で使われている仕組みです。

一言でいうと、VPC 内のリソースから、AWS サービス・他の VPC のサービス・SaaS 事業者のサービスへ、インターネットを経由せずにプライベート接続するための AWS の仕組みです。

PrivateLink を使うには、サービスにアクセスする必要があるサブネットに VPC エンドポイントを作成し、それによって ENI が作られます。そして、その ENI がサービスへのトラフィックの入口になります。

結論:PrivateLinkは「裏側の専用通路」と言える

Interface 型 VPC エンドポイントでは、VPC 内に エンドポイント ENI が作られます。

Private Subnet
├── EC2
└── Interface VPC Endpoint
    └── Endpoint ENI
        └── Private IP: 10.0.1.x

EC2 は、この ENI のプライベート IP に通信します。その先で、AWS PrivateLink が AWS サービスへつないでくれます。

EC2
 ↓
Endpoint ENI
 ↓
AWS PrivateLink
 ↓
AWSサービス

つまり、EC2 から見えるのは VPC 内のプライベート IP までです。その先の AWS サービス側への接続を裏で担当しているのが AWS PrivateLink です。

Secrets Manager へアクセスする場合

たとえば、EC2 から Secrets Manager へアクセスする場合です。

通常であれば、EC2 は以下のような AWS サービスのエンドポイントへアクセスします。

secretsmanager.ap-northeast-1.amazonaws.com
 ↓ DNS
10.0.1.10

そして EC2 は、10.0.1.10 に HTTPS 通信します。

EC2
 ↓ HTTPS:443
10.0.1.10
 ↓
Interface VPC Endpoint
 ↓
AWS PrivateLink
 ↓
Secrets Manager

重要なのは、EC2 が直接 Secrets Manage rの実体に向かっているわけではないことです。

EC2 はあくまで、VPC 内に作られた エンドポイントENI に通信しています。

PrivateLink がやっていること

PrivateLink がない場合、プライベートサブネットの EC2 からAWS サービスへアクセスするには、一般的には NAT Gateway などを使います。

EC2
 ↓
NAT Gateway
 ↓
インターネット側のAWSサービスエンドポイント
 ↓
AWSサービス

この場合、通信先は AWS サービスですが、経路としてはパブリックな AWS サービスエンドポイントへ向かいます。

一方、PrivateLink を使うとこうなります。

EC2
 ↓
Interface VPC Endpoint
 ↓
AWS PrivateLink
 ↓
AWSサービス

この構成では、EC2 にパブリック IP は不要です。NAT Gateway も不要です。Internet Gateway も不要です。

PrivateLink を使った Interface 型 VPC エンドポイントでは、Internet Gateway、NAT デバイス、VPN 接続、Direct Connect 接続なしにサービスへプライベートアクセスでき、VPC 内のインスタンスにパブリック IP は不要と説明されています。

PrivateLinkの登場人物

PrivateLink を理解するときは、登場人物を分けると分かりやすいです。

1. サービスコンシューマー

サービスを使う側です。

今回の例では、EC2 がある VPC 側です。

EC2があるVPC

この VPC に Interface 型 VPC エンドポイントを作ります。

Interface 型 VPC エンドポイント

使う側の VPC に作る接続口です。

Interface VPC Endpoint
└── Endpoint ENI
    └── Private IP

EC2はこのENIに通信します。

Interface 型 VPC エンドポイントを作ると、指定したサブネットにエンドポイントネットワークインターフェイスが作成されます。この ENI が対象サービスへのトラフィックのエントリポイントになります。

エンドポイントサービス

接続される側のサービスです。

たとえば AWS サービスなら、以下が挙げられます。

Secrets Manager
SSM
ECR
CloudWatch Logs
KMS
STS

AWS サービス以外にも、自分で作ったサービスや、他社 SaaS のサービスを PrivateLink で公開することもできます。AWS サービスだけでなく、AWS の顧客やパートナーが所有するサービスにも PrivateLink で接続できます。

AWS サービスに接続する場合

AWS サービスに接続する場合は、以下の流れになります。

自分のVPC
└── EC2
    ↓
    Interface VPC Endpoint
    ↓
    AWS PrivateLink
    ↓
    AWSサービス

Secrets Manager なら Terraform ではこう書きます。

resource "aws_vpc_endpoint" "secretsmanager" {
  vpc_id              = aws_vpc.main.id
  service_name        = "com.amazonaws.ap-northeast-1.secretsmanager"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = [aws_subnet.private_a.id]
  security_group_ids  = [aws_security_group.vpce.id]
  private_dns_enabled = true
}

セキュリティグループの考え方

Interface 型 VPC エンドポイントには、セキュリティグループを付けられます。

ここが Gateway 型との大きな違いです。

Interface 型エンドポイントに関連付けられたセキュリティグループのルールによって、VPC 内リソースからエンドポイント ENI への通信を制御すると説明されています。

今回の構成では、以下のように考えます。

EC2のSecurity Group
  outbound 443 → Endpoint Security Group

EndpointのSecurity Group
  inbound 443 ← EC2のSecurity Group

つまり、EC2 からエンドポイント ENI の443番ポートへ通信できるようにするという設定が必要です。

AWS サービスへの API アクセスは基本的に HTTPS なので、通常は TCP 443 を許可します。

エンドポイントポリシーの考え方

Interface 型 VPC エンドポイントにも、Gateway 型と同じように エンドポイントポリシー を設定できます。

たとえば EC2 から Secrets Manager の Secret を取得する場合、最低限見るポイントは3つあります。

1. EC2のIAMロール
   → secretsmanager:GetSecretValue を許可しているか

2. VPCエンドポイントポリシー
   → そのSecretへのアクセスを許可しているか

3. Secret側のリソースポリシー
   → クロスアカウント等の場合、EC2ロールを許可しているか

同一アカウントで通常利用するだけなら、Secret側のリソースポリシーは不要なことも多いです。

ただし、エンドポイントポリシーで絞る場合は、

このVPCエンドポイント経由では、このSecretだけ取得可能

のように制限できます。

Terraformコード

今回は、以下を作ります。

  • VPC
  • Private Subnet
  • EC2 用 Security Group
  • Interface Endpoint 用 Security Group
  • EC2 IAM ロール
  • EC2 インスタンス
  • Secrets Manager 用 Interface VPC Endpoint
  • テスト用 Secret
  • エンドポイントポリシー

なお、Terraformの aws_vpc_endpoint では、Interface 型の場合に security_group_ids を関連付けられます。

構成図

上図の構成図と同じ構成図です。

main.tf

terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

data "aws_region" "current" {}

data "aws_caller_identity" "current" {}

# 最新のAmazon Linux 2023 AMIを取得
data "aws_ami" "al2023" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }

  filter {
    name   = "architecture"
    values = ["x86_64"]
  }

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

########################################
# VPC
########################################

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "interface-endpoint-vpc"
  }
}

########################################
# Private Subnet
########################################

resource "aws_subnet" "private_a" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "ap-northeast-1a"
  map_public_ip_on_launch = false

  tags = {
    Name = "private-subnet-a"
  }
}

########################################
# Route Table
# NAT Gateway / Internet Gateway は作らない
########################################

resource "aws_route_table" "private" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "private-route-table"
  }
}

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

########################################
# Security Group for EC2
########################################

resource "aws_security_group" "ec2" {
  name        = "ec2-sg"
  description = "Security group for private EC2"
  vpc_id      = aws_vpc.main.id

  # 今回は外部からSSHしない前提のため、inboundはなし

  egress {
    description     = "Allow HTTPS to VPC endpoint"
    from_port       = 443
    to_port         = 443
    protocol        = "tcp"
    security_groups = [aws_security_group.vpce.id]
  }

  tags = {
    Name = "ec2-sg"
  }
}

########################################
# Security Group for Interface VPC Endpoint
########################################

resource "aws_security_group" "vpce" {
  name        = "secretsmanager-vpce-sg"
  description = "Security group for Secrets Manager interface endpoint"
  vpc_id      = aws_vpc.main.id

  ingress {
    description     = "Allow HTTPS from EC2"
    from_port       = 443
    to_port         = 443
    protocol        = "tcp"
    security_groups = [aws_security_group.ec2.id]
  }

  egress {
    description = "Allow all outbound from endpoint ENI"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "secretsmanager-vpce-sg"
  }
}

########################################
# Test Secret
########################################

resource "aws_secretsmanager_secret" "sample" {
  name = "sample/interface-endpoint-secret"

  tags = {
    Name = "sample-interface-endpoint-secret"
  }
}

resource "aws_secretsmanager_secret_version" "sample" {
  secret_id = aws_secretsmanager_secret.sample.id

  secret_string = jsonencode({
    username = "test-user"
    password = "test-password"
  })
}

########################################
# IAM Role for EC2
########################################

resource "aws_iam_role" "ec2" {
  name = "interface-endpoint-ec2-role"

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

resource "aws_iam_policy" "ec2_secretsmanager" {
  name = "interface-endpoint-ec2-secretsmanager-policy"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "AllowGetSampleSecret"
        Effect = "Allow"
        Action = [
          "secretsmanager:GetSecretValue",
          "secretsmanager:DescribeSecret"
        ]
        Resource = aws_secretsmanager_secret.sample.arn
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "ec2_secretsmanager" {
  role       = aws_iam_role.ec2.name
  policy_arn = aws_iam_policy.ec2_secretsmanager.arn
}

resource "aws_iam_instance_profile" "ec2" {
  name = "interface-endpoint-ec2-instance-profile"
  role = aws_iam_role.ec2.name
}

########################################
# EC2
########################################

resource "aws_instance" "private_ec2" {
  ami                         = data.aws_ami.al2023.id
  instance_type               = "t3.micro"
  subnet_id                   = aws_subnet.private_a.id
  vpc_security_group_ids      = [aws_security_group.ec2.id]
  iam_instance_profile        = aws_iam_instance_profile.ec2.name
  associate_public_ip_address = false

  tags = {
    Name = "private-ec2-for-interface-endpoint"
  }
}

########################################
# Interface VPC Endpoint for Secrets Manager
########################################

resource "aws_vpc_endpoint" "secretsmanager" {
  vpc_id              = aws_vpc.main.id
  service_name        = "com.amazonaws.${data.aws_region.current.name}.secretsmanager"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = [aws_subnet.private_a.id]
  security_group_ids  = [aws_security_group.vpce.id]
  private_dns_enabled = true

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "AllowGetSpecificSecretViaEndpoint"
        Effect = "Allow"
        Principal = "*"
        Action = [
          "secretsmanager:GetSecretValue",
          "secretsmanager:DescribeSecret"
        ]
        Resource = aws_secretsmanager_secret.sample.arn
      }
    ]
  })

  tags = {
    Name = "secretsmanager-interface-endpoint"
  }
}

このTerraformで作られるリソース

このコードを適用すると、プライベートサブネット内に EC2 が作成されます。

ただし、この EC2 には以下がありません。

Internet Gateway なし
NAT Gateway なし
Public IP なし

そのため、通常のインターネット経由では Secrets Manager へ到達できません。

しかし、Secrets Manager 用の Interface 型 VPC エンドポイントを作っているため、EC2 から Secrets Manager への API 通信は可能になります。

private_dns_enabled = true が重要

ここが Interface 型 VPC エンドポイントの重要ポイントです。

private_dns_enabled = true

これを有効にすると、EC2 から通常の AWS サービス名へアクセスしたときに、VPC 内では Interface 型 VPC エンドポイントのプライベート IP へ名前解決されます。

たとえば、通常は以下の名前にアクセスします。

secretsmanager.ap-northeast-1.amazonaws.com

private_dns_enabled = true の場合、VPC 内ではこの DNS 名がエンドポイント ENI のプライベート IP へ解決されます。

そのため、アプリケーション側の接続先 URL を変えなくてもよくなります。これは非常に大きなメリットです。

Gateway型とInterface型の通信の違い

Gateway 型は、ルートテーブルで S3 や DynamoDB への通信をエンドポイントに向けます。

EC2
 ↓
ルートテーブルを見る
 ↓
S3宛てならGateway Endpointへ
 ↓
S3

Interface 型は、DNS でエンドポイントENIに向けます。

EC2
 ↓
AWSサービス名を名前解決
 ↓
エンドポイントENIのプライベートIPが返る
 ↓
Interface EndpointへHTTPS通信
 ↓
AWS PrivateLink
 ↓
AWSサービス

以下のように覚えると分かりやすいです。

Gateway型 = ルートテーブル型
Interface型 = ENI + DNS + Security Group型

設計ポイント

1. サービスごとにエンドポイントが必要

Interface 型は基本的に、

Secrets Manager用エンドポイント
SSM用エンドポイント
ECR用エンドポイント
CloudWatch Logs用エンドポイント

のように、サービスごとに作ります。

2. セキュリティグループで通信元を絞る

Interface 型 VPC エンドポイントにはセキュリティグループを付けられるため、

EC2のSecurity Groupからのみ443を許可

のようにできます。

これは非常に分かりやすい制御です。

ingress {
  from_port       = 443
  to_port         = 443
  protocol        = "tcp"
  security_groups = [aws_security_group.ec2.id]
}

この設定により、指定したEC2 Security GroupからのHTTPS通信だけを許可できます。

3. エンドポイントポリシーでアクセス先を絞る

セキュリティグループはネットワークレベルの制御です。

一方、エンドポイントポリシーは AWS API レベルの制御です。

たとえば、今回の例では、

このVPCエンドポイント経由では、特定のSecretだけ取得可能

にしています。

policy = jsonencode({
  Version = "2012-10-17"
  Statement = [
    {
      Sid    = "AllowGetSpecificSecretViaEndpoint"
      Effect = "Allow"
      Principal = "*"
      Action = [
        "secretsmanager:GetSecretValue",
        "secretsmanager:DescribeSecret"
      ]
      Resource = aws_secretsmanager_secret.sample.arn
    }
  ]
})

ただし、これだけではEC2がSecretを取得できるわけではありません。

EC2のIAMロールにも許可が必要です。

IAMロールで許可
かつ
エンドポイントポリシーで許可

の両方が必要です。

4. private_dns_enabled を有効にする

すでに上述していますが、通常はこれを有効にします。

private_dns_enabled = true

これにより、アプリケーション側は通常の AWS サービスエンドポイントをそのまま使えます。

無効にすると、VPC エンドポイント固有の DNS 名を指定する必要が出てきて、アプリケーション側の設定が面倒になります。