AWS
docker
ECS
docker-compose
Fargate
0

AmazonECS / Fargate でカナリアデプロイを実現する

先日、AmazonECS / Fargate 本番運用のための構築とデプロイ方法まとめというタイトルでECS/Fargateを本番運用するための構成とデプロイについての記事を書き、HiromuMasuda/ecs-deployというデプロイ用のスクリプトを公開しました。

Screen Shot 2018-10-23 at 14.02.21.png

すると、ECS/Fargateを用いた時のカナリアデプロイの手法が意外と話題になったため、前回は簡単に紹介してしまったカナリアデプロイを、今回は少し首を突っ込んで紹介したいと思います。

アーキテクチャ全体像

全体像はこちらです。

Screen Shot 2018-10-20 at 17.50.52.png

ECS/Fargateってなんだ?という方や、まだ全記事を読んでない方はまずはこちらを見てください。

AmazonECS / Fargate 本番運用のための構築とデプロイ方法まとめ

カナリアデプロイをECSで実現する

service内でtaskが2台動いている状態から、カナリアデプロイをして33%配信を実現するという例を紹介します。

Screen Shot 2018-10-23 at 14.11.10.png

なお、デプロイ用のスクリプトはこちらで公開しております。

HiromuMasuda/ecs-deploy

1. 新しいバージョンのイメージをbuildしてpushする

Screen Shot 2018-10-23 at 14.11.18.png

バージョンの識別のために、タイムスタンプと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の新しいリビジョンを作成する

Screen Shot 2018-10-23 at 14.11.26.png

最新の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を生成する

Screen Shot 2018-10-23 at 14.11.33.png

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をロードバランサのターゲットグループに追加する

Screen Shot 2018-10-23 at 14.11.40.png

まず、先ほど作成した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