先日、AmazonECS / Fargate 本番運用のための構築とデプロイ方法まとめというタイトルでECS/Fargateを本番運用するための構成とデプロイについての記事を書き、HiromuMasuda/ecs-deployというデプロイ用のスクリプトを公開しました。
すると、ECS/Fargateを用いた時のカナリアデプロイの手法が意外と話題になったため、前回は簡単に紹介してしまったカナリアデプロイを、今回は少し首を突っ込んで紹介したいと思います。
アーキテクチャ全体像
全体像はこちらです。
ECS/Fargateってなんだ?という方や、まだ全記事を読んでない方はまずはこちらを見てください。
カナリアデプロイをECSで実現する
service内でtaskが2台動いている状態から、カナリアデプロイをして33%配信を実現するという例を紹介します。
なお、デプロイ用のスクリプトはこちらで公開しております。
1. 新しいバージョンのイメージをbuildしてpushする
バージョンの識別のために、タイムスタンプとgitのコミットハッシュをタグとしてビルドしたイメージにつけて、ECRにpushします。
def push_latest_image
tag_timestamp = Time.now.strftime("%Y%m%d_%H%M")
tag_git_commit_hash = `git rev-parse HEAD`
tags = [tag_timestamp, tag_git_commit_hash]
puts "-----> Push latest image. Tag: #{tags.join(", ")}"
cmd_build = `docker build -t #{@ecr_name} #{@dockerfile_path}`
tags.each do |tag|
cmd = `
docker tag #{@ecr_name}:latest #{get_ecr_image_name(tag)}
docker push #{get_ecr_image_name(tag)}`
end
return get_ecr_image_name(tag_timestamp)
end
2. pushしたイメージを参照するTaskDefinitionの新しいリビジョンを作成する
最新のTaskDefinitionを取得し、ContainerDefinitionの中の参照するimageのバージョンを新しいものに変更し、リビジョンを更新します。
def update_task_definition(image_name)
puts "-----> Update task definition"
task_definition = get_latest_task_definition_description
container_definitions = task_definition["containerDefinitions"]
new_container_definitions = []
container_definitions.each do |container|
container["image"] = image_name if container["name"] == "#{@container_name}"
new_container_definitions << container
end
new_revision = `aws ecs register-task-definition \
--family #{@task} \
--task-role-arn #{task_definition["taskRoleArn"]} \
--execution-role-arn #{task_definition["executionRoleArn"]} \
--network-mode #{task_definition["networkMode"]} \
--volumes '#{task_definition["volumes"].to_json}' \
--cpu #{task_definition["cpu"]} \
--memory #{task_definition["memory"]} \
--requires-compatibilities #{task_definition["requiresCompatibilities"][0]} \
--container-definitions '#{new_container_definitions.to_json}'`
new_task_definition_arn = JSON.parse(new_revision)["taskDefinition"]["taskDefinitionArn"]
puts "-----> New task definition arn: #{new_task_definition_arn}"
return new_task_definition_arn
end
3. 作成したTaskDefinitionを元に1台だけTaskを生成する
2で作成したTaskDefinitionを元に、taskを新しく作成します。この時、上の図のようにserviceの外に作ります。そうすることで、上の2台のtaskは前のリビジョンのTaskDefinitionから、下の1台は新しいリビジョンのTaskDefinitionを参照していることになります。また、今作成したtaskがカナリアデプロイによるものだと判別がつくように、started_by
というスペースにタグを追加します。このスクリプトではcanary
という文字列を指定しています。
def run_task(task_definition, started_by_tag)
puts "-----> Run new task"
service_desc = get_service_description
conf = service_desc["networkConfiguration"]["awsvpcConfiguration"]
cmd = `aws ecs run-task \
--cluster #{@cluster} \
--task-definition #{task_definition} \
--network-configuration "awsvpcConfiguration={\
subnets=[#{conf["subnets"].join(",")}],\
securityGroups=[#{conf["securityGroups"].join(",")}],\
assignPublicIp="DISABLED"}" \
--launch-type FARGATE \
--started-by #{started_by_tag}`
return JSON.parse(cmd)
end
4. 生成したTaskのPrivateIPをロードバランサのターゲットグループに追加する
まず、先ほど作成したtaskからprivateIPを取ってきます。taskが作成されてから数秒しないとprivateIPが生成されないため、取得するまでループを回しています。
def canary_deploy
...
task_arn = new_task["tasks"][0]["taskArn"]
puts "-----> task ARN: #{task_arn}"
# take several seconds to get IP
while true
private_ip = get_task_private_ip(task_arn)
if !private_ip.nil?
puts "-----> private IP: #{private_ip}"
break
end
sleep(1)
end
add_task_to_target_group(private_ip)
...
end
次に、取得したprivateIPをELBのターゲットグループに追加します。これにより、リクエストがカナリアデプロイした1台のtaskにも分散されて流れて来ます。
def add_task_to_target_group(private_ip)
puts "-----> Add #{private_ip} to the target group"
cmd = `aws elbv2 register-targets \
--target-group-arn #{@target_group_arn} \
--targets Id=#{private_ip},Port=8000`
return cmd
end
以上のようにして、カナリアデプロイを実現しました。
まとめ
カナリアデプロイをスクリプト化できたことにより、デプロイによる障害の影響が小さく済むようになりました。ぜひ試して見てください。
また、Twitterでは常に技術系・筋トレ系のアウトプットをしているのでぜひフォローしてください!👉 https://twitter.com/hiromu_bdy