every Tech Blog

株式会社エブリーのTech Blogです。

トモニテでEKSからECSに移行した話

はじめに

こんにちは トモニテ でバックエンド周りの開発を行っている rymiyamoto です。

トモニテ ではこれまで構成を AWS の EKS を使ってきましたが、2 月の初旬に ECS に移行したのでその内容を紹介していきます。

経緯

移行を決断したのは最大の理由は、現状のメンバーで kubernetes(以下 k8s) のをメンテナンスしていくコストが高すぎるためです。

k8s 自体が高頻度にアップデートが進んでおり、日々の業務を進めがらのキャッチアップが難しく、いざアップデートするのは EKS のサポートが切れる間際になってしまい後手に回っていました。 (大体年 1 回ぐらいのペースでやっていました)

対応をすすめる際もバージョンが大きく飛んでしまうため、リリースノートを追ってちゃんとアップデートを完了するにはだいたい 1 メンバー 1 ヶ月ぐらいはかかってしまいます。

かかる工数がもったいなく、また社内の別プロダクトで ECS の運用実績がしっかりとあるので合わせることとなりました。

移行までのロードマップ

基本的には EKS 部分を ECS に乗り換えるに止め、全体的なアーキテクチャの変更はしない方向で進めました。 理由としては諸事情により対応期間があまり取れなかったためです。

実際進めていったときのロードマップは以下のようになります。

  1. AWS コンソール上で ECS 環境を用意
  2. ECS 環境の IaC 化 & CI/CD の整備
  3. ECS 環境に切り替え
  4. 本番移行も移行
  5. EKS 環境の破棄

AWS コンソール上で ECS 環境を用意

まずは ECS に乗り換えてアプリケーションレベルので修正が必要かを確認するために、DEV 環境のコンソール上から環境を構築していきました。

  • クラスターの作成
  • API server やら web 等をタスク定義
  • タスク定義をもとに ECS のサービスを Fargate で作成
  • これらに伴う role や policy の作成/変更

ECS 環境の IaC 化 & CI/CD の整備

アプリケーション側の調整が済み次第、terraform におこし反映していきます。

ただ ECS のタスク定義は初回の生成移行それぞれのサービスで環境変数やコンテナイメージを管理したいため、それぞれの repository で json ファイルとして管理するようになりました。

タスク定義(task-definition.json)

{
  "taskDefinition": {
    "containerDefinitions": [
      {
        "name": "api-server",
        "image": "111111111111.dkr.ecr.ap-northeast-1.amazonaws.com/api-server:develop",
        "cpu": 0,
        "portMappings": [
          {
            "containerPort": 1323,
            "hostPort": 1323,
            "protocol": "tcp"
          }
        ],
        "essential": true,
        "command": ["/bin/sh", "-c", "'./run.sh'"], // 起動コマンド、スクリプトあるのでそれを実行
        "linuxParameters": {
          "initProcessEnabled": true
        },
        "environment": [
          // more...
        ],
        "ulimits": [
          {
            "name": "nofile",
            "softLimit": 1024,
            "hardLimit": 4096
          }
        ],
        "mountPoints": [],
        "volumesFrom": [],
        "secrets": [
          // more...
        ],
        "logConfiguration": {
          "logDriver": "awslogs",
          "options": {
            "awslogs-create-group": "true",
            "awslogs-group": "/ecs/api-server",
            "awslogs-region": "ap-northeast-1",
            "awslogs-stream-prefix": "ecs"
          }
        }
      }
    ],
    "family": "api-server",
    "taskRoleArn": "arn:aws:iam::111111111111:role/server-ecs-task-role",
    "executionRoleArn": "arn:aws:iam::111111111111:role/ecsTaskExecutionRole",
    "networkMode": "awsvpc",
    "volumes": [],
    "placementConstraints": [],
    "requiresCompatibilities": ["FARGATE"],
    "cpu": "256",
    "memory": "512",
    "runtimePlatform": {
      "cpuArchitecture": "X86_64",
      "operatingSystemFamily": "LINUX"
    },
    "tags": []
  }
}

サービス関連

今回は別プロダクトでも運用している ecs-deploy を利用しました。 (こちらは現在メンテナンスモードで機能追加はありませんが保守は滞りなく進んでいるようです)

先程のパートで作成したタスク定義の json を渡すことで簡単にデプロイすることができます。

# ecs-deployの用意
curl -sL https://github.com/silinternational/ecs-deploy/archive/3.10.7.tar.gz | tar zxvf -
mv ecs-deploy-3.10.7 ecs-deploy
chmod +x ecs-deploy/ecs-deploy

# 途中でdocker image 作成やECRへのアップロード等の処理
# 省略...

# サービスへデプロイ
ecs-deploy/ecs-deploy --cluster ecs-cluster \
  --task-definition-file task-definition.json \
  --service-name api-server \
  --region ap-northeast-1 \
  --timeout 600 \
  --image 111111111111.dkr.ecr.ap-northeast-1.amazonaws.com/api-server:develop

余談ですが ecspresso の採用も視野にありましたが、今回は対応できる期間も短く社内で運用実績ないため採用は見送りとなっています。

バッチ関連

これまで k8s 上では定期実行を cronjob、ショット実行で job を用いており、またバッチの docker image は全てまとめていました。 (バッチが増えるたびに image を ECR へ登録していくのは面倒なので)

そのためそのまま AWS CLI で作成すると CloudWatch のロググループがひとまとめになり見づらくなってしまう + ショット実行の方法が面倒になってしまいます。

今回は scheduled task を管理するためにecscheduleで反映するようにしました。理由としては、スケジュール、override 含めて yaml で管理可能なところと run での即時実行にも対応していたためです。

共通化できる部分は yaml のテンプレート化したいのでyttを採用しています。

構成イメージとしては以下のようになります。

.
├── base-task-def.json(テンプレートとなるタスク定義)
├── config.yaml(ecsscheduleで使う設定ファイル)
├── tasks(各batchの設定ファイル)
    ├── batch-hoge.yaml
    └── batch-fuga.yaml
    └── more...

base-task-def.json

{
    "executionRoleArn": "arn:aws:iam::111111111111:role/ecsTaskExecutionRole",
    "containerDefinitions": [
        {
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "/ecs/server-batch",
                    "awslogs-region": "ap-northeast-1",
                    "awslogs-stream-prefix": $job_name
                }
            },
            "entryPoint": [],
            "portMappings": [],
            "command": [],
            "cpu": 0,
            "environment": [
                // more...
            ],
            "mountPoints": [],
            "secrets": [
                // more...
            ],
            "volumesFrom": [],
            "image": "111111111111.dkr.ecr.ap-northeast-1.amazonaws.com/server-batch:develop",
            "name": "server-batch"
        }
    ],
    "placementConstraints": [],
    "memory": "1024",
    "taskRoleArn": "arn:aws:iam::111111111111:role/scheduled-task",
    "family": $job_name,
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "networkMode": "awsvpc",
    "runtimePlatform": {
        "operatingSystemFamily": "LINUX"
    },
    "cpu": "512",
    "volumes": []
}

config.yaml

#@ load("@ytt:data", "data")

region: ap-northeast-1
cluster: ecs-cluster
rules: #@ data.values.rules

batch-hoge.yaml

#@data/values
---
rules:
  - name: batch-hoge
    scheduleExpression: cron(0 0 * * ? *)
    taskDefinition: batch-hoge
    containerOverrides:
      - name: server-batch
        command:
          - ./exec
        environment:
          # more...
    launch_type: FARGATE
    platform_version: LATEST
    network_configuration:
      aws_vpc_configuration:
        subnets:
          - subnet-hoge
          - subnet-fuga
          - subnet-foo
        security_groups:
          - sg-hoge
        assign_public_ip: DISABLED
    disabled: false

この構成のもとに CI 側でタスク定義と scheduled task の登録を行っています。

# tasks配下のbatchをもとにタスク定義
for item in `ls tasks`
do
  job=`basename ${item} .yaml`
  new_task_definition=$(jq -n --argjson job_name "\"$job\"" -f "base-task-def.json")
  aws ecs register-task-definition --cli-input-json "$new_task_definition"
done

# yttでecscheduleで反映させるためのyaml生成
ytt -f "config.yaml" -f "tasks" --file-mark 'config.yaml:exclusive-for-output=true' > "ecschedule.yaml"

# 生成したyamlを元に反映
ecschedule -conf "ecschedule.yaml" apply -all

また余談ですがこの構成のときショット実行の際は以下のように実行できます。

# ショット実行したいタスク
item="batch-hoge.yaml"
job=`basename ${item} .yaml`

# yttでecscheduleで反映させるためのyaml生成
ytt -f "config.yaml" -f "tasks/${job}" --file-mark 'config.yaml:exclusive-for-output=true' > "ecschedule.yaml"

# 生成したyamlを元に反映
ecschedule -conf "ecschedule.yaml" run -rule $job

ECS 環境に切り替え

一気に新環境に切り替えていくのは、不測の事態があったときに対応が大変になるので、Route53 で新環境へ加重を数日かけて少しず増やしていきました。

また batch 系は 二重で走らないように EKS 側を停止した状態でスケジュールタスクを active に変更して反映させています。

EKS 環境の破棄

不要になった EKS 向けの CI/CD や関連する aws リソースの削除を進めていきました。

一気に terraform で関連リソースを削除しようとすると、都合上依存すると別のリソースまで影響してしまいます。 そのため信頼関係を見つつ地道にモジュール削除を進めていきました。

移行してみて

よかったところ

k8s の頃に比べてサービスやバッチの構成をマニュフェストで管理するよりはシンプルな構造になりました。

また最大の課題であった EKS のアップデート作業からの開放でよりサービス開発に重きを降ることができるようになったと感じます。

大変だったこと

EKS on EC2 で運用していたものを ECS Fargate に変えたことで厳密にリソースの管理をしないといけないので、これまで見えていなかったメモリリークが起きていたことに気付かされました。 (この対応の話は以下の記事で紹介しています)

tech.every.tv

また k8s 自体が活発なコミュニティ故にツールが豊富でしたが、ECS は AWS に縛られる形となるのでデファクトスタンダードと呼べるデプロイ方法が見当たらず何かと創意工夫が必要となってしまいました。

最後に

世間的の逆の流れを行く対応となっていますが、k8s を採用する場合はサービスと組織の規模感を意識しておかないと後々のメンテナンスが辛くなってしまいます。

もちろん k8s を正しく使いこなせると様々な機能の恩恵を受けれるので、体制的にちゃんと面倒を見れるかどうかを判断してとり入れるのは問題ないと思います。

EKS(k8s)と ECS をどっちですすめるか迷っている方や、同じように ECS への移行を検討している方の手助けになれば幸いです。