├── README.md ├── ScreenShotFlare.png ├── ScreenShotSankey.png ├── cf-stack-mapping-blog-post.md ├── create-exports.json ├── example-imports.json ├── example-imports1.json ├── example-imports2.json ├── flare.csv ├── get_data_flare.py ├── get_data_sankey.py ├── index-flare.html ├── index-sankey.html ├── index.html ├── sankey.js └── sourcedata-output.json /README.md: -------------------------------------------------------------------------------- 1 | # cf-cross-stack-mapping 2 | CloudFormation Cross Stack Reference Mapping 3 | -------------------------------------------------------------------------------- /ScreenShotFlare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1Strategy/cf-cross-stack-mapping/124a9f60a8bc0087f5af8dae880dcde705bb1fe6/ScreenShotFlare.png -------------------------------------------------------------------------------- /ScreenShotSankey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1Strategy/cf-cross-stack-mapping/124a9f60a8bc0087f5af8dae880dcde705bb1fe6/ScreenShotSankey.png -------------------------------------------------------------------------------- /cf-stack-mapping-blog-post.md: -------------------------------------------------------------------------------- 1 | # Visualizing CloudFormation Cross Stack References 2 | --- 3 | ### Introduction 4 | Some time ago AWS released a feature for CloudFormation called cross stack references. (https://aws.amazon.com/blogs/aws/aws-cloudformation-update-yaml-cross-stack-references-simplified-substitution/) Cross stack references are very useful when you have parameters or resources that you need to reference in other stacks. This is extremely helpful because you can make your CloudFormation stacks much more modular and easier to maintain. For example, typically when creating infrastructure you would need to create VPC, subnet, route table, NACL, load balancer, EC2 instance, and database resources all in one CloudFormation stack. With cross stack references you can create separate stacks for network resources, application resources, databases, etc. The cross stack references can also be very useful when integrating with an existing environment or CloudFormation template as you can retrieve VPC IDs, Subnet IDs, etc. and use them within a new stack. Cross stack references save a lot of manual work, where previously you would need to look up all the IDs needed and add them to parameters, or create Lambda functions to retrieve the IDs. 5 | 6 |  7 | 8 | Cross stack references are created by adding an “Export” declaration to a normal CloudFormation stack output and using a new built in function to access them. That new function, Fn::ImportValue, does have a few restrictions that are important to note if you are setting out on creating your first cross-stack reference with it. Namely, the export names must be unique within an account and region, and the function will not allow you to create references across regions. In addition, AWS will not allow you to delete a stack that is being referenced by another stack and your outputs cannot be changed or removed while they are being referenced by a stack. 9 | 10 | ### The Problem 11 | There is one problem with cross stack references. When your infrastructure is fairly complex with multiple stacks and many references within those stacks, it can be very difficult to see and understand all the connections and references. Then when it comes time to make changes to a resource or reference it can be hard to see where those changes will have an impact. 12 | 13 | ### The Solution 14 | To solve the problems with complex cross stack references, I fell back to my tried and trusted friends data, analysis, and visualization. In this case not much analysis was needed. All that's needed, in this case, is some way to get data about all the cross stack references, and then some way to visualize them. 15 | 16 | ### Getting the Data 17 | To get the necessary data I used the AWS Python SDK Boto3 (https://aws.amazon.com/sdk-for-python/). I first hit the AWS account and retrieve all the CloudFormation exports and the name of the template from which they are being exported. I then iterate over all those exports and retrieve all the places where they are being imported and the name of that template. I then write the output to a json file that will be used for the visualization. Here is a link to the code: (https://github.com/1Strategy/cf-cross-stack-mapping/blob/master/get_data_sankey.py) 18 | 19 | ### D3 and the Sankey Diagram 20 | The next step was to somehow show all the connections between all the stacks and all the exports and imports. To accomplish this I chose to use a Sankey Diagram (https://en.wikipedia.org/wiki/Sankey_diagram). I chose the Sankey Diagram instead of the usual Network Diagram mainly because I wanted to see the flow of the references between the stacks versus just the connections. Sankey Diagrams are not in your typical visualization tools. So, to show and share the visualization I chose to use the D3 javascript library (https://d3js.org/). D3 is easy to use and it creates a nice visualization that is easy to view and share with any modern web browser. The other nice feature of the D3 Sankey Diagram is that it is interactive, and you can grab the nodes that represent the stacks and move them around as needed. I also experimented with a Flare Chart for a different type of view. However, the Flare Chart lacked a way to show the interconnections that I wanted to be able to see. 21 | 22 | The code for the visuals can be found here (https://github.com/1Strategy/cf-cross-stack-mapping). The interactive visualizations can be seen here (https://1strategy.github.io/cf-cross-stack-mapping/), and below are example screen shots of each of the visualizations. 23 | 24 | **Sankey Diagram Example:** 25 |  26 | 27 | **Flare Chart Example:** 28 |  29 | 30 | ### Summary and Conclusions. 31 | In the end a customer can see the complete relationships between all the stacks in their environment. So far the most useful feature has been to see when there are exports that are not being used and removing them to make the cross stack references more efficient and easier to maintain. The other thing this may be useful for is to see an overall picture of your AWS infrastructure and how things are related within it. 32 | 33 | **Some future considerations:** 34 | Better color coding of the nodes. For instance, one color to denote stacks and another color to denote exports. 35 | 36 | A drop down to select only the stacks that you want to see. This would be especially helpful when the number of stacks deployed gets very large. 37 | -------------------------------------------------------------------------------- /create-exports.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "AWS CloudFormation Template for creating sample exports to test the visualization", 4 | "Parameters": { 5 | "VPCID": { 6 | "Description": "Enter the VPC ID where the dummy SecurityGroup will be created.", 7 | "Type": "String", 8 | "Default": "vpc-********" 9 | } 10 | }, 11 | "Resources": { 12 | "DummySecurityGroup" : { 13 | "Type": "AWS::EC2::SecurityGroup", 14 | "Properties": { 15 | "GroupDescription": "Dummy security group which allowed the creation of a CloudFormation stack that exports values.", 16 | "VpcId": {"Ref": "VPCID"} 17 | } 18 | } 19 | }, 20 | "Outputs": { 21 | "Subnet1": { 22 | "Description": "First example Subnet", 23 | "Value": "subnet-abcd1234", 24 | "Export": {"Name":"CreateExports-Subnet1"} 25 | }, 26 | "Subnet2": { 27 | "Description": "Second example Subnet", 28 | "Value": "subnet-abcd5678", 29 | "Export": {"Name":"CreateExports-Subnet2"} 30 | }, 31 | "DMZSubnet1": { 32 | "Description": "First example DMZSubnet", 33 | "Value": "subnet-abcd4321", 34 | "Export": {"Name":"CreateExports-DMZSubnet1"} 35 | }, 36 | "DMZSubnet2": { 37 | "Description": "Second example DMZSubnet", 38 | "Value": "subnet-abcd8765", 39 | "Export": {"Name":"CreateExports-DMZSubnet2"} 40 | }, 41 | "CoreVpc": { 42 | "Description": "Core Network VPC ID", 43 | "Value": {"Ref": "VPCID"}, 44 | "Export": {"Name":"CreateExports-CoreVpc"} 45 | }, 46 | "DMZVpc": { 47 | "Description": "DMZ Network VPC ID", 48 | "Value": {"Ref": "VPCID"}, 49 | "Export": {"Name":"CreateExports-DMZVpc"} 50 | }, 51 | "CoreServerSecurityGroup": { 52 | "Description": "Core Server Security Group", 53 | "Value": "sg-abcd1234", 54 | "Export" : { "Name" : "CreateExports-CoreServerSecurityGroup" } 55 | }, 56 | "DMZServerSecurityGroup": { 57 | "Description": "DMZ Server Security Group", 58 | "Value": "sg-abcd5678", 59 | "Export" : { "Name" : "CreateExports-DMZServerSecurityGroup" } 60 | }, 61 | "S3Endpoint": { 62 | "Description":"S3 VPC Endpoint", 63 | "Value": "vpce-abcd1234", 64 | "Export":{"Name":"CreateExports-S3Endpoint"} 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /example-imports.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "Example Imports from Create Exports Stack", 4 | "Resources": { 5 | "TestingCoreSecurityGroup" : { 6 | "Type": "AWS::EC2::SecurityGroup", 7 | "Properties": { 8 | "GroupDescription": "Testing Core Security Group", 9 | "VpcId": { "Fn::ImportValue" : "CreateExports-CoreVpc" } 10 | } 11 | }, 12 | "TestingDMZSecurityGroup" : { 13 | "Type": "AWS::EC2::SecurityGroup", 14 | "Properties": { 15 | "GroupDescription": "Testing DMZ Security Group", 16 | "VpcId": { "Fn::ImportValue" : "CreateExports-DMZVpc" } 17 | } 18 | } 19 | }, 20 | "Outputs": { 21 | "TestingSGOutput": { 22 | "Description": "Testing SecurityGroup Output", 23 | "Value": {"Ref":"TestingCoreSecurityGroup"}, 24 | "Export": {"Name":"ExampleImports-SecurityGroup"} 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example-imports1.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "Example Imports from Create Exports Stack", 4 | "Resources": { 5 | "TestingCoreSecurityGroup1" : { 6 | "Type": "AWS::EC2::SecurityGroup", 7 | "Properties": { 8 | "GroupDescription": "Testing Core Security Group 1", 9 | "VpcId": { "Fn::ImportValue" : "CreateExports-CoreVpc" } 10 | } 11 | }, 12 | "TestingDMZSecurityGroup1" : { 13 | "Type": "AWS::EC2::SecurityGroup", 14 | "Properties": { 15 | "GroupDescription": "Testing DMZ Security Group 1", 16 | "VpcId": { "Fn::ImportValue" : "CreateExports-DMZVpc" } 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example-imports2.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "Example Imports from Create Exports Stack", 4 | "Resources": { 5 | "TestingCoreSecurityGroup2" : { 6 | "Type": "AWS::EC2::SecurityGroup", 7 | "Properties": { 8 | "GroupDescription": "Testing Core Security Group 2", 9 | "VpcId": { "Fn::ImportValue" : "CreateExports-CoreVpc" } 10 | } 11 | }, 12 | "TestingDMZSecurityGroup2" : { 13 | "Type": "AWS::EC2::SecurityGroup", 14 | "Properties": { 15 | "GroupDescription": "Testing DMZ Security Group 2", 16 | "VpcId": { "Fn::ImportValue" : "CreateExports-DMZVpc" } 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /flare.csv: -------------------------------------------------------------------------------- 1 | id,value 2 | Templates, 3 | Templates.Create-Exports-Stack.CreateExports-CoreServerSecurityGroup, 4 | Templates.Create-Exports-Stack, 5 | Templates.Create-Exports-Stack.CreateExports-CoreVpc, 6 | Templates.Create-Exports-Stack.CreateExports-CoreVpc.Example-Imports1, 7 | Templates.Create-Exports-Stack.CreateExports-CoreVpc.Example-Imports0, 8 | Templates.Create-Exports-Stack.CreateExports-CoreVpc.Example-Imports2, 9 | Templates.Create-Exports-Stack.CreateExports-CoreVpc.Example-Imports-New, 10 | Templates.Create-Exports-Stack.CreateExports-DMZServerSecurityGroup, 11 | Templates.Create-Exports-Stack.CreateExports-DMZSubnet1, 12 | Templates.Create-Exports-Stack.CreateExports-DMZSubnet1.Example-Imports-New, 13 | Templates.Create-Exports-Stack.CreateExports-DMZSubnet2, 14 | Templates.Create-Exports-Stack.CreateExports-DMZVpc, 15 | Templates.Create-Exports-Stack.CreateExports-DMZVpc.Example-Imports1, 16 | Templates.Create-Exports-Stack.CreateExports-DMZVpc.Example-Imports0, 17 | Templates.Create-Exports-Stack.CreateExports-DMZVpc.Example-Imports2, 18 | Templates.Create-Exports-Stack.CreateExports-DMZVpc.Example-Imports-New, 19 | Templates.Create-Exports-Stack.CreateExports-S3Endpoint, 20 | Templates.Create-Exports-Stack.CreateExports-Subnet1, 21 | Templates.Create-Exports-Stack.CreateExports-Subnet1.Example-Imports-New, 22 | Templates.Create-Exports-Stack.CreateExports-Subnet2, 23 | Templates.Create-Exports-Stack.CreateExports-Subnet2.Example-Imports-New, 24 | Templates.Example-Imports0.ExampleImports-SecurityGroup, 25 | Templates.Example-Imports0, 26 | Templates.main-vpc.main-vpc-AZAPrivate1SubnetId, 27 | Templates.main-vpc, 28 | Templates.main-vpc.main-vpc-AZAPublic1SubnetId, 29 | Templates.main-vpc.main-vpc-AZBPrivate1SubnetId, 30 | Templates.main-vpc.main-vpc-AZBPublic1SubnetId, 31 | Templates.main-vpc.main-vpc-AZCPrivate1SubnetId, 32 | Templates.main-vpc.main-vpc-AZCPublic1SubnetId, 33 | Templates.main-vpc.main-vpc-VPCID, 34 | -------------------------------------------------------------------------------- /get_data_flare.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import boto3 3 | import re 4 | import os 5 | 6 | inuser = boto3.session.Session(profile_name='default') 7 | client = inuser.client('cloudformation') 8 | response = client.list_exports() 9 | f1=open('temp.in', 'w+') 10 | l = response.get('Exports', {}) 11 | print >>f1, 'id,value' 12 | print >>f1, 'Templates,' 13 | for x in l: 14 | s = x['ExportingStackId'] 15 | t = x["Name"] 16 | print >>f1, '.'.join(['Templates',re.search('.*/(.*)/.*', s).group(1),t]) + ',' 17 | j = re.search('.*/(.*)/.*', s).group(1) 18 | print >>f1, '.'.join(['Templates',j]) + ',' 19 | try: 20 | if client.list_imports(ExportName=t): 21 | cl = client.list_imports(ExportName=t) 22 | im = cl["Imports"] 23 | for x in im: 24 | print >>f1, '.'.join(['Templates',j,t,x]) + ',' 25 | except: 26 | pass 27 | #for x in l: 28 | # s = x['ExportingStackId'] 29 | # print '.'.join(['Templates',re.search('.*/(.*)/.*', s).group(1)]) + ',' 30 | f1.close() 31 | 32 | lines_seen = set() 33 | outfile = open('flare.csv', "w") 34 | for line in open('temp.in', "r"): 35 | if line not in lines_seen: # not a duplicate 36 | outfile.write(line) 37 | lines_seen.add(line) 38 | outfile.close() 39 | 40 | os.remove('temp.in') 41 | -------------------------------------------------------------------------------- /get_data_sankey.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import boto3 3 | import re 4 | 5 | f1=open('sourcedata-output.json', 'w+') 6 | inuser = boto3.session.Session(profile_name='default') 7 | client = inuser.client('cloudformation') 8 | response = client.list_exports() 9 | l = response.get('Exports', {}) 10 | print >>f1, '{ "links": [' 11 | output1 = [] 12 | for x in l: 13 | s = x['ExportingStackId'] 14 | t = x["Name"] 15 | o1 = str('{"source":"' + re.search('.*/(.*)/.*', s).group(1) + '","target":"' + t + '","value":"1.0"}') 16 | output1.append(o1) 17 | try: 18 | if client.list_imports(ExportName=t): 19 | cl = client.list_imports(ExportName=t) 20 | im = cl["Imports"] 21 | for x in im: 22 | o2 = str('{"source":"' + t + '","target":"' + x + '","value":"1.0"}') 23 | output1.append(o2) 24 | except: 25 | pass 26 | print >>f1, ",\n".join(output1) 27 | print >>f1, '], "nodes": [' 28 | output2 = [] 29 | for x in l: 30 | s = x['ExportingStackId'] 31 | o1 = str('{"name":"' + re.search('.*/(.*)/.*', s).group(1) + '"}') 32 | output2.append(o1) 33 | for x in l: 34 | o2 = str('{"name":"' + x["Name"] + '"}') 35 | output2.append(o2) 36 | t = x["Name"] 37 | try: 38 | if client.list_imports(ExportName=t): 39 | cl = client.list_imports(ExportName=t) 40 | im = cl["Imports"] 41 | for z in im: 42 | o3 = str('{"name":"' + z + '"}') 43 | output2.append(o3) 44 | except: 45 | pass 46 | print >>f1, ",\n".join(list(set(output2))) 47 | print >>f1, ']}' 48 | f1.close() 49 | -------------------------------------------------------------------------------- /index-flare.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
31 | 32 | 33 | 34 | 147 | 148 | 149 |