50
48

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

先日、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

50
48
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
50
48

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?