自己紹介
SRE歴2年目、普段はAWS/GCPなどのインフラとrailsでサーバサイドをやっています。
たまに、GolangとDart書いてます。
基本的になんでも屋さん。
やりたいこと
みなさん、AWS環境でsshはどうしていますか??
ほとんどの方はEC2で作って、そこにsshの公開鍵を置いたりしているはずです。
最近だと、Instance ConnectやSession Managerなど公開鍵を登録せずにシェルに入れる仕組みが増えてきました。
ただ、ほとんどの人がssh用にサーバは立ち上げた状態のはずです。
セキュリティグループなどを適切に設定していれば問題ありませんが、なんか怖い。
そんな人のために、必要なときだけ立ち上げて接続できるコンテナ環境を今回は作っていきます
やること
今回は、Fargateを使ってssh環境を作ってみます。
Fargateを使ってsshする場合、以下が候補になると思います。
- sshのpublic keyをコンテナに埋め込みssh接続
- パスワードとユーザ名でssh接続
- aws systems manager(session manager)で接続
この記事ではsession managerを使って接続してみたいと思います。
参考URLと使うサービス
今回はこの記事を参考にさせて頂いています!
ちなみに使うものは以下になります。
- クラウド環境
- AWS ECS Fargate
- AWS ECR
- AWS SSM Session Manager
- AWS SSM Managed Instances
- AWS SSM Hybrid Activations
- AWS Secrets Manager
- AWS VPC
- AWS CloudWatch Logs
- その他ツール
- Terraform 0.12.7
- fargatecli
ソースコード
ソースコードはGithubに公開しているので参考にしてみてください。
ポイント
コンテナ
-
ssmへの登録はdocker buildのタイミングで行います
- コンテナ起動時に登録すると、再起動のたびにssmに登録されるのであまりオススメしません
- 今回は登録数の上限を10個にしています
-
Fargateで使うDockerコンテナはamazonlinux2を使用しています
- yumでamazon-ssm-agentをインストールするために使っています
FROM amazonlinux:2
ARG SSM_AGENT_CODE
ARG SSM_AGENT_ID
ARG AWS_REGION
ARG ACCESS_KEY_ID
ARG SECRET_ACCESS_KEY
RUN yum update -y && \
yum install -y amazon-ssm-agent
RUN amazon-ssm-agent \
-register \
-code ${SSM_AGENT_CODE} \
-id ${SSM_AGENT_ID} \
-region ${AWS_REGION}
COPY entrypoint.sh .
CMD ["./entrypoint.sh"]
- Entrypointではシェルを実行しています
- ここでagentを起動しています
- 起動から一時間後に再起動するようにしています
#!/bin/sh
amazon-ssm-agent &
sleep 3600
task definition (container definition)
- ログはCloudWatch Logsに保存
- 環境変数はterraformで読み込む際にrenderで置き換えています
[
{
"essential": true,
"image": "${DOCKER_IMAGE_URL}",
"name": "fargate_ssh",
"network_mode": "awsvpc",
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "${AWS_LOGS_GROUP}",
"awslogs-region": "${AWS_LOGS_REGION}",
"awslogs-stream-prefix": "logs"
}
}
}
]
ECS Fargate
-
サブネットはPrivateにして、Internet Gatewayをつけないようにしています
- session managerはVPC Endpointを使って接続するようです
-
セキュリティグループのingressは開放せず、トラフィックを遮断しています
- session managerは厳密にはsshではないので、インターネット接続とポートの開放が必要ありません
-
パブリックIPアドレスも付与しないようにしています
module "fargate_ssh" {
source = "../modules/fargate"
name = "${local.name}-fargate_ssh"
subnets = module.subnet.private_subnet_ids
security_groups = [module.sg_deny_ingress.id]
assign_public_ip = false
task_cpu = 256
task_memory = 512
log_group_name = "/aws/ecs/${var.project}/${local.ws}/fargate_ssh"
tags = local.tags
container_definitions = file("task-definitions/fargate_ssh.json")
container_definitions_vars = {
DOCKER_IMAGE_URL = module.ecr_ssh.repository_url
AWS_LOGS_GROUP = "/aws/ecs/${var.project}/${local.ws}/fargate_ssh"
AWS_LOGS_REGION = var.region
}
}
# ECS Cluster
resource "aws_ecs_cluster" "fargate" {
name = var.name
}
## ECS Service
resource "aws_ecs_service" "fargate" {
name = var.name
cluster = aws_ecs_cluster.fargate.id
task_definition = aws_ecs_task_definition.fargate.arn
desired_count = var.desired_count
launch_type = "FARGATE"
network_configuration {
subnets = var.subnets
security_groups = var.security_groups
assign_public_ip = var.assign_public_ip
}
lifecycle {
ignore_changes = [desired_count]
}
}
## ECS Task
resource "aws_ecs_task_definition" "fargate" {
family = var.name
container_definitions = data.template_file.fargate.rendered
task_role_arn = aws_iam_role.fargate.arn
execution_role_arn = aws_iam_role.fargate.arn
network_mode = "awsvpc"
cpu = var.task_cpu
memory = var.task_memory
requires_compatibilities = ["FARGATE"]
tags = var.tags
}
data "template_file" "fargate" {
template = var.container_definitions
vars = var.container_definitions_vars
}
## CloudWatch logs
resource "aws_cloudwatch_log_group" "fargate" {
name = var.log_group_name
tags = var.tags
}
## IAM
resource "aws_iam_role" "fargate" {
name = var.name
assume_role_policy = data.aws_iam_policy_document.assume_policy.json
tags = var.tags
}
data "aws_iam_policy_document" "assume_policy" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["ecs-tasks.amazonaws.com"]
}
}
}
resource "aws_iam_role_policy_attachment" "task_execution_role_policy" {
role = aws_iam_role.fargate.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
resource "aws_iam_role_policy_attachment" "fargate" {
role = aws_iam_role.fargate.name
policy_arn = aws_iam_policy.fargate.arn
}
resource "aws_iam_policy" "fargate" {
name = "${var.name}_fargate"
policy = data.aws_iam_policy_document.fargate.json
}
data "aws_iam_policy_document" "fargate" {
statement {
actions = [
"kms:Decrypt",
"secretsmanager:GetSecretValue",
"ssm:*",
"iam:PassRole",
]
resources = [
"*",
]
}
}
- IAMの権限ではSecrets Managerから秘匿情報を取得できるようにしています
- エージェント経由でコンテナを登録するため
ssm:*
の権限も付与しています
VPC Endpoint
- コンテナイメージはVPC Endpoint経由で取得します
- FargateはPrivateサブネットで動かすので、インターネット経由でECRからコンテナを取得できません
- s3を追加しているのは、ECRの裏でS3が使われているためです
- CloudWatch Logsも同様でPrivateサブネットからアクセス可能にします
resource "aws_vpc_endpoint" "ecr_api" {
service_name = "com.amazonaws.${var.region}.ecr.api"
vpc_endpoint_type = "Interface"
vpc_id = var.vpc_id
subnet_ids = var.subnet_ids
security_group_ids = var.security_group_ids
private_dns_enabled = true
tags = var.tags
}
resource "aws_vpc_endpoint" "ecr_dkr" {
service_name = "com.amazonaws.${var.region}.ecr.dkr"
vpc_endpoint_type = "Interface"
vpc_id = var.vpc_id
subnet_ids = var.subnet_ids
security_group_ids = var.security_group_ids
private_dns_enabled = true
tags = var.tags
}
resource "aws_vpc_endpoint" "logs" {
service_name = "com.amazonaws.${var.region}.logs"
vpc_endpoint_type = "Interface"
vpc_id = var.vpc_id
subnet_ids = var.subnet_ids
security_group_ids = var.security_group_ids
private_dns_enabled = true
tags = var.tags
}
resource "aws_vpc_endpoint" "s3" {
service_name = "com.amazonaws.${var.region}.s3"
vpc_endpoint_type = "Gateway"
vpc_id = var.vpc_id
route_table_ids = var.private_route_table_ids
tags = var.tags
}
環境作成と動作確認
- 今回の場合、ECSサービスの
desired_cout
は0
にしているので初期状態ではコンテナは動いていません。
terraformでAWS環境を作成
- インストールなどは飛ばします
$ cd terraform/test
$ terraform init
$ terraform plan
$ terraform apply
# outputの結果はメモしておきます
コンテナイメージの作成
- ローカル環境でBUILDして、ECRにPUSHします
# aws profileの設定
$ aws configure --profile test
$ export AWS_PROFILE=test
# push_ecr.shを修正して、環境変数を入力
$ vi push_ecr.sh
## ここを修正
export ECR_URL=""
export SSM_AGENT_CODE=""
export SSM_AGENT_ID=""
export AWS_REGION="ap-northeast-1"
export ACCESS_KEY_ID=""
export SECRET_ACCESS_KEY=""
$ ./push_ecr.sh
コンテナ起動
- コンテナの起動にはfargatecliを使います
# インストール
$ go get -u github.com/awslabs/fargatecli
$ export PATH=$HOME/go/bin:$PATH
$ export AWS_PROFILE=test
$ export AWS_REGION=ap-northeast-1
$ fargatecli --version
fargate version 0.3.2
# ECSサービスの確認
$ fargatecli service list \
--cluster aws-sample-test-fargate_ssh
# コンテナを1個だけ起動
$ fargatecli service scale aws-sample-test-fargate_ssh 1 \
--cluster aws-sample-test-fargate_ssh
# 起動確認
$ fargatecli service info aws-sample-test-fargate_ssh \
--cluster aws-sample-test-fargate_ssh
Service Name: aws-sample-test-fargate_ssh
Status:
Desired: 1
Running: 0 #ここが1になればOK
Pending: 1
session managerからコンテナに接続
# 登録したインスタンスIDを確認
$ aws ssm describe-instance-information --query 'InstanceInformationList[0].InstanceId'
"mi-XXXXXXXX"
# コンテナに接続
$ aws ssm start-session --target mi-XXXXXXXX
コンテナを停止
$ fargatecli service scale aws-sample-test-fargate_ssh 0 \
--cluster aws-sample-test-fargate_ssh
利点と欠点
- 利点
- パブリックIPアドレスを付与せずにsshぽいことが可能
- 必要なときに立ち上げて接続可能
- こまめに止めればお得
- 欠点
- sshではないので、ポートフォワードなどができない
- 設定次第ではできるみたい
- /var/log/secureにログが残らない
- セッションのログはSSMに残っている
- 止め忘れると利用料がお高くなる
- ssmのagentがインストールできるコンテナのみ利用可能
- sshではないので、ポートフォワードなどができない
まとめ
便利な半面、利用料が高かったり、sshの便利機能を使えなかったりします。
今後、Fargateのサービスが増えてくれば、標準でこういう機能もでてくると思います。
今後に期待!!!