みなさん、こんにちは。ソリューションアーキテクトの渡邉(@gentaw0)です。今回は、AWS DevOps
Blogから、AWS CloudFormationを自分の環境に迅速にデプロイするためにテンプレートを最適する方法について解説した記事をご紹介します。原文は、ソリューションアーキテクトのJulien LepineによるOptimize AWS CloudFormation Templatesです。
______________________________________________________________________________________
お客様からときどき大きなAWS CloudFormationテンプレートを最適化してスタックを数分でデプロイできるようにする方法があるかと聞かれることがあります。リソースがプロビジョンされる前に別のリソースのアベイラビリティに依存しているためスタックの作成に時間がかかる場合があります。以下のような例があります:
- フロントエンドWebサーバーがアプリケーションサーバに依存している
- サービスがほかのリモートサービスが利用可能になるのを待機している
この記事では、リソースがほかのリソースに対して依存関係があるときにスタックの作成を高速化する方法について解説します。
注: Windows PowerShellでWindowsインスタンスを起動する方法を説明しますが、同じ考え方がシェルスクリプトでLinuxインスタンスを起動するときにも適用できます。
CloudFormationがどのようにスタックを作成するか
CloudFormationが2台のインスタンスをプロビジョニングするとき、ランダムにプロビジョンされます。テンプレートであるリソースをほかよりも前に定義してもCloudFormationはそのリソースをはじめにプロビジョンすることを保証しません。インスタンスをプロビジョンする順番をCloudFormationに明示的に指定する必要があります。
どうやってやるかをデモンストレーションするために、以下のようなCloudFormationテンプレートからはじめます:
{
"AWSTemplateFormatVersion" : "2010-09-09",
"Description": "This is a demonstration AWS CloudFormation template containing two instances",
"Parameters": {
"ImageId" : {
"Description": "Identifier of the base Amazon Machine Image (AMI) for the instances in this sample (please use Microsoft Windows Server 2012 R2 Base)",
"Type" : "AWS::EC2::Image::Id"
},
"InstanceType" : {
"Description": "EC2 instance type to use for the instances in this sample",
"Type" : "String"
},
},
"Resources" : {
"Instance1": {
"Type": "AWS::EC2::Instance",
"Properties": {
"ImageId": { "Ref" : "ImageId" },
"InstanceType": { "Ref": "InstanceType" },
}
},
"Instance2": {
"Type": "AWS::EC2::Instance",
"Properties": {
"ImageId": { "Ref" : "ImageId" },
"InstanceType": { "Ref": "InstanceType" },
}
}
}
}
CloudFormationは以下のシーケンスでスタックを作成します:
これだと高速ですが、Instance2がInstance1に依存関係があると、Instance1がはじめにプロビジョンされることを保証するため通常のプロビジョニングシーケンスをハードコードするかスクリプトする必要があります。
依存関係の指定
CloudFormationがほかのリソースがプロビジョンされるまであるリソースをプロビジョンするのを待つ必要がある場合、DependsOn 属性を使用します。
"Instance2": {
"DependsOn": ["Instance1"]
"Type": "AWS::EC2::Instance",
"Properties": {
"ImageId": { "Ref" : "ImageId" },
"InstanceType": { "Ref": "InstanceType" }
}
}
{ "Ref": "MyResource" } または { "Fn::GetAtt" : [ "MyResource" , "MyAttribute" ] } ファンクションを使用して属性間の関連を取り入れることもできます。これらのファンクションのうちひとつを使用すると、CloudFormationはリソースにDependsOn属性を追加したのと同じように動作します。以下の例では、Instance1のIDがInstance2のタグに使用されています。
"Instance2": {
"Type": "AWS::EC2::Instance",
"Properties": {
"ImageId": { "Ref" : "ImageId" },
"InstanceType": { "Ref": "InstanceType" },
"Tags": [ { "Key" : "Dependency", "Value" : { "Ref": "Instance1" } } ]
}
}
依存関係を指定する手法のどちらでも結果として同じシーケンスとなります:
これで、CloudFormationはInstance2をプロビジョニングする前にInstance1のプロビジョニングをまちます。しかしInstance1でホストされるサービスは保証されないため、テンプレートのなかで対処していきます。
CloudFormationによってインスタンスがクイックにプロビジョンされることに注意してください。実際に、Amazon Elastic Compute Cloud (EC2) APIのRunInstancesをコールするのにかかる時間だけですみます。しかしインスタンスをプロビジョンするよりもインスタンスが完全に起動するのに時間がかかります。
インスタンス上の構成を待機するためにCreation Policyを使用する
インスタンスをただしい順序でプロビジョンするだけでなく、Instance1の内部で特定のセットアップマイルストーンが達成されることを接続の前に確認します。そのためには、CreationPolicy 属性を使用します。CreationPolicyは完全に初期化されるまでmarkedCREATE_COMPLETEになるのをふせぐためにインスタンスに追加することができる属性です。
CreationPolocy属性を追加するほかに、Instance1が初期化が完了したらCloudFormationに通知するようにしたいと思います。これはインスタンスのUserDataセクションで可能です。Windowsインスタンスでは、ブートストラッピングと呼ばれるプロセスのなかでこのセクションを使用してバッチファイルまたはWindows Powershellのコードを実行することができます。
バッチスクリプトを実行して、Instance1が準備できたことを通知するシグナルを送信することで作成プロセスが完了したことをCloudFormationに知らせるようにします。こちらがCreationPolicfy属性とcfn-signal.exeを呼び出すスクリプトをふくむUserDatasectionです:
"Instance1": {
"Type": "AWS::EC2::Instance",
"CreationPolicy" : {
"ResourceSignal" : {
"Timeout": "PT15M",
"Count" : "1"
}
},
"Properties": {
"ImageId": { "Ref" : "ImageId" },
"InstanceType": { "Ref": "InstanceType" },
"UserData": {
"Fn::Base64": {
"Fn::Join": [ "\n", [
"<script>",
"REM ...Do any instance configuration steps deemed necessary...",
{ "Fn::Join": ["", [ "cfn-signal.exe -e 0 --stack \"", { "Ref": "AWS::StackName" }, "\" --resource \"Instance1\" --region \"", { "Ref" : "AWS::Region" }, "\"" ] ] },
"</script>"
] ]
}
}
}
}
すでにInstance1を待機するようにコードされているため、Instance2の定義を変更する必要はありません。これでInstance2がプロビジョンされる前にInstance1が完全にセットアップされることがわかります。このシーケンスは以下のようになります:
パラレルプロビジョニングによるプロセスの最適化
CloudFormationでインスタンスをプロビジョンするには数秒だけしかかかりませんが、完全なOSのブートシーケンス、アクティベーションとUserDataスクリプトの実行を待機する必要があるためインスタンスがブートして準備できるまでには数分ほどかかります。図からわかるように、完全なCloudFormationスタックの作成にはリソースのブートと初期化にかかる時間のおよそ2倍かかります。プロセスの複雑さによって、ブートには10分以上かかることがあります。
インスタンスの作成をパラレルに実行して必要なときのみ、すなわちアプリケーションが構成される前に待機するようにすることで待ち時間を削減することができます。これはインスタンスの準備を2つのステップに分割することで可能になります: ブートは両方のインスタンスで並行に行われますが、Instance2の初期化はInstance1が完全に準備完了したときにのみ開始されます。
こちらがあたらしいシーケンスです:
いくつかのタスクを並行で実行することにより、Instance2が利用可能になるまでの時間が短縮されます。
唯一の問題はCloudFormationには別のリソースのブートプロセスの途中に依存関係を入力するために組み込まれた要素がないことです。この解決策について考えてみましょう。
Wait Conditionsの使用
作成ポリシーは通知のメカニズムをあわせて提供します。wait conditionを使用することでインスタンスの作成についての通知をインスタンスが完全に準備完了したときの通知と分離することができます。
"Instance1WaitCondition" : {
"Type" : "AWS::CloudFormation::WaitCondition",
"DependsOn" : ["Instance1"],
"CreationPolicy" : {
"ResourceSignal" : {
"Timeout": "PT15M",
"Count" : "1"
}
}
}
それからInstance1にプロセスが完了した後、自分自身ではなくwait conditionに通知するようにする必要があります。そのためにはインスタンスのUserDataセクションを使用します。
"Instance1": {
"Type": "AWS::EC2::Instance",
"Properties": {
"ImageId": { "Ref" : "ImageId" },
"InstanceType": { "Ref": "InstanceType" },
"UserData": {
"Fn::Base64": {
"Fn::Join": [ "\n", [
"<script>",
"REM ...Do any instance configuration steps deemed necessary...",
{ "Fn::Join": ["", [ "cfn-signal.exe -e 0 --stack \"", { "Ref": "AWS::StackName" }, "\" --resource \"Instance1WaitCondition\" --region \"", { "Ref" : "AWS::Region" }, "\"" ] ] },
"</script>"
] ]
}
}
}
}
CreatePolicyがInstance1WaitConditionの内部で定義されるようになっていること、そしてcfn-signal.exeがInstance1ではなくInstance1WaitConditionに通知するようにしていることに注意してください。
これでInstance1の異なった2つの状態を知らせる2つのリソースがあります:
- Instance1はプロビジョンされてすぐに作成済みとしてマークされます。
- Instance1WaitConditionはInstance1が完全に初期化されたときだけ作成済みとしてマークされます。
このテクニックを使用してどのようにブートプロセスを最適化できるかをみてみましょう。
PowerShellによるレスキュー
DependsOn属性はリソースのトップレベルでのみ有効ですが、Instance2のブートまでInstance1を待機したいとします。それを実現するにはInstance1WaitConditionのリソース作成が完了したときにそれがわかるようにインスタンスの初期化スクリプトのなかでリソースのステータスをチェックする必要があります。Windows PowerShellをつかって自動化を実現してみましょう。
インスタンスの初期化スクリプトのなかでリソースのステータスをチェックするために、Amazon Web ServicesによってすべてのMicrosoft Windows Serverイメージにデフォルトでインストールされているパッケージである、 AWS Tools for Windows PowerShellをつかいます。このパッケージには1,100以上のコマンドレットがふくまれており、AWSクラウドで利用可能なすべてのAPIへのアクセスを可能にします。
Get-CFNStackResourcesコマンドレットによってInstance1WantConditoinのリソース作成が完了したことを確認することができます。このPowerShellスクリプトはリソースが作成されるまでループします:
$region = ""
$stack = ""
$resource = "Instance1WaitCondition"
$output = (Get-CFNStackResources -StackName $stack -LogicalResourceId $resource -Region $region)
while (($output -eq $null) -or ($output.ResourceStatus -ne "CREATE_COMPLETE") -and ($output.ResourceStatus -ne "UPDATE_COMPLETE"))
{
Start-Sleep 10
$output = (Get-CFNStackResource -StackName $stack -LogicalResourceId $resource -Region $region)
}
リソースへのセキュアなアクセス
AWS APIをコールするときには、認証と認可の必要があります。APIコールごとにアクセスキーとシークレットキーを提供することでこれを実現していますが、さらによい方法があります。インスタンスにAWS Identity and Access Management (IAM) ロールを作成することができます。インスタンスにIAMロールがあれば、インスタンス上で動作するコード(UserDataにあるPowerShellコードなど)はロールに許可されたAWS APIへのコールを実行するために認証されます。
IAMでロールを作成するときに、必要なアクションだけを指定して、これらのアクションを現在のCloudFormationスタックだけに制限します。
"DescribeRole": {
"Type" : "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version" : "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": [ "ec2.amazonaws.com" ] },
"Action": [ "sts:AssumeRole" ]
}
]
},
"Path": "/",
"Policies": [
{
"PolicyName" : "DescribeStack",
"PolicyDocument": {
"Version" : "2012-10-17",
"Statement": [
{
"Effect" : "Allow",
"Action" : ["cloudformation:DescribeStackResource", "cloudformation:DescribeStackResources"],
"Resource" : [ { "Ref" : "AWS::StackId" } ]
}
]
}
}
]
}
},
"DescribeInstanceProfile": {
"Type" : "AWS::IAM::InstanceProfile",
"Properties": {
"Path" : "/",
"Roles": [ { "Ref": "DescribeRole" } ]
}
}
リソースの作成
Instance1WaitConditionとInstance1の記述は正常ですが、Instance2をアップデートしてIAMロールを追加しPowerShell waitスクリプトをふくめる必要があります。UserDataセクションで、Instance1WaitConditionにscriptedreferenceを追加します。この"ソフト"リファレンスには単なる文字列なのでCloudFormationに対する依存関係はありません。さらにUserDataセクションに、Instance1へのGetAttリファレンスを追加して、完全にインスタンスがブートするのを待つ必要なく順次プロビジョンがクイックにおこなわれるようにします。さらに作成したIAMロールをIamInstanceProfileとして指定することでAPIコールをセキュアにする必要があります。
"Instance2": {
"Type": "AWS::EC2::Instance",
"Properties": {
"ImageId": { "Ref" : "ImageId" },
"InstanceType": { "Ref": "InstanceType" },
"IamInstanceProfile": { "Ref": "DescribeInstanceProfile" },
"UserData": {
"Fn::Base64": {
"Fn::Join": [ "\n", [
"",
"$resource = \"Instance1WaitCondition\"",
{ "Fn::Join": ["", [ "$region = '", { "Ref" : "AWS::Region" }, "'" ] ] },
{ "Fn::Join": ["", [ "$stack = '", { "Ref" : "AWS::StackId" }, "'" ] ] },
"#...Wait for instance 1 to be fully available...",
"$output = (Get-CFNStackResources -StackName $stack -LogicalResourceId $resource -Region $region)",
"while (($output -eq $null) -or ($output.ResourceStatus -ne \"CREATE_COMPLETE\") -and ($output.ResourceStatus -ne \"UPDATE_COMPLETE\")) {",
" Start-Sleep 10",
" $output = (Get-CFNStackResources -StackName $stack -LogicalResourceId $resource -Region $region)",
"}",
"#...Do any instance configuration steps you deem necessary...",
{ "Fn::Join": ["", [ "$instance1Ip = '", { "Fn::GetAtt" : [ "Instance1" , "PrivateIp" ] }, "'" ] ] },
"#...You can use the private IP address from Instance1 in your configuration scripts...",
""
] ]
}
}
}
}
これで、CloudFormationがInstance1のすぐあとにInstance2をプロビジョンして、Instance1がブート中にInstance2をブートすることで多くの時間を削減できるようになりましたが、Instance2は構成を完了する前にInstance1が完全に操作できるようになるのを待機します。
新規に環境を作成する間、スタックに大量のリソースがあると、依存関係がカスケードになることがあるため、このテクニックは多くの時間を節約できます。そして環境を本当にクイックに立ち上げる必要があるとき、たとえばディザスタリカバリを実行する場合などにこれは重要です。
さらなる最適化オプション
CloudFormationにおいてインスタンス上で複数のスクリプトを実行するもっと安定した方法がほしいのであれば、AWS::CloudFormation::cfn-initをチェックすると、スタート時にインスタンスを構成する柔軟でパワフルな方法を提供しています。インスタンスのスクリプト化を自動化してシンプルにすることでインスタンスの自動ドメイン参加による恩恵を受けるには、Amazon EC2 Simple Systems Manager (SSM)を参照してください。完全なDevOps環境でWindowsインスタンスを操作するには、AWS OpsWorksの使用を検討してください。