はじめに

AWS試験対策の問題を解くと、しばしばEC2へのセキュアな接続方式として、 Systems Manager Session Managerを利用する方法を目にします。

今回は知識整理のため、Session Managerを利用してプライベートサブネットのEC2へ接続できる環境を、 Terraformで構築してみました。

今回作成する環境

output

Session ManagerでプライベートサブネットのEC2へ接続するために必要なこと

大まかに以下の作業が必要となります。

  • EC2にSSM Agentをインストールする。
  • Session Manager(及びSSM)へのアクセスを許可したIAMロールをEC2に付与する。
  • VPCエンドポイントを作成する。
  • EC2とVPCエンドポイントが相互に接続できるセキュリティグループを設定する。

それぞれの作業がTerraformコードのどの部分に対応するかは後述します。

なお、EC2がインターネット接続できる場合は、VPCエンドポイントの作成は不要です。

参考

利用したTerraformコード

環境

  • OS:Windows 11 Pro
    • バージョン:23H2
    • OSビルド:22631.4037
  • WSL2
    • Ubuntu 22.04.3

Terraformバージョン

$ terraform providers -version
Terraform v1.11.0
on linux_amd64
+ provider registry.terraform.io/hashicorp/aws v5.94.1

ディレクトリ構成

$ tree
.
├── ec2.tf
├── iam.tf
├── provider.tf
├── security_group.tf
├── variable.tf
└── vpc.tf

コード内容

以下に利用したTerraformコードを記載します。

Session Managerで接続するために必要なこと等、特筆すべきことがあれば適宜記載します。

  • provider.tf
terraform {
  required_version = ">= 1.0.0, < 2.0.0"

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

provider "aws" {
  region  = "ap-northeast-1"
}
  • variable.tf
locals {
  region       = "ap-northeast-1"
  https_port   = 443
  any_protocol = -1
  tcp_protocol = "tcp"
}

variable "vpc_cidr" {
  description = "CIDR block for VPC"
  type        = string
  default     = "10.0.0.0/16"
}

variable "subnet" {
  description = "CIDR block and region for subnet"
  type = object({
    az   = string
    cidr = string
  })

  default = {
    az   = "ap-northeast-1a"
    cidr = "10.0.1.0/24"
  }
}

variable "vpc_endpoint" {
  description = "List of VPC endpoint name"
  type        = list(string)
  default     = ["ssm", "ssmmessages"]
}
  • vpc.tf
# VPCの作成
resource "aws_vpc" "test_vpc" {
  cidr_block           = var.vpc_cidr
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "test_vpc"
  }
}

# プライベートサブネットの作成
resource "aws_subnet" "test_private_subnet" {
  vpc_id            = aws_vpc.test_vpc.id
  cidr_block        = var.subnet.cidr
  availability_zone = var.subnet.az

  tags = {
    Name = "test_private_subnet"
  }
}

# 空のルートテーブルを作成
resource "aws_route_table" "test_private_route_table" {
  vpc_id = aws_vpc.test_vpc.id

  route = []

  tags = {
    Name = "test_private_route_table"
  }
}

# ルートテーブルをプライベートサブネットへアタッチ
resource "aws_route_table_association" "test_private_route_table_association" {
  subnet_id      = aws_subnet.test_private_subnet.id
  route_table_id = aws_route_table.test_private_route_table.id
}

# VPCエンドポイントの作成
resource "aws_vpc_endpoint" "test_vpc_endpoint" {
  for_each = toset(var.vpc_endpoint)

  vpc_id              = aws_vpc.test_vpc.id
  subnet_ids          = [aws_subnet.test_private_subnet.id]
  service_name        = "com.amazonaws.${local.region}.${each.value}"
  vpc_endpoint_type   = "Interface"
  security_group_ids  = [aws_security_group.vpc_endpoint_sg.id]
  private_dns_enabled = true
}

こちらのコードでVPC、サブネット、VPCエンドポイントを作成しています。

Session Managerの接続には、以下のVPCエンドポイントの作成が必要になります。

  • com.amazonaws.region.ssm
  • com.amazonaws.region.ssmmessages

variable.tfでVPCエンドポイントの名前を要素に持つリストを作成しておき、 for_eachで一括で作成する方法にしてみました。

なお、以前まではec2messages.region.amazonaws.comというVPCエンドポイントも作成が必要でしたが、 直近のSSM Agentではec2messagesは不要になったとのことです。

必要なVPCエンドポイントの情報は、以下の記事を大変参考にさせていただきました。

『SSM セッションマネージャーに必要なVPCエンドポイントが2つになっていた』(https://dev.classmethod.jp/articles/vpcendpoint-ec2messages-not-required/)

  • iam.tf
# 信頼されたエンティティを定義
data "aws_iam_policy_document" "assume_role" {
  statement {
    actions = [
      "sts:AssumeRole"
    ]

    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
  }
}

# IAMロールの作成
resource "aws_iam_role" "test_iam_role" {
  name               = "test_role"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

# IAMインスタンスプロファイルの作成
resource "aws_iam_instance_profile" "test_profile" {
  name = "test_profile"
  role = aws_iam_role.test_iam_role.name
}

# AmazonSSMManagedInstanceCoreのARNをフェッチ
data "aws_iam_policy" "ssm_access" {
  arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

# IAMロールにAmazonSSMManagedInstanceCoreをアタッチ
resource "aws_iam_role_policy_attachment" "test_attachment" {
  role       = aws_iam_role.test_iam_role.name
  policy_arn = data.aws_iam_policy.ssm_access.arn
}

こちらのコードでEC2にアタッチするロールを定義しています。

EC2にSession Managerへのアクセスを許可するため、AWS管理ポリシーのAmazonSSMManagedInstanceCoreをIAMロールにアタッチしています。

  • ec2.tf
# 最新のAmazon Linux 2023 x86_64版のAMI IDを取得
data "aws_ami" "amzn_linux_2023_ami" {
  most_recent = true
  owners      = ["amazon"]

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

resource "aws_instance" "test_ec2" {
  ami                    = data.aws_ami.amzn_linux_2023_ami.id
  instance_type          = "t2.micro"
  subnet_id              = aws_subnet.test_private_subnet.id
  iam_instance_profile   = aws_iam_instance_profile.test_profile.name
  vpc_security_group_ids = [aws_security_group.ec2_sg.id]

  tags = {
    Name = "test_ec2"
  }

}

こちらのコードでEC2の作成を定義しています。

今回はデフォルトでSSM AgentがインストールされているAmazon Linux 2023を利用しました。

また、aws_amiData Sourcesを利用して、自動で最新のAmazon Linux 2023のAMIを指定するようにしています。

  • security_group.tf
# EC2用セキュリティグループの作成
resource "aws_security_group" "ec2_sg" {
  name   = "ec2_sg"
  vpc_id = aws_vpc.test_vpc.id

  tags = {
    Name = "ec2_sg"
  }
}

# VPCエンドポイント用セキュリティグループの作成
resource "aws_security_group" "vpc_endpoint_sg" {
  name   = "vpc_endpoint_sg"
  vpc_id = aws_vpc.test_vpc.id

  tags = {
    Name = "vpc_endpoint_sg"
  }
}

# HTTPSのインバウンド許可設定を作成し、VPCエンドポイント用セキュリティグループへアタッチ
resource "aws_vpc_security_group_ingress_rule" "allow_https_ingress" {
  security_group_id = aws_security_group.vpc_endpoint_sg.id
  cidr_ipv4         = var.subnet.cidr
  from_port         = local.https_port
  ip_protocol       = local.tcp_protocol
  to_port           = local.https_port
}

# HTTPSのアウトバウンド許可設定を作成し、EC2用セキュリティグループへアタッチ
resource "aws_vpc_security_group_egress_rule" "allow_https_egress" {
  security_group_id = aws_security_group.ec2_sg.id
  cidr_ipv4         = var.subnet.cidr
  from_port         = local.https_port
  ip_protocol       = local.tcp_protocol
  to_port           = local.https_port
}

最後にこちらのコードでセキュリティグループを定義しています。

Session Managerを利用するために、EC2とVPCエンドポイントでそれぞれの以下の通りセキュリティグループを設定は必要になります。

種別 許可設定
EC2 VPCエンドポイントへのHTTPS(ポート443)アウトバウンド
VPCエンドポイント EC2からのHTTPS(ポート443)インバウンド

Terraformの実行

上記コードを作成後、以下のコマンドでterraformによる構築を開始します。

$ terraform init
$ terraform apply

Session Managerでの接続

Terraformで環境構築が終わったら、Session Manager経由でのEC2接続を試すことができます。

Session Managerへの接続方法として、マネジメントコンソールからの接続方法とAWS CLIを用いた方法があるのでそれぞれ記載します。

マネジメントコンソールでの接続

EC2 > インスタンス > 接続したインスタンスを選択 > 接続タブをクリックし、Session Managerの接続ページへアクセスします。

instance_list connecnt_instance

Session Managerの画面が開き、シェル操作ができるようになります。

terminal

Terraformで環境構築をした直後だと、以下のようなエラーが出る場合がありました。 その場合は数分程度待てば接続できるようになりました。

session_manager_error

AWS CLIでの接続

AWS CLIでSession Managerに接続する場合は、Session Managerプラグインをインストールする必要があります。

今回は、WSL2のUbuntuに導入するため、以下の手順を実行しました。

『Debian Server と Ubuntu Server での Session Manager プラグインのインストール』 https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/install-plugin-debian-and-ubuntu.html

Session Managerプラグインをインストール後、以下のコマンドを実行するとSession Manager経由でEC2の操作ができます。

$ aws ssm start-session --target インスタンスID --region リージョン名 --profile プロファイル名(プロファイルを使用していれば)

Starting session with SessionId: botocore-session-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxx
sh-5.2$ uname -n
ip-10-0-1-172.ap-northeast-1.compute.internal
sh-5.2$

終わりに

Session Managerを利用すると、EC2のセキュリティグループでSSHを許可しなくてもよかったり、SSH鍵を用意しなくても接続できて便利そうです。

個人のAWSアカウントで検証する際は、積極的に利用したいと思います。