セッションマネージャーを使用してプライベートサブネットのLinux用EC2にアクセス(NAT Gateway編)

今回はセッションマネージャー + NAT Gateway編だよ!!
2021.06.08

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは!コンサル部のinomaso(@inomasosan)です。

先日、セッションマネジャーとVPCエンドポイントによる、プライベートサブネットのLinux用EC2への接続のブログを書きました。
ただ、VPCエンドポイントの代わりにNAT Gatewayを利用することもできるので、今回はNAT Gateway編を書かせていただきます。

前回と内容が重複する部分はありますが、手順等については本記事のみで完結できるように、あえて省略しておりませんので予めご了承ください。

この記事で学べること

  • セッションマネージャーとNAT Gateway使用したプライベートサブネットのLinux用EC2への接続方法
  • Terraformでのコードの書き方

前提知識

1. AWS Systems Manager Session Managerとは

セッションマネージャはAWS Systems Managerの機能の一つです。
AWSマネージメントコンソールやAWS CLI経由で、EC2に接続することができます。

2. NAT Gatewayとは

以下、公式ドキュメントからの抜粋です。

ネットワークアドレス変換 (NAT) ゲートウェイを使用して、プライベートサブネットのインスタンスからはインターネットや他の AWS のサービスに接続できるが、インターネットからはこれらのインスタンスとの接続を開始できないようにすることができます。

プライベートサブネットにあるEC2等から外部へアクセス可能にするサービスとなります。

システム構成図

今回、検証で構築するシステム構成図です。
必要なAWSリソースや、ネットワーク要件を記載しています。

セッションマネージャーへアクセスするために、IGWやNatGateway等を前提としております。 ALB + EC2 + RDSの構成で、EC2をプライベートサブネットに配置する場合は、このようなネットワーク構成が多いかと思います。

必要な手順

AWSマネージメントコンソールからEC2へアクセスする場合は、1. セッションマネージャー関連のリソース作成のみの手順となります。
AWS CLIやSSHでアクセスしたい場合は2. AWS CLIやSSHでアクセスする準備も必要となります。

1. セッションマネージャー関連のリソース作成

  • EC2にSSM Agentのバージョン2.3.68.0以降をインストール
  • EC2にAmazonSSMManagedInstanceCoreポリシーを含むIAMロールをアタッチ
  • パブリックサブネットにNAT Gateway作成し、Internet Gateway経由でセッションマネージャーにアクセス

2. AWS CLIやSSHでアクセスする準備

  • クライアントPCからAWS CLIで接続するために、以下のポリシーをもつIAMユーザかロールを作成
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "ssm:StartSession",
            "Resource": [
                "arn:aws:ec2:*:*:instance/(インスタンスID)",
                "arn:aws:ssm:*:*:document/AWS-StartSSHSession"
            ]
        }
    ]
}
  • クライアントPCにAWS CLI 用の Session Manager plugin をインストール
  • SSH 設定ファイル(~/.ssh/config)を更新して、Session Manager セッションを開始し、接続を介してすべてのデータを転送するプロキシコマンドを実行できるようにします。
    今回はAWS CLIでセッションマネージャー接続用にssmsessionプロファイルを作成しています。プロファイルを分けていないdafaultの場合は--profile ssmsessionは不要となります。
# SSH over Session Manager
host i-* mi-*
    ProxyCommand sh -c "aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p' --profile ssmsession"

やってみた

1. 環境

今回実行した環境は以下の通りです。

  • macOS Big Sur 11.4
  • Terraform 0.14.8
  • AWSプロバイダー 3.35.0

2. コード

Terraformのコードが長いので、スッキリさせるために折り畳んで表記しています。
コードを展開したい場合は、各項目の「▶︎」をクリックしてください。

・VPC関連

VPCやサブネット、ルートテーブル、IGWやNAT Gateway等を作成します。

VPC関連のコード
####################
# VPC
####################
resource "aws_vpc" "vpc" {
  cidr_block           = "10.0.0.0/16"
  instance_tenancy     = "default"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "inomaso-dev-vpc"
  }
}

####################
# Subnet
####################
resource "aws_subnet" "sub_front_1a" {
  vpc_id     = aws_vpc.vpc.id
  cidr_block = "10.0.1.0/24"
  # パブリック IPv4 アドレスの自動割り当て
  map_public_ip_on_launch = true
  availability_zone       = "ap-northeast-1a"

  tags = {
    Name = "inomaso-dev-front-subnet-a"
  }
}

resource "aws_subnet" "sub_app_1a" {
  vpc_id     = aws_vpc.vpc.id
  cidr_block = "10.0.11.0/24"
  availability_zone = "ap-northeast-1a"

  tags = {
    Name = "inomaso-dev-app-subnet-a"
  }
}

####################
# Internet Gateway
####################
resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "inomaso-dev-igw"
  }
}

####################
# Route Table
####################
resource "aws_route_table" "front_rt" {
  vpc_id = aws_vpc.vpc.id

  # インターネットゲートウェイ向けのルート追加
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }

  tags = {
    Name = "inomaso-dev-front-rtb"
  }
}

resource "aws_route_table" "app_rt" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "inomaso-dev-app-rtb"
  }
}

####################
# Route Association
####################
resource "aws_route_table_association" "sub_front_1a_rt_assocication" {
  subnet_id      = aws_subnet.sub_front_1a.id
  route_table_id = aws_route_table.front_rt.id
}

resource "aws_route_table_association" "sub_app_1a_rt_assocication" {
  subnet_id      = aws_subnet.sub_app_1a.id
  route_table_id = aws_route_table.app_rt.id
}

####################
# NatGateway
####################
resource "aws_eip" "ngw_a" {
  vpc = true
  # EIPは暗黙的にIGWに依存している。
  depends_on = [aws_internet_gateway.igw]

  tags = {
    Name = "inomaso-dev-ngw-a-eip"
  }
}

resource "aws_nat_gateway" "sub_front_1a" {
  allocation_id = aws_eip.ngw_a.id
  subnet_id     = aws_subnet.sub_front_1a.id
  # NATゲートウェイは暗黙的にIGWに依存している。
  depends_on = [aws_internet_gateway.igw]
  tags = {
    Name = "inomaso-dev-ngw"
  }
}

resource "aws_route" "app_rt" {
  route_table_id         = aws_route_table.app_rt.id
  nat_gateway_id         = aws_nat_gateway.sub_front_1a.id
  destination_cidr_block = "0.0.0.0/0"
}

・EC2関連

今回はSSM Agentがインストール済みである、最新のAmazon Linux 2のAMIを使用してEC2を起動します。

セキュリティグループは、今回のネットワーク要件だとEC2へのインバウンドルールは不要なため、アウトバウンドルールのみの設定となります。

EC2のIAMロールには、AWS Systems Managerのサービスコア機能を使用できるようにAmazonSSMManagedInstanceCoreポリシーを指定します。

尚、Amazon Linux 2へ明示的にSSM Agentの最新バージョンをインストールしたい場合は、以下のユーザデータを設定しEC2起動時に実行させてください。

#!/bin/bash
sudo yum install -y https://s3.ap-northeast-1.amazonaws.com/amazon-ssm-ap-northeast-1/latest/linux_amd64/amazon-ssm-agent.rpm
EC2関連のコード
####################
# EC2
####################
resource "aws_instance" "ec2" {
  # Amazon Linux 2
  ami                         = "ami-06098fd00463352b6"
  instance_type               = "t3.micro"
  vpc_security_group_ids      = [aws_security_group.ec2.id]
  key_name  = "aws-ssh-key"
  subnet_id = aws_subnet.sub_app_1a.id

  # EBS最適化を有効
  ebs_optimized = "true"

  # IAM Role
  iam_instance_profile = "EC2RoleforSSM"

  # EBSのルートボリューム設定
  root_block_device {
    # ボリュームサイズ(GiB)
    volume_size = 8
    # ボリュームタイプ
    volume_type = "gp3"
    # GP3のIOPS
    iops = 3000
    # GP3のスループット
    throughput = 125
    # EC2終了時に削除
    delete_on_termination = true

    # EBSのNameタグ
    tags = {
      Name = "inomaso-dev-ec2"
    }
  }

  # EC2のNameタグ
  tags = {
    Name = "inomaso-dev-ec2"
  }
}

####################
# EC2 Security Group
####################
resource "aws_security_group" "ec2" {
  name        = "inomaso-dev-ec2-sg"
  description = "inomaso-dev-ec2-sg"
  vpc_id      = aws_vpc.vpc.id
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

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


####################
# EC2 IAM Role
####################
data "aws_iam_policy_document" "ssm_role" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
  }
}

resource "aws_iam_instance_profile" "ssm_role" {
  name = "EC2RoleforSSM"
  role = aws_iam_role.ssm_role.name
}

resource "aws_iam_role" "ssm_role" {
  name               = "EC2RoleforSSM"
  assume_role_policy = data.aws_iam_policy_document.ssm_role.json
}

resource "aws_iam_role_policy_attachment" "ssm_role" {
  role       = aws_iam_role.ssm_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

・クライアントPC関連

クライアントPCからAWS CLI使用してセッションを開始するためのIAM作成です。
今回使用する環境は、AssumeRole+MFAが必要なためIAMロールを作成します。

また、IAMポリシーでは、任意のEC2へのセッション開始を許可しました。
リージョンやアカウントID、インスタンスIDを指定することで制限を厳しくすることもできます。

クライアントPC関連のコード
```
####################
# VPC Endpoint IAM Role
####################
data "aws_iam_policy_document" "assume_role" {
  statement {
    effect    = "Allow"
    actions   = [ "sts:AssumeRole" ]
    principals {
      type = "AWS"
      identifiers = [ "arn:aws:iam::1234567890:user/hogehoge" ]
    }
    condition {
      test     = "Bool"
      variable = "aws:MultiFactorAuthPresent"
      values   = ["true"]
    }
  }
}

resource "aws_iam_role" "session_role" {
  name               = "SessionRoleforSSM"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

data "aws_iam_policy_document" "ssm_startsesstion" {
  statement {
    effect    = "Allow"
    actions   = [ "ssm:StartSession" ]
    resources = [ 
      "arn:aws:ec2:*:*:instance/*",
      "arn:aws:ssm:*:*:document/AWS-StartSSHSession"
    ]
  }
}

resource "aws_iam_role_policy" "session_role" {
  name = "SSMSession_policy"
  role = aws_iam_role.session_role.id
  policy = data.aws_iam_policy_document.ssm_startsesstion.json
}
```

3. 接続確認

下記いずれかの方法でEC2とのセッションを開始します。

・AWSマネジメントコンソール(EC2コンソール)

  1. AWSマネジメントコンソールにログインします。
  2. EC2コンソールを開き、接続したいEC2にチェックを入れ、接続をクリックします。
  3. セッションセッションマネージャータブを選択し、接続をクリックします。
    ※環境作成してすぐの場合は、接続できない場合があるので十数分ほどお待ちください。
  4. 接続が確立され、下記のような画面が表示されればbashコマンドを実行可能です。

・AWS CLI

--targetにEC2のinstance-id指定し、セッションを開始します。 特にプロファイルを分けていない場合は、--profileの指定は不要です。

% aws ssm start-session --target instance-id --profile ssmsession

Starting session with SessionId: hoge.hoge-instance-id
sh-4.2$

・SSH

セッションマネージャ経由でSSH接続します。
/path/my-key-pair.pemにキーペアを、usernameにはEC2のOSユーザ名(ec2-user等)を指定してください。

% ssh -i /path/my-key-pair.pem username@instance-id
The authenticity of host 'instance-id (<no hostip for proxy command>)' can't be established.
ECDSA key fingerprint is SHA256:123456789ABCDEFGHIJKLMN.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'instance-id' (ECDSA) to the list of known hosts.

       __|  __|_  )
       _|  (     /   Amazon Linux 2 AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-2/
[ec2-user@ip-10-0-11-200 ~]$

・その他

上記以外にも接続方法はあります。
詳細は公式ドキュメントをご参照願います。

まとめ

セキュリティの関係で、プライベートサブネットにEC2を構築し、NAT Gatewayで外部へアクセスできるような構成は割と多いかと思います。
既にNAT Gatewayがあればセッションマネージャの導入は比較的楽かと思いますので、踏み台サーバの運用でお悩みの方にオススメです。

この記事が、どなたかのお役に立てば幸いです。それでは!