├── License.md ├── README.md └── multiacctcf.py /License.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 DonMills 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # multiacct-CF-orchestrate 2 | A python script to orchestrate multi-account multi-threaded Cloud Formation runs 3 | -------------------------------------------------------------------------------- /multiacctcf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from __future__ import print_function 3 | 4 | import threading 5 | import boto3 6 | import botocore 7 | import argparse 8 | from time import ctime 9 | ############### 10 | # Some Global Vars 11 | ############## 12 | lock = threading.Lock() 13 | 14 | awsaccts = [{'acct': 'acct1ID', 15 | 'name': 'master', 16 | 'cffile': 'location of cloudformation file in S3'}, 17 | {'acct': 'acct2ID', 18 | 'name': 'dev', 19 | 'cffile': 'location of cloudformation file in S3'}, 20 | {'acct': 'acct3ID', 21 | 'name': 'staging', 22 | 'cffile': 'location of cloudformation file in S3'}, 23 | {'acct': 'acct4ID', 24 | 'name': 'test', 25 | 'cffile': 'location of cloudformation file in S3'}, 26 | {'acct': 'acct5ID', 27 | 'name': 'QA', 28 | 'cffile': 'location of cloudformation file in S3'}] 29 | ################################### 30 | # This results dict is prepopulated with the info for the master vpc in a region. It will be overwritten 31 | # if the master cloudform is run 32 | ################################### 33 | results = { 34 | 'master': { 35 | 'CIDRblock': '172.0.1.0/22', 36 | 'RTBint': [ 37 | 'rtb-xxxxxxxx', 38 | 'rtb-xxxxxxxx'], 39 | 'VPCID': 'vpc-xxxxxxxx'}} 40 | threads = [] 41 | 42 | ####################### 43 | # The function that does CloudFormation and peering requests 44 | ####################### 45 | 46 | 47 | def run_cloudform(acct, acctname, region, cffile, nopeer, results): 48 | ################ 49 | # Don't like these, but necessary due to scoping 50 | ############### 51 | cfgood = None 52 | ismaster = None 53 | cidrblock = None 54 | vpcid = None 55 | rtbid = None 56 | rtb_inta = None 57 | rtb_intb = None 58 | 59 | threadname = threading.current_thread().name 60 | if acctname == "master": 61 | ismaster = True 62 | ################### 63 | # If we are running in master, we don't need sts creds 64 | ################### 65 | if ismaster: 66 | try: 67 | cf = boto3.client('cloudformation', 68 | region_name=region) 69 | validate = cf.validate_template( 70 | TemplateURL=cffile 71 | ) 72 | cfgood = True 73 | print( 74 | "[%s] %s CloudFormation file %s validated successfully for account %s" % 75 | (ctime(), threadname, cffile, acctname)) 76 | except botocore.exceptions.ClientError as e: 77 | print( 78 | "[%s] %s CloudFormation file %s validation failed for account %s with error: %s" % 79 | (ctime(), threadname, cffile, acctname, e)) 80 | cfgood = False 81 | ################### 82 | # Otherwise, we do. 83 | ################### 84 | else: 85 | with lock: 86 | print( 87 | "[%s] %s is assuming STS role for account %s" % 88 | (ctime(), threadname, acctname)) 89 | try: 90 | with lock: 91 | sts = boto3.client('sts') 92 | role = sts.assume_role( 93 | RoleArn='arn:aws:iam::' + acct + ':role/MasterAcctRole', 94 | RoleSessionName='STSTest', 95 | DurationSeconds=900 96 | ) 97 | accesskey = role["Credentials"]["AccessKeyId"] 98 | secretkey = role["Credentials"]["SecretAccessKey"] 99 | sessiontoken = role["Credentials"]["SessionToken"] 100 | print( 101 | "[%s] %s successfully assumed STS role for account %s" % 102 | (ctime(), threadname, acctname)) 103 | except botocore.exceptions.ClientError as e: 104 | with lock: 105 | print( 106 | "[%s] %s failed to assume role for account %s with error: %s" % 107 | (ctime(), threadname, acctname, e)) 108 | with lock: 109 | print( 110 | "[%s] %s is verifying CloudFormation file %s for account %s" % 111 | (ctime(), threadname, cffile, acctname)) 112 | try: 113 | cf = boto3.client('cloudformation', 114 | aws_access_key_id=accesskey, 115 | aws_secret_access_key=secretkey, 116 | aws_session_token=sessiontoken, 117 | region_name=region) 118 | validate = cf.validate_template( 119 | TemplateURL=cffile 120 | ) 121 | cfgood = True 122 | with lock: 123 | print( 124 | "[%s] %s CloudFormation file %s validated successfully for account %s" % 125 | (ctime(), threadname, cffile, acctname)) 126 | except botocore.exceptions.ClientError as e: 127 | with lock: 128 | print( 129 | "[%s] %s CloudFormation file %s validation failed for account %s with error: %s" % 130 | (ctime(), threadname, cffile, acctname, e)) 131 | cfgood = False 132 | ########################## 133 | # Ok the CF should be validated (cfgood=True), so let's run it. 134 | ######################### 135 | if cfgood: 136 | with lock: 137 | print( 138 | "[%s] %s Preparing to run CloudFormation file %s in account %s" % 139 | (ctime(), threadname, cffile, acctname)) 140 | stackid = cf.create_stack( 141 | StackName=region + "-" + acctname, 142 | TemplateURL=cffile, 143 | Parameters=[ 144 | { 145 | }, 146 | ], 147 | Tags=[ 148 | { 149 | 'Key': 'Purpose', 150 | 'Value': 'Infrastructure' 151 | }, 152 | ] 153 | )['StackId'] 154 | with lock: 155 | print("[%s] %s StackID %s is running in account %s" % 156 | (ctime(), threadname, stackid, acctname)) 157 | waiter = cf.get_waiter('stack_create_complete') 158 | waiter.wait(StackName=stackid) 159 | with lock: 160 | print( 161 | "[%s] %s StackID %s completed creation in account %s" % 162 | (ctime(), threadname, stackid, acctname)) 163 | stack = cf.describe_stacks(StackName=stackid) 164 | for item in stack['Stacks'][0]['Outputs']: 165 | if item['OutputKey'] == "VPCId": 166 | vpcid = item["OutputValue"] 167 | elif item['OutputKey'] == "VPCCIDRBlock": 168 | cidrblock = item["OutputValue"] 169 | elif item['OutputKey'] == "RouteTableId": 170 | rtbid = item["OutputValue"] 171 | elif item['OutputKey'] == "InternalRouteTableA": 172 | rtbid_inta = item["OutputValue"] 173 | elif item['OutputKey'] == "InternalRouteTableB": 174 | rtbid_intb = item["OutputValue"] 175 | pcxid = "None" 176 | ########################### 177 | # Don't do peering if we are master vpc or if nopeer is set via cli 178 | # otherwise, this is the peering code 179 | ########################## 180 | if not ismaster and not nopeer: 181 | with lock: 182 | print( 183 | "[%s] %s Preparing to request peering with Master vpc in account %s" % 184 | (ctime(), threadname, acctname)) 185 | try: 186 | ec2 = boto3.client('ec2', 187 | aws_access_key_id=accesskey, 188 | aws_secret_access_key=secretkey, 189 | aws_session_token=sessiontoken, 190 | region_name=region) 191 | pcx = ec2.create_vpc_peering_connection( 192 | VpcId=vpcid, 193 | PeerVpcId=results['master']['VPCID'], 194 | PeerOwnerId='masteracctID' 195 | ) 196 | pcxid = pcx['VpcPeeringConnection']['VpcPeeringConnectionId'] 197 | with lock: 198 | print( 199 | "[%s] %s Peering Connection request ID %s sent from account %s" % 200 | (ctime(), threadname, pcxid, acctname)) 201 | print( 202 | "[%s] %s Preparing to add route to table %s to Peer Connection ID %s in account %s" % 203 | (ctime(), threadname, rtbid, pcxid, acctname)) 204 | route = ec2.create_route( 205 | DestinationCidrBlock=results['master']['CIDRblock'], 206 | VpcPeeringConnectionId=pcxid, 207 | RouteTableId=rtbid 208 | ) 209 | if route['Return']: 210 | print( 211 | "[%s] Route added to route table %s for network %s to peer connection %s in account %s" % 212 | (ctime(), rtbid, results['master']['CIDRblock'], pcxid, acctname)) 213 | else: 214 | print( 215 | "[%s] Failed adding to route table %s for network %s to peer connection %s in account %s" % 216 | (ctime(), rtbid, results['master']['CIDRblock'], pcxid, acctname)) 217 | except botocore.exceptions.ClientError as e: 218 | with lock: 219 | print( 220 | "[%s] %s Peering Connection request failed for account %s with error: %s" % 221 | (ctime(), threadname, acctname, e)) 222 | 223 | results[acctname] = { 224 | "CIDRblock": cidrblock, 225 | "VPCID": vpcid, 226 | "PCXID": pcxid} 227 | ############################ 228 | # master results need the route table ids of both internal tables to add routes to both 229 | ########################### 230 | if ismaster: 231 | results[acctname].update({'RTBint': [rtbid_inta, rtbid_intb]}) 232 | 233 | 234 | def printdata(results, acctname): 235 | print( 236 | "The CIDRBlock for VPC %s in account %s is %s. The VPC peering id is %s" % 237 | (results[acctname]['VPCID'], 238 | acctname, 239 | results[acctname]['CIDRblock'], 240 | results[acctname]['PCXID'])) 241 | 242 | 243 | def printdatamaster(results): 244 | print( 245 | "The CIDRBlock for VPC %s in master account is %s. The internal route table ids are %s and %s" % 246 | (results['master']['VPCID'], 247 | results['master']['CIDRblock'], 248 | results['master']['RTBint'][0], 249 | results['master']['RTBint'][1])) 250 | 251 | 252 | def main(): 253 | ############################# 254 | # Parse CLI options - setup the parser 255 | ############################ 256 | parser = argparse.ArgumentParser( 257 | description='An orchestration script that runs multi-account CloudFormation and can set up peering relationships between the VPCs created') 258 | parser.add_argument( 259 | "region", 260 | type=str, 261 | choices=[ 262 | "us-west-2", 263 | "us-east-1"], 264 | help="The AWS Region you would like to operate in") 265 | parser.add_argument( 266 | "-sa", 267 | "--single_account", 268 | action='append', 269 | help="Provide a single account name(dev,hdp,test,beps) and only operate on that account. You can perform this action multiple times to operate on more than one account.") 270 | parser.add_argument( 271 | "-np", 272 | "--no_peering", 273 | action='store_true', 274 | dest='no_peering', 275 | help="Run the CloudFormation, but don't do the inter-VPC peering") 276 | ################################# 277 | # Parse CLI options - read the parser 278 | ################################# 279 | nopeer = None 280 | 281 | args = parser.parse_args() 282 | region = args.region 283 | acct = args.single_account 284 | if args.no_peering: 285 | nopeer = True 286 | ############################ 287 | # Do single account or multiple single account runs 288 | ############################ 289 | if acct: 290 | for line in acct: 291 | foundacct = None 292 | print( 293 | "[%s] Single account selected: Preparing to run CloudFormation on %s account" % 294 | (ctime(), line)) 295 | print("[%s] Preparing to spawn thread" % ctime()) 296 | for entry in awsaccts: 297 | if entry['name'] == line: 298 | t = threading.Thread( 299 | target=run_cloudform, 300 | args=( 301 | entry['acct'], 302 | entry['name'], 303 | region, 304 | entry['cffile'], 305 | nopeer, 306 | results)) 307 | threads.append(t) 308 | t.start() 309 | foundacct = True 310 | if not foundacct: 311 | print("[%s] No matching account name found!" % ctime()) 312 | print("[%s] Current configured accounts are:" % ctime()) 313 | for entry in awsaccts: 314 | print( 315 | "[%s] Account ID: %s Account Name: %s" % 316 | (ctime(), entry['acct'], entry['name'])) 317 | for i in range(len(threads)): 318 | threads[i].join() 319 | ############################# 320 | # Or run the whole shebang 321 | ############################# 322 | else: 323 | print( 324 | "[%s] Preparing to run CloudFormation across all AWS accounts" % 325 | ctime()) 326 | print("[%s] Preparing to run Master account CloudFormation" % ctime()) 327 | masteracct = list( 328 | (entry for entry in awsaccts if entry['name'] == 'master'))[0] 329 | run_cloudform( 330 | masteracct['acct'], 331 | masteracct['name'], 332 | region, 333 | masteracct['cffile'], 334 | nopeer, 335 | results) 336 | printdatamaster(results) 337 | print("[%s] Preparing to spawn threads" % ctime()) 338 | subaccts = (entry for entry in awsaccts if entry['name'] != 'master') 339 | ############################## 340 | # do the threading for subaccts 341 | ############################# 342 | for entry in subaccts: 343 | t = threading.Thread( 344 | target=run_cloudform, 345 | args=( 346 | entry['acct'], 347 | entry['name'], 348 | region, 349 | entry['cffile'], 350 | nopeer, 351 | results)) 352 | threads.append(t) 353 | t.start() 354 | for i in range(len(threads)): 355 | threads[i].join() 356 | print("[%s] All CloudFormations run!" % ctime()) 357 | if len(results) > 1: 358 | print("[%s] Printing outputs:" % ctime()) 359 | for entry in (entry for entry in results if entry != 'master'): 360 | printdata(results, entry) 361 | ############################### 362 | # Accept peering and add final routes to peering vpcs 363 | ############################## 364 | if not nopeer and len(results) > 1: 365 | print( 366 | "[%s] Attempting to accept peering requests in Master" % 367 | ctime()) 368 | try: 369 | master = boto3.client('ec2', 370 | region_name=region) 371 | subaccts = (entry for entry in results if entry != "master") 372 | for entry in subaccts: 373 | pcx = master.accept_vpc_peering_connection( 374 | VpcPeeringConnectionId=results[entry]['PCXID'] 375 | ) 376 | print( 377 | "[%s] VPC Peering connection from %s with ID %s is status: %s" % 378 | (ctime(), 379 | entry, 380 | results[entry]['PCXID'], 381 | pcx['VpcPeeringConnection']['Status']['Code'])) 382 | for table in results['master']['RTBint']: 383 | route = master.create_route( 384 | DestinationCidrBlock=results[entry]['CIDRblock'], 385 | VpcPeeringConnectionId=results[entry]['PCXID'], 386 | RouteTableId=table 387 | ) 388 | if route['Return']: 389 | print( 390 | "[%s] Route added to Master route table %s for network %s to peer connection %s" % 391 | (ctime(), table, results[entry]['CIDRblock'], results[entry]['PCXID'])) 392 | else: 393 | print( 394 | "[%s] Adding route to Master route table %s for network %s to peer connection %s failed!" % 395 | (ctime(), table, results[entry]['CIDRblock'], results[entry]['PCXID'])) 396 | 397 | except botocore.exceptions.ClientError as e: 398 | print( 399 | "[%s] Failed to manipulate account %s with error: %s" % 400 | (ctime(), "Master", e)) 401 | 402 | print("[%s] Finished" % ctime()) 403 | 404 | if __name__ == '__main__': 405 | main() 406 | --------------------------------------------------------------------------------