コンテンツにスキップ

踏み台Fargateの構築の話 - TORANA TECH BLOG

踏み台Fargateの構築の話 - TORANA TECH BLOG SREチームのクラシマです。

先日のblogでFargate移行完了について書きました。https://tech.torana.co.jp/entry/2023/02/17/123000

さて、EC2が無くなって困るのが踏み台サーバも無くなってしまうことです。EC2にssmを使ってport forwardしていたのですが、使えなくなってしまいます。

折よくFargate内のssm-agentのバージョンアップによりssmで接続できるようになったので、踏み台Fargateを構築しました。

↓こちらを参考にさせていただきました!https://zenn.dev/quiver/articles/1458e453118254

bastion-clusterにbastion-serviceを構築する

平日の日中は常時立っていてほしいですが、夜間や土日は必要なときだけで良さそうなので、Application AutoScalingを使って平日09:00に起動して、21:00には0インスタンスになるようにします。また、踏み台以外のことはしてほしくないので、できるだけ小さなイメージが良いです。ということで、busyboxをdockerhubに覗きに行ったところ、uclibcというのを使ってるイメージが一番小さい。試してみたら動いたのでこれにします。(https://ja.wikipedia.org/wiki/UClibc 組み込みLinux向けの小型標準Cライブラリ、とのこと。小さいわけですね)

terraformで書きます。IAMロールやECSクラスタなどは省いています。 ↓

```(hcl) locals { container_definitions_bastion = [ { "command" : ["sleep", "43200"], "essential" : true, "image" : "busybox:uclibc", "mountPoints" : [], "name" : "bastion-\({var.env}", "portMappings" : [], "secrets" : [] "ulimits" : [], "volumesFrom" : [], "logConfiguration" : { "logDriver" : "awslogs", "options" : { "awslogs-group" : "bastion-\)", "awslogs-region" : "ap-northeast-1", "awslogs-stream-prefix" : "bastion-${var.env}" } }, } ] }

resource "aws_ecs_task_definition" "bastion" { container_definitions = jsonencode(local.container_definitions_bastion) cpu = "256" execution_role_arn = "arn:aws:iam::\({data.aws_caller_identity.self.account_id}:role/ecsTaskExecutionRole" family = "bastion-\)" memory = "512" network_mode = "awsvpc" requires_compatibilities = [ "FARGATE" ]

task_role_arn = module.bastion_task_role.role.arn

depends_on = [ module.ecs_cluster_bastion ]

lifecycle { ignore_changes = [ container_definitions ] } }

resource "aws_ecs_service" "bastion" { name = "bastion-\({var.env}" cluster = module.ecs_cluster_bastion.cluster.id task_definition = replace(aws_ecs_task_definition.bastion.arn, "/:\\d+\)/", "") enable_execute_command = true

capacity_provider_strategy { capacity_provider = "FARGATE_SPOT" base = 0 weight = 1 }

network_configuration { assign_public_ip = "true" security_groups = [module.bastion_sg.id] subnets = var.alb_subnet_ids }

lifecycle { ignore_changes = [ desired_count, task_definition, ] } }

resource "aws_appautoscaling_target" "bastion" { service_namespace = "ecs" resource_id = "service/\({module.ecs_cluster_bastion.cluster.name}/\)" scalable_dimension = "ecs:service:DesiredCount" role_arn = "arn:aws:iam::${data.aws_caller_identity.self.account_id}:role/aws-service-role/ecs.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_ECSService" min_capacity = 0 max_capacity = 1

lifecycle { ignore_changes = [ min_capacity ] } }

resource "aws_appautoscaling_scheduled_action" "bastion_up" { name = "bastion-scheduled-scale-up-${var.env}" service_namespace = aws_appautoscaling_target.bastion.service_namespace resource_id = aws_appautoscaling_target.bastion.resource_id scalable_dimension = aws_appautoscaling_target.bastion.scalable_dimension schedule = "cron(0 9 ? * MON-FRI *)" timezone = "Asia/Tokyo"

scalable_target_action { min_capacity = 1 max_capacity = 1 } }

resource "aws_appautoscaling_scheduled_action" "bastion_down" { name = "bastion-scheduled-scale-in-${var.env}" service_namespace = aws_appautoscaling_target.bastion.service_namespace resource_id = aws_appautoscaling_target.bastion.resource_id scalable_dimension = aws_appautoscaling_target.bastion.scalable_dimension schedule = "cron(0 21 * * ? *)" timezone = "Asia/Tokyo"

scalable_target_action { min_capacity = 0 max_capacity = 1 } }

肝は、コンテナ定義のcommandのところです。12時間sleepして停止します。
で、その頃にはApp AutoScalingでmin_capacityが0になってる、というわけです。

### ecsta を AWS SSOと一緒に使う

@fujiwaraさんのツールにはいつもお世話になっています。ecspresso、lambrollに続き、ecstaも利用させてもらうことにしました。またBashスクリプトを書いている...のですが、まぁ、ちょうどAWSアカウント移行の時期で書き換えが多かったのでプロトタイプ的に書くには丁度いいんですよ(誰にともなく言い訳)。

で、夜間や土日に踏み台が起動していないときを考慮して、インスタンス数が0のときは1になるようにaws cliを実行します。あとはecstaに渡すパラメータを組み立てて渡しているだけです。
プロファイル名はaws configure ssoを実行したときのデフォルト(ロール名 - アカウントID)を使っているので、ここは運用でカバーになっててかっこ悪いところです。

```(shell)
❯ cat portforward.bash
#!/usr/bin/env bash
set -eo pipefail

usage() {
    echo "Usage: ENV=(stg|prd) $(basename "$0") <app> <db|es|redis> [localPort]"
}

if [[ ${ENV} != "stg" ]] && [[ ${ENV} != "prd" ]]; then
    usage && exit 1
fi

PRD_ACCOUNT=888888888888
STG_ACCOUNT=999999999999

app=$1
datastore=$2

bastion=${app}-bastion-${ENV}

app_datastore_env="${app}-${datastore}-${ENV}"

case "$datastore" in
"db")
    port=3306
    localPort=${3:-$port}
    ;;
"es")
    port=443
    localPort=${3:-9200}
    ;;
"redis")
    port=6379
    localPort=${3:-$port}
    ;;
*) usage && exit 1 ;;
esac

case "$app_datastore_env" in
"app1-db-prd")
    host=hoge.cluster-hoge.ap-northeast-1.rds.amazonaws.com
    profile="AdministratorAccess-${PRD_ACCOUNT}"
    ;;
"app1-db-stg")
    host=fuga.cluster-fuga.ap-northeast-1.rds.amazonaws.com
    profile="AdministratorAccess-${STG_ACCOUNT}"
    ;;
"app1-es-prd")
    host=vpc-elasticsearch-service-prd-hoge.ap-northeast-1.es.amazonaws.com
    profile="AdministratorAccess-${PRD_ACCOUNT}"
    ;;
*) usage && exit 1 ;;
esac

# bastionが未起動の場合は起動する
if [[ $(AWS_PROFILE="$profile" aws ecs describe-services \
    --services "$bastion" \
    --cluster "$bastion" \
    --query services[0].desiredCount) -eq 0 ]]; then
    AWS_PROFILE="$profile" aws ecs update-service \
        --service "$bastion" \
        --cluster "$bastion" \
        --desired-count 1 \
        --no-cli-pager

    echo "bastion task is starting..."

    while [[ "$(AWS_PROFILE=$profile aws ecs list-tasks \
        --cluster "$bastion" \
        --query taskArns[])" == "[]" ]]; do
        echo -n .
        sleep 3
    done

    task_id=$(AWS_PROFILE="$profile" aws ecs list-tasks \
        --cluster "$bastion" \
        --query taskArns[] \
        --output text |
        cut -d'/' -f3)

    while [[ "$(AWS_PROFILE=$profile aws ecs describe-tasks \
        --cluster "$bastion" \
        --tasks "$task_id" \
        --query tasks[0].lastStatus \
        --output text)" != "RUNNING" ]]; do
        echo -n .
        sleep 3
    done
fi

AWS_PROFILE="$profile" ecsta portforward \
    --local-port="$localPort" \
    --remote-port="$port" \
    --remote-host="$host" \
    --id="$(AWS_PROFILE="$profile" aws ecs list-tasks \
        --cluster "$bastion" \
        --family "$bastion" \
        --query taskArns[0] \
        --output text |
        cut -d'/' -f3)" \
    --family="$bastion" \
    --service="$bastion" \
    --cluster="$bastion" \
    --container="$bastion"

まとめ

実はまだEC2が数インスタンス残っておりまして、これからどうにかするところです。でも、もうEC2が無くなっても大丈夫になりました。

#トラーナテックブログ