CloudFormation テンプレートはコンパクトに

1つのVPC内で、異なる構成のサーバー群(スタック)を複数一気に立ち上げたり、一部を残して他を一気に削除したいことがあります。クラウドデザインパターン(CDP)Stack Deployment パターン の場面で、複数のスタックを扱うイメージです。( Multi Stack Deployment パターンと呼びたい...)


そこで今回、 CloudFormation のテンプレートを3つに分けて作成してみました。

作成したテンプレート

  1. Public サブネットと Private サブネットを Multi-AZ で持つ VPC (VPC)
  2. Public サブネットに EIP を関連づけた EC2 インスタンスを Multi-AZ で配置、 Private サブネットに RDS を Multi-AZ で配置 (Stack1)
  3. Public サブネットに AutoScaling グループを持つ ELB を作成、 Private サブネットに RDS を Multi-AZ で配置 (Stack2)

テンプレートの作り方

AWS CloudFormation Templates に、CloudFormation のサンプルテンプレートが公開されているので、それを参考にします。


では、作成したテンプレートを実行してみます。

テンプレートからスタックを作成

1つ目のテンプレートで VPC を作成します。 作成が終わったら Outputs ビューから VPCID と SubnetId を取得します。




2つ目、3つ目のテンプレートでは、パラメータとして作成済みの VPC の情報、作成したいDBの設定を入力できるようにしました。これで同じ VPC 上で構成や設定の異なるサーバー群を構築できます。



3つ目まで完了すると、スタックが3つあることを確認できます。




Stack2 を削除してみました。この方法で管理すると、不要になったスタックだけを削除することで VPC と他の構成だけを簡単に残すことができます。




VPC からインスタンスまでまとめて起動するテンプレートが適したケースもあるかと思いますが、少しでもコンパクトに分けておくと便利なのでおすすめです。


以下、作成したテンプレートになります。(ちょっと長いですが...)

作成したテンプレート

Public サブネットと Private サブネットを Multi-AZ で持つ VPC (VPC)

{
    "AWSTemplateFormatVersion" : "2010-09-09",
    "Description" : "VPC with multiple subnets",

    "Resources" : {
    "CfnUser" : {
      "Type" : "AWS::IAM::User",
      "Properties" : {
        "Path": "/",
        "Policies": [{
          "PolicyName": "root",
          "PolicyDocument": { "Statement":[{
            "Effect":"Allow",
            "Action":"cloudformation:DescribeStackResource",
            "Resource":"*"
          }]}
        }]
      }
    },

    "VPC" : {
        "Type" : "AWS::EC2::VPC",
        "Properties" : {
        "CidrBlock" : "10.1.0.0/16",
        "Tags" : [
            {"Key" : "Application", "Value" : { "Ref" : "AWS::StackName"} },
            {"Key" : "Network", "Value" : "Public" }
        ]}},

    "PublicSubnetA" : {
        "Type" : "AWS::EC2::Subnet",
        "Properties" : {
        "VpcId" : { "Ref" : "VPC" },
        "CidrBlock" : "10.1.10.0/24",
        "AvailabilityZone" : "ap-northeast-1a",
        "Tags" : [
            {"Key" : "Application", "Value" : { "Ref" : "AWS::StackName"} },
            {"Key" : "Network", "Value" : "Public" }
        ]}},
    "PublicSubnetB" : {
        "Type" : "AWS::EC2::Subnet",
        "Properties" : {
        "VpcId" : { "Ref" : "VPC" },
        "CidrBlock" : "10.1.11.0/24",
        "AvailabilityZone" : "ap-northeast-1b",
        "Tags" : [
            {"Key" : "Application", "Value" : { "Ref" : "AWS::StackName"} },
            {"Key" : "Network", "Value" : "Public" }
        ]}},

    "InternetGateway" : {
        "Type" : "AWS::EC2::InternetGateway",
        "Properties" : {
        "Tags" : [
            {"Key" : "Application", "Value" : { "Ref" : "AWS::StackName"} },
            {"Key" : "Network", "Value" : "Public" }
        ]}},

    "AttachGateway" : {
        "Type" : "AWS::EC2::VPCGatewayAttachment",
        "Properties" : {
        "VpcId" : { "Ref" : "VPC" },
        "InternetGatewayId" : { "Ref" : "InternetGateway" }
        }},

    "PublicRouteTable" : {
        "Type" : "AWS::EC2::RouteTable",
        "Properties" : {
        "VpcId" : {"Ref" : "VPC"},
        "Tags" : [
            {"Key" : "Application", "Value" : { "Ref" : "AWS::StackName"} },
            {"Key" : "Network", "Value" : "Public" }
        ]}},

    "PublicRoute" : {
        "Type" : "AWS::EC2::Route",
        "Properties" : {
        "RouteTableId" : { "Ref" : "PublicRouteTable" },
        "DestinationCidrBlock" : "0.0.0.0/0",
        "GatewayId" : { "Ref" : "InternetGateway" }
        }},

    "PublicSubnetRouteTableAssociationA" : {
        "Type" : "AWS::EC2::SubnetRouteTableAssociation",
        "Properties" : {
        "SubnetId" : { "Ref" : "PublicSubnetA" },
        "RouteTableId" : { "Ref" : "PublicRouteTable" }
        }},
    "PublicSubnetRouteTableAssociationB" : {
        "Type" : "AWS::EC2::SubnetRouteTableAssociation",
        "Properties" : {
        "SubnetId" : { "Ref" : "PublicSubnetB" },
        "RouteTableId" : { "Ref" : "PublicRouteTable" }
        }},

    "PublicNetworkAcl" : {
        "Type" : "AWS::EC2::NetworkAcl",
        "Properties" : {
        "VpcId" : {"Ref" : "VPC"},
        "Tags" : [
            {"Key" : "Application", "Value" : { "Ref" : "AWS::StackName"} },
            {"Key" : "Network", "Value" : "Public" }
        ]}},

    "InboundSSHPublicNetworkAclEntry" : {
        "Type" : "AWS::EC2::NetworkAclEntry",
        "Properties" : {
        "NetworkAclId" : {"Ref" : "PublicNetworkAcl"},
        "RuleNumber" : "10",
        "Protocol" : "6",
        "RuleAction" : "allow",
        "Egress" : "false",
        "CidrBlock" : "0.0.0.0/0",
        "PortRange" : {"From" : "22", "To" : "22"}
        }
    },

    "InboundHTTPPublicNetworkAclEntry" : {
        "Type" : "AWS::EC2::NetworkAclEntry",
        "Properties" : {
        "NetworkAclId" : {"Ref" : "PublicNetworkAcl"},
        "RuleNumber" : "20",
        "Protocol" : "6",
        "RuleAction" : "allow",
        "Egress" : "false",
        "CidrBlock" : "0.0.0.0/0",
        "PortRange" : {"From" : "80", "To" : "80"}
        }},

    "InboundDynamicPortsPublicNetworkAclEntry" : {
        "Type" : "AWS::EC2::NetworkAclEntry",
        "Properties" : {
        "NetworkAclId" : {"Ref" : "PublicNetworkAcl"},
        "RuleNumber" : "50",
        "Protocol" : "6",
        "RuleAction" : "allow",
        "Egress" : "false",
        "CidrBlock" : "0.0.0.0/0",
        "PortRange" : {"From" : "1024", "To" : "65535"}
        }
    },

    "OutboundHTTPPublicNetworkAclEntry" : {
        "Type" : "AWS::EC2::NetworkAclEntry",
        "Properties" : {
        "NetworkAclId" : {"Ref" : "PublicNetworkAcl"},
        "RuleNumber" : "20",
        "Protocol" : "6",
        "RuleAction" : "allow",
        "Egress" : "true",
        "CidrBlock" : "0.0.0.0/0",
        "PortRange" : {"From" : "80", "To" : "80"}
        }},

    "OutboundDynamicPortPublicNetworkAclEntry" : {
        "Type" : "AWS::EC2::NetworkAclEntry",
        "Properties" : {
        "NetworkAclId" : {"Ref" : "PublicNetworkAcl"},
        "RuleNumber" : "50",
        "Protocol" : "6",
        "RuleAction" : "allow",
        "Egress" : "true",
        "CidrBlock" : "0.0.0.0/0",
        "PortRange" : {"From" : "1024", "To" : "65535"}
        }},

    "PublicSubnetNetworkAclAssociationA" : {
        "Type" : "AWS::EC2::SubnetNetworkAclAssociation",
        "Properties" : {
        "SubnetId" : { "Ref" : "PublicSubnetA" },
        "NetworkAclId" : { "Ref" : "PublicNetworkAcl" }
        }},
    "PublicSubnetNetworkAclAssociationB" : {
        "Type" : "AWS::EC2::SubnetNetworkAclAssociation",
        "Properties" : {
        "SubnetId" : { "Ref" : "PublicSubnetB" },
        "NetworkAclId" : { "Ref" : "PublicNetworkAcl" }
        }},

    "PrivateSubnetA" : {
        "Type" : "AWS::EC2::Subnet",
        "Properties" : {
        "VpcId" : { "Ref" : "VPC" },
        "CidrBlock" : "10.1.20.0/24",
        "AvailabilityZone" : "ap-northeast-1a",
        "Tags" : [
            {"Key" : "Application", "Value" : { "Ref" : "AWS::StackName"} },
            {"Key" : "Network", "Value" : "Private" }
        ]}},
    "PrivateSubnetB" : {
        "Type" : "AWS::EC2::Subnet",
        "Properties" : {
        "VpcId" : { "Ref" : "VPC" },
        "CidrBlock" : "10.1.21.0/24",
        "AvailabilityZone" : "ap-northeast-1b",
        "Tags" : [
            {"Key" : "Application", "Value" : { "Ref" : "AWS::StackName"} },
            {"Key" : "Network", "Value" : "Private" }
        ]}},

    "PrivateRouteTable" : {
        "Type" : "AWS::EC2::RouteTable",
        "Properties" : {
        "VpcId" : {"Ref" : "VPC"},
        "Tags" : [
            {"Key" : "Application", "Value" : { "Ref" : "AWS::StackName"} },
            {"Key" : "Network", "Value" : "Private" }
        ]}},

    "PrivateSubnetRouteTableAssociationA" : {
        "Type" : "AWS::EC2::SubnetRouteTableAssociation",
        "Properties" : {
        "SubnetId" : { "Ref" : "PrivateSubnetA" },
        "RouteTableId" : { "Ref" : "PrivateRouteTable" }
        }},
    "PrivateSubnetRouteTableAssociationB" : {
        "Type" : "AWS::EC2::SubnetRouteTableAssociation",
        "Properties" : {
        "SubnetId" : { "Ref" : "PrivateSubnetB" },
        "RouteTableId" : { "Ref" : "PrivateRouteTable" }
        }},

    "PrivateNetworkAcl" : {
        "Type" : "AWS::EC2::NetworkAcl",
        "Properties" : {
        "VpcId" : {"Ref" : "VPC"},
        "Tags" : [
            {"Key" : "Application", "Value" : { "Ref" : "AWS::StackName"} },
            {"Key" : "Network", "Value" : "Private" }
        ]}},

    "InboundPrivateNetworkAclEntry" : {
        "Type" : "AWS::EC2::NetworkAclEntry",
        "Properties" : {
        "NetworkAclId" : {"Ref" : "PrivateNetworkAcl"},
        "RuleNumber" : "100",
        "Protocol" : "-1",
        "RuleAction" : "allow",
        "Egress" : "false",
        "CidrBlock" : "10.1.0.0/16"
        }},

    "OutboundPrivateNetworkAclEntry" : {
        "Type" : "AWS::EC2::NetworkAclEntry",
        "Properties" : {
        "NetworkAclId" : {"Ref" : "PrivateNetworkAcl"},
        "RuleNumber" : "100",
        "Protocol" : "-1",
        "RuleAction" : "allow",
        "Egress" : "true",
        "CidrBlock" : "10.1.0.0/16"
        }},

    "PrivateSubnetNetworkAclAssociationA" : {
        "Type" : "AWS::EC2::SubnetNetworkAclAssociation",
        "Properties" : {
        "SubnetId" : { "Ref" : "PrivateSubnetA" },
        "NetworkAclId" : { "Ref" : "PrivateNetworkAcl" }
        }},
    "PrivateSubnetNetworkAclAssociationB" : {
        "Type" : "AWS::EC2::SubnetNetworkAclAssociation",
        "Properties" : {
        "SubnetId" : { "Ref" : "PrivateSubnetB" },
        "NetworkAclId" : { "Ref" : "PrivateNetworkAcl" }
        }}
    },

    "Outputs":{
    "PublicSubnetAId":{
        "Value":{"Ref": "PublicSubnetA"},
        "Description":"Id of PublicSubnetA"
    },
    "PublicSubnetBId":{
        "Value":{"Ref": "PublicSubnetB"},
        "Description":"Id of PublicSubnetB"
    },
    "PrivateSubnetAId":{
        "Value":{"Ref": "PrivateSubnetA"},
        "Description":"Id of PrivateSubnetA"
    },
    "PrivateSubnetBId":{
        "Value":{"Ref": "PrivateSubnetB"},
        "Description":"Id of PrivateSubnetB"
    },
    "VPCID":{
        "Value":{ "Ref" : "VPC" },
        "Description":"Id of VPC"
    }
    }
}

Public サブネットに EIP を関連づけた EC2 インスタンスを Multi-AZ で配置、 Private サブネットに RDS を Multi-AZ で配置 (Stack1)

{
  "AWSTemplateFormatVersion" : "2010-09-09",
  "Description" : "In an existing VPC and multiple subnets. The public subnets contains an instances with an Elastic IP address, the private subnets contains a RDS Multi-AZ DBInstances.",

  "Parameters" : {
    "KeyName" : {
      "Description" : "Name of and existing EC2 KeyPair to enable SSH access to the instance",
      "Type" : "String"
    },

	"VpcId" : {
      "Type" : "String",
      "Description" : "VpcId of your existing VPC"
    },

    "PublicSubnetA" : {
      "Type" : "String",
      "Description" : "The SubnetId in the region in your VPC"
    },

    "PublicSubnetB" : {
      "Type" : "String",
      "Description" : "The SubnetId in the region in your VPC"
    },

    "PrivateSubnets" : {
      "Type" : "CommaDelimitedList",
      "Description" : "The list of SubnetIds, one in each AZ in the region in your VPC"
    },

    "InstanceType" : {
      "Description" : "WebServer EC2 instance type",
      "Type" : "String",
      "Default" : "t1.micro",
      "AllowedValues" : [ "t1.micro","m1.small","m1.medium","m1.large","m1.xlarge","m2.xlarge","m2.2xlarge","m2.4xlarge","m3.xlarge","m3.2xlarge","c1.medium","c1.xlarge","cc1.4xlarge","cc2.8xlarge","cg1.4xlarge"],
      "ConstraintDescription" : "must be a valid EC2 instance type."
    },

    "DBName": {
      "Default": "MyDatabase",
      "Description" : "MySQL database name",
      "Type": "String",
      "MinLength": "1",
      "MaxLength": "64",
      "AllowedPattern" : "[a-zA-Z][a-zA-Z0-9]*",
      "ConstraintDescription" : "must begin with a letter and contain only alphanumeric characters."
    },

    "DBUsername": {
      "NoEcho": "true",
      "Description" : "Username for MySQL database access",
      "Type": "String",
      "MinLength": "1",
      "MaxLength": "16",
      "AllowedPattern" : "[a-zA-Z][a-zA-Z0-9]*",
      "ConstraintDescription" : "must begin with a letter and contain only alphanumeric characters."
    },

    "DBPassword": {
      "NoEcho": "true",
      "Description" : "Password for MySQL database access",
      "Type": "String",
      "MinLength": "1",
      "MaxLength": "41",
      "AllowedPattern" : "[a-zA-Z0-9]*",
      "ConstraintDescription" : "must contain only alphanumeric characters."
    },

    "DBAllocatedStorage": {
      "Default": "5",
      "Description" : "The size of the database (Gb)",
      "Type": "Number",
      "MinValue": "5",
      "MaxValue": "1024",
      "ConstraintDescription" : "must be between 5 and 1024Gb."
    },

    "DBInstanceClass": {
      "Default": "db.m1.small",
      "Description" : "The database instance type",
      "Type": "String",
      "AllowedValues" : [ "db.m1.small", "db.m1.large", "db.m1.xlarge", "db.m2.xlarge", "db.m2.2xlarge", "db.m2.4xlarge" ],
      "ConstraintDescription" : "must select a valid database instance type."
    },

    "MultiAZDatabase": {
      "Default": "true",
      "Description" : "Create a multi-AZ MySQL Amazon RDS database instance",
      "Type": "String",
      "AllowedValues" : [ "true", "false" ],
      "ConstraintDescription" : "must be either true or false."
    }
  },

  "Mappings" : {
    "AWSInstanceType2Arch" : {
      "t1.micro"    : { "Arch" : "64" },
      "m1.small"    : { "Arch" : "64" },
      "m1.medium"   : { "Arch" : "64" },
      "m1.large"    : { "Arch" : "64" },
      "m1.xlarge"   : { "Arch" : "64" },
      "m2.xlarge"   : { "Arch" : "64" },
      "m2.2xlarge"  : { "Arch" : "64" },
      "m2.4xlarge"  : { "Arch" : "64" },
      "m3.xlarge"   : { "Arch" : "64" },
      "m3.2xlarge"  : { "Arch" : "64" },
      "c1.medium"   : { "Arch" : "64" },
      "c1.xlarge"   : { "Arch" : "64" }
    },

    "AWSRegionArch2AMI" : {
      "us-east-1"      : { "32" : "ami-aba768c2", "64" : "ami-81a768e8" },
      "us-west-1"      : { "32" : "ami-458fd300", "64" : "ami-b18ed2f4" },
      "us-west-2"      : { "32" : "ami-fcff72cc", "64" : "ami-feff72ce" },
      "eu-west-1"      : { "32" : "ami-018bb975", "64" : "ami-998bb9ed" },
      "sa-east-1"      : { "32" : "ami-a039e6bd", "64" : "ami-a239e6bf" },
      "ap-southeast-1" : { "32" : "ami-425a2010", "64" : "ami-5e5a200c" },
      "ap-southeast-2" : { "32" : "ami-f98512c3", "64" : "ami-43851279" },
      "ap-northeast-1" : { "32" : "ami-7871c579", "64" : "ami-7671c577" }
    },
	
    "RegionMap" : {
      "us-east-1"      : { "AMI" : "ami-7f418316" },
      "us-west-1"      : { "AMI" : "ami-951945d0" },
      "us-west-2"      : { "AMI" : "ami-16fd7026" },
      "eu-west-1"      : { "AMI" : "ami-24506250" },
      "sa-east-1"      : { "AMI" : "ami-3e3be423" },
      "ap-southeast-1" : { "AMI" : "ami-74dda626" },
      "ap-southeast-2" : { "AMI" : "ami-b3990e89" },
      "ap-northeast-1" : { "AMI" : "ami-dcfa4edd" }
    }
  },

  "Resources" : {
    "WebServerSecurityGroup" : {
      "Type" : "AWS::EC2::SecurityGroup",
      "Properties" : {
        "GroupDescription" : "Enable HTTP access on the configured port",
        "VpcId" : { "Ref" : "VpcId" },
        "SecurityGroupIngress" : [ { "IpProtocol" : "tcp", "FromPort" : "80", "ToPort" : "80", "CidrIp" : "0.0.0.0/0" } ]
      }
    },

    "IPAddress1" : {
      "Type" : "AWS::EC2::EIP",
      "Properties" : {
        "Domain" : "vpc",
        "InstanceId" : { "Ref" : "Ec2Instance1" }
      }
    },

    "IPAddress2" : {
      "Type" : "AWS::EC2::EIP",
      "Properties" : {
        "Domain" : "vpc",
        "InstanceId" : { "Ref" : "Ec2Instance2" }
      }
    },

    "Ec2Instance1" : {
      "Type" : "AWS::EC2::Instance",
      "Properties" : {
        "ImageId" : { "Fn::FindInMap" : [ "RegionMap", { "Ref" : "AWS::Region" }, "AMI" ]},
        "SecurityGroupIds" : [{ "Ref" : "WebServerSecurityGroup" }],
        "SubnetId" : { "Ref" : "PublicSubnetA" },
        "KeyName" : { "Ref" : "KeyName" }
      }
    },
	
    "Ec2Instance2" : {
      "Type" : "AWS::EC2::Instance",
      "Properties" : {
        "ImageId" : { "Fn::FindInMap" : [ "RegionMap", { "Ref" : "AWS::Region" }, "AMI" ]},
        "SecurityGroupIds" : [{ "Ref" : "WebServerSecurityGroup" }],
        "SubnetId" : { "Ref" : "PublicSubnetB" },
        "KeyName" : { "Ref" : "KeyName" }
      }
    },

    "MySQLDBSubnetGroup" : {
      "Type" : "AWS::RDS::DBSubnetGroup",
      "Properties" : {
        "DBSubnetGroupDescription" : "Subnets available for the RDS DB Instance",
        "SubnetIds" : { "Ref" : "PrivateSubnets" }
      }
    },

    "MySQLDBSecurityGroup" : {
      "Type" : "AWS::RDS::DBSecurityGroup",
      "Properties" : {
        "GroupDescription" : "Security group for RDS DB Instance",
        "EC2VpcId" : { "Ref" : "VpcId" }
      }
    },

    "MySQLDB" : {
      "Type" : "AWS::RDS::DBInstance",
      "Properties" : {
        "DBName" : { "Ref" : "DBName" },
		"MultiAZ" : { "Ref": "MultiAZDatabase" },
        "AllocatedStorage" : { "Ref" : "DBAllocatedStorage" },
        "DBInstanceClass" : { "Ref" : "DBInstanceClass" },
        "Engine" : "MySQL",
        "EngineVersion" : "5.5",
        "MasterUsername" : { "Ref" : "DBUsername" } ,
        "MasterUserPassword" : { "Ref" : "DBPassword" },
        "DBSubnetGroupName" : { "Ref" : "MySQLDBSubnetGroup" },
        "DBSecurityGroups" : [ { "Ref" : "MySQLDBSecurityGroup" } ]
      }
  	}
  },

  "Outputs" : {
    "IPAddress1" : {
      "Value" : { "Ref" : "IPAddress1" },
      "Description" : "Public IP address of instance"
    },

    "IPAddress2" : {
      "Value" : { "Ref" : "IPAddress2" },
      "Description" : "Public IP address of instance"
    },
	
    "InstanceId1" : {
      "Value" : { "Ref" : "Ec2Instance1" },
      "Description" : "Instance Id of newly created instance"
    },

    "InstanceId2" : {
      "Value" : { "Ref" : "Ec2Instance2" },
      "Description" : "Instance Id of newly created instance"
    },

    "PDOConnectionString": {
      "Description" : "PDO connection string for database",
      "Value" : { "Fn::Join": [ "", [ "'mysql:host=",
                                      { "Fn::GetAtt": [ "MySQLDB", "Endpoint.Address" ] },
	  								  ";port=",
                                      { "Fn::GetAtt": [ "MySQLDB", "Endpoint.Port" ] },
	  								  ";dbname=",
                                      { "Ref": "DBName" },
	  								  "','",
                                      { "Ref": "DBUsername" },
									  "'"
									  ]]}
    }
  }
}

Public サブネットに AutoScaling グループを持つ ELB を作成、 Private サブネットに RDS を Multi-AZ で配置 (Stack2)

{
  "AWSTemplateFormatVersion" : "2010-09-09",
  "Description" : "In an existing VPC and multiple subnets, the public subnets contains an Auto Scaling group behind a load balancer, and the private subnets contains a RDS Multi-AZ DBInstances.",

  "Parameters" : {
    "KeyName" : {
      "Description" : "Name of and existing EC2 KeyPair",
      "Type" : "String"
    },

	"VpcId" : {
      "Type" : "String",
      "Description" : "VpcId of your existing VPC"
    },

    "PublicSubnets" : {
      "Type" : "CommaDelimitedList",
      "Description" : "The list of SubnetIds in your VPC"
    },

    "AZs" : {
      "Type" : "CommaDelimitedList",
      "Description" : "The list of AZs for your VPC"
    },

    "InstanceCount" : {
      "Description" : "Number of EC2 instances to launch",
      "Type" : "Number",
      "Default" : "1"
    },

    "WebServerPort" : {
      "Description" : "TCP/IP port of the web server",
      "Type" : "String",
      "Default" : "80"
    },

	"PrivateSubnets" : {
      "Type" : "CommaDelimitedList",
      "Description" : "The list of SubnetIds, one in each AZ in the region in your VPC"
    },

    "InstanceType" : {
      "Description" : "WebServer EC2 instance type",
      "Type" : "String",
      "Default" : "t1.micro",
      "AllowedValues" : [ "t1.micro","m1.small","m1.medium","m1.large","m1.xlarge","m2.xlarge","m2.2xlarge","m2.4xlarge","m3.xlarge","m3.2xlarge","c1.medium","c1.xlarge","cc1.4xlarge","cc2.8xlarge","cg1.4xlarge"],
      "ConstraintDescription" : "must be a valid EC2 instance type."
    },

    "DBName": {
      "Default": "MyDatabase",
      "Description" : "MySQL database name",
      "Type": "String",
      "MinLength": "1",
      "MaxLength": "64",
      "AllowedPattern" : "[a-zA-Z][a-zA-Z0-9]*",
      "ConstraintDescription" : "must begin with a letter and contain only alphanumeric characters."
    },

    "DBUsername": {
      "NoEcho": "true",
      "Description" : "Username for MySQL database access",
      "Type": "String",
      "MinLength": "1",
      "MaxLength": "16",
      "AllowedPattern" : "[a-zA-Z][a-zA-Z0-9]*",
      "ConstraintDescription" : "must begin with a letter and contain only alphanumeric characters."
    },

    "DBPassword": {
      "NoEcho": "true",
      "Description" : "Password for MySQL database access",
      "Type": "String",
      "MinLength": "1",
      "MaxLength": "41",
      "AllowedPattern" : "[a-zA-Z0-9]*",
      "ConstraintDescription" : "must contain only alphanumeric characters."
    },

    "DBAllocatedStorage": {
      "Default": "5",
      "Description" : "The size of the database (Gb)",
      "Type": "Number",
      "MinValue": "5",
      "MaxValue": "1024",
      "ConstraintDescription" : "must be between 5 and 1024Gb."
    },

    "DBInstanceClass": {
      "Default": "db.m1.small",
      "Description" : "The database instance type",
      "Type": "String",
      "AllowedValues" : [ "db.m1.small", "db.m1.large", "db.m1.xlarge", "db.m2.xlarge", "db.m2.2xlarge", "db.m2.4xlarge" ],
      "ConstraintDescription" : "must select a valid database instance type."
    },

    "MultiAZDatabase": {
      "Default": "true",
      "Description" : "Create a multi-AZ MySQL Amazon RDS database instance",
      "Type": "String",
      "AllowedValues" : [ "true", "false" ],
      "ConstraintDescription" : "must be either true or false."
    }
  },

  "Mappings" : {
    "AWSInstanceType2Arch" : {
      "t1.micro"    : { "Arch" : "64" },
      "m1.small"    : { "Arch" : "64" },
      "m1.medium"   : { "Arch" : "64" },
      "m1.large"    : { "Arch" : "64" },
      "m1.xlarge"   : { "Arch" : "64" },
      "m2.xlarge"   : { "Arch" : "64" },
      "m2.2xlarge"  : { "Arch" : "64" },
      "m2.4xlarge"  : { "Arch" : "64" },
      "m3.xlarge"   : { "Arch" : "64" },
      "m3.2xlarge"  : { "Arch" : "64" },
      "c1.medium"   : { "Arch" : "64" },
      "c1.xlarge"   : { "Arch" : "64" }
    },

    "AWSRegionArch2AMI" : {
      "us-east-1"      : { "32" : "ami-aba768c2", "64" : "ami-81a768e8" },
      "us-west-1"      : { "32" : "ami-458fd300", "64" : "ami-b18ed2f4" },
      "us-west-2"      : { "32" : "ami-fcff72cc", "64" : "ami-feff72ce" },
      "eu-west-1"      : { "32" : "ami-018bb975", "64" : "ami-998bb9ed" },
      "sa-east-1"      : { "32" : "ami-a039e6bd", "64" : "ami-a239e6bf" },
      "ap-southeast-1" : { "32" : "ami-425a2010", "64" : "ami-5e5a200c" },
      "ap-southeast-2" : { "32" : "ami-f98512c3", "64" : "ami-43851279" },
      "ap-northeast-1" : { "32" : "ami-7871c579", "64" : "ami-7671c577" }
    },
	
    "RegionMap" : {
      "us-east-1"      : { "AMI" : "ami-7f418316" },
      "us-west-1"      : { "AMI" : "ami-951945d0" },
      "us-west-2"      : { "AMI" : "ami-16fd7026" },
      "eu-west-1"      : { "AMI" : "ami-24506250" },
      "sa-east-1"      : { "AMI" : "ami-3e3be423" },
      "ap-southeast-1" : { "AMI" : "ami-74dda626" },
      "ap-southeast-2" : { "AMI" : "ami-b3990e89" },
      "ap-northeast-1" : { "AMI" : "ami-dcfa4edd" }
    }
  },

  "Resources" : {
    "ElasticLoadBalancer" : {
      "Type" : "AWS::ElasticLoadBalancing::LoadBalancer",
      "Properties" : {
        "SecurityGroups" : [ { "Ref" : "LoadBalancerSecurityGroup" } ],
        "Subnets" : { "Ref" : "PublicSubnets" },
        "Listeners" : [ {
          "LoadBalancerPort" : "80",
          "InstancePort" : { "Ref" : "WebServerPort" },
          "Protocol" : "HTTP"
        } ],
        "HealthCheck" : {
          "Target" : { "Fn::Join" : [ "", ["HTTP:", { "Ref" : "WebServerPort" }, "/"]]},
          "HealthyThreshold" : "3",
          "UnhealthyThreshold" : "5",
          "Interval" : "90",
          "Timeout" : "60"
        }
      }
    },

    "LoadBalancerSecurityGroup" : {
      "Type" : "AWS::EC2::SecurityGroup",
      "Properties" : {
        "GroupDescription" : "Enable HTTP access on port 80",
        "VpcId" : { "Ref" : "VpcId" },
        "SecurityGroupIngress" : [ { "IpProtocol" : "tcp", "FromPort" : "80", "ToPort" : "80", "CidrIp" : "0.0.0.0/0" } ],
        "SecurityGroupEgress" : [ { "IpProtocol" : "tcp", "FromPort" : "80", "ToPort" : "80", "CidrIp" : "0.0.0.0/0" } ]
	  }
    },

    "WebServerGroup" : {
      "Type" : "AWS::AutoScaling::AutoScalingGroup",
      "Properties" : {
        "AvailabilityZones" : { "Ref" : "AZs" },
        "VPCZoneIdentifier" : { "Ref" : "PublicSubnets" },
        "LaunchConfigurationName" : { "Ref" : "LaunchConfig" },
        "MinSize" : "1",
        "MaxSize" : "10",
        "DesiredCapacity" : { "Ref" : "InstanceCount" },
        "LoadBalancerNames" : [ { "Ref" : "ElasticLoadBalancer" } ]
      }
    },

    "LaunchConfig" : {
      "Type" : "AWS::AutoScaling::LaunchConfiguration",
      "Properties" : {
        "ImageId" : { "Fn::FindInMap" : [ "AWSRegionArch2AMI", { "Ref" : "AWS::Region" },
                                          { "Fn::FindInMap" : [ "AWSInstanceType2Arch", { "Ref" : "InstanceType" }, 
                                          "Arch" ] } ] },
        "UserData" : { "Fn::Base64" : { "Ref" : "WebServerPort" }},
        "SecurityGroups" : [ { "Ref" : "WebServerSecurityGroup" } ],
        "InstanceType" : { "Ref" : "InstanceType" }
      }
    },

    "WebServerSecurityGroup" : {
      "Type" : "AWS::EC2::SecurityGroup",
      "Properties" : {
        "GroupDescription" : "Enable HTTP access on the configured port",
        "VpcId" : { "Ref" : "VpcId" },
        "SecurityGroupIngress" : [ { "IpProtocol" : "tcp", "FromPort" : "80", "ToPort" : "80", "SourceSecurityGroupId" : { "Ref" : "LoadBalancerSecurityGroup" } } ]
      }
    },

    "MySQLDBSubnetGroup" : {
      "Type" : "AWS::RDS::DBSubnetGroup",
      "Properties" : {
        "DBSubnetGroupDescription" : "Subnets available for the RDS DB Instance",
        "SubnetIds" : { "Ref" : "PrivateSubnets" }
      }
    },

    "MySQLDBSecurityGroup" : {
      "Type" : "AWS::RDS::DBSecurityGroup",
      "Properties" : {
        "GroupDescription" : "Security group for RDS DB Instance",
        "EC2VpcId" : { "Ref" : "VpcId" }
      }
    },

    "MySQLDB" : {
      "Type" : "AWS::RDS::DBInstance",
      "Properties" : {
        "DBName" : { "Ref" : "DBName" },
		"MultiAZ" : { "Ref": "MultiAZDatabase" },
        "AllocatedStorage" : { "Ref" : "DBAllocatedStorage" },
        "DBInstanceClass" : { "Ref" : "DBInstanceClass" },
        "Engine" : "MySQL",
        "EngineVersion" : "5.5",
        "MasterUsername" : { "Ref" : "DBUsername" } ,
        "MasterUserPassword" : { "Ref" : "DBPassword" },
        "DBSubnetGroupName" : { "Ref" : "MySQLDBSubnetGroup" },
        "DBSecurityGroups" : [ { "Ref" : "MySQLDBSecurityGroup" } ]
      }
  	}
  },

  "Outputs" : {
    "URL" : {
      "Description" : "URL of the website",
      "Value" :  { "Fn::Join" : [ "", [ "http://", { "Fn::GetAtt" : [ "ElasticLoadBalancer", "DNSName" ]}]]}
    },
	
    "PDOConnectionString": {
      "Description" : "PDO connection string for database",
      "Value" : { "Fn::Join": [ "", [ "'mysql:host=",
                                      { "Fn::GetAtt": [ "MySQLDB", "Endpoint.Address" ] },
	  								  ";port=",
                                      { "Fn::GetAtt": [ "MySQLDB", "Endpoint.Port" ] },
	  								  ";dbname=",
                                      { "Ref": "DBName" },
	  								  "','",
                                      { "Ref": "DBUsername" },
									  "'"
									  ]]}
    }
  }
}

「コンパクト」に、という割に長いですね...

シェルスクリプトで Multiple Subnets in VPC

AWS Advent Calender 2012 の15日目担当の @satotech です。


最近は、 AWS CloudFormationAmazon Virtual Private Cloud(Amazon VPC) の話題が多いですね。
CloudFormationは、ホントに便利ですよね!


今回は、シェルスクリプトVPCの構築をしてみたいと思います。
次のようなイメージのVPCを構築します。

複数のAZにそれぞれ複数のサブネットを持つVPCを作成します。

1つ目はPublicサブネットで、Elastic IP を取得、セキュリティグループを作成してEC2 インスタンスを起動します。
2つ目はPrivateサブネットで、バックエンドのEC2 インスタンスやRDS用になります。

手順

  • 10.0.0.0/16 で VPC を作成
  • Zone A に PublicサブネットA 10.0.10.0/24 を作成
  • Zone B に PublicサブネットB 10.0.11.0/24 を作成
  • インターネットゲートウェイを作成
  • Publicサブネットの Route Table を作成
  • Publicサブネットに NetworkACL(NACL) を定義
    • Inbound用にHTTPポートを許可
    • Inbound用にSSHポートを許可
    • Inbound用に1024から65535のダイナミックポートを許可
    • Outbound用にHTTPポートを許可
    • Outbound用にHTTPSポートを許可
    • Outbound用に1024から65535のダイナミックポートを許可
  • Publicサブネットに対してNACLを関連づけ
  • Zone A にPrivateサブネットA 10.0.20.0/24 を作成
  • Zone B にPrivateサブネットB 10.0.21.0/24 を作成
  • Privateサブネットに NACL を定義
    • Inbound用にVPC内との全ポートを許可
    • Outbound用にVPC内との全ポートを許可
  • Publicサブネット用にセキュリティグループを定義
    • Inbound用にHTTPポートを許可
    • Inbound用にSSHポートを許可
  • インスタンスを起動
    • タグを設定
  • EIPを取得

シェルスクリプト

main.sh
#!/bin/bash

source ~/aws/export.cfg
source ~/aws/functions.sh

REGION="ap-northeast-1"
ZONE_A="${REGION}a"
ZONE_B="${REGION}b"
OPTS="--region ${REGION} -H --show-empty-fields"
SSH_PORT="22"
GROUP_NAME="PublicSubnetGroup"
DESCRIPTION="Enable SSH access via port ${SSH_PORT}"
AMI_ID="ami-dcfa4edd"
SIZE="10"
DELETE_VOLUME="true"
KEYPAIR="vpcsample"
TYPE="t1.micro"
AKI_ID="aki-ec5df7ed"
RUN_OPTS="${AMI_ID} -b /dev/sda1=:${SIZE}:${DELETE_VOLUME} -b /dev/sdc=ephemeral0 -k ${KEYPAIR} -t ${TYPE} --instance-initiated-shutdown-behavior stop --kernel ${AKI_ID}"



VPC_ID=`create-vpc 10.0.0.0/16 "${OPTS}"`

PUBLIC_SUBNET_ID_A=`create-subnet ${VPC_ID} 10.0.10.0/24 ${ZONE_A} "${OPTS}"`
PUBLIC_SUBNET_ID_B=`create-subnet ${VPC_ID} 10.0.11.0/24 ${ZONE_B} "${OPTS}"`

IGW_ID=`create-internet-gateway ${VPC_ID} "${OPTS}"`

PUBLIC_ROUTE_TABLE_ID=`create-route-table ${VPC_ID} "${OPTS}"`
ec2-create-route ${PUBLIC_ROUTE_TABLE_ID} -r 0.0.0.0/0 -g ${IGW_ID} ${OPTS}
ec2-associate-route-table ${PUBLIC_ROUTE_TABLE_ID} -s ${PUBLIC_SUBNET_ID_A} ${OPTS}
ec2-associate-route-table ${PUBLIC_ROUTE_TABLE_ID} -s ${PUBLIC_SUBNET_ID_B} ${OPTS}

PUBLIC_ACL_ID=`create-network-acl ${VPC_ID} "${OPTS}"`
ec2-create-network-acl-entry ${PUBLIC_ACL_ID} -n 100 -P 6 -r 0.0.0.0/0 -p 80 --allow ${OPTS}
ec2-create-network-acl-entry ${PUBLIC_ACL_ID} -n 101 -P 6 -r 0.0.0.0/0 -p ${SSH_PORT} --allow ${OPTS}
ec2-create-network-acl-entry ${PUBLIC_ACL_ID} -n 102 -P 6 -r 0.0.0.0/0 -p 1024-65535 --allow ${OPTS}
ec2-create-network-acl-entry ${PUBLIC_ACL_ID} -n 100 --egress -P 6 -r 0.0.0.0/0 -p 80 --allow ${OPTS}
ec2-create-network-acl-entry ${PUBLIC_ACL_ID} -n 101 --egress -P 6 -r 0.0.0.0/0 -p 443 --allow ${OPTS}
ec2-create-network-acl-entry ${PUBLIC_ACL_ID} -n 102 --egress -P 6 -r 0.0.0.0/0 -p 1024-65535 --allow ${OPTS}

replace-network-acl-association ${PUBLIC_SUBNET_ID_A} ${PUBLIC_ACL_ID} "${OPTS}"
replace-network-acl-association ${PUBLIC_SUBNET_ID_B} ${PUBLIC_ACL_ID} "${OPTS}"

PRIVATE_SUBNET_ID_A=`create-subnet ${VPC_ID} 10.0.20.0/24 ${ZONE_A} "${OPTS}"`
PRIVATE_SUBNET_ID_B=`create-subnet ${VPC_ID} 10.0.21.0/24 ${ZONE_B} "${OPTS}"`

PRIVATE_ROUTE_TABLE_ID=`create-route-table ${VPC_ID} "${OPTS}"`
ec2-associate-route-table ${PRIVATE_ROUTE_TABLE_ID} -s ${PRIVATE_SUBNET_ID_A} ${OPTS}
ec2-associate-route-table ${PRIVATE_ROUTE_TABLE_ID} -s ${PRIVATE_SUBNET_ID_B} ${OPTS}

PRIVATE_ACL_ID=`create-network-acl ${VPC_ID} "${OPTS}"`
ec2-create-network-acl-entry ${PRIVATE_ACL_ID} -n 100 -P all -r 10.0.0.0/16 --allow ${OPTS}
ec2-create-network-acl-entry ${PRIVATE_ACL_ID} -n 100 --egress -P all -r 10.0.0.0/16 --allow ${OPTS}

replace-network-acl-association ${PRIVATE_SUBNET_ID_A} ${PRIVATE_ACL_ID} "${OPTS}"
replace-network-acl-association ${PRIVATE_SUBNET_ID_B} ${PRIVATE_ACL_ID} "${OPTS}"

PUBLIC_GROUP_ID=`create-group ${GROUP_NAME} "${DESCRIPTION}" ${VPC_ID} "${OPTS}"`
ec2-authorize ${PUBLIC_GROUP_ID} -P 6 -p 80 -s 0.0.0.0/0 ${OPTS}
ec2-authorize ${PUBLIC_GROUP_ID} -P 6 -p ${SSH_PORT} -s 0.0.0.0/0 ${OPTS}

INSTANCE_ID_A=`run-instances "${RUN_OPTS}" ${PUBLIC_GROUP_ID} ${ZONE_A} ${PUBLIC_SUBNET_ID_A} 10.0.10.5 host00 "${OPTS}"`
INSTANCE_ID_B=`run-instances "${RUN_OPTS}" ${PUBLIC_GROUP_ID} ${ZONE_B} ${PUBLIC_SUBNET_ID_B} 10.0.11.5 host01  "${OPTS}"`

ALLOCATION_ID_A=`associate-address ${INSTANCE_ID_A} "${OPTS}"`
ALLOCATION_ID_B=`associate-address ${INSTANCE_ID_B} "${OPTS}"`
functions.sh
#!/bin/bash

function create-vpc()
{
    local VPC_CIDR=$1
    local OPTS=$2
    local VPC_ID=""

    while [[ -z ${VPC_ID} ]]; do
        VPC_ID=$(ec2-create-vpc ${VPC_CIDR} ${OPTS} | grep VPC | awk '{print $2}')
    done

    while [[ -z "$(ec2-describe-vpcs ${VPC_ID} --filter \"state=available\" ${OPTS})" ]]; do
        sleep 10
    done

    echo "${VPC_ID}"
}

function create-subnet()
{
    local VPC_ID=$1
    local CIDR=$2
    local ZONE=$3
    local OPTS=$4
    local SUBNET_ID=""

    while [[ -z ${SUBNET_ID} ]]; do
        SUBNET_ID=$(ec2-create-subnet -c ${VPC_ID} -i ${CIDR} -z ${ZONE} ${OPTS} | grep SUBNET | awk '{print $2}')
    done

    while [[ -z "$(ec2-describe-subnets ${SUBNET_ID} --filter \"state=available\" ${OPTS})" ]]; do
        sleep 10
    done

    echo "${SUBNET_ID}"
}

function create-internet-gateway()
{
    local VPC_ID=$1
    local OPTS=$2
    local IGW_ID=""

    while [[ -z ${IGW_ID} ]]; do
        IGW_ID=$(ec2-create-internet-gateway ${OPTS} | grep ^INTERNETGATEWAY | awk '{print $2}')
    done

    ec2-attach-internet-gateway ${IGW_ID} -c ${VPC_ID} ${OPTS} > /dev/null 2>&1

    while [[ -z "$(ec2-describe-internet-gateways ${IGW_ID} --filter \"attachment.vpc-id=${VPC_ID}\" --filter \"attachment.state=available\" ${OPTS})" ]]; do
        sleep 10
    done

    echo "${IGW_ID}"
}

function create-route-table()
{
    local VPC_ID=$1
    local OPTS=$2
    local ROUTE_TABLE_ID=""

    while [[ -z ${ROUTE_TABLE_ID} ]]; do
        ROUTE_TABLE_ID=$(ec2-create-route-table ${VPC_ID} ${OPTS} | grep ROUTETABLE | awk '{print $2}')
    done

    echo "${ROUTE_TABLE_ID}"
}

function create-network-acl()
{
    local VPC_ID=$1
    local OPTS=$2
    local ACL_ID=""

    while [[ -z ${ACL_ID} ]]; do
        ACL_ID=$(ec2-create-network-acl ${VPC_ID} ${OPTS} | grep NETWORKACL | awk '{print $2}')
    done

    echo "${ACL_ID}"
}

function replace-network-acl-association()
{
    local SUBNET_ID=$1
    local ACL_ID=$2
    local OPTS=$3
    local ASSOCIATION=""

    while [[ -z ${ASSOCIATION} ]]; do
        ASSOCIATION=$(ec2-describe-network-acls --filter \"association.subnet-id=${SUBNET_ID}\" ${OPTS} | grep ASSOCIATION | grep ${SUBNET_ID} | awk '{print $2}')
    done

    ec2-replace-network-acl-association ${ASSOCIATION} -a ${ACL_ID} ${OPTS} > /dev/null 2>&1

    echo "${ASSOCIATION}"
}

function create-group()
{
    local GROUP_NAME=$1
    local DESCRIPTION=$2
    local VPC_ID=$3
    local OPTS=$4
    local GROUP_ID=""

    while [[ -z ${GROUP_ID} ]]; do
        GROUP_ID=$(ec2-create-group ${GROUP_NAME} -d "${DESCRIPTION}" -c ${VPC_ID} ${OPTS} | grep GROUP | awk '{print $2}')
    done

    echo "${GROUP_ID}"
}

function run-instances()
{
    local RUN_OPTS=$1
    local GROUP_ID=$2
    local ZONE=$3
    local SUBNET_ID=$4
    local PRIVATE_IP=$5
    local SERVER_NAME=$6
    local OPTS=$7
    local INSTANCE_ID=""

    while [[ -z ${INSTANCE_ID} ]]; do
        INSTANCE_ID=$(ec2-run-instances ${RUN_OPTS} -g ${GROUP_ID} -z ${ZONE} -s ${SUBNET_ID} --private-ip-address ${PRIVATE_IP} ${OPTS} | grep INSTANCE | awk '{print $2}')
    done

    ec2-create-tags ${INSTANCE_ID} --tag Name=${SERVER_NAME} ${OPTS} > /dev/null 2>&1

    while [[ -z "$(ec2-describe-instance-status ${INSTANCE_ID} ${OPTS} | grep INSTANCESTATUS | grep passed)" ]]; do
        sleep 10
    done

    echo "${INSTANCE_ID}"
}

function associate-address()
{
    local INSTANCE_ID=$1
    local OPTS=$2
    local ALLOCATION_ID=""

    ALLOCATION_ID=$(ec2-allocate-address -d vpc ${OPTS} | grep ADDRESS | awk '{print $5}')

    ec2-associate-address -a ${ALLOCATION_ID} -i ${INSTANCE_ID} ${OPTS} > /dev/null 2>&1

    echo "${ALLOCATION_ID}"
}

main部分は、手順とほぼ同じステップで記述できました。
では、スクリプトを実行していきます。


Macbook Air のファンの音が大きくなってきましたw
Management Console で各項目を見ながら進行状況を確認します...
...
3分40秒で終了しました。
確認してみましょう。



無事、EIPとの関連づけまでできていることを確認できました。


確認も終わったので削除します。

後片づけ

Management Console から次の4ステップで終わりです。
簡単ですね!

感想

今回、 ELB や RDS の配置まで書こうと思っていたのですが、間に合いませんでした。。
やっぱり CloudFormation は便利ですね!!

あとがき

先日ラスベガスで行なわれた AWS re:Invent ツアーに参加させていただきました。

Keynote や 各Session( CDP 盛り上がりましたね!)はもちろん、 Technical Bootcamp や Code Challenge 、シアトルのAWS本社訪問など刺激を受ける場面ばかりでした。
ご一緒させていただいた皆様、ありがとうございました。


来年もラスベガスでの開催が決まっているそうです。
気になる方はぜひご参加下さい!


ありがとうございました!