s3bundler-spotfleet.cfn.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. ##
  2. ## Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
  3. ##
  4. ## Licensed under the Amazon Software License (the "License"). You may not use this
  5. ## file except in compliance with the License. A copy of the License is located at
  6. ##
  7. ## http://aws.amazon.com/asl/
  8. ##
  9. ## or in the "license" file accompanying this file. This file is distributed on an
  10. ## "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied.
  11. ## See the License for the specific language governing permissions and limitations
  12. ## under the License.
  13. ##
  14. import troposphere
  15. import troposphere.ec2
  16. import troposphere.s3
  17. import troposphere.cloudformation
  18. import troposphere.sqs
  19. import troposphere.logs
  20. import troposphere.iam
  21. import troposphere.ecs
  22. import ipaddress
  23. import requests
  24. import argparse
  25. import boto3
  26. import os
  27. r = requests.get("http://www.ec2instances.info/instances.json")
  28. pricing = r.json()
  29. instancespecs = dict()
  30. for prod in pricing:
  31. instancespecs[prod['instance_type']] = prod
  32. parser = argparse.ArgumentParser(description="build a cloudformation template to run a spotfleet cluster.")
  33. parser.add_argument("-f", "--family", nargs='+',
  34. help="list of EC2 instance families. e.g. c3 or i3")
  35. parser.add_argument("-s", "--cidr",
  36. help="CIDR block", default="172.21.0.0/16")
  37. parser.add_argument("--region", default=os.environ.get('AWS_DEFAULT_REGION', 'us-east-1'))
  38. args = parser.parse_args()
  39. instancetypes = [x for x in instancespecs.keys() if x.split('.')[0] in args.family and x.split('.')[1] != "metal"]
  40. ec2 = boto3.client('ec2', region_name=args.region)
  41. response = ec2.describe_availability_zones(Filters=[{'Name': 'state', 'Values': ['available'] }])
  42. azs = [az['ZoneName'] for az in response['AvailabilityZones']]
  43. vpccidrblock = ipaddress.ip_network(args.cidr)
  44. t = troposphere.Template()
  45. t.add_version("2010-09-09")
  46. ami = t.add_parameter(troposphere.Parameter(
  47. "ECSOptimizedAMI",
  48. Description="EC2 AMI for use with ECS",
  49. Type="String",
  50. Default="ami-275ffe31"
  51. ))
  52. ECSCluster = t.add_parameter(troposphere.Parameter(
  53. "ECSClusterName",
  54. Description="ECS Cluster name for spot fleet instances to join",
  55. Type="String"
  56. ))
  57. SSHKeyPair = t.add_parameter(troposphere.Parameter(
  58. "SSHKeyPair",
  59. Description="SSH Key registered in this region",
  60. Type="AWS::EC2::KeyPair::KeyName",
  61. ))
  62. VCPUTarget = t.add_parameter(troposphere.Parameter(
  63. "VCPUTarget",
  64. Default="0",
  65. Description="Number of VPCUs for the ECS Spotfleet cluster",
  66. Type="Number",
  67. ))
  68. VCPUSpotBid = t.add_parameter(troposphere.Parameter(
  69. "VCPUSpotBid",
  70. Default="0.00",
  71. Description="Spot bid per VCPU in the ECS Spotfleet cluster",
  72. Type="String",
  73. ))
  74. VPC = t.add_resource(troposphere.ec2.VPC(
  75. "VPC",
  76. CidrBlock=str(vpccidrblock),
  77. ))
  78. IGW = t.add_resource(troposphere.ec2.InternetGateway(
  79. "IGW",
  80. ))
  81. AttachGateway = t.add_resource(troposphere.ec2.VPCGatewayAttachment(
  82. "AttachGateway",
  83. VpcId=troposphere.Ref(VPC),
  84. InternetGatewayId=troposphere.Ref(IGW),
  85. ))
  86. RouteTable = t.add_resource(troposphere.ec2.RouteTable(
  87. "RouteTable",
  88. VpcId=troposphere.Ref("VPC"),
  89. ))
  90. Route = t.add_resource(troposphere.ec2.Route(
  91. "Route",
  92. DependsOn="AttachGateway",
  93. RouteTableId=troposphere.Ref(RouteTable),
  94. GatewayId=troposphere.Ref(IGW),
  95. DestinationCidrBlock="0.0.0.0/0",
  96. ))
  97. subnets = []
  98. for az, cidr in zip(azs, vpccidrblock.subnets(4)):
  99. subnet = t.add_resource(troposphere.ec2.Subnet(
  100. "Subnet{}".format(az[-1:]),
  101. AvailabilityZone=az,
  102. CidrBlock=str(cidr),
  103. VpcId=troposphere.Ref("VPC"),
  104. MapPublicIpOnLaunch=True,
  105. ))
  106. subnets.append(subnet)
  107. RTA = t.add_resource(troposphere.ec2.SubnetRouteTableAssociation(
  108. "RTA{}".format(az[-1:]),
  109. RouteTableId=troposphere.Ref(RouteTable),
  110. SubnetId=troposphere.Ref(subnet),
  111. ))
  112. spotfleetrole = t.add_resource(troposphere.iam.Role(
  113. "spotfleetrole",
  114. AssumeRolePolicyDocument={
  115. "Statement": [
  116. {
  117. "Action": "sts:AssumeRole",
  118. "Principal": {
  119. "Service": "spotfleet.amazonaws.com"
  120. },
  121. "Effect": "Allow",
  122. "Sid": ""
  123. }
  124. ],
  125. "Version": "2012-10-17"
  126. },
  127. ManagedPolicyArns=["arn:aws:iam::aws:policy/service-role/AmazonEC2SpotFleetRole"]
  128. ))
  129. containerinstancerole = t.add_resource(troposphere.iam.Role(
  130. "containerinstancerole",
  131. AssumeRolePolicyDocument={
  132. "Version": "2012-10-17",
  133. "Statement": [
  134. {
  135. "Principal": {
  136. "Service": "ec2.amazonaws.com"
  137. },
  138. "Action": "sts:AssumeRole",
  139. "Sid": "",
  140. "Effect": "Allow"
  141. }
  142. ]
  143. },
  144. ManagedPolicyArns=["arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role"]
  145. ))
  146. containerinstanceprofile = t.add_resource(troposphere.iam.InstanceProfile(
  147. "containerinstanceprofile",
  148. Path="/",
  149. Roles=[troposphere.Ref(containerinstancerole)]
  150. ))
  151. s3bundlersg = t.add_resource(troposphere.ec2.SecurityGroup(
  152. "s3bundlersg",
  153. VpcId=troposphere.Ref(VPC),
  154. GroupDescription="s3bundler instances"
  155. ))
  156. def mklaunchspecification(InstanceType, WeightedCapacity, VolumeCount):
  157. spec = {}
  158. spec["SubnetId"] = troposphere.Join(",", [troposphere.Ref(subnet) for subnet in subnets])
  159. spec["ImageId"] = troposphere.Ref(ami)
  160. spec["IamInstanceProfile"] = troposphere.ec2.IamInstanceProfile(Arn=troposphere.GetAtt(containerinstanceprofile, "Arn"))
  161. spec["WeightedCapacity"] = WeightedCapacity
  162. spec["SecurityGroups"] = [troposphere.ec2.SecurityGroups(GroupId=troposphere.Ref(s3bundlersg))]
  163. spec["InstanceType"] = InstanceType
  164. spec["KeyName"] = troposphere.Ref(SSHKeyPair)
  165. spec["BlockDeviceMappings"] = [
  166. troposphere.ec2.BlockDeviceMapping(DeviceName="/dev/xvda",
  167. Ebs=troposphere.ec2.EBSBlockDevice(
  168. VolumeType="gp2",
  169. VolumeSize=8,
  170. DeleteOnTermination=True)),
  171. troposphere.ec2.BlockDeviceMapping(DeviceName="/dev/xvdcz", VirtualName="ephemeral99")] #Spotfleet doesn't seem to support NoDevice: {} in cfn
  172. [spec["BlockDeviceMappings"].append(
  173. troposphere.ec2.BlockDeviceMapping(
  174. DeviceName="/dev/xvdc{}".format(chr(ord('a') + i)),
  175. VirtualName="ephemeral{}".format(str(i))
  176. )) for i in range(0,VolumeCount)]
  177. spec["UserData"] = troposphere.Base64(troposphere.Join("", ['''#cloud-boothook
  178. rootvol=`mount | grep " on / " | cut -d\ -f1`
  179. if [ "$rootvol" = "/dev/xvda1" ] || [ "$rootvol" = "/dev/xvde1" ]
  180. then
  181. prefix="xvd"
  182. else
  183. prefix="sd"
  184. fi
  185. devices=""
  186. #detect and use instance store volumes
  187. for ephemeral in `curl -s http://169.254.169.254/2016-09-02/meta-data/block-device-mapping/ | grep ephemeral`
  188. do
  189. device=`curl -s http://169.254.169.254/2016-09-02/meta-data/block-device-mapping/$ephemeral`
  190. #determine the device path and make sure it matches what the OS uses
  191. case $device in
  192. sd*)
  193. device_path="/dev/`echo $device | sed s/sd/$prefix/`"
  194. ;;
  195. xvd*)
  196. device_path="/dev/`echo $device | sed s/xvd/$prefix/`"
  197. ;;
  198. esac
  199. if [ -b $device_path ]
  200. then
  201. devices="$devices $device_path"
  202. fi
  203. done
  204. #detect and use NVMe devices
  205. for n in /dev/nvme*n1
  206. do
  207. if [ -b $n ]
  208. then
  209. EXT4OPT="-E nodiscard"
  210. devices="$devices $n"
  211. fi
  212. done
  213. ephemeral_count=$(IFS=' '; set -- $devices; echo $#)
  214. if [ "$ephemeral_count" -gt 0 ]; then
  215. for device in $devices; do
  216. umount $device
  217. dd if=/dev/zero of=$device bs=512 count=1
  218. pvcreate -f -y $device
  219. done
  220. vgcreate EphemeralVG $devices
  221. lvcreate -ay -l 100%FREE -i $ephemeral_count EphemeralVG -n Ephemeral
  222. mkfs.ext4 $EXT4OPT /dev/mapper/EphemeralVG-Ephemeral
  223. mount -o noatime /dev/mapper/EphemeralVG-Ephemeral /mnt
  224. echo "/dev/mapper/EphemeralVG-Ephemeral /mnt ext4 noatime 0 0" >> /etc/fstab
  225. mkdir /mnt/ecs
  226. chmod 1777 /mnt/ecs
  227. mkdir /mnt/docker
  228. echo 'OPTIONS="${OPTIONS} -g /mnt/docker"' >> /etc/sysconfig/docker
  229. echo 'ECS_DOCKER_GRAPH_PATH=/mnt/docker' >> /etc/ecs/ecs.config
  230. fi
  231. cat << 'EOF' >> /etc/ecs/ecs.config
  232. ECS_CLUSTER=''', troposphere.Ref(ECSCluster), "\n",
  233. '''ECS_DOCKER_GRAPH_PATH=/mnt/docker
  234. EOF
  235. ''']))
  236. return troposphere.ec2.LaunchSpecifications(**spec)
  237. SpotFleet = t.add_resource(troposphere.ec2.SpotFleet(
  238. "SpotFleet",
  239. SpotFleetRequestConfigData=troposphere.ec2.SpotFleetRequestConfigData(
  240. IamFleetRole=troposphere.GetAtt(spotfleetrole, "Arn"),
  241. SpotPrice=troposphere.Ref(VCPUSpotBid),
  242. TargetCapacity=troposphere.Ref(VCPUTarget),
  243. AllocationStrategy="diversified",
  244. LaunchSpecifications=[mklaunchspecification(i,
  245. instancespecs[i]['vCPU'],
  246. instancespecs[i].get('storage', {}).get('devices', 0)) for i in instancetypes])
  247. ))
  248. t.add_output(troposphere.Output(
  249. "SpotFleet",
  250. Value=troposphere.Ref(SpotFleet)
  251. ))
  252. print(t.to_json())