AWS Systems ManagerにSession Managerという機能があります。 Session Managerを使用することで、専用の踏み台サーバーの用意や、SSHおよびRDPポートを開けずに、ブラウザやCLIからプライベートネットワークにアクセスできます。
また、セッションログを出力でき、実際に操作した人やその内容の監査が可能です。
今回は、Session Managerを用いて、プライベートネットワークにセキュアにアクセスするための環境を構築する方法を紹介します。
また、この記事はFOLIO Advent calendar 2020の7日目の記事でもあります。(3日遅れの投稿です…)
今回のゴールと構成
今回は、プライベートネットワークにあるAmazon AuroraのMySQLデータベースに接続することをゴールとします。 作成する構成は以下の図のとおりです。

SSMエージェントを動作させているサーバー、つまり、踏み台の役目をするサーバーをECS on Fargeteにすることで、サーバーレスな構成にしています。
環境構築
上記の図の構成を実現するために大きく分けて、
- Dockerイメージの準備
- AWSの設定
が必要となります。
それぞれを構築方法を順に紹介します。
Dockerイメージの準備
まずは、踏み台の役目となるSSMエージェントを動作させるためのDockerコンテナのイメージを作成します。 用意するファイルは以下の3つです。
├── Dockerfile├── run.sh└── seelog.xml.mustacheseelog.xml.mustache- SSMエージェントがログ出力のために使用しているseelogロギングライブラリ
- コンテナ起動時に動的にファイルを生成したいため、テンプレートエンジンとしてmustacheとして用意
- 環境変数を埋め込めればよいのですが、機能が提供されていないため、このようなことをしている
- 環境変数サポートは開発要望としてすでに挙げており、AWSのバックログに積まれている
Dockerfile
Dockerイメージを作成するためのファイルです。 必要なツールをインストールして、SSMエージェントを起動するための処理を実行するスクリプトを実行するようにしています。
SSMエージェントはrootユーザーで実行しなければいけないため、non-rootユーザーに切り替えていません。
FROM amazonlinux:2
ENV AWS_REGION ${AWS_REGION}ENV INSTANCE_NAME ${INSTANCE_NAME}ENV INSTANCE_IAM_ROLE ${INSTANCE_IAM_ROLE}ENV CLOUDWATCH_RECEIVER_LOG_GROUP ${CLOUDWATCH_RECEIVER_LOG_GROUP}
RUN yum localinstall -y https://dev.mysql.com/get/mysql80-community-release-el7-3.noarch.rpm && \ yum install -y \ # mysql CLIのメジャーバーションを指定するためにyum-config-managerが必要なのでインストール yum-utils \ # run.sh内で使用しているのでインストール jq \ # aws CLIをインストールする際に必要なのでインストール unzip \ # SSMエージェントをインストール amazon-ssm-agent \ # セッションログを取得する際に必要になるのでインストール screen \ # aws CLIはhelpコマンドなどでlessを要求してくるのでインストール less \ # Amazon Linux 2に入っているmysql CLIはバーションを指定できないのでアンインストール && yum remove mariadb-libs \ # mysql 5.7系のCLIをインストール && yum-config-manager --disable mysql80-community \ && yum-config-manager --enable mysql57-community \ && yum install -y mysql-community-client \ # yum関連で必要なくなったリソースを削り、少しでもDockerイメージを軽くする && rm -rf /var/cache/yum/* \ && yum clean all
# run.sh内で使用するmustache CLIをインストールRUN curl -sfL -o /usr/local/bin/mo https://git.io/get-mo && \ chmod +x /usr/local/bin/mo
WORKDIR /tmp
# run.sh内で使用するaws CLIをインストールRUN curl -sfL -o "awscliv2.zip" https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip && \ unzip awscliv2.zip && \ ./aws/install
WORKDIR /
# /tmpディレクトリはいくつかのツールのインストールが終わったあと必要なくなったファイルが存在するため、少しでもDockerイメージを軽くするRUN rm -rf /tmp/*
COPY seelog.xml.mustache .COPY run.sh .
CMD ["./run.sh"]seelog.xml.mustache
SSMエージェントがログ出力のために使用しているseelogロギングライブラリをコンテナ起動時に動的にファイルを生成したいため、テンプレートエンジンとしてmustacheのファイルとして用意しています。
本来、環境変数を埋め込めればよいのですが、機能が提供されていないため、このようなことをしています。1
また、seelogの形式や設定方法については、Examples of Seelog usageを参照してください。
今回はコンテナ起動時に、CloudWatch Logsのロググループ名が入っているCLOUDWATCH_RECEIVER_LOG_GROUP環境変数が存在した場合、指定された値でcloudwatch_receiverを設定するようにしています。
<seelog type="adaptive" mininterval="2000000" maxinterval="100000000" critmsgcount="500" minlevel="info"> <exceptions> <exception filepattern="test*" minlevel="error"/> </exceptions> <outputs formatid="fmtinfo"> <console formatid="fmtinfo"/> {{#CLOUDWATCH_RECEIVER_LOG_GROUP}} <custom name="cloudwatch_receiver" formatid="fmtinfo" data-log-group="{{CLOUDWATCH_RECEIVER_LOG_GROUP}}"/> {{/CLOUDWATCH_RECEIVER_LOG_GROUP}} <filter levels="error,critical" formatid="fmterror"> <console formatid="fmterror"/> {{#CLOUDWATCH_RECEIVER_LOG_GROUP}} <custom name="cloudwatch_receiver" formatid="fmterror" data-log-group="{{CLOUDWATCH_RECEIVER_LOG_GROUP}}"/> {{/CLOUDWATCH_RECEIVER_LOG_GROUP}} </filter> </outputs> <formats> <format id="fmterror" format="%Date %Time %LEVEL [%FuncShort @ %File.%Line] %Msg%n"/> <format id="fmtinfo" format="%Date %Time %LEVEL %Msg%n"/> </formats></seelog>run.sh
Dockerfile内で呼び出すShell Scriptをまとめたものです。
ECSでコンテナを起動する場合、EC2と異なり、自動でSession Managerのインスタンスとして登録されません。 そのため、Session Managerのハイブリッドアクティベーションを使用してコンテナをマネージドインスタンスとしてSession Managerに登録するようにします。
#!/bin/bash
# CLOUDWATCH_RECEIVER_LOG_GROUPを動的に設定したいため、テンプレートエンジンを使用してseelog.xmlを配置mo seelog.xml.mustache > /etc/amazon/ssm/seelog.xml
# aws ssm create-activationコマンドの実行時に動的に設定する# 必須の値なのでバリデーションを行っているif [[ -z "${INSTANCE_IAM_ROLE}" ]];then echo "INSTANCE_IAM_ROLE was not specified. Please set this variable." exit 1fi
# aws ssm create-activationコマンドの実行時に動的に設定する# 必須の値なのでバリデーションを行っているif [[ -z "${INSTANCE_NAME}" ]];then echo "INSTANCE_NAME was not specified. Please set this variable." exit 1fi
# ハイブリッドアクティベーションのIDとコードを発行ACTIVATE_PARAMETERS=$(aws ssm create-activation \--default-instance-name "${INSTANCE_NAME}" \--iam-role "${INSTANCE_IAM_ROLE}" \--region "${AWS_REGION}" \--registration-limit 1)SSM_AGENT_CODE=$(echo "${ACTIVATE_PARAMETERS}" | jq -r .ActivationCode)SSM_AGENT_ID=$(echo "${ACTIVATE_PARAMETERS}" | jq -r .ActivationId)
# このコンテナをSession Managerのマネージドインスタンスとして登録するamazon-ssm-agent -register -code "${SSM_AGENT_CODE}" -id "${SSM_AGENT_ID}" -region "${AWS_REGION}"
# ハイブリッドアクティベーションのコードはもう必要ないため、削除するaws ssm delete-activation --activation-id "${SSM_AGENT_ID}"
export REGISTRATION_FILE="/var/lib/amazon/ssm/registration"
cleanup() { # このコンテナのSession Managerのマネージドインスタンス登録を解除する aws ssm deregister-managed-instance --instance-id "$(cat "${REGISTRATION_FILE}" | jq -r .ManagedInstanceID)" || true exit 0}
# コンテナが終了する際にcleanup関数が動作するようにTRAPを仕込むtrap 'cleanup' EXIT
# SSMエージェントを起動して、Session Managerとコネクションを構築するamazon-ssm-agent start &SSM_AGENT_PID="$!"
# Session Managerとのコネクション登録が環境するのを待つwhile :;do sleep 1 if [ -e "${REGISTRATION_FILE}" ];then break fidone# registrationファイルは600で作成されるため、全ユーザーに読み込み権限を与えている# 入っているデータは、マネージドインスタンスのIDとリージョン名だけであるため、権限を付与しても問題ないchmod +r "${REGISTRATION_FILE}"
# SSMエージェントが終了するまで、つまり永久的にSSMエージェントを起動するwait "${SSM_AGENT_PID}"run.shについていくつかのポイント
aws ssm create-activation \--default-instance-name "${INSTANCE_NAME}" \--iam-role "${INSTANCE_IAM_ROLE}" \--region "${AWS_REGION}" \--registration-limit 1default-instance-nameオプションはマネージドインスタンスの名前になるので、引数で渡して判別しやすいようにしています。

iam-roleオプションは、立ち上げたコンテナが指定したロールにAssumeRoleされて実行されます。
このロールにAsuumeRoleされるのは、amazon-ssm-agent startを行ってインスタンスが登録されたあとである点に注意が必要です。
このスクリプトでは、
aws ssm delete-activation --activation-id "${SSM_AGENT_ID}"がECSのタスクIAMロールで指定したロール、
cleanup() { # このコンテナのSession Managerのマネージドインスタンス登録を解除する aws ssm deregister-managed-instance --instance-id "$(cat "${REGISTRATION_FILE}" | jq -r .ManagedInstanceID)" || true exit 0}TRAPで実行されるこの関数はオプションで指定したロールで実行されます。
また、後述するセッションログの送信には、このロールに適切な権限を付与する必要があります。
AWSのインフラ環境構築
- Session Managerの構築
- VPCの構築
- IAMロールの構築
- ECSのクラスタ構築
の順でAWSのインフラ環境を構築していきます。
インフラ環境の構築に関しては、Terraformのコードで表現していきます。 Terraformのバーションは以下の通りです。
$ terraform versionTerraform v0.13.5それでは先立って、使い回す値を変数として設定しておきます。
### var.tfdata "aws_region" "current" {}data "aws_caller_identity" "self" {}
locals { log_group_name = "/ssm/session-manager" s3_prefix = "ssm/session-log"}1. Session Managerの構築
Session Managerでは、セッションログと呼ばれる実行したコマンドの内容を記録する機能があります。 今回は、セッションログを暗号化できるようにSession Managerを構築します。
前述したDockerイメージを登録方法、およびイメージを登録するためのECRのリソース作成に関しては割愛します。
セッションログをCloudWatch Logsに流すためのロググループの設定
ログをもとに何かしらアクションを起こしたい場合はCloudWatch Logsに出力するのが良いです。
CloudWatch Logsのロググループはサーバーサイド暗号化が可能です。 これにはKMSのKeyが必要であるため、合わせてKeyを用意します。
また、Keyの暗号、復号などの操作の権限を絞るために、対応したロググループおよびrootアカウントのみに制限します。
### log.tfdata "aws_iam_policy_document" "log_group_policy" { statement { effect = "Allow" actions = ["kms:*"] resources = ["*"]
principals { type = "AWS" identifiers = ["arn:aws:iam::${data.aws_caller_identity.self.account_id}:root"] } }
statement { effect = "Allow" actions = [ "kms:Encrypt*", "kms:Decrypt*", "kms:ReEncrypt*", "kms:GenerateDataKey*", "kms:Describe*" ] resources = ["*"]
principals { type = "Service" identifiers = ["logs.${data.aws_region.current.name}.amazonaws.com"] }
condition { test = "ArnEquals" values = ["arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.self.account_id}:log-group:${local.log_group_name}"] variable = "kms:EncryptionContext:aws:logs:arn" } }}
resource "aws_kms_key" "log_group_encryption_key" { description = "${local.log_group_name} log group encription key" customer_master_key_spec = "SYMMETRIC_DEFAULT" key_usage = "ENCRYPT_DECRYPT" enable_key_rotation = true deletion_window_in_days = 7 policy = data.aws_iam_policy_document.log_group_policy.json}
resource "aws_kms_alias" "log_group_encryption" { name = "alias/kms-ssm-log-group-encryption-key" target_key_id = aws_kms_key.log_group_encryption_key.key_id}
resource "aws_cloudwatch_log_group" "ssm" { name = local.log_group_name kms_key_id = aws_kms_key.ssm_encryption_key.arn}セッションログを保存するためのS3の設定
ログをアーカイブしたい場合はS3に出力すると良いです。 S3ではKMSのKeyなしでサーバーサイド暗号化が可能です。
### s3.tfresource "aws_s3_bucket" "ssm" { bucket = "session-manager" acl = "private"
versioning { enabled = true }
server_side_encryption_configuration { rule { apply_server_side_encryption_by_default { sse_algorithm = "AES256" } } }}
resource "aws_s3_bucket_public_access_block" "ssm" { bucket = aws_s3_bucket.ssm.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true}Session Managerのセッションログの設定
Session ManagerのセッションログをKMSのKeyで暗号化してS3とCloudWatch Logsに転送する設定をします。
ポイントなのは、aws_ssm_documentのnameです。
この属性に設定しているSSM-SessionManagerRunShellというドキュメント名は、AWS Webコンソール上で操作する際に使用される特別なものです。
また、AWS CLIを用いてセッションを作成する際には、オプションでドキュメント名を指定できますが、デフォルト値としてこのドキュメントが使用されます。
### ssm.tfresource "aws_kms_key" "session_log" { description = "Session log encription key" customer_master_key_spec = "SYMMETRIC_DEFAULT" key_usage = "ENCRYPT_DECRYPT" enable_key_rotation = true deletion_window_in_days = 7}
resource "aws_kms_alias" "session_log_encryption" { name = "alias/kms-ssm-session-log-encryption-key" target_key_id = aws_kms_key.session_log.key_id}
resource "aws_ssm_document" "session_log" { name = "SSM-SessionManagerRunShell"
document_type = "Session" document_format = "JSON"
content = jsonencode({ schemaVersion = "1.0" description = "Document to hold regional settings for Session Manager" sessionType = "Standard_Stream" inputs = { s3BucketName = aws_s3_bucket.ssm.bucket s3KeyPrefix = local.s3_prefix s3EncryptionEnabled = true cloudWatchLogGroupName = aws_cloudwatch_log_group.ssm.name cloudWatchEncryptionEnabled = true kmsKeyId = aws_kms_key.session_log.arn } })}2. VPCの構築
今回のテーマである、パブリックには一切口を開けずにネットワークを構築してみます。
VPCの設定
Session ManagerとAmazon Aurora MySQL用に、それぞれプライベートサブネットを用意します。
### vpc.tfresource "aws_vpc" "vpc" { cidr_block = "10.10.0.0/16" enable_dns_hostnames = true}
resource "aws_subnet" "session_manager" { for_each = { subnet_a = { az = "ap-northeast-1a", tail_octet = "11" }, subnet_c = { az = "ap-northeast-1c", tail_octet = "12" }, } vpc_id = aws_vpc.vpc.id availability_zone = each.value.az cidr_block = "10.10.${each.value.tail_octet}.0/24" tags = { "Name" = "session-manager-${each.key}" }}
resource "aws_route_table" "session_manager" { vpc_id = aws_vpc.vpc.id}
resource "aws_route_table_association" "session_manager" { for_each = aws_subnet.session_manager subnet_id = each.value.id route_table_id = aws_route_table.session_manager.id}セキュリティの設定
プライベートなネットワーク構成にするのはよいですが、ECS on FargateでSSMエージェントを動作させる場合、以下のことが行われます。
- ECRからDockerイメージをPull
- Dockerイメージのレイヤ情報はS3に保存されているため、S3へのアクセスが必要
- SSMエージェントがSession Managerに通信
- セッションログをS3とCloudWatch Logsに送信
- セッションログを暗号化する際にKMSのKey情報を取得
これらをプライベートなネットワークで実現するにはPrivate Linkを使用する必要があります。 Private Linkを使用することで、パブリックなインターネットを介さずにAWSが提供するサービスに接続できます。
これが今回の肝です。
また、Private Linkはセキュリティグループに紐付くので、関係がわかるように合わせてセキュリティグループグループの作成も行ってみます。
### security.tfresource "aws_vpc_endpoint" "session_manager_s3" { vpc_id = aws_vpc.vpc.id service_name = "com.amazonaws.${data.aws_region.current.name}.s3" vpc_endpoint_type = "Gateway" route_table_ids = [aws_route_table.session_manager.id]}
resource "aws_security_group" "session_manager" { name = "session-manager-security-group" description = "Allow ingress/egress for session manager" vpc_id = aws_vpc.vpc.id
# AWSの各種サービスに通信する際にHTTPSでアクセスが行われるため、アクセスを許可 ingress { from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = [aws_vpc.vpc.cidr_block] }
# S3のエンドポイントはGatewayなので、S3エンドポイントのCIDRへのアクセスを許可する egress { from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = concat([aws_vpc.vpc.cidr_block], aws_vpc_endpoint.session_manager_s3.cidr_blocks) }
# SSMエージェントはインスタンスメタデータを取得するため、インスタンスメタデータが配信されているIPへのアクセスを許可する egress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["169.254.169.254/32"] }
# MySQLのアクセスを許可 egress { from_port = 3306 to_port = 3306 protocol = "tcp" cidr_blocks = [for middleware in aws_subnet.middleware : middleware.cidr_block] }}
resource "aws_security_group" "mysql_from_session_manager" { name = "mysql-from-session_manager-security-group" description = "Allow access to mysql from session manager" vpc_id = aws_vpc.vpc.id
# Session Managerのマネージドインスタンスからのアクセスを許可 ingress { from_port = 3306 to_port = 3306 protocol = "tcp" security_groups = [aws_security_group.session_manager.id] }
egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] }}
resource "aws_vpc_endpoint" "session_manager_logs" { vpc_id = aws_vpc.vpc.id service_name = "com.amazonaws.${data.aws_region.current.name}.logs" vpc_endpoint_type = "Interface" subnet_ids = [for subnet in aws_subnet.session_manager : subnet.id] security_group_ids = [aws_security_group.session_manager.id] private_dns_enabled = true}
resource "aws_vpc_endpoint" "session_manager_ecr" { vpc_id = aws_vpc.vpc.id service_name = "com.amazonaws.${data.aws_region.current.name}.ecr.dkr" vpc_endpoint_type = "Interface" subnet_ids = [for subnet in aws_subnet.session_manager : subnet.id] security_group_ids = [aws_security_group.session_manager.id] private_dns_enabled = true}
resource "aws_vpc_endpoint" "session_manager_ssm" { vpc_id = aws_vpc.vpc.id service_name = "com.amazonaws.${data.aws_region.current.name}.ssm" vpc_endpoint_type = "Interface" subnet_ids = [for subnet in aws_subnet.session_manager : subnet.id] security_group_ids = [aws_security_group.session_manager.id] private_dns_enabled = true}
resource "aws_vpc_endpoint" "session_manager_ssmmessages" { vpc_id = aws_vpc.vpc.id service_name = "com.amazonaws.${data.aws_region.current.name}.ssmmessages" vpc_endpoint_type = "Interface" subnet_ids = [for subnet in aws_subnet.session_manager : subnet.id] security_group_ids = [aws_security_group.session_manager.id] private_dns_enabled = true}
resource "aws_vpc_endpoint" "session_manager_kms" { vpc_id = aws_vpc.vpc.id service_name = "com.amazonaws.${data.aws_region.current.name}.kms" vpc_endpoint_type = "Interface" subnet_ids = [for subnet in aws_subnet.session_manager : subnet.id] security_group_ids = [aws_security_group.session_manager.id] private_dns_enabled = true}3. IAMロールの構築
今回登場するIAMロールは、以下の4つです。
- ECSのタスク実行ロールのためのロール
- ECSのタスクロールのためのロール
- SSMエージェントのためのロール
- オペレーターがSession Managerを使用するためのロール
ECSのタスク実行ロールの設定
タスクロール実行ロールは、ECSのコンテナエージェントが使用するロールです。 ECRからDockerイメージのPullやコンテナログCloudWatchに送信する際に必要です。
AWSが管理しているポリシーで実現する場合、作成するロールにアタッチすべき最低限必要なポリシーは以下のとおりです。
| ポリシー | 用途 |
|---|---|
arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy |
ECSのコンテナのログを送信 |
arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly |
ECRからイメージをpull |
arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceRole |
Elastic Load BalancingロードバランサがECSコンテナインスタンスを登録および登録解除 |
arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy |
ECSのタスク実行ロールに一般的に必要なアクセス |
また、作成するロールにはecs-tasks.amazonaws.comを信頼関係に追加する必要があります。
### ecs_task_execution_role.tfdata "aws_iam_policy_document" "ecs_task_execution_assume_role" { statement { actions = ["sts:AssumeRole"]
principals { type = "Service" identifiers = ["ecs-tasks.amazonaws.com"] } }}
resource "aws_iam_role" "ecs_task_execution" { name = "ecs-task-execution" assume_role_policy = data.aws_iam_policy_document.ecs_task_execution_assume_role.json}
resource "aws_iam_role_policy_attachment" "ecs_task_execution" { for_each = { dummy_key_1 = "arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy" dummy_key_2 = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" dummy_key_3 = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceRole" dummy_key_4 = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" } role = aws_iam_role.ecs_task_execution.name policy_arn = each.value}ECSのタスクロールの設定
タスクロールはコンテナが使用するロールです。 コンテナ内のアプリケーション(処理)がAWSサービスに接続する際に必要です。
前述したDockerイメージでは、
- ECSのタスクからSystems ManagerにIAMロールを渡す
- System Managerのアクティベーションの発行
- System Managerのアクティベーションの削除
が行われているため、それらに対応する権限を付与します。
また、SSMエージェントを起動するために必要なAWSが管理しているポリシーであるAmazonSSMManagedInstanceCoreも合わせて付与します。
### ecs_task_role.tfdata "aws_iam_policy_document" "ecs_task_assume_role" { statement { actions = ["sts:AssumeRole"]
principals { type = "Service" identifiers = ["ecs-tasks.amazonaws.com"] } }}
resource "aws_iam_role" "ecs_task_role" { name = "ecs-task-role" assume_role_policy = data.aws_iam_policy_document.ecs_task_assume_role.json}
data "aws_iam_policy_document" "ecs_task_role" { statement { actions = ["iam:PassRole"] # SSMエージェントが動作するロールに対してのみ制限をかけてもよい resources = ["*"] condition { test = "StringEquals" variable = "iam:PassedToService" values = ["ssm.amazonaws.com"] } }
statement { actions = [ "ssm:CreateActivation", "ssm:DeleteActivation" ] resources = ["*"] }}
resource "aws_iam_policy" "ecs_task_role" { name = "ecs-task-role" policy = data.aws_iam_policy_document.ecs_task_role.json}
resource "aws_iam_role_policy_attachment" "ecs_task_role" { policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" role = aws_iam_role.ecs_task_role.name}SSMエージェント用のロールの設定
SSMエージェントがSession Managerに登録するために必要な権限を付与します。
また、Dockerイメージの設定で言うところのrun.shのamazon-ssm-agent start &からこのロールで実行されます。
具体的には、
- タスク終了時にマネージドインスタンスの登録の解除
- セッションログの暗号化
が行われるため、それらに対応する権限を付与します。
### ssm_service_role.tfdata "aws_iam_policy_document" "ssm_service_assume_role" { statement { actions = ["sts:AssumeRole"]
principals { type = "Service" identifiers = ["ssm.amazonaws.com"] } }}
resource "aws_iam_role" "ssm_service_role" { name = "ssm-agent-role" assume_role_policy = data.aws_iam_policy_document.ssm_service_assume_role.json}
data "aws_iam_policy_document" "ssm_service_role" { # See also: https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/ssm-agent-minimum-s3-permissions.html statement { effect = "Allow" actions = [ "s3:GetObject" ] resources = [ "arn:aws:s3:::aws-ssm-${data.aws_region.current.name}/*", "arn:aws:s3:::amazon-ssm-${data.aws_region.current.name}/*", "arn:aws:s3:::amazon-ssm-packages-${data.aws_region.current.name}/*", "arn:aws:s3:::${data.aws_region.current.name}-birdwatcher-prod/*", "arn:aws:s3:::patch-baseline-snapshot-${data.aws_region.current.name}/*" ] }
# セッションログを保存できるようにする statement { effect = "Allow" actions = [ "s3:PutObject" ] resources = [ "arn:aws:s3:::${aws_s3_bucket.ssm.bucket}/${local.s3_prefix}/*" ] }
# セッションログを暗号化して保存する際にS3バケットのデフォルト暗号化設定が必要 statement { effect = "Allow" actions = [ "s3:GetEncryptionConfiguration" ] resources = ["*"] }
# セッションログをCloud Watch Logsに保存するために必要 statement { effect = "Allow" actions = [ "logs:CreateLogStream", "logs:PutLogEvents", "logs:DescribeLogGroups", "logs:DescribeLogStreams" ] resources = [aws_cloudwatch_log_group.ssm.arn] }
# セッションログの暗号化のためのKMSキーで復号化するために必要 statement { effect = "Allow" actions = [ "kms:Decrypt" ] resources = [aws_kms_key.session_log.arn] }
# run.sh内でコンテナ終了時にマネージドインスタンスの登録解除するようにしているので、そのための権限を付与 statement { effect = "Allow" actions = [ "ssm:DeregisterManagedInstance" ] resources = ["*"] }}
resource "aws_iam_policy" "ssm_service_role" { name = "ssm-service-role-policy" policy = data.aws_iam_policy_document.ssm_service_role.json}
resource "aws_iam_role_policy_attachment" "ssm_service_role" { for_each = { dummy_key_1 = "arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy" dummy_key_2 = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" dummy_key_3 = aws_iam_policy.ssm_service_role.arn } policy_arn = each.value role = aws_iam_role.ssm_service_role.name}オペレーターがSession Managerを使用する用のロールの設定
オペレーターがSession Managerを操作できるようにするための権限を付与します。 また、セッションログの暗号化に使用されるデータ暗号化キーを生成する必要があるため、そのための権限を付与します。
### session_manager_operator_role.tfdata "aws_iam_policy_document" "session_manager_operator_assume_role" { statement { actions = ["sts:AssumeRole"]
principals { type = "Service" identifiers = ["ec2.amazonaws.com"] } }}
resource "aws_iam_role" "session_manager_operator" { name = "session-manager-operator" assume_role_policy = data.aws_iam_policy_document.session_manager_operator_assume_role.json}
# See also: https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/getting-started-restrict-access-quickstart.htmldata "aws_iam_policy_document" "session_manager_operator" { statement { effect = "Allow" actions = [ "ssm:StartSession" ] resources = [ "arn:aws:ec2:*:*:instance/*", "arn:aws:ssm:${data.aws_region.current.name}:${data.aws_caller_identity.self.account_id}:document/${aws_ssm_document.session_log.name}" ] }
statement { effect = "Allow" actions = [ "ssm:TerminateSession", "ssm:ResumeSession", "ssm:DescribeSessions", "ssm:GetConnectionStatus" ] resources = ["*"] }
statement { effect = "Allow" actions = [ "ssm:GetDocument" ] resources = ["arn:aws:ssm:::document/${aws_ssm_document.session_log.name}"] }
statement { effect = "Allow" actions = [ "kms:GenerateDataKey" ] resources = [aws_kms_key.session_log.arn] }}
resource "aws_iam_policy" "session_manager_operator" { name = "session-manager-for-operator" policy = data.aws_iam_policy_document.session_manager_operator.json}
resource "aws_iam_role_policy_attachment" "session_manager_operator" { policy_arn = aws_iam_policy_document.session_manager_operator.arn role = aws_iam_role.session_manager_operator.name}4. ECSのクラスタ構築
ECS on FargateでDockerコンテナを動かすための設定をします。 そのためには、
- ECSのクラスタの構築
- Dockerイメージをタスクとして実行するためのタスク定義
- 使用するコンピュータリソースの設定
- どのイメージを使用するかの設定
- コンテナ起動時の環境変数の設定
が必要です。
### ecs.tfresource "aws_ecs_cluster" "ssm_agent_ecs_cluster" { name = "ssm-agent-ecs-cluster" setting { name = "containerInsights" value = "enabled" }}
resource "aws_cloudwatch_log_group" "ssm_agent_log_group" { name = "/ecs/ssm_agent"}
locals { container_definitions = [ { name = "ssm_agent" image = "xxxxxx.dkr.ecr.${data.aws_region.current.name}.amazonaws.com/YOUR_REPOSITORY_NAME:YOUR_DOCKER_IMAGE_TAG" environment = [ { name = "INSTANCE_NAME" value = "ssm-agent-from-ecs" }, { name = "INSTANCE_IAM_ROLE" value = aws_iam_role.ssm_service_role.name }, { name = "AWS_REGION" value = data.aws_region.current.name }, { name = "CLOUDWATCH_RECEIVER_LOG_GROUP" value = aws_cloudwatch_log_group.ssm_agent_log_group.name } ] logConfiguration = { logDriver = "awslogs" options = { "awslogs-group" = aws_cloudwatch_log_group.ssm_agent_log_group.name "awslogs-region" = data.aws_region.current.name "awslogs-stream-prefix" = "ecs" } } essential = true startTimeout = 120 stopTimeout = 120 } ]}
resource "aws_ecs_task_definition" "ssm_agent" { family = "ssm-agent" container_definitions = jsonencode(local.container_definitions) task_role_arn = aws_iam_role.ecs_task_role.arn execution_role_arn = aws_iam_role.ssm_service_role.arn network_mode = "awsvpc" memory = "2048" cpu = "1024" requires_compatibilities = ["FARGATE"]}用意したファイルのおさらい
Terraformはどのファイルにどの設定を書いてもよいですが、今回は以下のようなファイルを用意しました。
├── var.tf # 共通で使用する変数の定義├── log.tf # セッションログをCloudWatch Logsに流すためのロググループの設定├── s3.tf # セッションログを保存するためのS3の設定├── ssm.tf # Session Managerのセッションログの設定├── vpc.tf # VPCの設定├── security.tf # セキュリティの設定├── ecs_task_execution_role.tf # ECSのタスク実行ロールの設定├── ecs_task_role.tf # ECSのタスクロールの設定├── ssm_service_role.tf # SSMエージェント用のロールの設定├── session_manager_operator_role.tf # Session Managerを使用するオペレーター用のロールの設定└── ecs.tf # ECSクラスタおよびタスク定義の設定SSMエージェントの起動
環境構築が終わったので、あとはSSMエージェントのコンテナを起動するだけです。 構築したセキュリティグループでタスクを実行すれば、セッションマネージャーを使用できます。

注意点
アドバンスドインスタンスティアについて
ハイブリッドアクティベーションの場合、インスタンス層がアドバンスドインスタンスティアとなります。
ハイブリッドアクティベーション経由で登録したマネージドインスタンスが存在する状態で、Webコンソール上のSystem ManagerのSession Managerのページに遷移すると、アドバンスドインスタンスティアへの変更許可を求めるダイアログが自動でてくるので、ティアを変更できます。
事前に許可したい場合は、以下のコマンドを実行します。
aws ssm update-service-setting --setting-id arn:aws:ssm:YOUR_REGION:YOUR_ACCOUNT_ID:servicesetting/ssm/managed-instance/activation-tier --setting-value advanced終わりに
今回はSession Managerでプライベートネットワークにセキュアにアクセスするために、
- SSMエージェントを動かすDockerイメージの作成
- AWSのインフラ構築
について紹介しました。
見ていただいたように、サーバーレスとはいえ、いくつか準備しないといけないことがあるため、まだまだ面倒です。 Azure Bastionのように、AWSもマネージドな踏み台のようなものを用意してほしいと思いました。
また、運用を始めると、まとまったファイルなどを入力および出力したくなることもあります。
その際は、専用のS3バケットを用意して入力および出力するのが良いと考えています。 S3を介することで、引き続きセキュアな環境が保てます。さらに、もし監査が必要な場合でも、対応が可能です。
参考記事
- AWS Systems Manager Session Managerのシェル操作をログ出力する | Developers.IO
- Fargate containerにSession Managerでログインする - あしたから本気だす
- 続:「Bastion ~ AWS Fargateで実現するサーバーレスな踏み台設計」 - How elegant the tech world is…!
Footnotes
-
環境変数サポートは開発要望としてすでに挙げており、AWSのバックログに積まれている。 ↩