こんにちは。katoです。
10月のAWS Release Noteを見ていたら、 「Amazon Linux AMIにSSMエージェントがプリインストール」 との記事を見かけました。
最初はスルーしていましたが、使ってみたら結構便利だったので今回ご紹介させていただきます。
概要
今回試すのはCloudFormationを使った静的WEBサイトの構築になります。 CloudFormationで環境構築、SSM Document作成を行い、stackの作成後にRun Commandを実行するだけで静的WEBサイトの環境を立ち上げることが可能です。
構築する環境は下図の様になります。
CloudFormationテンプレート
いきなりですが、CloudFormationテンプレートを載せさせていただきます。 キーペアとバケット名は変更する必要が御座います(キーペアが存在しない場合は事前に作成する必要が御座います)。 記載している内容は簡単なものなので、必要に応じてカスタマイズ可能です。
{ "AWSTemplateFormatVersion" : "2010-09-09", "Resources" : { "VPC" : { "Type" : "AWS::EC2::VPC", "Properties" : { "CidrBlock" : "10.0.0.0/16", "Tags" : [ {"Key" : "Name", "Value" : "AWS-DC" } ] } }, "DMZ1" : { "Type" : "AWS::EC2::Subnet", "Properties" : { "VpcId" : { "Ref" : "VPC" }, "CidrBlock" : "10.0.0.0/24", "AvailabilityZone" : "ap-northeast-1a", "Tags" : [ {"Key" : "Name", "Value" : "AWS-DC-public-primary" } ] } }, "DMZ2" : { "Type" : "AWS::EC2::Subnet", "Properties" : { "VpcId" : { "Ref" : "VPC" }, "CidrBlock" : "10.0.1.0/24", "AvailabilityZone" : "ap-northeast-1c", "Tags" : [ {"Key" : "Name", "Value" : "AWS-DC-public-secondary" } ] } }, "AP1" : { "Type" : "AWS::EC2::Subnet", "Properties" : { "VpcId" : { "Ref" : "VPC" }, "CidrBlock" : "10.0.2.0/24", "AvailabilityZone" : "ap-northeast-1a", "Tags" : [ {"Key" : "Name", "Value" : "AWS-DC-private-primary" } ] } }, "AP2" : { "Type" : "AWS::EC2::Subnet", "Properties" : { "VpcId" : { "Ref" : "VPC" }, "CidrBlock" : "10.0.3.0/24", "AvailabilityZone" : "ap-northeast-1c", "Tags" : [ {"Key" : "Name", "Value" : "AWS-DC-private-secondary" } ] } }, "DB1" : { "Type" : "AWS::EC2::Subnet", "Properties" : { "VpcId" : { "Ref" : "VPC" }, "CidrBlock" : "10.0.4.0/24", "AvailabilityZone" : "ap-northeast-1a", "Tags" : [ {"Key" : "Name", "Value" : "AWS-DC-db-primary" } ] } }, "DB2" : { "Type" : "AWS::EC2::Subnet", "Properties" : { "VpcId" : { "Ref" : "VPC" }, "CidrBlock" : "10.0.5.0/24", "AvailabilityZone" : "ap-northeast-1c", "Tags" : [ {"Key" : "Name", "Value" : "AWS-DC-db-secondary" } ] } }, "InternetGateway" : { "Type" : "AWS::EC2::InternetGateway", "Properties" : { "Tags" : [ {"Key" : "Name", "Value" : "AWS-DC-IGW" } ] } }, "AttachGateway" : { "Type" : "AWS::EC2::VPCGatewayAttachment", "Properties" : { "VpcId" : { "Ref" : "VPC" }, "InternetGatewayId" : { "Ref" : "InternetGateway" } } }, "PublicRouteTable" : { "Type" : "AWS::EC2::RouteTable", "Properties" : { "VpcId" : {"Ref" : "VPC"}, "Tags" : [ {"Key" : "Name", "Value" : "AWS-DC-Public-RouteTable" } ] } }, "PublicRoute" : { "Type" : "AWS::EC2::Route", "Properties" : { "RouteTableId" : { "Ref" : "PublicRouteTable" }, "DestinationCidrBlock" : "0.0.0.0/0", "GatewayId" : { "Ref" : "InternetGateway" } } }, "PublicSubnetRouteTableDMZ1" : { "Type" : "AWS::EC2::SubnetRouteTableAssociation", "Properties" : { "SubnetId" : { "Ref" : "DMZ1" }, "RouteTableId" : { "Ref" : "PublicRouteTable" } } }, "PublicSubnetRouteTableDMZ2" : { "Type" : "AWS::EC2::SubnetRouteTableAssociation", "Properties" : { "SubnetId" : { "Ref" : "DMZ2" }, "RouteTableId" : { "Ref" : "PublicRouteTable" } } }, "PrivateRouteTable" : { "Type" : "AWS::EC2::RouteTable", "Properties" : { "VpcId" : {"Ref" : "VPC"}, "Tags" : [ {"Key" : "Name", "Value" : "AWS-DC-Private-RouteTable" } ] } }, "PrivateSubnetRouteTableAP1" : { "Type" : "AWS::EC2::SubnetRouteTableAssociation", "Properties" : { "SubnetId" : { "Ref" : "AP1" }, "RouteTableId" : { "Ref" : "PrivateRouteTable" } } }, "PrivateSubnetRouteTableAP2" : { "Type" : "AWS::EC2::SubnetRouteTableAssociation", "Properties" : { "SubnetId" : { "Ref" : "AP2" }, "RouteTableId" : { "Ref" : "PrivateRouteTable" } } }, "PrivateSubnetRouteTableDB1" : { "Type" : "AWS::EC2::SubnetRouteTableAssociation", "Properties" : { "SubnetId" : { "Ref" : "DB1" }, "RouteTableId" : { "Ref" : "PrivateRouteTable" } } }, "PrivateSubnetRouteTableDB2" : { "Type" : "AWS::EC2::SubnetRouteTableAssociation", "Properties" : { "SubnetId" : { "Ref" : "DB2" }, "RouteTableId" : { "Ref" : "PrivateRouteTable" } } }, "NatGateway" : { "Type" : "AWS::EC2::NatGateway", "Properties" : { "AllocationId" : { "Fn::GetAtt" : ["EIP", "AllocationId"] }, "SubnetId" : { "Ref" : "DMZ1" } } }, "EIP" : { "Type" : "AWS::EC2::EIP", "Properties" : { "Domain" : "vpc" } }, "PrivateRoute" : { "Type" : "AWS::EC2::Route", "Properties" : { "RouteTableId" : { "Ref" : "PrivateRouteTable" }, "DestinationCidrBlock" : "0.0.0.0/0", "NatGatewayId" : { "Ref" : "NatGateway" } } }, "SecurityGroupSSH" : { "Type" : "AWS::EC2::SecurityGroup", "Properties" : { "GroupName" : "AWS-SSH-Access", "GroupDescription" : "AWS-SSH-Access", "SecurityGroupIngress" : [{ "IpProtocol" : "tcp", "FromPort" : "22", "ToPort" : "22", "CidrIp" : "10.0.0.0/16" }], "VpcId" : { "Ref" : "VPC" } } }, "SecurityGroupHTTP" : { "Type" : "AWS::EC2::SecurityGroup", "Properties" : { "GroupName" : "AWS-HTTP-Access", "GroupDescription" : "AWS-HTTP-Access", "SecurityGroupIngress" : [{ "IpProtocol" : "tcp", "FromPort" : "80", "ToPort" : "80", "CidrIp" : "10.0.0.0/16" }], "VpcId" : { "Ref" : "VPC" } } }, "SecurityGroupELB" : { "Type" : "AWS::EC2::SecurityGroup", "Properties" : { "GroupName" : "AWS-ELB-Access", "GroupDescription" : "AWS-ELB-Access", "SecurityGroupIngress" : [{ "IpProtocol" : "tcp", "FromPort" : "80", "ToPort" : "80", "CidrIp" : "0.0.0.0/0" }, { "IpProtocol" : "tcp", "FromPort" : "443", "ToPort" : "443", "CidrIp" : "0.0.0.0/0" }], "VpcId" : { "Ref" : "VPC" } } }, "SecurityGroupFromELB" : { "Type" : "AWS::EC2::SecurityGroup", "Properties" : { "GroupName" : "AWS-From-ELB-Access", "GroupDescription" : "AWS-From-ELB-Access", "SecurityGroupIngress" : [{ "IpProtocol" : "tcp", "FromPort" : "80", "ToPort" : "80", "SourceSecurityGroupId" : { "Ref" : "SecurityGroupELB" } }], "VpcId" : { "Ref" : "VPC" } } }, "SecurityGroupBastionSSH" : { "Type" : "AWS::EC2::SecurityGroup", "Properties" : { "GroupName" : "AWS-Bastion-SSH", "GroupDescription" : "AWS-Bastion-SSH", "SecurityGroupIngress" : [{ "IpProtocol" : "tcp", "FromPort" : "22", "ToPort" : "22", "CidrIp" : "10.0.0.0/16" }], "VpcId" : { "Ref" : "VPC" } } }, "SecurityGroupVpcAll" : { "Type" : "AWS::EC2::SecurityGroup", "Properties" : { "GroupName" : "AWS-VPC-ALL", "GroupDescription" : "AWS-VPC-ALL", "SecurityGroupIngress" : [{ "IpProtocol" : "-1", "FromPort" : "-1", "ToPort" : "-1", "CidrIp" : "10.0.0.0/16" }], "VpcId" : { "Ref" : "VPC" } } }, "SSMRole" : { "Type" : "AWS::IAM::Role", "Properties" : { "AssumeRolePolicyDocument": { "Version" : "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": [ "ec2.amazonaws.com" ] }, "Action": [ "sts:AssumeRole" ] } ] }, "Path" : "/", "Policies" : [{ "PolicyName": "CustomSSM", "PolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ssm:DescribeAssociation", "ssm:GetDeployablePatchSnapshotForInstance", "ssm:GetDocument", "ssm:GetManifest", "ssm:GetParameters", "ssm:ListAssociations", "ssm:ListInstanceAssociations", "ssm:PutInventory", "ssm:PutComplianceItems", "ssm:PutConfigurePackageResult", "ssm:UpdateAssociationStatus", "ssm:UpdateInstanceAssociationStatus", "ssm:UpdateInstanceInformation" ], "Resource": "*" }, { "Effect": "Allow", "Action": [ "ec2messages:AcknowledgeMessage", "ec2messages:DeleteMessage", "ec2messages:FailMessage", "ec2messages:GetEndpoint", "ec2messages:GetMessages", "ec2messages:SendReply" ], "Resource": "*" }, { "Effect": "Allow", "Action": [ "cloudwatch:PutMetricData" ], "Resource": "*" }, { "Effect": "Allow", "Action": [ "ec2:DescribeInstanceStatus" ], "Resource": "*" }, { "Effect": "Allow", "Action": [ "ds:CreateComputer", "ds:DescribeDirectories" ], "Resource": "*" }, { "Effect": "Allow", "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:DescribeLogGroups", "logs:DescribeLogStreams", "logs:PutLogEvents" ], "Resource": "*" }, { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject", "s3:AbortMultipartUpload", "s3:ListMultipartUploadParts", "s3:ListBucket", "s3:ListBucketMultipartUploads" ], "Resource": "*" } ] } }], "RoleName" : "SSM" } }, "InstanceProfile" : { "Type" : "AWS::IAM::InstanceProfile", "Properties": { "Path" : "/", "Roles" : [ { "Ref" : "SSMRole" } ], "InstanceProfileName" : "CustomProfile" } }, "Web01" : { "Type" : "AWS::EC2::Instance", "Properties" : { "AvailabilityZone" : "ap-northeast-1a", "ImageId" : "ami-2a69be4c", "InstanceType" : "t2.micro", "IamInstanceProfile": { "Ref" : "InstanceProfile" }, "KeyName" : "xxxxxxxx", "PrivateIpAddress" : "10.0.2.10", "SecurityGroupIds" : [ { "Ref" : "SecurityGroupSSH" }, { "Ref" : "SecurityGroupFromELB" } ], "SubnetId" : { "Ref" : "AP1" }, "Tags" : [{ "Key" : "Name", "Value" : "web01" }] } }, "Web02" : { "Type" : "AWS::EC2::Instance", "Properties" : { "AvailabilityZone" : "ap-northeast-1c", "ImageId" : "ami-2a69be4c", "InstanceType" : "t2.micro", "IamInstanceProfile": { "Ref" : "InstanceProfile" }, "KeyName" : "xxxxxxxx", "PrivateIpAddress" : "10.0.3.10", "SecurityGroupIds" : [ { "Ref" : "SecurityGroupSSH" }, { "Ref" : "SecurityGroupFromELB" } ], "SubnetId" : { "Ref" : "AP2" }, "Tags" : [{ "Key" : "Name", "Value" : "web02" }] } }, "NFS" : { "Type" : "AWS::EC2::Instance", "Properties" : { "AvailabilityZone" : "ap-northeast-1a", "ImageId" : "ami-2a69be4c", "InstanceType" : "t2.micro", "BlockDeviceMappings" : [ { "DeviceName" : "/dev/xvda", "Ebs" : { "VolumeSize" : "10" } }, { "DeviceName" : "/dev/xvdb", "Ebs" : { "VolumeSize" : "30" } } ], "IamInstanceProfile": { "Ref" : "InstanceProfile" }, "KeyName" : "xxxxxxxx", "PrivateIpAddress" : "10.0.4.10", "SecurityGroupIds" : [ { "Ref" : "SecurityGroupSSH" }, { "Ref" : "SecurityGroupVpcAll" } ], "SubnetId" : { "Ref" : "DB1" }, "Tags" : [{ "Key" : "Name", "Value" : "NFS" }] } }, "Bastion" : { "Type" : "AWS::EC2::Instance", "Properties" : { "AvailabilityZone" : "ap-northeast-1a", "ImageId" : "ami-2a69be4c", "InstanceType" : "t2.micro", "KeyName" : "xxxxxxxx", "PrivateIpAddress" : "10.0.0.10", "SecurityGroupIds" : [ { "Ref" : "SecurityGroupBastionSSH" } ], "SubnetId" : { "Ref" : "DMZ1" }, "Tags" : [{ "Key" : "Name", "Value" : "bastion" }] } }, "BastionEIP" : { "Type" : "AWS::EC2::EIP", "Properties" : { "InstanceId" : { "Ref" : "Bastion" } } }, "NFSDocument" : { "Type" : "AWS::SSM::Document", "Properties" : { "Content" : { "schemaVersion" : "1.2", "parameters" : { "commands" : { "type" : "String", "default": "" } }, "runtimeConfig" : { "aws:runShellScript" : { "properties" : [ { "id" : "0.aws:runShellScript", "runCommand" : [ "#!/bin/bash", "yum install -y xfsprogs expect", "echo \"#! /usr/bin/expect -f\" >> /tmp/expect", "echo \"spawn fdisk /dev/xvdb\">> /tmp/expect", "echo \"expect \\\"Command \\\" {send \\\"n\\n\\\"}\" >> /tmp/expect", "echo \"expect \\\"Select \\\" {send \\\"\\n\\\"}\" >> /tmp/expect", "echo \"expect \\\"Partition number\\\" {send \\\"1\\n\\\"}\" >> /tmp/expect", "echo \"expect \\\"First sector\\\" {send \\\"\\n\\\"}\" >> /tmp/expect", "echo \"expect \\\"Last sector\\\" {send \\\"\\n\\\"}\" >> /tmp/expect", "echo \"expect \\\"Command \\\" {send \\\"w\\n\\\"}\" >> /tmp/expect", "echo \"expect eof\" >> /tmp/expect", "expect /tmp/expect", "mkdir /share-vol", "mkfs -t xfs /dev/xvdb1", "mount -t xfs /dev/xvdb1 /share-vol", "echo \"/share-vol 10.0.2.10/255.255.255.0(sync,rw,no_root_squash)\" >> /etc/exports", "echo \"/share-vol 10.0.3.10/255.255.255.0(sync,rw,no_root_squash)\" >> /etc/exports", "/etc/rc.d/init.d/rpcbind restart", "chkconfig rpcbind on", "/etc/rc.d/init.d/nfs restart", "chkconfig nfs on" ] } ] } } } } }, "APDocument" : { "Type" : "AWS::SSM::Document", "Properties" : { "Content" : { "schemaVersion" : "1.2", "parameters" : { "commands" : { "type" : "String", "default": "" } }, "runtimeConfig" : { "aws:runShellScript" : { "properties" : [ { "id" : "0.aws:runShellScript", "runCommand" : [ "#!/bin/bash", "yum install -y nfs-utils httpd", "/etc/rc.d/init.d/rpcbind restart", "chkconfig rpcbind on", "/etc/rc.d/init.d/nfs restart", "chkconfig nfs on", "mount -t nfs 10.0.4.10:/share-vol /var/www/html", "echo \"10.0.4.10:/share-vol /var/www/html nfs defaults 0 0\" >> /etc/fstab", "service httpd restart", "chkconfig httpd on" ] } ] } } } } }, "ALB" : { "Type" : "AWS::ElasticLoadBalancingV2::LoadBalancer", "Properties" : { "Name" : "AWS-DC-ALB", "Scheme" : "internet-facing", "SecurityGroups" : [ { "Ref" : "SecurityGroupELB" } ], "Subnets" : [ { "Ref" : "DMZ1" }, { "Ref" : "DMZ2" } ], "Tags" : [ { "Key" : "Name", "Value" : "AWS-DC-ALB" } ] } }, "ALBTargetGroup" : { "Type" : "AWS::ElasticLoadBalancingV2::TargetGroup", "Properties" : { "HealthCheckPort" : "traffic-port", "HealthCheckProtocol" : "HTTP", "Name" : "AWS-DC-ALB-Targets", "Port" : "80", "Protocol" : "HTTP", "Tags" : [ { "Key" : "Name", "Value" : "AWS-DC-ALB-Targets" } ], "Targets" : [ { "Id" : { "Ref" : "Web01" }, "Port" : "80" }, { "Id" : { "Ref" : "Web02" }, "Port" : "80" } ], "VpcId" : { "Ref" : "VPC"} } }, "ALBListener" : { "Type" : "AWS::ElasticLoadBalancingV2::Listener", "Properties" : { "DefaultActions" : [{ "Type" : "forward", "TargetGroupArn" : { "Ref" : "ALBTargetGroup" } }], "LoadBalancerArn" : { "Ref" : "ALB" }, "Port" : "80", "Protocol" : "HTTP" } }, "S3Bucket": { "Type": "AWS::S3::Bucket", "Properties": { "BucketName" : "xxxxxxxx", "LifecycleConfiguration" : { "Rules" : [ { "Status" : "Enabled", "Id" : "365-delete", "ExpirationInDays" : 365 } ] } } }, "BucketPolicy" : { "Type" : "AWS::S3::BucketPolicy", "Properties" : { "Bucket" : {"Ref" : "S3Bucket"}, "PolicyDocument" : { "Version": "2012-10-17", "Statement": [ { "Sid": "AWSCloudTrailAclCheck", "Effect": "Allow", "Principal": { "Service":"cloudtrail.amazonaws.com" }, "Action": "s3:GetBucketAcl", "Resource": { "Fn::Join" : ["", ["arn:aws:s3:::", {"Ref":"S3Bucket"}]]} }, { "Sid": "AWSCloudTrailWrite", "Effect": "Allow", "Principal": { "Service":"cloudtrail.amazonaws.com" }, "Action": "s3:PutObject", "Resource": { "Fn::Join" : ["", ["arn:aws:s3:::", {"Ref":"S3Bucket"}, "/AWSLogs/", {"Ref":"AWS::AccountId"}, "/*"]]}, "Condition": { "StringEquals": { "s3:x-amz-acl": "bucket-owner-full-control" } } } ] } } }, "myTrail" : { "DependsOn" : ["BucketPolicy"], "Type" : "AWS::CloudTrail::Trail", "Properties" : { "IncludeGlobalServiceEvents" : true , "S3BucketName" : {"Ref":"S3Bucket"}, "IsMultiRegionTrail" : true , "IsLogging" : true, "TrailName" : "AWS-DC-CloudTrail" } } } }
ネットワーク回りに関しては一般的なテンプレートなので説明を省略させていただきます。
SSMRole
「AmazonEC2RoleforSSM」と同じポリシーを持ったポリシーを作成し、新規ロールにアタッチしています。 既存のポリシーを使っても良かったのですが、stack作成時にIAM関連はカスタム名つけろと警告が出ていたので新規作成としています。
InstanceProfile
インスタンスプロファイルの作成になります。 インスタンスのPropertiesでRefで呼び出して、インスタンスにSSMRoleを割当てています。
Document
run command用のdocumentを作成します。 shell scriptの形式で、「runCommand」以降にコマンドを記載していきます。 なお、ここで指定した論理ID(リソース名)がrun command実行時のdocument名に反映されます。
ALB
今回ロードバランサーはALBを利用しています。 ALBは「ElasticLoadBalancingV2」との記載になるので注意してください(ターゲット、リスナーも同様)。 基本的にはAWSのドキュメント通りです。
S3
S3に関してはCloudTrail用にバケットを用意し、ライフサイクルのルールを設定しています。
Stackの作成
上記テンプレートを適宜修正し、jsonファイルとしてアップロードします。 注意する点はstack名の指定とIAMに関しての承認のみです。
stack名はrun commandのdocument名に反映されるので、わかりやすいものを指定することをお勧めします。
IAMの承認は、一意のカスタム名がつけられていること、権限が適切に設定されていることを確認してきています。 承認のチェックボックスにチェックを入れてstackを作成してください。
stackの作成後に「CREATE_COMPLETE」と表示されればOKです。
Run Commandの実行
CloudFormationでDocumentの作成、IAMロールの割り当てが完了しているので、コマンドを実行するだけになります。
Documentは上図の様に作成されます(stack名-論理ID-ランダム文字列)。 ドキュメントを選択後、対象のインスタンスを選択し、「Run」でコマンドを実行します。
今回の場合、NFSのマウントがあるので、NFSのDocumentから必ず実行してください。
コマンドが正常に実行されれば、WEBサイトの環境構築が完了となります。 コンテンツをアップロードし、ALBにアクセスするとWEBサイトの確認ができます。
注意事項
・今回の構成では各インスタンスへのSSH接続は踏み台(Bastion)経由となります。 ・踏み台へのSSH接続用のセキュリティグループは別途開ける必要が御座います。 ・stackの削除後もresourceを残す場合は、「"DeletionPolicy" : "Retain"」を各リソースに設定する必要が御座います。 ・amazon linux以外のAMIを利用する場合は、SSMエージェントを各サーバにインストールする必要が御座います(UserDataでインストールも可能)。
まとめ
今回はWEBサイト環境を、CloudFormationとrun commandで作成するというものを試しましたが、SSMエージェントがインストールされていれば、他のインスタンスでもDocumentが利用可能となるので、ansibleのような感覚で簡単に環境構築ができるというイメージを抱きました。
Documentを事前に用意しておき、要件に応じた設定を複数サーバにまとめて設定、という感じで作業の工数を大幅に削減することも可能となります。
便利な機能なので一度試してみてはいかがでしょうか。