Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make CloudFormation easier to compose #127

Closed
errordeveloper opened this issue Jul 18, 2018 · 6 comments · Fixed by #132
Closed

Make CloudFormation easier to compose #127

errordeveloper opened this issue Jul 18, 2018 · 6 comments · Fixed by #132
Labels
kind/feature New feature or request

Comments

@errordeveloper
Copy link
Contributor

errordeveloper commented Jul 18, 2018

At the moment we have a number of issues where compassable CloudFormation would help (#42, #69, #120, #122).

Managing templates as YAML and embedding them in Go using bindata is not particularly delightful, but not an issue per-se. What's hard is piecing together Go code and any logic that is implemented with CloudFormation intrinsic functions, stack parameters and conditions.

Additionally, it would be much more convenient if we have a single stack composed of optional parts (VPC, IAM role, control plane), and separate (nested?) stacks for nodegroups. This would simplify much of our code.

So we could either invent something ourselves, if we wanted to, but I found that goformation already does what's needed (and I made an improvement (awslabs/goformation#95, awslabs/goformation#103) to it that allows to use Refs and other intrinsics).

The idea would be to compose the stack using Go structs. First of all we would use Go instead of CloudFormation for conditionals, as it's much easier to grasp all the parameters. We would leverage some CloudFormation runtime functionality to the minimum extended needed, but anything that would take more then one nested intrinsic function would be done in Go, unless information is only available inside of CloudFormation runtime (e.g. resource IDs for VPCs, subnets etc).

@errordeveloper errordeveloper added the kind/feature New feature or request label Jul 18, 2018
@errordeveloper
Copy link
Contributor Author

errordeveloper commented Jul 18, 2018

@Raffo
Copy link

Raffo commented Jul 21, 2018

I've never used goformation myself, but it definitely look promising and I am generally against having too many big yamls that gets too complicated... so going "code first" would be IMO a very good thing (once your PR upstream is merged).

@errordeveloper
Copy link
Contributor Author

errordeveloper commented Jul 22, 2018

I have started on this in #132, here is what it begins to look like:

func makeRef(refName string) interface{} {
	return map[string]string{"Ref": refName}
}

type clusterResourceSet struct {
	template *cloudformation.Template
	vpcRefs  *resourceRefsForVPC
}

type resourceRefsForVPC struct {
	vpc            interface{}
	subnets        []interface{}
	securityGroups []interface{}
}

func newClusterResourceSet() *clusterResourceSet {
	return &clusterResourceSet{
		template: cloudformation.NewTemplate(),
	}
}

func (c *clusterResourceSet) newResource(name string, resource interface{}) interface{} {
	c.template.Resources[name] = resource
	return makeRef(name)
}

func (c *clusterResourceSet) addResourcesForVPC(globalCIDR *net.IPNet, subnets map[string]*net.IPNet) {
	refs := &resourceRefsForVPC{}
	refs.vpc = c.newResource("VPC", &cloudformation.UntypedAWSEC2VPC{
		CidrBlock:          globalCIDR.String(),
		EnableDnsSupport:   true,
		EnableDnsHostnames: true,
	})

	refIG := c.newResource("InternetGateway", &cloudformation.AWSEC2InternetGateway{})
	c.newResource("VPCGatewayAttachment", &cloudformation.UntypedAWSEC2VPCGatewayAttachment{
		InternetGatewayId: refIG,
		VpcId:             refs.vpc,
	})

	refRT := c.newResource("RouteTable", &cloudformation.UntypedAWSEC2RouteTable{
		VpcId: refs.vpc,
	})

	c.newResource("Route", &cloudformation.UntypedAWSEC2Route{
		RouteTableId:         refRT,
		DestinationCidrBlock: "0.0.0.0/0",
		GatewayId:            makeRef("InternetGateway"),
	})

	for az, subnet := range subnets {
		refSubnet := c.newResource("Subnet_"+az, &cloudformation.UntypedAWSEC2Subnet{
			AvailabilityZone: az,
			CidrBlock:        subnet.String(),
			VpcId:            refs.vpc,
		})
		c.newResource("RouteTableAssociation_"+az, &cloudformation.UntypedAWSEC2SubnetRouteTableAssociation{
			SubnetId:     refSubnet,
			RouteTableId: refRT,
		})
		refs.subnets = append(refs.subnets, refSubnet)
	}

	refSG := c.newResource("ControlPlaneSecurityGroup", &cloudformation.UntypedAWSEC2SecurityGroup{
		GroupDescription: "Cluster communication with worker nodes",
		VpcId:            refs.vpc,
	})
	refs.securityGroups = []interface{}{refSG}

	c.vpcRefs = refs
}

So as you can see, we can probably avoid most of parameter passing and substitute parameters directly, and only use references where it's absolutely necessary.

@nckturner
Copy link
Contributor

+1, This seems like it would be really useful as things get more complicated.

@richardcase
Copy link
Contributor

I think this approach is good, its better for composability and testing.

I'm wondering if we could you use a 'builder' and/or fluent interface around goformat for composing the the template?

@errordeveloper
Copy link
Contributor Author

@richardcase if you have any specific thoughts around that, please comment in #132.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind/feature New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants