Ansibleで Amazon EC2 Auto Scaling で起動するEC2インスタンスを更新する
この記事は Ansible Advent Calender 2019 21日目の記事として投稿しています。
アドカレのエントリを眺めているとAnsible × AWSネタが無かったため、あえて書いてみました。
・・・需要があるかは不明です。
はじめに
世間ではコンテナ技術が流行っており、AWSのECSやEKS等のコンテナサービスも多く利用されていると思います。
しかし、まだまだ仮想サーバー(Amazon EC2)上で稼働しているサービス、アプリケーションは多いのではないでしょうか。
事情や理由はさておき、そのような状況下でも保守・運用作業は極力省力化したいものです。
省力化に向けてできることの1つとして、AnsibleでのAWSリソースの自動化を「Auto Scalingで起動するEC2インスタンスの更新」を例に挙げ、紹介させていただきます。
★AWS用語の解説は簡潔に記載してるか、省いています。その点ご了承下さい。★
なぜAnsibleを選択したか
AWSのリソースに対する操作ですとCloudFormation, terraformを思い浮かべる方が多いと思います。
なぜAnsibleを選択したか、その理由は以下の通りです。
- ガッツリとした構成管理が目的ではなく、作業の自動化をしたいだけ
- 日頃からサーバの保守、運用作業でAnsibleを利用している
- サーバ、AWSに対する作業の自動化を "Ansibleで共通化" することで、学習コストを抑えたい
Ansibleはバージョンアップが盛んであり、AWS系のモジュールも増え続けているため、日に日に出来ることは増えています。そのため、Ansibleを活用したAWSリソースの変更作業の自動化、構成管理は現実的なものであると思っています。
何よりAnsibleは、プラットフォームが異なる対象(クラウドサービス、サーバ、NW機器)への操作を同じツールで行えることが嬉しいです。
ツール選択は適材適所だと思っていますが、運用現場であれこれツールを増やすと反感を買う可能性がある・・・という現実問題もあります。(小声)
やりたいこと
以下のシナリオを "すべてAnsibleで" 自動化する想定です。
- 作業用のEC2インスタンスを起動
- 作業用のEC2インスタンスへの何らかの処理、テスト等(※1)
- 作業用のEC2インスタンスのAMIを取得
- AMIの世代管理
- 作業用のEC2インスタンスを削除
- 起動設定を新規作成
- Auto Scaling(※2)で起動するEC2インスタンスを更新
- 古い起動設定を削除
※1 (2) については、本記事では省略させていただきます。
※2 Auto Scaling:負荷状況やリクエスト数等の増減に応じてEC2インスタンスの起動台数を増減させる機能。AMIと起動設定で構成される。
一言に「Auto Scalingで起動するEC2インスタンスを更新する」と言っても案外やることが多く、これらを全て手作業で行うのは辛いです・・・。
以下、簡単に各シナリオの内容を説明します。
1. 作業用のEC2インスタンスを起動
最新のAMI(※)から、作業用のEC2インスタンス(一時インスタンス)を起動します。
このインスタンスに対して、Ansibleから処理を実行します。(処理については省略します)
2. AnsibleからEC2インスタンスへの何らかの処理、テスト等
OS設定の変更、M/Wの設定変更・バージョンアップ、それに伴うユニットテスト等を行う想定です。
3. 作業用のEC2インスタンスのAMIを取得
(2) の作業を終えたら、作業用のEC2インスタンスのAMIを作成します。
4. AMIの世代管理
新たにAMIが作成されたため、古いAMIの世代管理を行います。
塵も積もればなんとやらで、AMIの世代管理を行わないと課金額が膨れ上がってしまうため、AMIを一定の世代数だけ保持し、古いAMIは都度削除したいです。
せっかくなのでAMIの世代管理も自動化するため、Playbookに組み込みます。
- 余談
Ansible Tower(AWX)のジョブスケジュール機能を活用すれば、AMIの世代管理処理を日次で "自動で" 行う、ということも実現可能です。
5. 作業用のEC2インスタンスを削除
作業用のEC2インスタンスは一時的な作業用のものです。
AMI取得後は不要であるため、削除(terminate)します。
6. 起動設定を新規作成
ここでは、(3)で取得したAMIを起動時に使用するAMIに指定し、起動設定(※1)を新規作成(※2)します。
※1 起動設定:Auto Scalingグループで起動するEC2インスタンスを、どのような設定で起動するのかを定義するテンプレート的なものです。
※2 起動設定は作成後に変更ができないため、都度新規作成しています。
7. Auto Scalingで起動するEC2インスタンスを更新
(6) で作成した起動設定をAuto Scalingグループに割り当てます。
Auto Scalingグループに割り当てられる起動設定は1つのみなので、起動設定を更新する都度、新しいものに差し替えます。
なお更新はローリングアップデートで行い、稼働中のサービスへの影響が無いようにします。
8. 古い起動設定を削除
古い起動設定を削除します。
切り戻しは行わない(※)想定なので、Auto Scalingグループに設定されているもの以外は削除します。
※不具合が発生した場合は、原因箇所を修正し、シナリオを再度実行すればいいと考えています。
構成
検証環境の情報
以下の環境で動作検証をしています。
- Ansible 2.9.2
- Python 2.7.16
システム構成
ごくごくシンプルな構成です。
Multi-AZ構成のAuto Scalingで稼働するEC2インスタンスを、ローリングアップデートにより無停止で更新(インスタンスの新旧入れ替え)を行います。
全ての操作を、構成図上のAnsibleサーバから行います。
Ansibleのディレクトリ構成
ディレクトリ構成は以下の通りです。
PlaybookやRoleを複数システムで共用する場合は、Playbookとrolesディレクトリを切り出す想定です。
. └ AWS | ├ group_vars | ├ uat | | └ aws.yml | └ prod | └ aws.yml | ├ host_vars | ├ uat-web01 | | └ all.yml | └ prod-web01 | └ all.yml | ├ roles | ├ CreateAMI | ├ CreateLaunchConfig | ├ DeregisterAMI | ├ LaunchTemporarilyInstance | ├ TerminateTemporarilyInstance | └ UpdateAutoScalingGroup | ├ ansible.cfg | ├ inventory | └ UpdateAutoScaling.yml
group_vars
システム環境毎にグループを分けています。
環境毎にAWSアカウントもしくはVPCを分けることを想定し、各環境のグループ変数にそれぞれパラメータを定義したいためです。
- 補足
制御ノード(Ansibleをインストールしたサーバ)とするEC2インスタンスには、本シナリオの実行に必要な権限を付与したIAMロールを割り当てています。そのため、Ansibleのコード上にアクセスキー、シークレットキーは定義していません。
オンプレミス環境からAWSリソースを操作する場合は、group_varsにキーを定義する想定です。
host_vars
作業用のEC2インスタンス(一時インスタンス)のパラメータ、AutoScalingに関するパラメータを各ホストのホスト変数に定義します。
roles
各RoleでやることはRole名の通りなので、簡潔に記載します。
Role名 | 処理概要 |
---|---|
CreateAMI | 作業用のEC2インスタンスのAMIを取得する |
CreateLaunchConfig | 起動設定 (LaunchConfg) を作成する |
DeregisterAMI | 定義した世代数以上に存在するAMIを、古いものから登録解除する |
LaunchEC2Instance | EC2インスタンスを起動する |
TerminateEC2Instance | EC2インスタンスを終了(削除)する |
UpdateAutoScalingGroup | Auto Scalingグループを更新する |
inventory
inventoryファイルの内容は以下の通りです。
各環境毎に親グループを作成し、各環境下のホストグループを所属させ、各環境用のグループ変数を読み込むようにします。
[uat-web] uat-web01 ansible_host=<IP Address> [uat:children] uat-web [prod-web] prod-web01 ansible_host=<IP Address> [prod:children] prod-web
実際のコード
長いため、折りたたんでいます。ファイルパスをクリックして展開して下さい。
変数は検証時に適当にセットしたものです。また、各リソースのIDはマスクしています。
group_vars/uat/aws.yml
---
region: us-west-2
vpc_id: <VPC ID>>
host_vars/uat-web01/all.yml
---
# General
hostname: uat-web01
# Temporarily instance
temp_ec2_instance_type: t3.medium
temp_ec2_security_groups:
- <Security Group ID>
temp_ec2_subnet_id: subnet-
# AMI
ami_no_reboot: no
ami_wait: yes
ami_wait_timeout: 1800
ami_generation: 3
# Launch Config
lc_instance_type: t3.medium
lc_detail_monitoring: yes
lc_iam_role: <IAM Role Name>
lc_security_groups:
- <Security Group ID>
# AutoScaling Group
asg_name: <Auto Scaling Group Name>
asg_health_check_period: 300
asg_wait_timeout: 1800
asg_vpc_subnet_id:
- <Subnet-A ID>
- <Subnet-B ID>
asg_health_check_type: ELB
roles/CreateAMI/tasks/main.yml
---
- name: Get InstanceID
ec2_instance_facts:
filters:
network-interface.addresses.private-ip-address: "{{ ansible_host }}"
region: "{{ region }}"
delegate_to: localhost
register: return_instance_info
- name: Set InstanceID
set_fact:
_instance_id: "{{ return_instance_info.instances.0.instance_id }}"
- name: Get current date and time
setup:
filter: ansible_date_time
delegate_to: localhost
register: return_datetime
- name: Set current date and time
set_fact:
_current_datetime: "{{ return_datetime.ansible_facts.ansible_date_time }}"
- name: Set AMI name
set_fact:
_ami_name: "{{ hostname }}-{{ _current_datetime.date | regex_replace('-','') }}-{{ _current_datetime.hour }}{{ _current_datetime.minute }}"
- name: Create AMI
ec2_ami:
instance_id: "{{ _instance_id }}"
name: "{{ _ami_name }}"
no_reboot: "{{ ami_no_reboot | default('no') }}"
wait: "{{ ami_wait | default('yes') }}"
wait_timeout: "{{ ami_wait_timeout | default(omit) }}"
region: "{{ region }}"
delegate_to: localhost
roles/CreateLaunchConfig/tasks/main.yml
---
- name: Get current date and time
setup:
filter: ansible_date_time
delegate_to: localhost
register: return_datetime
- name: Set current datetime
set_fact:
_current_datetime: "{{ return_datetime.ansible_facts.ansible_date_time }}"
- name: Shaping datetime
set_fact:
_current_datetime: "{{ _current_datetime.date | regex_replace('-','') }}{{ _current_datetime.hour }}{{ _current_datetime.minute }}"
- name: Set Launch Config name
set_fact:
_lc_name: "asg-lc-{{ hostname }}-{{ _current_datetime }}"
- name: Get AMI list
ec2_ami_facts:
filters:
name: "{{ hostname }}*"
region: "{{ region }}"
delegate_to: localhost
register: return_ami_list
- name: Sort a AMI list
set_fact:
_ami_list: "{{ return_ami_list.images | sort(attribute='creation_date',reverse=True) }}"
- name: Set latest AMI ID to variable
set_fact:
_ami_id: "{{ _ami_list.0.image_id }}"
- name: Create new Launch Config
ec2_lc:
name: "{{ _lc_name }}"
image_id: "{{ _ami_id }}"
instance_monitoring: "{{ lc_detail_monitoring }}"
instance_profile_name: "{{ lc_iam_role }}"
instance_type: "{{ lc_instance_type }}"
security_groups: "{{ lc_security_groups }}"
vpc_id: "{{ vpc_id }}"
region: "{{ region }}"
delegate_to: localhost
roles/DeregisterAMI/tasks/main.yml
---
- name: Get AMI list
ec2_ami_facts:
filters:
name: "{{ hostname }}*"
region: "{{ region }}"
delegate_to: localhost
register: return_ami_list
- name: Set AMI list
set_fact:
_ami_list: "{{ return_ami_list.images | sort(attribute='creation_date',reverse=True) }}"
- name: Deregister AMI
ec2_ami:
image_id: "{{ item.image_id }}"
state: absent
delete_snapshot: yes
region: "{{ region }}"
when: ansible_loop.index > ami_retained
loop: "{{ _ami_list }}"
loop_control:
label: "{{ item.image_id }}"
extended: yes
delegate_to: localhost
roles/LaunchEC2Instance/tasks/main.yml
---
- name: Get AMI list
ec2_ami_facts:
filters:
name: "{{ hostname }}*"
region: "{{ region }}"
delegate_to: localhost
register: return_ami_list
- name: Sort AMI list
set_fact:
_ami_list: "{{ return_ami_list.images | sort(attribute='creation_date',reverse=True) }}"
- name: Set latest AMI ID
set_fact:
_ami_id: "{{ _ami_list.0.image_id }}"
- name: Launch EC2 instance
ec2_instance:
image_id: "{{ _ami_id }}"
instance_type: "{{ temp_ec2_instance_type }}"
vpc_subnet_id: "{{ temp_ec2_subnet_id }}"
security_groups: "{{ temp_ec2_security_groups }}"
network:
assign_public_ip: true
private_ip_address: "{{ ansible_host }}"
tags:
Name: "{{ hostname }}_temp"
wait: yes
region: "{{ region }}"
delegate_to: localhost
roles/TerminateEC2Instance/tasks/main.yml
---
- name: Get instance ID
ec2_instance_facts:
filters:
network-interface.addresses.private-ip-address: "{{ ansible_host }}"
region: "{{ region }}"
delegate_to: localhost
register: return_instance_info
- name: Set instance ID
set_fact:
_instance_id: "{{ return_instance_info.instances.0.instance_id }}"
- name: Terminate EC2 instance
ec2_instance:
instance_ids: "{{ _instance_id }}"
state: absent
wait: yes
region: "{{ region }}"
delegate_to: localhost
roles/UpdateAutoScalingGroup/tasks/main.yml
---
- name: Get Auto Scaling Group facts
ec2_asg_facts:
name: "{{ asg_name }}"
delegate_to: localhost
register: return_asg_facts
- name: Set current Launch Config
set_fact:
_old_lc_name: "{{ return_asg_facts.results.0.launch_config_name }}"
- name: Get latest Launch Config
ec2_lc_find:
name_regex: "asg-lc-{{ hostname }}*"
sort_order: descending
limit: 1
region: "{{ region }}"
delegate_to: localhost
register: return_lc_info
- name: Set latest Launch Config
set_fact:
_latest_lc_name: "{{ return_lc_info.results.0.name }}"
- name: Update Auto Scaling Group
ec2_asg:
name: "{{ asg_name }}"
health_check_period: "{{ asg_health_check_period }}"
health_check_type: "{{ asg_health_check_type }}"
launch_config_name: "{{ _latest_lc_name }}"
metrics_collection: yes
region: "{{ region }}"
replace_all_instances: yes
replace_batch_size: 1
termination_policies: OldestLaunchConfiguration
wait_for_instances: yes
wait_timeout: "{{ asg_wait_timeout }}"
vpc_zone_identifier: "{{ asg_vpc_subnet_id }}"
delegate_to: localhost
- name: Delete old Launch Config
ec2_lc:
name: "{{ _old_lc_name }}"
state: absent
delegate_to: localhost
inventory
[uat-web]
uat-web01 ansible_host=<IP Address>
[uat:children]
uat-web
[prod-web]
prod-web01 ansible_host=<IP Address>
[prod:children]
prod-web
ansible.cfg
[defaults]
deprecation_warnings = False
force_valid_group_names = ignore
interpreter_python = auto_legacy_silent
※ OS以上に対して何か処理をしたい場合は、Role: LaunchEC2Instance 〜 CreateAMIの間に、やりたいことを行うRoleを挿入します。UpdateAutoScalingGroup.yml
---
- hosts: all
gather_facts: False
roles:
- LaunchEC2Instance
- CreateAMI
- DeregisterAMI
- TerminateEC2Instance
- CreateLaunchConfig
- UpdateAutoScalingGroup
実行
Playbookの実行結果です。(クリックして展開して下さい)
# ansible-playbook -l uat-web01 -i inventory UpdateAutoScaling.yml
PLAY [all] ***************************************************************************************************************************************************************************************
TASK [LaunchEC2Instance : Get AMI list] **********************************************************************************************************************************************************
ok: [uat-web01 -> localhost]
TASK [LaunchEC2Instance : Sort AMI list] *********************************************************************************************************************************************************
ok: [uat-web01]
TASK [LaunchEC2Instance : Set latest AMI ID] *****************************************************************************************************************************************************
ok: [uat-web01]
TASK [LaunchEC2Instance : Launch EC2 instance] ***************************************************************************************************************************************************
changed: [uat-web01 -> localhost]
TASK [CreateAMI : Get InstanceID] ****************************************************************************************************************************************************************
ok: [uat-web01 -> localhost]
TASK [CreateAMI : Set InstanceID] ****************************************************************************************************************************************************************
ok: [uat-web01]
TASK [CreateAMI : Get current date and time] *****************************************************************************************************************************************************
ok: [uat-web01 -> localhost]
TASK [CreateAMI : Set current date and time] *****************************************************************************************************************************************************
ok: [uat-web01]
TASK [CreateAMI : Set AMI name] ******************************************************************************************************************************************************************
ok: [uat-web01]
TASK [CreateAMI : Create AMI] ********************************************************************************************************************************************************************
changed: [uat-web01 -> localhost]
TASK [DeregisterAMI : Get AMI list] **************************************************************************************************************************************************************
ok: [uat-web01 -> localhost]
TASK [DeregisterAMI : Set AMI list] **************************************************************************************************************************************************************
ok: [uat-web01]
TASK [DeregisterAMI : Deregister AMI] ************************************************************************************************************************************************************
skipping: [uat-web01] => (item=ami-xxxxxxxxxxxxxxxxx)
skipping: [uat-web01] => (item=ami-xxxxxxxxxxxxxxxxx)
skipping: [uat-web01] => (item=ami-xxxxxxxxxxxxxxxxx)
changed: [uat-web01 -> localhost] => (item=ami-xxxxxxxxxxxxxxxxx)
changed: [uat-web01 -> localhost] => (item=ami-xxxxxxxxxxxxxxxxx)
TASK [TerminateEC2Instance : Get instance ID] ****************************************************************************************************************************************************
ok: [uat-web01 -> localhost]
TASK [TerminateEC2Instance : Set instance ID] ****************************************************************************************************************************************************
ok: [uat-web01]
TASK [TerminateEC2Instance : Terminate EC2 instance] *********************************************************************************************************************************************
changed: [uat-web01 -> localhost]
TASK [CreateLaunchConfig : Get current date and time] ********************************************************************************************************************************************
ok: [uat-web01 -> localhost]
TASK [CreateLaunchConfig : Set current datetime] *************************************************************************************************************************************************
ok: [uat-web01]
TASK [CreateLaunchConfig : Shaping datetime] *****************************************************************************************************************************************************
ok: [uat-web01]
TASK [CreateLaunchConfig : Set Launch Config name] ***********************************************************************************************************************************************
ok: [uat-web01]
TASK [CreateLaunchConfig : Get AMI list] *********************************************************************************************************************************************************
ok: [uat-web01 -> localhost]
TASK [CreateLaunchConfig : Sort a AMI list] ******************************************************************************************************************************************************
ok: [uat-web01]
TASK [CreateLaunchConfig : Set latest AMI ID to variable] ****************************************************************************************************************************************
ok: [uat-web01]
TASK [CreateLaunchConfig : Create new Launch Config] *********************************************************************************************************************************************
changed: [uat-web01 -> localhost]
TASK [UpdateAutoScalingGroup : Get Auto Scaling Group facts] *************************************************************************************************************************************
ok: [uat-web01 -> localhost]
TASK [UpdateAutoScalingGroup : Set current Launch Config] ****************************************************************************************************************************************
ok: [uat-web01]
TASK [UpdateAutoScalingGroup : Get latest Launch Config] *****************************************************************************************************************************************
ok: [uat-web01 -> localhost]
TASK [UpdateAutoScalingGroup : Set latest Launch Config] *****************************************************************************************************************************************
ok: [uat-web01]
TASK [UpdateAutoScalingGroup : Update Auto Scaling Group] ****************************************************************************************************************************************
changed: [uat-web01 -> localhost]
TASK [UpdateAutoScalingGroup : Delete old Launch Config] *****************************************************************************************************************************************
changed: [uat-web01 -> localhost]
PLAY RECAP ***************************************************************************************************************************************************************************************
uat-web01 : ok=31 changed=7 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ec2_asgモジュール
個人的に気になったec2_asgモジュールのパラメータを解説します。
termination_policies
Auto Scalingで稼働中のインスタンス(入れ替え前のインスタンス)を、どのような基準で削除するかを指定します。
本シナリオではOldestLaunchConfigurationを指定し、古い起動設定で起動したインスタンスから順に削除するようにしています。
replace_batch_size
新しいインスタンスが起動される際に、同時に起動するインスタンスの台数を指定します。
本パラメータで指定した台数が一気に置き換えられるのではない、というのがポイントです。
wait_for_instances
入れ替え後のインスタンスが正常にサービスインしたかを確認したいため、サービスインするまで待機するようにyesを指定します。
インスタンスがサービスインしたかの判定はELBのステータスを見て(InServiceになっているか)判定したいため、Auto Scalingグループの設定項目「 ヘルスチェックのタイプ(Health Check Type)」では「ELB」を指定しておきます。
wait_timeout
本パラメータで指定するタイムアウト値は、全インスタンスの入れ替えが完了するまでのタイムアウト値です。
そのため、インスタンスがサービスイン状態になるまで掛かる時間、インスタンスの稼働台数によってチューニングが必要となります。
ローリングアップデート時の動作
下図のようにインスタンスの更新(ローリングアップデート)が行われます。
AZ-Aが完了すると、続けてAZ-Bのインスタンスが更新されます。
更新前の状態です。
新しいAMI(新しく作成した起動設定)でインスタンスが起動されます。
この時点ではまだ、新しいインスタンスへバランシングは行われていません。
インスタンスがAutoScalingグループのステータスで "Healthy" になると、インスタンスはALBのターゲットグループにアタッチされます。
ヘルスチェックに合格すると、ターゲットグループでのステータスが "healthy" となり、バランシングが開始されます。
その後、元々起動していたインスタンス(古い起動設定により起動していたインスタンス)は終了(terminate)されます。
これで、入れ替えが完了します。
さいごに
いかがでしたでしょうか。サクッと書く予定が、結構なボリュームになってしまいました。
私がCloudFormation, terraformの経験が無いため、実装・構成や利便性、実運用してみての比較等は書けませんでしたが・・・AnsibleによるAWSリソースの操作も割とシンプルに行えることはご理解いただけたと思います。
繰り返しとなりますが、AWSリソースの操作とOS上の操作が入れ交じるシナリオはAnsibleで一本化できるため、複数のツールを運用して苦労されてる方は、お試しいただければと思います。
ハッピーオートメーション!
バトンリレー
明日は @akira6592 さんです!
よろしくお願いしまーす!!!