AWSのプライベートサブネットにあるEC2にSession Managerを使ってセキュアにアクセスする

この記事は個人ブログと同じ内容です

Session Managerについて

Session ManagerはAWS Systems Managerの一部であり、安全にEC2インスタンスなどに接続するためのツールです。Session Managerを使用すれば、SSHキーやパブリックIPを割り当てることなく、AWSのプライベートサブネットに配置されたEC2インスタンスに接続することができます。

VPCエンドポイントについて

VPCエンドポイントは、VPC内のリソース(例:EC2 インスタンス)がインターネットを経由せずに、他のAWSサービスに接続できるようにするための仮想デバイスです。

VPCエンドポイントを使ったSession Managerでのアクセスに必要なもの

・IAMポリシーのAmazonSSMManagedInstanceCoreを持ったIAMロールを持つEC2
・SSM エージェントがインストールされているEC2
・以下の4つのインターフェース型のVPCエンドポイント
 ・com.amazonaws.region.ec2.
 ・com.amazonaws.region.ec2messages.
 ・com.amazonaws.region.ssm.
 ・com.amazonaws.region.ssmmessages.
VPCエンドポイントへの443通信の許可.

今回作るもの

今回はプライベートサブネットにあるEC2にVPCエンドポイント経由でSession Managerを使ってセキュアにアクセスできるようにします。

作るのは、以下の通りです。
VPCとサブネット.
・EC2インスタンス.
・IAMロール.
VPCエンドポイント.

図にするとこんな感じです。

実際に作っていく

ステップ1 VPCとサブネットを作る

VPCとプライベートサブネットをEC2用とVPCエンドポイント用で2つ作っていきます。

VPCとサブネットのcidr_blockと設計は以下の通りです。
10.0.0.0/16 は全体のアドレス範囲、
10.0.0.0/18 はリージョン、AZ用(4個)、
10.0.0.0/20 はサブネット用(4個)、
アドレス部は12ビット(=4096-5個)
になるような設計をしました。

name cidr_block
VPC 10.0.0.0/16
EC2のサブネット 10.0.16.0/20
VPCエンドポイントのサブネット 10.0.48.0/20
locals {
  aws_region = "ap-northeast-1"
  name = "sample"
  cidr_blocks = {
    vpc       = "10.0.0.0/16"
    private  = "10.0.16.0/20"
    endpoint = "10.0.48.0/20"
  }
}

# VPC
# 10.0.0.0/16 は全体のアドレス範囲
# 10.0.0.0/18 はリージョン、AZ用(4個) 
# 10.0.0.0/20 はサブネット用(4個)
# アドレス部は12ビット(=4096-5個)

resource "aws_vpc" "main" {
  cidr_block           = local.cidr_blocks.vpc
  enable_dns_hostnames = true
  enable_dns_support   = true
  tags = {
    Name = local.name
  }
}
# EC2用のプライベートサブネット
resource "aws_subnet" "private" {
  availability_zone = "ap-northeast-1a"
  cidr_block        = local.cidr_blocks.private
  vpc_id            = aws_vpc.main.id
}

resource "aws_network_acl" "private" {
  vpc_id     = aws_vpc.main.id
  subnet_ids = [aws_subnet.main.id]
}

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

resource "aws_route_table_association" "private" {
  subnet_id      = aws_subnet.private.id
  route_table_id = aws_route_table.private.id
}
# VPCエンドポイント用のプライベートサブネット
resource "aws_subnet" "endpoint" {
  availability_zone = "ap-northeast-1a"
  cidr_block        = local.cidr_blocks.endpoint
  vpc_id            = aws_vpc.main.id
}

resource "aws_network_acl" "endpoint" {
  vpc_id     = aws_vpc.main.id
  subnet_ids = [aws_subnet.main.id]
}

resource "aws_route_table" "endpoint" {
  vpc_id = aws_vpc.main.id
}

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

ステップ2 IAMロールの作成

IAMポリシーのAmazonSSMManagedInstanceCoreを持ったIAMロールを作ります。

data "aws_iam_policy_document" "assume_role" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
  }
}
resource "aws_iam_role" "main" {
  name               = "ssm-iam-role"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

data "aws_iam_policy" "systems_manager" {
  arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

resource "aws_iam_role_policy_attachment" "main" {
  role       = aws_iam_role.main.name
  policy_arn = data.aws_iam_policy.systems_manager.arn
}

resource "aws_iam_instance_profile" "systems_manager" {
  name = "ssm-instance-profile"
  role = aws_iam_role.main.name
}

ステップ3 EC2インスタンスの作成

EC2用のプライベートサブネットに、先ほど作ったIAMロールを付与したEC2インスタンスを作っていきます。 AMIは、SSM エージェントがすでにインストールされているAmazon Linux2023を使います。

resource "aws_instance" "main" {
  ami                         = "ami-05779067e4eff0b9d"
  instance_type               = "t4g.nano"
  subnet_id                   = local.cidr_blocks.private
  associate_public_ip_address = false
  vpc_security_group_ids      = [aws_security_group.ec2.id]
  availability_zone           = "ap-northeast-1a"
  iam_instance_profile        = aws_iam_instance_profile.systems_manager.name
}

resource "aws_security_group" "ec2" {
  name        = "ec2-sg"
  vpc_id      = aws_vpc.main.id
  description = "ec2 security group"
}

ステップ4 VPCエンドポイントの作成

以下の4つのインターフェースエンドポイントを作成します。

・com.amazonaws.region.ec2.
・com.amazonaws.region.ec2messages.
・com.amazonaws.region.ssm.
・com.amazonaws.region.ssmmessages.

resource "aws_vpc_endpoint" "main" {
  for_each = {
    ec2         = "com.amazonaws.ap-northeast-1.ec2"
    ec2messages = "com.amazonaws.ap-northeast-1.ec2messages"
    ssm         = "com.amazonaws.ap-northeast-1.ssm"
    ssmmessages = "com.amazonaws.ap-northeast-1.ssmmessages"
  }

  service_name       = each.value
  vpc_endpoint_type  = "Interface"
  vpc_id             = var.vpc_id
  security_group_ids = [aws_security_group.vpc_endpoint.id]
  subnet_ids         = [aws_subnet.private.id]
}

resource "aws_security_group" "vpc_endpoint" {
  description = "vpc-endpoint"
  name        = "vpc-endpoint"
  vpc_id      = aws_vpc.main.id
}

ステップ5 セキュリティグループとNACLの調整

先ほど作成したエンドポイントに対して、EC2からポート443のアクセスができるようにセキュリティグループとNACLを調整します。
注意ポイント:
・行きの通信だけではなく、帰りの通信も通れるようにしよう
・NACLは帰りの通信も記載する必要がありますが、セキュリティグループはステートレスなので、行きの通信のみで良いです

NACLの調整

# ステップ1で作成した、EC2用のプライベートサブネットのファイルの続き

resource "aws_network_acl_rule" "subnet_private1_ingress" {
  for_each = {
    ephemeral         = { action = "allow", cidr_block = local.cidr_blocks.endpoint, from_port = 1024, to_port = 65535, protocol = "tcp", rule_no = 100 }, #ssmの443通信の帰り
  }

  network_acl_id = aws_network_acl.private.id
  rule_action    = each.value.action
  cidr_block     = each.value.cidr_block
  from_port      = each.value.from_port
  to_port        = each.value.to_port
  protocol       = each.value.protocol
  rule_number    = each.value.rule_no
  egress         = false
}

resource "aws_network_acl_rule" "subnet_private1_egress" {
  for_each = {
    https                = { action = "allow", cidr_block = local.cidr_blocks.endpoint, from_port = 443, to_port = 443, protocol = "tcp", rule_no = 100 },                  #ec2からインターネット通信するため
  }

  network_acl_id = aws_network_acl.private.id
  rule_action    = each.value.action
  cidr_block     = each.value.cidr_block
  from_port      = each.value.from_port
  to_port        = each.value.to_port
  protocol       = each.value.protocol
  rule_number    = each.value.rule_no
  egress         = true
}
# ステップ1で作成した、VPCエンドポイント用のプライベートサブネットのファイルの続き

resource "aws_network_acl_rule" "subnet_endpoint1_ingress" {
  for_each = {
    https_from_private = { action = "allow", cidr_block = local.cidr_blocks.private, from_port = 443, to_port = 443, protocol = "tcp", rule_no = 100 },
  }

  network_acl_id = aws_network_acl.endpoint.id
  rule_action    = each.value.action
  cidr_block     = each.value.cidr_block
  from_port      = each.value.from_port
  to_port        = each.value.to_port
  protocol       = each.value.protocol
  rule_number    = each.value.rule_no
  egress         = false
}

resource "aws_network_acl_rule" "subnet_endpoint1_egress" {
  for_each = {
    ephemeral_to_private1 = { action = "allow", cidr_block = local.cidr_blocks.private, from_port = 1024, to_port = 65535, protocol = "tcp", rule_no = 100 },
  }

  network_acl_id = aws_network_acl.endpoint.id
  rule_action    = each.value.action
  cidr_block     = each.value.cidr_block
  from_port      = each.value.from_port
  to_port        = each.value.to_port
  protocol       = each.value.protocol
  rule_number    = each.value.rule_no
  egress         = true
}

セキュリティグループの調整

# ステップ3で作ったEC2のファイルの続き

resource "aws_vpc_security_group_egress_rule" "ec2_egress" {
  from_port         = 443
  to_port           = 443
  ip_protocol       = "tcp"
  security_group_id = aws_security_group.ec2.id
  cidr_ipv4         = local.cidr_blocks.private
}
#ステップ4で作ったVPCエンドポイントのファイルの続き

resource "aws_vpc_security_group_ingress_rule" "vpc_endpoint_ingress" {
  from_port         = 443
  to_port           = 443
  ip_protocol       = "tcp"
  security_group_id = aws_security_group.vpc_endpoint.id
  cidr_ipv4         = local.cidr_blocks.private
}

ステップ6 Session Managerでアクセスする

  1. AWSにアクセスして、EC2のインスタンス選択して、接続ボタンを押す

  2. 画像のようになっていれば成功です!

まとめ

今回SSMでの接続をしましたが、個人的にネットワーク周りにかなり苦戦したので、うまくいかない時は入念に見直しましょう。
あと、公式ドキュメント読みにくくて、個人で書いたブログに行きがちですが、公式は間違いがないので最優先で読みましょう。
不明点や問題点などありましたらコメントお願いします。

参考記事.
https://zenn.dev/kazutech/articles/3559db9605d198
https://dev.classmethod.jp/articles/terraform-session-manager-linux-ec2-vpcendpoint/