目的・やりたいこと

よくあるAuto Scaling構成で、AMIの更新を現状構築Gでは手動でやっているところが多い。
コンテンツの更新頻度が多いと、運用に工数が取られるので、工数削減のため自動化をインフラ技術で検討する。

Auto Scaling構成でのAMI自動更新ソリューションについてお話しさせて頂きます。
よくあるAuto Scaling構成で、AMIの更新を手動でやっているところが多いかと思います。
しかし、コンテンツの更新頻度が多いと、運用に工数が取られてしまいます。
そこで、工数削減のための自動化を試みましたので、その方法を紹介いたします。

対象者

Systems Managerなどで自動化を行っているあるいは行ってみたい方(中級者~上級者)

対象となる技術

  • Systems Manager
  • Lambda
  • Auto Scaling
  • AMI

条件

・自動で、AMI取得、Auto Scaling起動テンプレート更新、ローリングアップデートまで行えること
・メンテが極力いらない構成であること
・失敗時にエラーを検知できること
・CFやTerraformなどで横展開できること

参考

注意事項

  • 対象AMIがWindowsの場合、デプロイコマンドをAWS-RunPowerShellScriptで実行している関係上、事前にAWS CLIを入れておく必要があります。運用上そういったものを入れておくことはできない場合は、Windowsでは本スクリプトは使えません。
  • ローリングアップデートは今動いているEC2に対しても更新をかけるため、ローリングアップデート前に何かしらアップデートや変更作業を実施していた場合、それが消えてしまいます。
  • EC2SSMRoleにcodedeployを実行するためのポリシーを付与する必要があります。
  • 途中どうしてもデプロイに失敗してハマってたが、どうやら権限やエージェントの再起動が必要だったらしい。CodeDeployでEC2へデプロイした結果、処理が1つも進まずに失敗してしまうときの対処方法が大変参考になった。起動設定でインスタンスプロファイルを割り当ててなかったのが原因
  • このため、起動テンプレートにCodeDeployDemo-EC2-Instance-Profileというインスタンスプロファイルを設定する必要があり、新しいEC2のロールにはこれが付与されています

作業の流れ

事前作業

  1. デプロイ環境が作成されていること
    今回はLinuxでデプロイ環境を作ってみました。Apacheを起動してWordPressを構築するサイトを作るものです。
/tmp/
|--WordPress/
|-- appspec.yml
|-- scripts/
| |-- change_permissions.sh
| |-- create_test_db.sh
| |-- install_dependencies.sh
| |-- start_server.sh
| |-- stop_server.sh
|-- wp-admin/
| |-- (various files...)
|-- wp-content/
| |-- (various files...)
|-- wp-includes/
| |-- (various files...)
|-- index.php
|-- license.txt
|-- readme.html
|-- (various files ending with .php...)

これらのファイルを用意し、アプリケーションのファイルを単一のアーカイブファイルにバンドルし、アーカイブファイルをS3にプッシュします。

aws deploy create-application --application-name WordPress_App
aws deploy push \
--application-name WordPress_App \
--s3-location s3://codedeploydemobucket/aaa/WordPressApp.zip \
--ignore-hidden-files

更なる準備としては、あらかじめSSMエージェントが入っているAmazon Linuxに、Codedeployエージェントを入れたEC2が対象になります。
Amazon Linux または RHEL 用の CodeDeploy エージェントをインストールする

wget https://aws-codedeploy-ap-northeast-1.s3.ap-northeast-1.amazonaws.com/latest/install

ちなみにWindowsでデプロイ環境を作る場合は、IISを起動して「Hello、World!」を表示する簡単なWebサイトになります。

REM Install Internet Information Server (IIS).
c:\Windows\Sysnative\WindowsPowerShell\v1.0\powershell.exe -Command Import-Module -Name ServerManager
c:\Windows\Sysnative\WindowsPowerShell\v1.0\powershell.exe -Command Install-WindowsFeature Web-Server

2.今回使うコードをGitHub – YoshiiRyo1/aws-autoscaling-amiupdateからクローンしておく
3.autoscaling-amiupdate.yaml の69行目「# deployment command here」を実際のデプロイコマンド(自分の場合は以下)に置き換えます。awsコマンド実行時に「You must specify a region.」と怒られないように、–region ap-northeast-1を指定しておくのがポイントです。

- aws deploy create-deployment --application-name WordPress_App --deployment-config-name CodeDeployDefault.OneAtATime --deployment-group-name WordPress_DepGroup --s3-location bucket=nozaki-codedeploybucket,bundleType=zip,key=aaa/WordPressApp.zip --region ap-northeast-1

4.自動化の中で起動する仮EC2で使用するインスタンスプロファイルを作成

$ bash 00-createinstanceprofile.sh

5.SSM ドキュメント実行時に使用する IAM ロールを作成

$ bash 01-createrole.sh

6.Lambda 関数実行時に使用する IAM ロールを作成

$ bash 02-createlambbarole.sh

7.自動化プロセスの一部であるLambda関数 Automation-UpdateAsg を作成

$ bash 02-createlambdafunction.sh

8.自動化プロセスを記述したSSMドキュメント CreateGoldenImageandupdateASG を作成

$ bash 03-createdocument.sh

SSMドキュメント作成ステップ

おおまかには以下のステップとなります。

No 作業内容
1 Systems Manager Automationを実行
2 既存のAMIから仮のEC2を起動
3 S3からデプロイコマンドを実行
4 仮EC2をシャットダウンし、新しいAMIを作成
5 起動テンプレートのAMI IDを新しいAMIのものに修正
6 Lambda関数Automation-UpdateAsgを呼び出す
7 Auto Scalingグループの起動テンプレートを新しいものに修正
8 ローリングアップデートにより、新しいAMIからAuto Scaling配下でEC2を起動
9 古くなったAMIを削除(都合上無効にしています)

SSMドキュメント入力項目

SSMドキュメントであるCreateGoldenImageandupdateASG実行時の入力項目として、主要なものを記載

設定項目 説明 入力値
AutomationAssumeRole CustomRoleSSMCreateImageASG を探す(実装手順通り実行した場合) CustomRoleSSMCreateImageASG
subnetId 起動するサブネット subnet-017d0155c89841afe
instanceprofileName EC2SSMRole(実装手順通り実行した場合) EC2SSMRole
targetASG 対象のAutoScalingグループ名 nozaki-ASG
sourceAMIid メモした最新ゴールデンイメージのAMI ID ami-0e5229fd4d28b4601(その都度入力)
securityGroupId 仮EC2用のセキュリティグループ sg-0ae5c41a8ae289cd9
targetAMIname GoldenImage-{{global:DATE_TIME}}
launchtemplateId 更新対象の起動テンプレートID lt-083faf0904270e2b9

なお、自分は都度入力が面倒だったので、autoscaling-amiupdate.yamlを編集して、
4行目から「default」を追加してパラメータを定義しています。
例)

parameters:
AutomationAssumeRole:
default: arn:aws:iam::532152701269:role/CustomRoleSSMCreateImageASG

準備

起動テンプレートと、それに対応したAuto Scaling Groupを用意します。

起動テンプレート

  起動テンプレート AMI
lt-083faf0904270e2b9 ami-0f4b172cebf2c200c
lt-083faf0904270e2b9 新ゴールデンイメージのAMI(ami-0568aaf88ab30a442)

Auto Scaling Group nozaki-ASG

  • 起動テンプレート: lt-083faf0904270e2b9
  • 希望・最小・最大容量:2

AMI IDが ami-0f4b172cebf2c200c のEC2が2台立ち上がっている事を確認しておきます。

では実際に動きを見てみよう

SSMドキュメントのオートメーションを実行し、上記二つのEC2のAMI IDが新しいものに変わっていれば成功です。

1.まずはSSMドキュメントで上記の入力項目を設定し、[実行]を押します

2.1台がSSMオートメーションによって新規に起動しています

3.オートメーションが全て成功していることを確認

4.現状のEC2で、AMIがami-0568aaf88ab30a442のものに置き換わっています。これは今回作成したGoldenImage-2022-08-09_04.58.58という名前のゴールデンイメージから生成したEC2です。このうちの1台のWebサイト(〜/WordPress/)にアクセスし、WordPressが見れれば成功です


5.WordPressが見れることを確認

6.起動テンプレートlt-0c630cffe6b0de93aにおいても、起動AMIがちゃんとami-0568aaf88ab30a442に書き変わっています。

7.ちなみに、デプロイに時間がかかるため、SSM上ではデプロイ成功として次のステップのEC2停止にすぐに移行してますが、実際はデプロイ完了までにさらに数分かかります。この仕組みに気づかずになぜ新規AMIにはデプロイが反映されてないんだろう?としばらくハマりましたw このため、EC2停止のステップに行く前に5分間sleepを入れました

8.今回デプロイグループの設定対象をAuto Scalingグループに設定してますが、これを既存のEC2単体に設定しても、後ほど更新されるゴールデンイメージで残りもローリングアップデートされます。

9.1で入力したソースAMI(ami-01871b0f817a0d150)が削除されていることを確認(後始末)

(カスタマイズ等について)

①からではなく途中からやりたい場合、どこを変更すればよい?といった柔軟性を求める要望もあると思います。その場合、設定ファイルのautoscaling-amiupdate.yamlをいじります。
例えば、④の新規AMI作成から実行したい場合、autoscaling-amiupdate.yamlのmainStepsにおいて、①〜③に相当する部分を削ります。

mainSteps:
① - name: startInstances
action: aws:runInstances
maxAttempts: 1
(略)
② - name: ec2AppDeploy
action: aws:runCommand
maxAttempts: 1
(略)
- name: sleep
action: aws:sleep
(略)
③ - name: stopInstance
action: aws:changeInstanceState
maxAttempts: 1

つまり、mainStepsは④に相当するcreateImageから始まることになります。

mainSteps:
④ - name: createImage
action: aws:createImage
maxAttempts: 1
onFailure: Continue
inputs:
InstanceId: "{{InstanceIds}}"
ImageName: "{{targetAMIname}}"
NoReboot: true
ImageDescription: "AMI created by EC2 Automation"
〜

あとは微調整として、インスタンスが既にある前提なので、①〜③で必要だったAMI IDの引数の代わりに、インスタンスIDを引数として求めるよう、parameters:の部分を修正します。

parameters:
sourceInstanceIds:
type: String
description: "(Required) Source InstanceIds"

一応こちらのカスタマイズ版であるautoscaling-amiupdate2.yamlを添付しておきます。

description: "Create GoldenImage and Update ASG"
schemaVersion: "0.3"
assumeRole: "{{AutomationAssumeRole}}"
parameters:
AutomationAssumeRole:
default: arn:aws:iam::532152701269:role/CustomRoleSSMCreateImageASG
type: String
description: "(Required) The ARN of the role that allows Automation to perform
the actions on your behalf. If no role is specified, Systems Manager Automation
uses your IAM permissions to execute this document."
sourceInstanceIds:
type: String
description: "(Required) Source InstanceIds"
subnetId:
default: subnet-9f6222c7
type: String
description: "(Required) The SubnetId where the instance is launched from the sourceAMIid."
securityGroupId:
type: String
description: "(Required) Security Group ID that associate the instance is launched from the sourceAMI"
default: sg-06491851b120bb029
instanceprofileName:
default: "EC2SSMRole"
type: String
description: "(Required) EC2 instance profile name that associate source AMI"
targetAMIname:
type: String
description: "(Required) Name of new AMI"
default: "GoldenImage-{{global:DATE_TIME}}"
targetASG:
type: String
description: "(Required) Auto Scaling group to Update"
default: nozaki-ASG
launchtemplateId:
default: lt-0c630cffe6b0de93a
type: String
description: "(Required) EC2 Launch Template ID"

mainSteps:
- name: createImage
action: aws:createImage
maxAttempts: 1
onFailure: Continue
inputs:
InstanceId: "{{InstanceIds}}"
ImageName: "{{targetAMIname}}"
NoReboot: true
ImageDescription: "AMI created by EC2 Automation"

- name: updateLaunchTemplate
action: aws:executeAwsApi
timeoutSeconds: 120
maxAttempts: 1
onFailure: Abort
inputs:
Service: ec2
Api: CreateLaunchTemplateVersion
LaunchTemplateId: "{{launchtemplateId}}"
LaunchTemplateData:
ImageId: "{{createImage.ImageId}}"
SourceVersion: '$Latest'

- name: instanceRefresh
action: aws:invokeLambdaFunction
timeoutSeconds: 300
maxAttempts: 1
onFailure: Abort
inputs:
FunctionName: "Automation-UpdateAsg"
Payload: "{\"targetASG\":\"{{targetASG}}\"}"