ECS Fargate の ARM64 版は長らくスポットが使えませんでしたが、ようやくやってきました。
利用するにあたって、いくつかポイントと注意点があるので、ザックリと把握しつつ自分用にどう構成するかを考えていく感じになると思います。
目次
はじめに
リリースや価格についてはこちらにまとまっています。東京だとオンデマンドに比べて 62% OFF くらいです。前は Blue/Green の場合にスポットを利用できませんでしたが、今は解消されドキュメントからもその記述は消えています。
落ちやすさはまだわかりませんが、EC2 の例だと X86 より ARM64 の方が安定している雰囲気があるので、Fargate においても速い・安い・安定を期待できるため、使えるようにしておくのがよいでしょう。今は SIGTERM 前に ALB 登録解除もしてくれるようになっています。
- Amazon ECS clusters for the Fargate launch type – Amazon Elastic Container Service
- ECS のアプリケーションを正常にシャットダウンする方法 | Amazon Web Services ブログ
- Amazon Elastic Container Service でサービスロードバランシングの精度を向上
この記事タイトルには ARM64 を含めましたが、今回の内容は X86 でも仕組み的には変わらないと思うので、既にスポットを利用している場合はたいして役に立たないかもです。その場合は過去の関連記事はこの辺ということで代わりにどうぞ。
- AWSコンテナ系アーキテクチャの選択肢を最適化する | 外道父の匠
- 続・AWS ECS Fargate のCPU性能と特徴 2023年版 | 外道父の匠
- AWS ECS Fargate のリージョン格差 | 外道父の匠
困ったこと
最終的には知らんかったのが悪い、というだけなのですが、ごく普通の思考でスポットの設定をするだけだと思いどおりに事が運びませんでした。そのへんの経過について、あたふたする雰囲気を知りたければ、以下のポストに吊るしたのを追ってもらえればと思います。起こり。
いつの間にか ARM64 Graviton の Fargate Spot がきてた。東京だと 62% OFF ってところ https://t.co/JsRrMi1HvG
— 外道父 | Noko (@GedowFather) September 8, 2024
思い直し。
先程の Fargate Spot の話、Blue/Green の場合は appspec.yml に CapacityProviderStrategy を記述しない場合に、デフォルト設定に戻される、という自分の見落としが原因な雰囲気がしてきたので、再調査します
— 外道父 | Noko (@GedowFather) September 9, 2024
主に困ったのが2点で、
なぜそうなるのか、この業界は無知は罪とはいえ、あまりないシステム挙動が原因だったので、喚き散らしたお詫びにちゃんとまとめさせていただきたく思います。
キャパシティプロバイダー戦略を有効にする
まずデフォルトの “起動タイプ” から “キャパシティプロバイダー戦略” に変更してスポットを使えるようにするには、Terraform ならこんな感じです。設定値を入れ込んでますが、公開用に編集するのめんどいのでコピペです。Cluster で指定しつつ、
1 2 3 4 |
resource "aws_ecs_cluster_capacity_providers" "default" { ... capacity_providers = ["FARGATE", "FARGATE_SPOT"] } |
通常は Service ごとに条件指定すると思うので、こちらにも記述します。launch_type と capacity_provider_strategy は共存できないことに注意してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
resource "aws_ecs_service" "default" { ... launch_type = lookup(each.value, "launch_type_fargate", true) ? "FARGATE" : null dynamic "capacity_provider_strategy" { for_each = ! lookup(each.value, "launch_type_fargate", true) ? [1] : [] content { capacity_provider = "FARGATE" base = lookup(each.value, "capacity_ondemand_base", 0) weight = lookup(each.value, "capacity_ondemand_weight", 1) } } dynamic "capacity_provider_strategy" { for_each = ! lookup(each.value, "launch_type_fargate", true) ? [1] : [] content { capacity_provider = "FARGATE_SPOT" base = lookup(each.value, "capacity_spot_base", 0) weight = lookup(each.value, "capacity_spot_weight", 0) } } ... lifecycle { ignore_changes = [ task_definition, load_balancer, desired_count, capacity_provider_strategy, ] } ... |
自分の場合の結論としては、CodePipeline 経由でデプロイする Blue/Green な Service の場合はキャパプロ戦略を、それ以外は 起動タイプ=FARGATE としました。
本来は全部の Service をキャパプロ戦略として、単に FARGATE のみ使う条件にしたり、FARGATE_SPOT も混ぜたり、とすればよいのですが、Blue/Green かつ Pipeline からデプロイしない場合に仕組みとして成り立たなかったので、このようにしました(理由は後述)。
あと ignore_changes に capacity_provider_strategy を入れ込みます。Terraform から直にこの値の調整をしようとすると service の再作成となるし、仮に更新になるとしても B/G の場合はエラーになるからです。
Blue/Green というか CODE_DEPLOY を使っていなければ、設定としてはこれだけで終わりの話です。B/G 愛用の場合は、以下へと続きます。
CodeDeployとキャパシティプロバイダー戦略の関係
CodeDeploy からデプロイを実行する際に、その内容を記述する仕組みがあります。CodeDeploy 単体的には Revision、CodePipeline 的には appspec.yml で、AppSpec というモノです。- CodeDeploy AppSpec ファイルリファレンス – AWS CodeDeploy
- AppSpec 「リソース」セクション (Amazon ECS と AWS Lambda デプロイのみ) – AWS CodeDeploy
1 2 3 4 5 6 7 8 9 10 11 12 13 |
version: 0.0 Resources: - TargetService: Type: AWS::ECS::Service Properties: ... CapacityProviderStrategy: - CapacityProvider: FARGATE Base: '0' Weight: '0' - CapacityProvider: FARGATE_SPOT Base: '0' Weight: '1' |
CodeDeploy でデプロイすると、必ずこの AppSpec の内容が反映されるのですが、そのうちの CapacityProviderStrategy という項目は、存在すればその内容で Service を更新し、存在しない場合はデフォルトの「起動タイプ = FARGATE」として扱われます。
つまり、ECS Service のこの部分の設定は、何かしらの内容で必ず上書き更新されるということになり、appspec.yml に記述していない場合はデプロイすると「起動タイプ」に戻ることになる。というのが自分がかかった罠になります。
ECS Service の戦略設定を更新しようとすると、「強制デプロイにしてね」とエラーになりつつ、裏ではデプロイが実行される挙動になっています。
管理画面や API からは、戦略値を変更できないのは、どうせデプロイ時に CodeDeploy から Service 設定が変更されるから、設定画面時点では変更しても意味がない=エラーになる、ということなのでしょう。
普段は IaC や管理画面から設定値を管理していたところが、AppSpec に任せる仕様となり気持ち悪い感じがしますが、そうなっている以上はこれをどう管理していくか決めていかなくてはいけません。
AppSpecの戦略管理方法
appspec.yml はおそらくアプリケーション・コードのリポジトリあたりに保存されていると思います。キャパプロ戦略のスポット比率などの条件は、環境(ENV) ごとに変えたいはずなので、1つの appspec.yml では対応できないことが確定します。環境分を用意する
例えば、環境の数だけ appspec を用意します。元々 deploy/appspec.yml にでも置いていたとしたら、- deploy/appspecs/production.yml
- deploy/appspecs/staging.yml
のようにし、Buildフェーズに ENV を持たせて、buildspec.yml で
1 |
cp deploy/appspecs/${ENV}.yml ./appspec.yml |
とでもすれば、環境ごとに設定上書きに対応できます。
これでも問題なければこれでいいですが、リソースの設定管理が IaC 側ではなくアプリコード側になってしまうので、運用管理的にバラけてしまうというのが嫌な場合は、次の方法をとることになります。
動的に用意する
Terraform から数値を管理したい場合、Buildフェーズで動的に用意することで実現可能です。まずは Build フェーズに環境変数を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
resource "aws_codepipeline" "ecs_app" { ... stage { name = "Build" ... EnvironmentVariables = jsonencode([ { name = "CAPACITY_ONDEMAND_BASE" value = "${lookup(each.value, "capacity_ondemand_base", 0)}" type = "PLAINTEXT" }, { name = "CAPACITY_ONDEMAND_WEIGHT" value = "${lookup(each.value, "capacity_ondemand_weight", 1)}" type = "PLAINTEXT" }, { name = "CAPACITY_SPOT_BASE" value = "${lookup(each.value, "capacity_spot_base", 0)}" type = "PLAINTEXT" }, { name = "CAPACITY_SPOT_WEIGHT" value = "${lookup(each.value, "capacity_spot_weight", 0)}" type = "PLAINTEXT" }, |
次に buildspec.yml で Python を実行して動的編集をします。
1 2 3 4 5 6 7 8 9 10 |
... phases: install: runtime-versions: python: 3.12 ... post_build: commands: ... - python3 deploy/appspec.py deploy/appspec.yml appspec.yml |
アプリコードに置く appspec.py はシンプルなYAML編集をして書き出すだけです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import os import sys import yaml args = sys.argv INPUT_FILE = args[1] if len(args) > 1 else 'deploy/appspec.yml' OUTPUT_FILE = args[2] if len(args) > 2 else 'appspec.yml' CAPACITY_ONDEMAND_BASE = os.environ.get('CAPACITY_ONDEMAND_BASE', 0) CAPACITY_ONDEMAND_WEIGHT = os.environ.get('CAPACITY_ONDEMAND_WEIGHT', 1) CAPACITY_SPOT_BASE = os.environ.get('CAPACITY_SPOT_BASE', 0) CAPACITY_SPOT_WEIGHT = os.environ.get('CAPACITY_SPOT_WEIGHT', 0) with open(INPUT_FILE, 'r') as f: appspec = yaml.safe_load(f) strategy = [ { "CapacityProvider": "FARGATE", "Base" : CAPACITY_ONDEMAND_BASE, "Weight" : CAPACITY_ONDEMAND_WEIGHT, }, { "CapacityProvider": "FARGATE_SPOT", "Base" : CAPACITY_SPOT_BASE, "Weight" : CAPACITY_SPOT_WEIGHT, }, ] appspec["Resources"][0]["TargetService"]["Properties"]["CapacityProviderStrategy"] = strategy with open(OUTPUT_FILE,'w')as f: print(f"Write appspec to {OUTPUT_FILE}") yaml.dump(appspec, f, default_flow_style=False, allow_unicode=True, sort_keys=False) |
これで、Pipeline ごとに任意の内容で appspec.yml を Deploy フェーズに渡して、ECS Service を更新することができます。
CodeDeploy単体の注意点
1つ注意する点があり、それは CodeDeploy 単体としてのデプロイです。デプロイは ECS Service + CodeBiuld + CodeDeploy + CodePipeline のように組むわけですが、ECS Service から直にデプロイすることで CodeDeploy を単体で実行することは普通に可能になっています。Build フェーズが不要だったり、設定値を変えたり動作確認で、強制デプロイしたりしますしね。
CodeDeploy 単体でのデプロイは、AppSpec が自動生成され「リビジョン」として残ります。この内容には、CapacityProviderStrategy が含まれないため、キャパプロ戦略を設定していた Service で単体デプロイすると、やはり「起動タイプ」に戻される結果となります。
この単体デプロイの AppSpec を任意の内容にする方法は何かしらあるかもですが(フックとか?)、少し調べて考えた程度じゃなかったので、ここに手を突っ込むのは汚いことになるかもしれません。
一番良いのは、AppSpec 自動生成が ECS Service の CapacityProviderStrategy 設定を見て、あれば埋め込んでくれることなので、改善されることに期待したいところです。
感想戦
ARM64 にスポットがきて、ウキウキしていざやってみると上手くいかず、でも AWS がこんなクリティカルなやらかしするかなーと疑いつつも、最初は解決できませんでした。自分は Fargate X86 とそのスポットが好きじゃなく、まともに使っていなかったのですが、同僚の後輩が使っていて AppSpec のヒントをくれて、一気に解決までもっていけたので、喚き散らしたのは恥ながらも早い進行で結果オーライということになりました。
まだ見落としている部分があったり、よりよい解決方法があるかもですが、いったんの解決をした今でもこの上書きされる仕組みは気持ち悪い感が拭えないので、まだ当分は気にかけておきたいところです。
どうしてコンテナ関連はこうも絶対的な正解はない選択肢が多いのか、がまた増えたわけで、飯のタネではありつつも、もっとシンプルにオイシイ目をみたいとも思うこの頃であります:-)