The central principle of cloud engineering is adopting software engineering practices. Refactoring is a technique for making changes to code that improve maintainability, enhance performance, scalability, and security without changing its external behavior. In devops, refactoring often occurs with modern applications; however, we can apply those same techniques to cloud infrastructure with [infrastructure as code](/what-is/what-is-infrastructure-as-code/).
Refactoring results in many advantages. First and foremost, the code is more readable and easier to understand for other team members –this aids in maintainability and well-organized code, providing a solid foundation for future releases. Overall, if done well, refactoring reduces complexity which makes future changes more efficient.
The topic of refactoring is a broad topic covering many different techniques. Many references, such as [Martin Fowler's Refactoring](https://martinfowler.com/books/refactoring.html), cover refactoring in depth. In this article, we examine standard practices used with infrastructure as code.
A major difference between refactoring application and infrastructure code is that resources, such as databases persist. Typically, if you change the name, type, or parent path of a resource, it is deleted and a new resource is created. Other [infrastructure as code](/what-is/what-is-infrastructure-as-code/) solutions tie a file or module to a resource's identity. An advantage of Pulumi is that moving a resource across file boundaries does not recreate the resource because a resource’s identity is based on its object ID, irrespective of its file location.
In cases where an object’s ID changes -- due to a rename, reparenting to a new component, or otherwise -- Pulumi provides [aliases](/docs/concepts/resources#aliases). An `Alias` accepts a list of identifiers that can include old `Alias` objects or old resource URNs. When a resource is annotated with an alias, Pulumi can connect the old to the new object state, and the resource can be updated in place.
A similar situation occurs with [`component resources`](/docs/concepts/resources#components), i.e., a logical grouping of related resources such as `VPC` with subnets pre-configured. When you change a parent resource of a component resource, the identity of the component resource also changes. Pulumi provides a [`parent`](/docs/concepts/resources#components) option to tie a child resource to the parent resource. The combination of `alias` and `parent` arguments to resources
In a previous blog post, Ringo de Smeti used TDD to [refactor Cumundi's code for provisioning repositories by customer](/blog/cumundi-guest-post). Ringo created a ComponentResource subclass which moved individual resources into a customer resource class.
Calling the `Project` class creates a new customer.
```typescript
const firstCustomer = new customer.Project("FirstCustomer",
{
customer: 'First Customer',
needsGoogleInfra: false,
gitlabNamespace: gitlabNamespace
}
)
const secondCustomerProject = new customer.Project('SecondCustomer',
{
customer: 'Second Customer',
needsGoogleInfra: true,
gitlabNamespace: gitlabNamespace
}
)
```
By using TDD, Ringo reduced code duplication, resulting in a clean and efficient codebase.
## Refactoring by Abstraction: Pull-Up/Push-Down
The Pull-Up/Push-Down method is used frequently to reduce duplicate code. It does this by abstracting methods into classes to implement inheritance and extraction. The Pull-Up method uses a superclass to consolidate similar methods, whereas Push-Down moves methods from a superclass into subclasses.
The Pull-Up method in this example deploys the [Kubernetes Guest Book](https://github.com/pulumi/examples/tree/master/kubernetes-ts-guestbook). The simple version is a port of the original YAML and the component version, shown below, creates a service class that creates both the service and deployment.
```typescript
export class ServiceDeployment extends pulumi.ComponentResource {
public readonly deployment: k8s.apps.v1.Deployment;
public readonly service: k8s.core.v1.Service;
public readonly ipAddress?: pulumi.Output<string>;
Composing is another refactoring method for reducing duplicate code. The most common way to use composing is through extraction and inline methods.
Extraction consolidates code by creating a new method and replacing duplicate methods with a call to the new method. For example, when we create services, we often export information about the service, such as IP address or hostname, as shown in the example below.
```typescript
export const publicIp = server.publicIp;
export const publicHostName = server.publicDns;
```
We can create a function that exports the information about a service and call it as needed.
```typescript
function public_server_info(server) {
export const publicIp = server.publicIp;
export const publicHostName = server.publicDns;
}
public_server_info(server);
```
Inline refactoring reduces unnecessary methods by finding all calls to the method and replacing them with the content of the method. In this policy as code example, we can refactor two methods into a single method.
```typescript
export function requireEbsVolumesOnEc2Instances(name: string): ResourceValidationPolicy {
// TODO: Check if EBS volumes are marked for deletion.
return {
name: name,
description: "EBS volumes should be attached to all EC2 instances",
if (instance.ebsBlockDevices !== undefined && instance.ebsBlockDevices.length === 0) {
reportViolation("EC2 Instance should have EBS volumes attached.");
}
}),
};
}
```
## Simplifying Method Calls
Simplifying method calls makes them easier to understand by clarifying the logic for interaction between classes. One way to simplify code is to add, remove, or introduce new parameters with explicit methods and method calls.
Roles in AWS are typically stored as JSON documents. In the example below, we create the role by passing in a string.
```typescript
lambda_role = iam.Role('lambdaRole',
assume_role_policy="""{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}"""
)
sfn_role = iam.Role('sfnRole',
assume_role_policy="""{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "states.%s.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}""" % config.region
)
```
You can write a function called `create_role` and pass in the name and policy as JSON to create many roles.
Moving features creates new classes and moves functionality between old and new classes. A utility class may not have some methods you need, but you can't add those methods to that class. The solution is to create a new class with the methods you need.
In this [example](https://github.com/pulumi/examples/tree/master/aws-ts-ec2-provisioners), we extend a class and add new methods that configure an EC2 instance post-provisioning.
```typescript
export class Provisioner<T,U> extends pulumi.dynamic.Resource {
Preparatory refactoring occurs when adding a new feature to a release. It is typically part of a software update instead of a separate refactoring process. Preparatory refactoring reduces future technical debt.
## Conclusion
Writing clean and efficient code is part of cloud engineering, and we can achieve this by using well-established refactoring methods used by application developers. Pulumi has many easy-to-understand examples in Node.js, Python, Go, and .Net. Because the examples are written explicitly to make them understandable, some could use refactoring. What code samples can you find in the [Pulumi Github repository](https://github.com/pulumi/examples) that could use some refactoring?