In the last couple of posts, I outlined my approach to what I have called Infrastructure as (Actual) Code. In my new role on the Portworx team at Pure Storage, I’m regularly building and tearing down infrastructure in different cloud environments. As I waded back into IaC tooling, I thought the difference between using a programming language with an SDK from the cloud vendor and learning the Domain-specific Language of these tools would be small. So far, I have worked with the Python SDK for AWS (Boto3) and this week I want to talk about my experiences with the AWS GO SDK (v2).
For a typical Kubernetes cluster deployment to run Portworx, in AWS I need to deploy a number of things:
- VPC
- subnet in the VPC
- routing
- VMs
- kubernetes install
- portworx
To start, I’m deploying a VM into the default VPC for the region. Easy enough, right? The GO SDK for AWS provides the same capabilities as the Python SDK, but you have to get yourself in a Go kinda mindset. I’m not going to go deep into the differences on Go vs. Python, but from my perspective, I’ve always thought of Go as a language where you need to think about data structures first, and then the coding logic follows from that. Maybe that’s just me? All development is about taking data from one place and moving to another, but with Go I always find myself mapping the data structures out more carefully, rather than just calling functions to do a thing. More on this as we progress through the code.
To start, I’m going to load all of the libraries I need to deploy a VM.
import (
"context"
"fmt"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
)
I’m using context for loading the configurations – AWS’s config library needs me to pass it a context. fmt is here because – of course it is! and the rest are AWS libraries.
After the main function declaration, I’m going to load the AWS config and create a client. My local AWS cli is configured with my access ID, secret key, and region, and LoadDefaultConfig gets these values.
cfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
panic("Configuration Error, " + err.Error())
}
client := ec2.NewFromConfig(cfg)
The cfg, err line Loads the config into cfg and then we check the err to make sure we don’t have any issues. The client line creates the AWS client for use when we create the instances. The next step is to build some data structures with values that we are going to use running the instances. We need BlockDeviceMapping data and TagSpecification data to send into a function later to create an instance.
ebsbdm := []types.BlockDeviceMapping{
{
DeviceName: aws.String("/dev/sdd"),
Ebs: &types.EbsBlockDevice{
DeleteOnTermination: aws.Bool(true),
VolumeSize: aws.Int32(80),
VolumeType: types.VolumeTypeGp2,
},
},
}
tags := []types.TagSpecification{
{
ResourceType: "instance",
Tags: []types.Tag{
{
Key: aws.String("Owner"),
Value: aws.String("csaunders"),
},
},
},
}
I’m loading data into struct literals just to make things simple. The BlockDeviceMapping defines the name of the EBS device that will appear in the OS, and I add the parameters to make sure the device is deleted when the VM is terminated. I added the size of the volume and the type of GP2. The TagSpecification includes the specification for the tags I want to apply to the VM. I broke these out as I was thinking about building a data load type function when I build a larger app that builds multiple VMs, etc. The next step here is to create a variable from the ec2.RunInstancesInput type and load with the rest of the data for the VM config.
input := ec2.RunInstancesInput{
ImageId: aws.String("ami-0b18956f"),
InstanceType: types.InstanceTypeT3Large,
MinCount: aws.Int32(1),
MaxCount: aws.Int32(1),
EbsOptimized: aws.Bool(true),
BlockDeviceMappings: ebsbdm,
TagSpecifications: tags,
// UserData: aws.String(""),
KeyName: aws.String("ssh-keyname"),
}
The RunInstancesInput type variable I created here includes all of the data required to create an instance. Loading the data here makes it super clear how the VMs are being built. In each case, the type actually expects pointers to the data, rather than the data itself. I’m not 100% sure why, but I’m not an API designer! Fortunately, the aws library includes functions to take a value and return a pointer to the value, which makes things simpler. This is why in the assignments you see aws.String , aws.Int32 and aws.Bool – they return a pointer to the values.
ImageId is the AMI id that I am using – this is HVM optimized Amazon Linux AMI in the Canada Central region. InstanceType defines the type of image I am deploying. In this case it’s a t3.large instance. MinCount and MaxCount controls the number of instances we are creating. This could easily be updated to deploy multiple nodes. EbsOptimized tells AWS this VM needs to be EBS optimized. BlockDeviceMappings and TagSpecifications we have reviewed. UserData can be used to funnel a bunch of post OS configuration details. KeyName is the keypair that has already been defined in AWS, in this region.
Once the input has been defined, creating the VM is as simple as running the RunInstances function and providing the parameters.
result, err := client.RunInstances(context.TODO(), &input)
if err != nil {
panic("Error creating instances " + err.Error())
}
This is just classic Go – result captures the output from running the RunInstances function feeding the function a context and a pointer to the input variable build with all of the data required. err captures any error for a panic message.
The result of this code is that one VM is created in the default VPC defined by the local AWS CLI configuration. In total, I have around 60 lines of code that will build a VM. This was what I was looking to accomplish – see what it takes to deploy a VM with just code! Is this easier or more difficult than using an IaC tool? The full code is below:
package main
import (
"context"
"fmt"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
)
func main() {
cfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
panic("Configuration Error, " + err.Error())
}
client := ec2.NewFromConfig(cfg)
ebsbdm := []types.BlockDeviceMapping{
{
DeviceName: aws.String("/dev/sdd"),
Ebs: &types.EbsBlockDevice{
DeleteOnTermination: aws.Bool(true),
VolumeSize: aws.Int32(80),
VolumeType: types.VolumeTypeGp2,
},
},
}
tags := []types.TagSpecification{
{
ResourceType: "instance",
Tags: []types.Tag{
{
Key: aws.String("Owner"),
Value: aws.String("csaunders"),
},
},
},
}
input := ec2.RunInstancesInput{
ImageId: aws.String("ami-0b18956f"),
InstanceType: types.InstanceTypeT3Large,
MinCount: aws.Int32(1),
MaxCount: aws.Int32(1),
EbsOptimized: aws.Bool(true),
BlockDeviceMappings: ebsbdm,
TagSpecifications: tags,
// UserData: aws.String(""),
KeyName: aws.String("ssh-keyname"),
}
result, err := client.RunInstances(context.TODO(), &input)
if err != nil {
panic("Error creating instances " + err.Error())
}
}
