LoginSignup
38
37

More than 5 years have passed since last update.

AnsibleでAWSのAutoScaling機能を利用してBlue-Greenデプロイを頑張った話

Last updated at Posted at 2015-12-17
version
ansible 1.9.2

条件

・イミュータブルインフラストラクチャーの実現
・ダウンタイム0でログロストもしないような安全なデプロイ
・最小のコストで最大限のパフォーマンスを出せるAutoScalingを活用する

準備

デプロイの流れ

  1. instance_idを受け取りAMIを作成する
  2. 作成したAMIからLaunchConfigrationを作成する
  3. AutoScalingGroupの設定を変更する
  4. ELBに新しいバージョンのサーバをServiceInさせる。(古いバージョンのものと同数)
  5. 新しいバージョンのサーバのServiceIn直後に古いバージョンのものをELBからServiceOutさせる
  6. 古いバージョンのものはログを送りきってからterminateさせる

上記の流れでBlue-Greenデプロイを行うplaybook

blue-green-deploy.yml
- hosts: localhost
  connection: local
  gather_facts: no
  vars:
    - aws:
        access_key: AAAAAAAAAA
        secret_key: BBBBBBBBBB
    - ec2:
        instance_type: t2.medium
        security_group_id: sg-xxxxxxx
        vpc_subnet_id_1a: subnet-aaaaaaaa
        vpc_subnet_id_1c: subnet-cccccccc
        availability_zone_1a: ap-northeast-1a
        availability_zone_1c: ap-northeast-1c
        region: ap-northeast-1
        associate_elb: elb-sample
    - heartbeat_timeout: 300
    - lifecycle_notify: arn:aws:sns:ap-northeast-1:xxxxx:hogehoge
    - lifecycle_role: arn:aws:iam::xxxxxxx:role/sample-asg-lifecycle
    - auto_scaling_group_name: asg_sample
  tasks:
    - name: AMIを作成
      ec2_ami:
        aws_access_key: "{{ aws.access_key }}"
        aws_secret_key: "{{ aws.secret_key }}"
        region: "{{ ec2.region }}"
        instance_id: "{{ ec2_instance_id }}"
        no_reboot: no
        wait: yes
      register: ami_result

    - name: LaunchConfigrationを作成
      ec2_lc:
        name: "lc_{{ ec2_instance_id }}"
        image_id: "{{ ami_result.image_id }}"
        region: "{{ ec2.region }}"
        security_groups: "{{ ec2.security_group_id }}"
        instance_type: "{{ ec2.instance_type }}"

    - name: 現在のauto-scaling-groupの情報を取ってくる
      shell: |
        aws autoscaling describe-auto-scaling-groups \
          --auto-scaling-group-names {{ auto_scaling_group_name }} \
        | jq .AutoScalingGroups[] \
        | jq \"{MinSize:.MinSize,MaxSize:.MaxSize,DesiredCapacity:.DesiredCapacity,Instances:[.Instances[]]}\"
      register: auto_scaling_group_desc

    - name: 現在のauto-scaling-groupの情報を変数にSETする
      set_fact:
        asg_desc: "{{ auto_scaling_group_desc.stdout|from_json }}"
      register: result
    - set_fact:
        asg_min_size: "{{ asg_desc.MinSize|int }}"
        asg_max_size: "{{ asg_desc.MaxSize|int }}"
        asg_desired_capacity: "{{ asg_desc.DesiredCapacity|int }}"
        asg_instances: "{{ asg_desc.Instances }}"

    - name: terminateさせるインスタンスを安全に落とすための設定をする
      shell: |
        aws autoscaling put-lifecycle-hook \
          --lifecycle-hook-name {{ auto_scaling_group_name }}-terminate \
          --auto-scaling-group-name {{ auto_scaling_group_name }} \
          --lifecycle-transition autoscaling:EC2_INSTANCE_TERMINATING \
          --notification-target-arn {{ lifecycle_notify }} \
          --role-arn {{ lifecycle_role }} \
          --heartbeat-timeout {{ heartbeat_timeout }}

    - name: auto-scaling-groupの設定をUPDATEする
      ec2_asg:
        name: "{{ auto_scaling_group_name }}"
        health_check_period: 300
        load_balancers: [ "{{ ec2.associate_elb }}" ]
        health_check_type: ELB
        availability_zones: [ "{{ ec2.availability_zone_1a }}", "{{ ec2.availability_zone_1c }}" ]
        launch_config_name: "{{ launch_config_name }}"
        min_size: "{{ asg_min_size }}"
        max_size: "{{ asg_max_size|int * 2 }}"
        desired_capacity: "{{ asg_desired_capacity|int * 2 }}"
        region: "{{ ec2.region }}"
        vpc_zone_identifier: [ "{{ ec2.vpc_subnet_id_1a }}", "{{ ec2.vpc_subnet_id_1c }}" ]
        state: present

    - name: termination-policiesを変更する(古いものから落とされるように)
      shell: |
        aws autoscaling update-auto-scaling-group \
          --auto-scaling-group-name {{ auto_scaling_group_name }} \
          --termination-policies \"OldestInstance\" \"OldestLaunchConfiguration\"

    - name: すべてのインスタンスが入れ替わるまで待つ
      shell: |
        aws elb describe-instance-health \
          --load-balancer-name {{ ec2.associate_elb }} \
        | jq 'map(.[]|select(.State == \"InService\"))|length'
      register: res_elb
      until: res_elb.stdout|int >= (asg_desired_capacity|int * 2)
      retries: 300
      delay: 5

    - name: 新しいLaunchConfigurationのインスタンスの台数を取得する
      shell: |
        aws autoscaling describe-auto-scaling-groups \
          --auto-scaling-group-names {{ auto_scaling_group_name }} \
        | jq .AutoScalingGroups[].Instances \
        | jq 'map(select(.LaunchConfigurationName == \"{{ launch_config_name }}\"))|length'
      register: new_instance_count

    - name: すべてのインスタンスが入れ替わったか確認する(うまくいっていなければ処理を止める)
      action: exit 1
      when: res_elb.attempts >= 300 or asg_desired_capacity|int > new_instance_count.stdout|int

    - name: 古いインスタンスをterminateする
      shell: |
        aws autoscaling terminate-instance-in-auto-scaling-group \
          --instance-id {{ item.InstanceId }} \
          --should-decrement-desired-capacity
      when: "{{ item.LifecycleState == 'InService' }}"
      with_items: "{{ asg_instances }}"

    - name: AutoScalingの設定を元に戻す
      shell: |
        aws autoscaling update-auto-scaling-group \
          --auto-scaling-group-name {{ auto_scaling_group_name }} \
          --max-size {{ asg_max_size }}

_人人人人人人人人人人_
> ほぼshell...orz <
 ̄YYYYYYYYYY ̄

すみませんほぼshellですね...
粗雑ですがある程度動いているので勘弁してください
(実際に動かしたものとは違いますが、動くはず...!)

いざ実行してみる

実行コマンド
# instance_idを渡すだけ
ansible-playbook blue-green-deploy.yml --extra-vars="ec2_instance_id=i-hogehoge"

解説

デプロイ時の流れを詳しく説明します

1)現在のAutoscalingの設定を取ってくる

2)現在のAutoscalingのサーバスケール(desired)と現在SeviceInしているサーバのInstanceIdをメモしておく

3)Autoscalingの設定を変更する

変更内容:
 ・launch_configを新しいバージョンのものにする(インスタンスの起動設定を最新版のものにする)
 ・desiredを現在のちょうど2倍の値にする。minとmaxは変更しない

4)古いバージョンのものと新しいバージョンのものがすべてELBから見てサービスインしている状態になるまで待つ

ELBにのヘルスチェックが通って安全な状態になるまで待つ
- name: すべてのインスタンスが入れ替わるまで待つ
  shell: |
    aws elb describe-instance-health \
      --load-balancer-name {{ ec2.associate_elb }} \
    | jq 'map(.[]|select(.State == \"InService\"))|length'
  register: res_elb
  until: res_elb.stdout|int >= (asg_desired_capacity|int * 2)
  retries: 300
  delay: 5

aws cliを利用してelbにInServiceとなっているサーバが取得した現在のdesired capacityの倍の数になるまでリクエスト5秒おきに300回まで繰り返します

5)念のためサービスインしているサーバのうち、新しいバージョンのものがいくつあるか確認するために情報を取ってくる

新しいバージョンのサーバ台数取得
- name: 新しいLaunchConfigurationのインスタンスの台数を取得する
  shell: |
    aws autoscaling describe-auto-scaling-groups \
      --auto-scaling-group-names {{ auto_scaling_group_name }} \
    | jq .AutoScalingGroups[].Instances \
    | jq 'map(select(.LaunchConfigurationName == \"{{ launch_config_name }}\"))|length'
  register: new_instance_count

6 - 安全にデプロイできているかの確認(できていない場合は処理を中断)

安全にデプロイができたか確認
- name: すべてのインスタンスが入れ替わったか確認する(うまくいっていなければ処理を止める)
  action: exit 1
  when: res_elb.attempts >= 300 or asg_desired_capacity|int > new_instance_count.stdout|int

=> 下記2点の場合は処理を中断させる。(ダウンタイムを作らないようにするため)
 ・ELBにサービスインしているサーバの台数を確認する処理がタイムアウトしていた場合
 ・新しいバージョンのサーバ台数取得が元のサーバ台数より少ない場合

7 - 6でちゃんと確認ができたら、2でメモしておいたInstanceIdをterminatingの状態に移す

古いインスタンスをterminateする
- name: 古いインスタンスをterminateする
  shell: |
    aws autoscaling terminate-instance-in-auto-scaling-group \
      --instance-id {{ item.InstanceId }} \
      --should-decrement-desired-capacity
  when: "{{ item.LifecycleState == 'InService' }}"
  with_items: "{{ asg_instances }}"

8 - デプロイ完了です

古いバージョンのものはすぐにterminateされないようにしている

lifecyclehookのterminating:waitの設定だけしといてあげれば、とりあえず実際にterminateされるまで猶予を与えることができる。(その時間でfluentd等がログをすべて送り切るようにする)

lifecyclehook登録
aws autoscaling put-lifecycle-hook \
  --lifecycle-hook-name {{ auto_scaling_group_name }}-terminate \
  --auto-scaling-group-name {{ auto_scaling_group_name }} \
  --lifecycle-transition autoscaling:EC2_INSTANCE_TERMINATING \
  --notification-target-arn {{ lifecycle_notify }} \
  --role-arn {{ lifecycle_role }} \
  --heartbeat-timeout {{ heartbeat_timeout }}

Ansibleのec2_asg:replace_instancesについて

Ansibleのautoscalingモジュールにデプロイ時にローリングアップデートで自動でゆっくりサーバを切り替えてくれるものがあります。

ec2_asg:
  replace_instances: yes

しかしこれだと遅い&terminateing:waitとの相性が良くありません

terminateing:waitの状態を待ってしまう
(例:terminating:waitが5分の場合一つinstanceをterminatingしていて、次のinstanceをterminateするのは5分後。その間古いバージョンのものがELBにぶら下がり続けてしまう。)

テスト

簡易なテストですが、下記スクリプトで本当にダウンタイムが0か確認しました。

テストスクリプト
watch -n 1 "curl -s https://sample.io/http_status -o /dev/null -w '%{http_code}\n'"

ELBに正常にサーバがぶら下がっていないと503が発生します。
replace_instancesを使うと若干503が発生してしまいましたが、上記のplaybookならすべて200を確認できました。

38
37
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
38
37