あんまりEC2ノードごと増やしたり減らしたりってのは、やらないで済むに越したことはないんですが、お金に限りがある世界では、無駄に何十台もサーバー立ててられないので、適宜減らしたりしたいわけです。
増やす分には、Serviceのdesired countを上げて、Autoscaling Group(以下ASG)のdesired capacityを上げればほっとけば勝手に収束してくれるので、特に問題ありません。
問題は減らす場合で、ASGのcapacityを単純に下げると、どのノードがterminateされるかを厳密にコントロールすることができません。
そうすると、現在タスク稼動中のノードが落ちたりして無駄なコンテナの上げ下げが発生したり、いざログの転送とかが詰まってたりすると、データロストの危険が高まって心理的に余り嬉しくない。
というわけで、順番に行儀良く落とす方法を考えてみました。
まずECSからderegisterが可能なコンテナインスタンスの一覧を取得する。
実は割とめんどくさくて二種類のAPIコールが必要。
def fetch_container_instances
arns = []
resp = nil
loop do
options = {cluster: cluster}
options.merge(next_token: resp.next_token) if resp && resp.next_token
resp = client.list_container_instances(options)
arns.concat(resp.container_instance_arns)
break unless resp.next_token
end
chunk_size = 50
container_instances = []
arns.each_slice(chunk_size) do |arn_chunk|
is = client.describe_container_instances(cluster: cluster, container_instances: arn_chunk).container_instances
container_instances.concat(is)
end
container_instances
end
ここから、稼働中のタスクが0のインスタンスを探す。
container_instances = fetch_container_instances
deregisterable_instances = container_instances.select do |i|
i.pending_tasks_count == 0 && i.running_tasks_count == 0
end
こいつらが、登録解除しても問題ないインスタンスなので、まずこいつらをクラスターからderegisterします。
diff = current_desired_capacity - target_desired_capacity
deregistered_instance_ids = []
deregisterable_instances.each do |i|
break if deregistered_instance_ids.size >= diff
begin
ecs_client.deregister_container_instance(cluster: service_config.cluster, container_instance: i.container_instance_arn, force: false)
deregistered_instance_ids << i.ec2_instance_id
rescue Aws::ECS::Errors::InvalidParameterException
end
end
force: false
にしておくのが大事です。
そうしておくと、タイミングの問題で何らかのタスクが動き始めてしまった場合はエラーにすることができます。
実際に登録が解除できたインスタンスIDのものを、まずASGからdetachしてから、terminateします。
client.detach_instances(
auto_scaling_group_name: name,
instance_ids: deregistered_instance_ids,
should_decrement_desired_capacity: true
)
sleep 1
ec2_client.terminate_instances(instance_ids: deregistered_instance_ids)
ASGからdetachする時にshould_decrement_desired_capacity: true
とセットしておくと、detachした数の分だけdesired capacityを減らしてくれます。
これで任意のインスタンスをASGから安全に除外しつつdesired capacityを調整できます。
その後で、terminateして完了です。
一応、これで安全に落とせるのですが、ネットワークの通信不良などで、どこかの処理がエラーになった場合、孤立したインスタンスが残ることになります。
なので、ECSのクラスタに所属しているインスタンスのIDと対応するASGに所属しているインスタンスのIDを比較して、ECSから接続が切れているのに稼動しっぱなしになっているインスタンスを落としたり、タグでインスタンスを探してASGから外れてるインスタンスを落とす等の処理が必要になるかもしれません。
ASGの設定でインスタンスの削除保護を有効にしておけば、事故る可能性はほぼ無くなるので、それと組み合わせれば、安全にECSのクラスタを縮退させられると思います。
なんか節約のために無駄な苦労を負っている気がしますが……。