How to Provision a New AWS EC2 Instance with PowerShell

EC2 is a wildly popular AWS service and is bound to be one you either have a lot of experience with or will at some point. One of the most basic tasks you can perform in EC2 is provisioning new instances. Deploying EC2 instances is fairly straightforward if using the management console. The management console walks you through all of the dependencies that an EC2 instance requires.
However, all of those dependencies are up to you to set up and define yourself if you’re setting up some automation script. In that case, you need to become familiar with not only setting up the EC2 instance itself but also all of the other resources that an EC2 instance depends on like a VPC, optional internet gateway, a route table, route, AMI image, route table, and a few more.
By understanding now just how to deploy an EC2 instance but also understanding everything else required will allow you to create a much smarter automation script.
In this article, we’re going to cover how to provision many common components that an EC2 instance needs as well as the EC2 instance itself. You’ll learn how each of these components fit together and take a peek at the PowerShell code necessary to deploy an EC2 instance from scratch with PowerShell.
Before we get too far, though, I’m going to be assuming that you already have an AWS account and have already installed the AWSPowerShell module available by running Install-Module AWSPowerShell in your PowerShell console. I’m also going to assume you’ve already been authenticated with Set-AWSCredential or some other means in your profile to make you ready to begin making changes in your AWS account with various PowerShell commands.
By the end of this article, you should have a running EC2 instance running Windows Server 2016 with a public DNS name that you can connect to via the Internet.
Let’s get started!

The Virtual Private Cloud (VPC)

In AWS-land, we don’t speak of vNets. Instead, we work with Virtual Private Clouds or VPCs. A VPC is similar to an Azure vNet in that it’s a network fabric that allows the virtual machines to connect with the rest of the cloud.
To keep the configuration as similar to Azure as possible, we’re not going to do anything fancy here. We’re simply going to create a VPC with a single subnet at it’s most basic level to replicate the same settings an Azure vNet might have.
To get started, we first need to know the network we’d like to create. I’ll create a network 10.10.0.0/24 just as an example. Once I know the network to create, I can then create it using the New-EC2Vpc command.
PS> $network = '10.0.0.0/16'
PS> $vpc = New-EC2Vpc -CidrBlock $network
PS> $vpc

CidrBlock                   : 10.0.0.0/24
CidrBlockAssociationSet     : {vpc-cidr-assoc-03f1edbc052e8c207}
DhcpOptionsId               : dopt-3c9c3047
InstanceTenancy             : default
Ipv6CidrBlockAssociationSet : {}
IsDefault                   : False
State                       : pending
Tags                        : {}
VpcId                       : vpc-03e8c773094d52eb3

Once we’ve got the VPC created, to mirror the Azure vNet, we must manually enable DNS support which Azure did for us automatically. This will point any EC2 instance’s DNS server attached to this VPC to an internal Amazon DNS server. We do this by enabling DNS support. Since Azure also, by default, gives us a public hostname and AWS does not, we must manually assign this as well by enabling DNS hostnames. You can see below that we are doing this with the Edit-EC2VpcAttribute command.
PS> Edit-EC2VpcAttribute -VpcId $vpc.VpcId -EnableDnsSupport $true
PS> Edit-EC2VpcAttribute -VpcId $vpc.VpcId -EnableDnsHostnames $true

The Internet Gateway

Next, we must create the Internet gateway. This allows our EC2 instance to route traffic to and from the Internet. Unlike Azure, again, we’ll need to manually create this functionality using the New-EC2InternetGateway command.
PS> $gw = New-EC2InternetGateway
PS> $gw
Attachments InternetGatewayId     Tags
----------- -----------------     ----
{}          igw-05ca5aaa3459119b1 {}
Now that we have the internet gateway created, we then need to attach it to our VPC to allow EC2 instances connected to that VPC to route traffic through it. We do that by using the Add-EC2InternetGateway command.
PS> Add-EC2InternetGateway -InternetGatewayId $gw.InternetGatewayId -VpcId $vpc.VpcId

Routes

Once the gateway is created, we’ll then need to create a route table and a route so that the EC2 instances on our VPC can access the Internet. To do that, we must first create a route table. A route table is required to eventually create the route in. We do this by using the New-EC2RouteTable command.
PS> $rt = New-EC2RouteTable -VpcId $vpc.VpcId
PS> $rt
Associations    : {}
PropagatingVgws : {}
Routes          : {}
RouteTableId    : rtb-09786c17af32005d8
Tags            : {}
VpcId           : vpc-03e8c773094d52eb3
Listing X-12: Creating a route table

Next, we’ll create a route inside of the route table that points to the gateway we just created. Since I’m creating a “default” route or “default gateway” if you’re familiar with the Windows client term, I’ll be routing all traffic (0.0.0.0/0) through our internet gateway. The command will return True if successful.
PS> New-EC2Route -RouteTableId $rt.RouteTableId -GatewayId $gw.InternetGatewayId -DestinationCidrBlock '0.0.0.0/0'

True

Subnet

Next, we must create a subnet inside of our larger VPC and associate it with our route table. To do this, we’ll use the New-EC2Subnet command to create the subnet and once created, we can then use the Register-EC2RouteTable command to register this subnet to the route table we built earlier. But first, we’ll need to define an availability zone for the subnet. If you’re not sure what to use, we can use the Get-EC2AvailabilityZone command to enumerate all of them. For our purposes, we’ll be using the us-east-1d availability zone.
PS> Get-EC2AvailabilityZone
Messages RegionName State     ZoneName
-------- ---------- -----     --------
{}       us-east-1  available us-east-1a
{}       us-east-1  available us-east-1b
{}       us-east-1  available us-east-1c
{}       us-east-1  available us-east-1d
{}       us-east-1  available us-east-1e
{}       us-east-1  available us-east-1f

Listing X-14: Enumerating EC2 availability zones
Once we know the availability zone to use, we can then create the EC2 subnet and register it with the route table.
PS> $sn = New-EC2Subnet -VpcId $vpc.VpcId -CidrBlock '10.0.1.0/24' -AvailabilityZone 'us-east-1d'
PS> Register-EC2RouteTable -RouteTableId $rt.RouteTableId -SubnetId $sn.SubnetId
rtbassoc-06a8b5154bc8f2d98
Once we have a subnet created, we’re done with the network stack!

Assigning an AMI to our EC2 instance

Once we’ve got the network stack all built out for our EC2 instance, we can now get to assigning an AMI or Amazon Machine Instance to our VM. To do that, we first need to find an existing AMI that suits our needs. I’d like to create a Windows Server 2016 instance so I’ll first need to figure out the name of the instance. I can enumerate all of the available instances with the Get-EC2ImageByName command. When I do that, I will find an image name called WINDOWS_2016_BASE. This is what we’re going to use.
Once I know the image name, I’ll then use Get-EC2ImageByName again and this time specify the image we’d like to use. This then provides the image object we need to pass to New-EC2Instance to create our EC2 instance.
PS> $ami = Get-EC2ImageByName -Name 'WINDOWS_2016_BASE'
PS> $ami
Architecture        : x86_64
BlockDeviceMappings : {/dev/sda1, xvdca, xvdcb, xvdcc...}
CreationDate        : 2018-08-15T02:27:20.000Z
Description         : Microsoft Windows Server 2016 with Desktop Experience Locale English AMI provided by Amazon
EnaSupport          : True
Hypervisor          : xen
ImageId             : ami-0b7b74ba8473ec232
ImageLocation       : amazon/Windows_Server-2016-English-Full-Base-2018.08.15
ImageOwnerAlias     : amazon
ImageType           : machine
KernelId            :
Name                : Windows_Server-2016-English-Full-Base-2018.08.15
OwnerId             : 801119661308
Platform            : Windows
ProductCodes        : {}
Public              : True
RamdiskId           :
RootDeviceName      : /dev/sda1
RootDeviceType      : ebs
SriovNetSupport     : simple
State               : available
StateReason         :
Tags                : {}
VirtualizationType  : hvm
Great! We’ve now got our image captured and ready to go. Next up and most importantly, finally, we can create our EC2 instance using the New-EC2Instance command. To do that, we’re going to need the instance type. Unfortunately, the instance types aren’t available via an AWS API, thus, no PowerShell cmdlet is available, but you can check out all of them on this AWS page. I’m cheap, so I’ll choose the free one which is t2.micro.
PS> $params = @{
>>      ImageId = $ami.ImageId
>>      AssociatePublicIp = $false
>>      InstanceType = 't2.micro'
>>      SubnetId = $sn.SubnetId
}
PS> New-EC2Instance @params

GroupNames    : {}
Groups        : {}
Instances     : {}
OwnerId       : 013223035658
RequesterId   :
ReservationId : r-05aa0d9b0fdf2df4f
Once this runs, you will now see a brand new EC2 instance available to you in your AWS Management Console or by running Get-EC2Instance.

Wrap up

We’ve now nailed down the code to create the EC2 instance, but, as-is, it’s cumbersome to use. Because creating an EC2 instance may be a frequent occurrence, it’s a good practice to create a script or function to do all of this for us. You can download an example function called New-EC2 Custom Instance from the TechSnips GitHub repo.
When the function is called, and all dependencies already exist except for the EC2 instance itself, we will see output similar to Listing X-46 when ran with the Verbose parameter. The function is a lot smarter than simply running code to create all of these resources alone. Below you can see an example of it being used.
PS> $parameters = @{
>>      VpcCidrBlock = '10.0.0.0/16'
>>      EnableDnsSupport = $true
>>      SubnetCidrBlock = '10.0.1.0/24'
>>      OperatingSystem = 'Windows Server 2016'
>>      SubnetAvailabilityZone = 'us-east-1d'
>>      InstanceType = 't2.micro'
>>      Verbose = $true
}
PS> New-CustomEC2Instance @parameters

VERBOSE: Invoking Amazon Elastic Compute Cloud operation 'DescribeVpcs' in region 'us-east-1'
VERBOSE: A VPC with the CIDR block [10.0.0.0/16] has already been created.
VERBOSE: Enabling DNS support on VPC ID [vpc-03ba701f5633fcfac]...
VERBOSE: Invoking Amazon EC2 operation 'ModifyVpcAttribute' in region 'us-east-1'
VERBOSE: Invoking Amazon EC2 operation 'ModifyVpcAttribute' in region 'us-east-1'
VERBOSE: Invoking Amazon Elastic Compute Cloud operation 'DescribeInternetGateways' in region 'us-east-1'
VERBOSE: An internet gateway is already attached to VPC ID [vpc-03ba701f5633fcfac].
VERBOSE: Invoking Amazon Elastic Compute Cloud operation 'DescribeRouteTables' in region 'us-east-1'
VERBOSE: Route table already exists for VPC ID [vpc-03ba701f5633fcfac].
VERBOSE: A default route has already been created for route table ID [rtb-0b4aa3a0e1801311f rtb-0aed41cac6175a94d].
VERBOSE: Invoking Amazon Elastic Compute Cloud operation 'DescribeSubnets' in region 'us-east-1'
VERBOSE: A subnet has already been created and registered with VPC ID [vpc-03ba701f5633fcfac].
VERBOSE: Invoking Amazon EC2 operation 'DescribeImages' in region 'us-east-1'
VERBOSE: Creating EC2 instance...
VERBOSE: Invoking Amazon EC2 operation 'RunInstances' in region 'us-east-1'

GroupNames    : {}
Groups        : {}
Instances     : {}
OwnerId       : 013223035658
RequesterId   :
ReservationId : r-0bc2437cfbde8e92a
We now have the tools we need to automate the provisioning of EC2 instances in AWS!

Popular posts from this blog

"Amazon.Lambda.RuntimeSupport" .NET Core with AWS Lambda (for Microsoft Developers)

Working with AWS Fargate (Lots of fun)

Azure to AWS services comparison/migration