Systems Manager ステートマネージャーを使用してよくあるWindowsサーバの初期設定をCloudFormation一撃化してみた
AWS事業本部の梶原@福岡オフィスです
CloudFormationでEC2インスタンスを立ち上げる場合、初期設定もまとめてやりたい時があります 以前からCloudFormationで初期設定をする場合はcfn-initでの実施やサンプルが多い状況かと思いますが 最近、AWSのセッションや、ブログ等で紹介されているAWS Systems Manager ステートマネージャーでの初期化 処理が紹介されていたので、ステートマネージャーでの設定の初期化をやってみたので、ご共有します。
ご共有しているテンプレテートでは
- 最新のAMIでWindows2019インスタンスを起動
- SSMに必要なRole(Profile)を作成
- Systems Manager ステートマネージャーで設定処理
- ログ用のS3バケットの作成
などを実施しています。ご参考ください
AWS Systems Manager ステートマネージャーとは?
https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/systems-manager-state.html
AWS Systems Manager ステートマネージャー は、安全でスケーラブルな設定管理サービスであり、Amazon EC2 およびハイブリッドインフラストラクチャを、定義された状態に保つプロセスを自動化します。
ということで、本来は1度きりの初期化ではなく、定期的にEC2インスタンス、オンプレミスサーバなどをスキャンして、自動化処理を各インスタンスに適用することができます。 紹介されている処理としては、
- 起動時に特定のソフトウェアを使用してインスタンスをブートストラップする
- 定義済みのスケジュールに従ってエージェント (SSM エージェント など) をダウンロードして更新する
- ネットワーク設定を構成する
- インスタンスを Windowsドメインに結合する (Windows Server インスタンスのみ)。
- ライフサイクルを通じてソフトウェアの更新でインスタンスをパッチする
- ライフサイクルを通じて Linux および Windows マネージドインスタンスでスクリプトを実行する
といった設定処理の自動化を行うことができます。
AWS Systems Managerステートマネージャーを使用する場合(対象のインスタンス)AWS Systems Managerの1機能になりますので、 実行にはAWS Systems Managerの前提条件を満たす必要がありますので、動かないなどといった場合は、こちらの条件をご確認いただき 割り当てるRoleなどを確認してみてください
AWS Systems Managerの前提条件
Systems Manager の前提条件
https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/systems-manager-prereqs.html
こちらを満たす必要があります。具体的には操作対象のEC2には以下のような条件が必要です
- 適切なRole(ProfileがEC2へ割り当たっている事)
- インターネットへの接続、もしくはSSM Endpointsへの接続が可能なこと
- ステートマネージャーでログを出力しますので、ログ用のバケットへの書き込み権限
AWS Systems Manager Quick Setup
今回は、Role等は、CloudFormationの中で、全部作ってしまいますが、既存のEC2に実施する場合はAWS Systems Manager Quick Setupを使うと AWS Systems ManagerのRoleの作成、適用まで実施してくれます。
https://dev.classmethod.jp/articles/systems-manager-quick-setup/
ちなみに、こちらの機能もまさに、本ブログ同様にCloudFormationとAWS Systems Manager ステートマネージャーの機能で実現されており、参考になります
やってみる
起動するWindowsサーバ
- OS: Windows Server 2019 Japanese-Full-Base
- AMI: 上記OSの最新
- インスタンスタイプ: t3.small
- VPC: default
- SecurityGroup: default
初期設定する内容
- タイムゾーンをJST(Tokyo)
- Windows Updateの実施(オプション)
- Windows Firewallの無効化
- NTP Serverの設定
- 時刻同期の実行
- EC2Launchの更新
- SSM Agentの更新
Windows Updateは時間がかかる、また、同時実行されて、失敗する場合があるので、オプション化しました。 スタックの更新で、UpdateをYesに変更すると後付け実行できます。 また、それぞれのスクリプトの実行成否に関しては、実行時のログを確認し、実際に適用されているかは確認するようにしてください
CloudFormationのスタック作成
- AWSコンソールへログイン
- 以下リンクをぽちっとする
[クイック作成リンク] - □AWS CloudFormation によって IAM リソースが作成される場合があることを承認します。にチェックをいれて
- 「スタックの作成」を選択するとWindowsインスタンスが起動し、初期設定までやってしまいます。
テンプレート抜粋(RunPowerShellScriptを使用)
タイムゾーンをJST(Tokyo)に設定
TimeZoneAssociation: # CloudFormation Resource Type that creates State Manager Associations Type: AWS::SSM::Association Properties: AssociationName: TimeZoneAssociation # Command Document that this Association will run Name: AWS-RunPowerShellScript WaitForSuccessTimeoutSeconds: 300 # Targeting Instance by InstanceId passed from the Logical ID of Instance being created # in CloudFormation Targets: - Key: InstanceIds Values: [ !Ref EC2 ] # The passing in the S3 Bucket that is created in the template that logs will be sent to OutputLocation: S3Location: OutputS3BucketName: !Ref SSMAssocLogs OutputS3KeyPrefix: 'logs/' # Parameters for the AWS-RunShellScript Parameters: commands: - | # 現在のタイムゾーンを取得 Write-Host 'Get current timezone...' Get-TimeZone | Select-Object Id, BaseUtcOffset | Out-String # タイムゾーンの変更 $timeZoneId = 'Tokyo Standard Time' # JSTに設定する場合(必要に応じて変更する) Write-Host 'Configure timezone...' Set-TimeZone -Id $timeZoneId | Out-String # 変更した結果を確認 Write-Host 'Get new timezone...' Get-TimeZone | Select-Object Id, BaseUtcOffset | Out-String
State Manager Associationを作成して、作成したEC2と紐づけています。 S3バケットへのログ出力 パラメータはSystems Manger のRunCommandの実行でドキュメントAWS-RunShellScriptを実行し、commandsの引数(実際のPowereShellのコマンド)を渡して実行しています
テンプレート抜粋(定義済みのドキュメントを使用)
InstallWindowsUpdatesAssociation: Condition: InstallWindowsUpdates # CloudFormation Resource Type that creates State Manager Associations Type: AWS::SSM::Association Properties: AssociationName: InstallWindowsUpdatesAssociation # Command Document that this Association will run Name: AWS-InstallWindowsUpdates WaitForSuccessTimeoutSeconds: 300 # Targeting Instance by InstanceId passed from the Logical ID of Instance being created # in CloudFormation Targets: - Key: InstanceIds Values: [ !Ref EC2 ] # The passing in the S3 Bucket that is created in the template that logs will be sent to OutputLocation: S3Location: OutputS3BucketName: !Ref SSMAssocLogs OutputS3KeyPrefix: 'logs/'
AWS-InstallWindowsUpdates を使用しています。 今回とくにパラメータは指定していないですが、詳細なパラメータが必要な場合は 該当するドキュメントのコンテンツなどを参考にして設定します
他の定義済みのドキュメントを使用する場合は
Name: AWS-InstallWindowsUpdates
の部分を定義済みのドキュメントに変更し、必要に応じてパラメータを追記します
実行結果
無事にCloudFormationテンプレートが実行されますとWindwosインスタンスが起動し、Systems Manger のステートマネージャーとの紐づけが作成されます。
AWS コンソール(Systems Manger のステートマネージャー) https://ap-northeast-1.console.aws.amazon.com/systems-manager/state-manager?region=ap-northeast-1
関連付けが成功し、実行がうまくいくとSucccess:1 となります。 複数インスタンスを起動して、参照付けをした場合は、Succcessの後の数値が増えていきます。
まとめ
従来の方法(cfn-init)でなく、ステートマネージャーでの設定を実施してみました。 メリットとしては、ログがS3にしっかり残ること、CloudFormationの作成と、設定の実行処理タイミングを分離できるので、時間がかかる処理などでタイムアウトを避けることもできるかとおもいます。また失敗していてもコマンドが冪等性をもった組み方をしていれば、再実行なども手軽にできます。
ステートマネージャーの組み方によっては、順序だてて実施したり、定期的にUpdateを実行したりする設定をいれたりできるので、一度きりの処理よりも設定の柔軟性が高いと感じました。
ステートマネージャー本来の能力は、1つのインスタンスの初期化というよりも、複数のインスタンス、オートスケールなどと組み合わせてやると便利さが実感できそうです。 実際、今回のようにCloudFormationで一撃可する使い方よりも、ステートマネージャーの処理を別にした方が有用な気がしますので、いい感じに利用してください
参考ページ
CloudFormation で cfn-init に代えて State Manager を利用する方法とその利点
Run Command を使用した Windows の更新プログラムの管理
テンプレート全文
AWSTemplateFormatVersion: "2010-09-09" Description: "" Parameters: WindowsLatestAmi: Type : AWS::SSM::Parameter::Value<String> Default: /aws/service/ami-windows-latest/Windows_Server-2019-Japanese-Full-Base InstanceType: Type: String Default: t3.small KeyName: Type: String InstallWindowsUpdates: Type: String Default: no AllowedValues: [yes,no] Conditions: InstallWindowsUpdates: !Equals [ !Ref InstallWindowsUpdates, yes ] Resources: SSMAssocLogs: Type: AWS::S3::Bucket SSMInstanceRole: Type : AWS::IAM::Role Properties: Policies: - PolicyDocument: Version: '2012-10-17' Statement: - Action: - s3:GetObject Resource: - !Sub 'arn:aws:s3:::aws-ssm-${AWS::Region}/*' - !Sub 'arn:aws:s3:::aws-windows-downloads-${AWS::Region}/*' - !Sub 'arn:aws:s3:::amazon-ssm-${AWS::Region}/*' - !Sub 'arn:aws:s3:::amazon-ssm-packages-${AWS::Region}/*' - !Sub 'arn:aws:s3:::${AWS::Region}-birdwatcher-prod/*' - !Sub 'arn:aws:s3:::patch-baseline-snapshot-${AWS::Region}/*' Effect: Allow PolicyName: ssm-custom-s3-policy - PolicyDocument: Version: '2012-10-17' Statement: - Action: - s3:GetObject - s3:PutObject - s3:PutObjectAcl - s3:ListBucket Resource: - !Sub 'arn:${AWS::Partition}:s3:::${SSMAssocLogs}/*' - !Sub 'arn:${AWS::Partition}:s3:::${SSMAssocLogs}' Effect: Allow PolicyName: s3-instance-bucket-policy Path: / ManagedPolicyArns: - !Sub 'arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore' AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "ec2.amazonaws.com" - "ssm.amazonaws.com" Action: "sts:AssumeRole" SSMInstanceProfile: Type: "AWS::IAM::InstanceProfile" Properties: Roles: - !Ref SSMInstanceRole EC2: Type: "AWS::EC2::Instance" Properties: ImageId: !Ref WindowsLatestAmi InstanceType: !Ref InstanceType KeyName: !Ref KeyName Tenancy: "default" EbsOptimized: true SourceDestCheck: true BlockDeviceMappings: - DeviceName: !Sub "/dev/sda1" Ebs: Encrypted: true VolumeSize: 100 VolumeType: "gp3" DeleteOnTermination: true IamInstanceProfile: !Ref SSMInstanceProfile HibernationOptions: Configured: false EC2EIP: Type: "AWS::EC2::EIP" Properties: Domain: "vpc" InstanceId: !Ref EC2 EC2EIPAssociation: Type: "AWS::EC2::EIPAssociation" Properties: AllocationId: !GetAtt EC2EIP.AllocationId InstanceId: !Ref EC2 TimeZoneAssociation: # CloudFormation Resource Type that creates State Manager Associations Type: AWS::SSM::Association Properties: AssociationName: TimeZoneAssociation # Command Document that this Association will run Name: AWS-RunPowerShellScript WaitForSuccessTimeoutSeconds: 300 # Targeting Instance by InstanceId passed from the Logical ID of Instance being created # in CloudFormation Targets: - Key: InstanceIds Values: [ !Ref EC2 ] # The passing in the S3 Bucket that is created in the template that logs will be sent to OutputLocation: S3Location: OutputS3BucketName: !Ref SSMAssocLogs OutputS3KeyPrefix: 'logs/' # Parameters for the AWS-RunShellScript Parameters: commands: - | # 現在のタイムゾーンを取得 Write-Host 'Get current timezone...' Get-TimeZone | Select-Object Id, BaseUtcOffset | Out-String # タイムゾーンの変更 $timeZoneId = 'Tokyo Standard Time' # JSTに設定する場合(必要に応じて変更する) Write-Host 'Configure timezone...' Set-TimeZone -Id $timeZoneId | Out-String # 変更した結果を確認 Write-Host 'Get new timezone...' Get-TimeZone | Select-Object Id, BaseUtcOffset | Out-String InstallWindowsUpdatesAssociation: Condition: InstallWindowsUpdates # CloudFormation Resource Type that creates State Manager Associations Type: AWS::SSM::Association Properties: AssociationName: InstallWindowsUpdatesAssociation # Command Document that this Association will run Name: AWS-InstallWindowsUpdates WaitForSuccessTimeoutSeconds: 300 # Targeting Instance by InstanceId passed from the Logical ID of Instance being created # in CloudFormation Targets: - Key: InstanceIds Values: [ !Ref EC2 ] # The passing in the S3 Bucket that is created in the template that logs will be sent to OutputLocation: S3Location: OutputS3BucketName: !Ref SSMAssocLogs OutputS3KeyPrefix: 'logs/' NetFirewallAssociation: # CloudFormation Resource Type that creates State Manager Associations Type: AWS::SSM::Association Properties: AssociationName: NetFirewallAssociation # Command Document that this Association will run Name: AWS-RunPowerShellScript WaitForSuccessTimeoutSeconds: 300 # Targeting Instance by InstanceId passed from the Logical ID of Instance being created # in CloudFormation Targets: - Key: InstanceIds Values: [ !Ref EC2 ] # The passing in the S3 Bucket that is created in the template that logs will be sent to OutputLocation: S3Location: OutputS3BucketName: !Ref SSMAssocLogs OutputS3KeyPrefix: 'logs/' # Parameters for the AWS-RunShellScript Parameters: commands: - | # 変更前設定を取得 Write-Host 'Get current Firewall configuration...' Get-NetFirewallProfile | Select-Object Name, Enabled | Out-String # Firewallを無効化 Write-Host 'Update Firewall configuration...' Get-NetFirewallProfile | Set-NetFirewallProfile -Enabled False | Out-String # 変更後設定を取得 Write-Host 'Get changed Firewall configuration...' Get-NetFirewallProfile | Select-Object Name, Enabled | Out-String NtpServerAssociation: # CloudFormation Resource Type that creates State Manager Associations Type: AWS::SSM::Association Properties: AssociationName: NtpServerAssociation # Command Document that this Association will run Name: AWS-RunPowerShellScript WaitForSuccessTimeoutSeconds: 300 # Targeting Instance by InstanceId passed from the Logical ID of Instance being created # in CloudFormation Targets: - Key: InstanceIds Values: [ !Ref EC2 ] # The passing in the S3 Bucket that is created in the template that logs will be sent to OutputLocation: S3Location: OutputS3BucketName: !Ref SSMAssocLogs OutputS3KeyPrefix: 'logs/' # Parameters for the AWS-RunShellScript Parameters: commands: - | # Windows Timeサービスが起動していない場合は起動しておく必要がある if ((Get-Service -Name W32Time).Status -eq 'Stopped') { Start-Service -Name W32Time } # 現在のNTPサーバーを取得 Write-Host 'Get current NTP Server...' Get-ItemProperty -Path 'HKLM:\System\CurrentControlSet\services\w32time\Parameters' -Name 'NtpServer' | Select-Object NtpServer | Out-String # NTPサーバーを更新 Write-Host 'Update NTP Server...' w32tm /config /manualpeerlist:169.254.169.123 /syncfromflags:manual /update | Out-String if (-not $?) { exit $LASTEXITCODE } # 変更後のNTPサーバーを取得 Write-Host 'Get changed NTP Server...' Get-ItemProperty -Path 'HKLM:\System\CurrentControlSet\services\w32time\Parameters' -Name 'NtpServer' | Select-Object NtpServer | Out-String # 時刻同期 w32tm /resync # 結果の確認 w32tm /query /status EC2LaunchUpdateAssociation: # CloudFormation Resource Type that creates State Manager Associations Type: AWS::SSM::Association Properties: AssociationName: EC2LaunchUpdateAssociation # Command Document that this Association will run Name: AWS-RunPowerShellScript WaitForSuccessTimeoutSeconds: 300 # Targeting Instance by InstanceId passed from the Logical ID of Instance being created # in CloudFormation Targets: - Key: InstanceIds Values: [ !Ref EC2 ] # The passing in the S3 Bucket that is created in the template that logs will be sent to OutputLocation: S3Location: OutputS3BucketName: !Ref SSMAssocLogs OutputS3KeyPrefix: 'logs/' # Parameters for the AWS-RunShellScript Parameters: commands: - | # # 最新バージョンの EC2Launch をインストールするスクリプト # Ref : https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/ec2launch-download.html # # ※ EC2Launch 設定ファイルのバックアップには対応していません。 # <# .SYNOPSIS EC2Launchの最新バージョンを取得します #> function Get-LatestEC2LaunchVersion() { # https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/ec2launch-version-details.html # より最新バージョンを取得 # # ※いつHTMLの構造が変わるかわからないので最悪 # return [Version]"1.3.2003150" # の様に固定値を設定しても良いと思われる try { $res = Invoke-WebRequest -Uri 'https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/ec2launch-version-details.html' -UseBasicParsing $versions = ([Xml]$res.Content).html.body.GetElementsByTagName("table") | Where-Object { $_.id -like 'w???????????????????' } | ForEach-Object { $_.tr } | Select-Object -Skip 1 | ForEach-Object { if ($_.td[0].p) { [Version]($_.td[0].p) } else { [Version]($_.td[0]) } } $latestVersion = $versions | Sort-Object -Descending | Select-Object -First 1 return $latestVersion } catch { Write-Host $_ return $null } } <# .SYNOPSIS 現在インストールされているEC2Launchのバージョンを取得します。 EC2Launchがインストールされていない場合は $null を返します。 #> function Get-EC2LaunchVersion() { $psdPath = 'C:\ProgramData\Amazon\EC2-Windows\Launch\Module\Ec2Launch.psd1' if (-not (Test-Path -Path $psdPath -PathType Leaf)) { Write-Warning "Ec2Launch isn't installed." return $null } return [Version](Import-PowerShellDataFile $psdPath).ModuleVersion } <# .SYNOPSIS EC2Launchの最新パッケージをダウンロードします。 #> function Save-LatestEC2LaunchPackage([string]$SavePath = $env:TEMP) { $packages = @( @{FileName = 'EC2-Windows-Launch.zip'; Uri = 'https://s3.amazonaws.com/ec2-downloads-windows/EC2Launch/latest/EC2-Windows-Launch.zip' }, @{FileName = 'install.ps1'; Uri = 'https://s3.amazonaws.com/ec2-downloads-windows/EC2Launch/latest/install.ps1' } ) foreach ($package in $packages) { Write-Host "Download $($package.FileName) ..." $outFile = Join-Path $SavePath ($package.FileName) Invoke-WebRequest -Uri $package.Uri -OutFile $outFile } } <# .SYNOPSIS ダウンロードしたEC2Launchの最新パッケージを削除します。 #> function Clear-LatestEC2LaunchPackage([string]$InstallerPath = $env:TEMP) { $items = @( (Join-Path $InstallerPath 'EC2-Windows-Launch.zip'), (Join-Path $InstallerPath 'install.ps1') ) foreach ($item in $items) { if (Test-Path -LiteralPath $item) { Write-Host "Remove $item ..." Remove-Item -LiteralPath $item -Force } } } <# .SYNOPSIS EC2Launchのインストーラーを実行します。 #> function Invoke-EC2LaunchInstaller([string]$InstallerPath = $env:TEMP) { $installer = Join-Path $InstallerPath 'install.ps1' try { Push-Location $InstallerPath Write-Host "Execute $installer ..." Invoke-Expression $installer return 0 } finally { Pop-Location } } <# .SYNOPSIS EC2LaunchがサポートされているOS(2016 Server以降)か判定します。 #> function Test-SupportedVersion() { $ProductType_WorkStation = 1 try { $versionInfo = Get-CimInstance -ClassName Win32_OperatingSystem -Property Version, ProductType, Caption Write-Host "OS : $($versionInfo.Caption)" if (([Version]$versionInfo.Version).Major -lt 10) { return $false } if ($versionInfo.ProductType -eq $ProductType_WorkStation ) { return $false } return $true } catch { return $false } } <# .SYNOPSIS Main関数です。 #> function Main() { if (-not (Test-SupportedVersion)) { Write-Host "This is unsupported OS." return -1 } # Check EC2Launch version $latestVersion = Get-LatestEC2LaunchVersion if ($null -eq $latestVersion) { Write-Host 'Failed to get the latest version.' return -1 } Write-Host "Latest EC2Launch version is $latestVersion ..." $currentVersion = Get-EC2LaunchVersion Write-Host "Current EC2Launch version is $currentVersion ..." if ($currentVersion -ge $latestVersion) { Write-Host "EC2Launch is the latest version." return 0 } # Update EC2Launch try { Save-LatestEC2LaunchPackage Invoke-EC2LaunchInstaller } finally { Clear-LatestEC2LaunchPackage } $currentVersion = Get-EC2LaunchVersion Write-Host "Current EC2Launch version is $currentVersion ." } exit Main UpdateSSMAgentAssociation: # CloudFormation Resource Type that creates State Manager Associations Type: AWS::SSM::Association Properties: AssociationName: UpdateSSMAgentAssociation # Command Document that this Association will run Name: AWS-UpdateSSMAgent WaitForSuccessTimeoutSeconds: 300 # Targeting Instance by InstanceId passed from the Logical ID of Instance being created # in CloudFormation Targets: - Key: InstanceIds Values: [ !Ref EC2 ] # The passing in the S3 Bucket that is created in the template that logs will be sent to OutputLocation: S3Location: OutputS3BucketName: !Ref SSMAssocLogs OutputS3KeyPrefix: 'logs/'