Functions/New-CmEC2Instance.ps1

Function New-CmEC2Instance           {
    <#
.Synopsis
    Creates a Windows EC2 On demand or spot Instance with minimal input.
.DESCRIPTION
    Creates a Windows EC2 On demand or spot Instance and, if a Name and DomainName are specified, creates or updates the Route 53 DNS entry for the instance and applies a Name Tag.
 
.NOTES
    Name: New-CMEC2Instance
    Author: Chad Miles
    DateUpdated: 2017-05-02
    Version: 1.2.0
 
.EXAMPLE
    C:\> New-CMEC2Instance -InstanceType t2.micro -Region us-east-1 -DomainName mydomain.com -Name MyInstance
     
    InstanceID : i-1234567890abcdef
    Region : us-east-1
    Name : MyInstance
    Hostname : MyInstance.Mydomain.com
    InstanceType : t2-micro
    BidPrice :
    OnDemandPrice : 0.017
    Savings : 0 %
    ImageName : WINDOWS_2016_BASE
    ImageID : ami-58a1a73e
    KeyName : MyKeyPair
     
.EXAMPLE
    C:\> New-CMEC2Instance -InstanceType t2.micro -Region us-east-1 -Name MyInstance
     
    InstanceID : i-1234567890abcdef
    Region : us-east-1
    Name : MyInstance
    Hostname : ec2-34-248-2-178.eu-west-1.compute.amazonaws.com
    InstanceType : t2-micro
    ImageName : WINDOWS_2016_BASE
    ImageID : ami-58a1a73e
    KeyName : MyKeyPair
 
.EXAMPLE
    C:\> New-CMEC2Instance -InstanceType t2.micro -Region us-east-1 -Name MyInstance -DomainName mydomain.com -OSVerion 2012R2
     
    InstanceID : i-1234567890abcdef
    Region : us-east-1
    Name : MyInstance
    Hostname : MyInstance.Mydomain.com
    InstanceType : t2-micro
    ImageName : WINDOWS_2012R2_BASE
    ImageID : ami-40003a26
    KeyName : MyKeyPair
 
.EXAMPLE
    C:\> New-CMEC2Instance -InstanceType t2.micro -Region us-east-1 -Name MyInstance -DomainName mydomain.com -SpotRequest
 
    InstanceID : i-1234567890abcdef
    Region : us-east-1
    Name : MyInstance
    Hostname : MyInstance.Mydomain.com
    InstanceType : t2-micro
    ImageName : WINDOWS_2016_BASE
    ImageID : ami-40003a26
    KeyName : MyKeyPair
 
.EXAMPLE
    C:\> New-CMEC2Instance -InstanceType m3.medium -Region us-east-1 -Name TestInstance -DomainName mydomain.com -RootVolumeSize 50 -SecondaryVolumeSize 100
     
    InstanceID : i-1234567890abcdef
    Region : us-east-1
    Name : TestInstance
    Hostname : TestInstance.Mydomain.com
    InstanceType : m3.medium
    ImageName : WINDOWS_2016_BASE
    ImageID : ami-58a1a73e
    KeyName : MyKeyPair
 
#>

    [CmdletBinding( 
        SupportsShouldProcess   = $true,
        DefaultParameterSetName = 'SearchBaseIds')]
    Param (
        [ValidateScript({@((Get-AWSRegion).Region)})]
        [string] $Region,
        [Parameter(Mandatory=$true)]
        [Alias("Type")] 
        [String] $InstanceType,
        # Applies this name tag to the instance after creation, if -DomainName is specified as well then registers a DNS CNAME for your instance using this name
        [string] $Name,
        # E.g mydomain.com. Must be used with -Name, then a DNS CNAME is registered, provided it is a Route53 hosted zone and you have rights.
        [string] $DomainName,
        # Version of Windows e.g. 2012R2 or 2016. Default is 2016
        [ValidateSet(
            "WindowsServer22H2", 
            "WindowsServer21H2",  
            "WindowsServer2022",
            "WindowsServer2019",
            "WindowsServer2016",
            "WindowsServer2012R2",
            "Ubuntu18.04",
            "Ubuntu20.04",
            "Ubuntu22.04",
            "AmazonLinux2023",
            "AmazonLinux2022",
            "AmazonLinux2",
            "AmazonLinux2NetCore"
        )]
        [Parameter(ParameterSetName='SearchImageIds')]
        [string] $OsVersion = "WindowsServer2022",

        [Parameter(ParameterSetName='SearchImageIds')]
        [ValidateSet("2022","2019","2017","2016","2014")]
        [string] $SqlVersion,

        [Parameter(ParameterSetName='SearchImageIds')]
        [ValidateSet("Express", "Web","Standard","Enterprise")]
        [string] $SqlEdition = "Standard",

        [switch] $Core,
        # Instance Profile (with IAM Role) to attach to new Instance
        [string] $InstanceProfile,
        # Path to User data file , using Your My Documents folder as a root
        [string] $UserData,
        #Specify an AMI id like ami-2b8c8452
        [Parameter(
            ValueFromPipeline               = $true,
            ValueFromPipelineByPropertyName = $true,
            ParameterSetName                = 'ImageId',
            Mandatory                       = $true)]
        [ValidatePattern("^ami-([\da-f]{8}|[\da-f]{17})$")]
        [string] $ImageId,
        # The name of the Security Group, not the Security Group ID. The Function will get the ID. If none is specified the default Security Group is used.
        [string] $SecurityGroupName,
        # Switch to specify that a Spot instance should be created instead of On Demand. Bid price will be automatically calculated based on current bid price plus a markup percentage of which the default is 1.
        [switch] $SpotInstance,
        # The Name of the EC2 KeyPair to use when creating the instance
        [string] $KeyName,
        # The ID of the VPC you wish to use, if not specified, the default one is used.
        [ValidatePattern("^vpc-([\da-f]{8}|[\da-f]{17})$")]
        [string] $VpcId,
        # Last letter of the Availability Zone like a, b, c, d or e, if none specified, a random one is used
        [string] $AZSuffix,
        # The Subnet in which to launch the Instance(s). Overrides VpcId if and AZsuffix parameters if specified. If not specified then a random subnet is chosen in the default VPC.
        [ValidatePattern("^subnet-([\da-f]{8}|[\da-f]{17})$")]
        [string] $SubNetId,
        [ValidateRange(1,15)]
        [Int]    $NetworkInterfaces = 1,
        [Int]    $Count = 1,
        # Specifies the Root EBS volume size in GB
        [Int]    $RootVolumeSize,
        # Speciies the Secondary EBS volume size if there is one present in the AMI. If not it will create a new volume and attach it.
        [Int]    $SecondaryVolumeSize,
        # Default is to Keep the secondary volume on instance terminations, but set this switch to kill it
        [switch] $TerminateSecondaryVolume,
        # The AWS CLI/SDK Credential Profile to use
        [string] $ProfileName


    )
    BEGIN 
    {
        $ErrorActionPreference    = "Stop"
        if ($AZSuffix){
            if (-not $Region) {$Region = (Get-DefaultAWSRegion).Region}
            if (-not $Region) {try {$Region = (Get-EC2InstanceMetadata -Category Region).SystemName} catch {}}
            $AvailabilityZone      = $Region+$AZSuffix
        }        
        $Architecture = (Get-EC2InstanceType -InstanceType $InstanceType).ProcessorInfo.SupportedArchitectures[0]
        Write-Verbose "Got Architecture $Architecture"
        $GeneralParams = @{}
        If ($Region)     { $GeneralParams.Region      = $Region}
        If ($ProfileName){ $GeneralParams.ProfileName = $ProfileName}
    }
    Process {
        if ($SqlVersion) {
            if ($InstanceType -like "t*.*" -and $SqlEdition -ne "Express") {
                Write-Error "Non Express editions of SQL Server are not permited to run on T2/T3 instance types"
            }
        }
        
        if (-not $ImageId) {
            Write-Verbose  "Getting Current AMI for Selected OS $OsVersion and Architecture $Architecture"
            $ImageParam  = @{
                OsVersion    = $OsVersion
                Architecture = $Architecture
            }
            If ($Core)       { $ImageParam.Core        = $true}
            If ($SqlVersion) { $ImageParam.SqlVersion  = $SqlVersion}
            If ($SqlEdition) { $ImageParam.SqlEdition  = $SqlEdition}
            $ImageId = ($Image = Get-CMEC2ImageId @ImageParam @GeneralParams).ImageId
        } else {
            $ImageParam      = @{ImageId = $ImageId}
            If ($Region)     { $ImageParam.Region      = $Region}
            If ($ProfileName){ $ImageParam.ProfileName = $ProfileName}
            try {$Image  = Get-EC2Image @ImageParam @GeneralParams}
            Catch {}
        }
        if (-not $ImageId -or -not $Image) { Write-Error "Could not find an image with Search criteria in region $Region"}
        Write-Verbose "Got image $ImageId ($($Image.Name))"
        if (-not $Keyname) {
            Write-Verbose  "Getting first KeyPair for Region"
            $KeyName  = (Get-EC2KeyPair @GeneralParams)[0].KeyName
    }
        if (-not $KeyName)    {Write-Error "No EC2 Key Pairs found in region $Region, please create one"}
        If ($UserData)    {$UserData64 = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($UserData))}
        If (-not $SubNetId) { 
            If (-not $VpcId)   
            {
                Write-Verbose   "Getting default VPC"
                $VpcId  = (Get-EC2Vpc @GeneralParams| Where-Object {$_.IsDefault -eq $true}).VpcId
            }
            If (-not $VpcId)   { Write-Error "Could not find default VPC in region $Region, Please specify either a VPC or a Subnet Id" }
            Write-Verbose       "Getting Subnet for name $AvailabilityZone"
            $SubNets = Get-EC2Subnet @GeneralParams -Filter @{Name='vpc-id';Values=$VpcId}
            If ($AvailabilityZone) {$SubNetId = ( $SubNets | Where-Object {$_.AvailabilityZone -eq $AvailabilityZone})[0].SubnetId}
            else {$SubNetId = ($SubNets | Where-Object {$_.VpcId -eq $VPCid})[(Get-Random -Maximum $($SubNets.Count-1))].SubnetId}
        } Else {
            $VpcId = (Get-EC2Subnet @GeneralParams -SubnetId $SubNetId).VpcId
        }
        If (-not $VpcId) {Write-Error "Could not determine VPC, check you have a default VPC in the region and if SubnetId is specified, make sure it is valid"}
    
        If ($SecurityGroupName)  {
            Write-Verbose       "finding Security Group named $SecurityGroupName"
            $SecurityGroupId   = (Get-EC2SecurityGroup @GeneralParams | Where-Object {$_.GroupName -eq $SecurityGroupName -and $_.VpcId -eq $VpcId})[0].GroupId
            If (!$SecurityGroupId) {Write-Warning "Security Group with $SecurityGroupName cannot be found, using default"}
        } 
        if (-not $SecurityGroupId) {
            Write-Verbose       "Getting default Security Group for VPC $VpcId"
            $SecurityGroupId   = (Get-EC2SecurityGroup @GeneralParams | Where-Object {$_.GroupName -eq "default" -and $_.VpcId -eq $VPCId})[0].GroupId
            If (!$SecurityGroupId) {Write-Error "No Default security group found in $VpcId"}
        }
        If ($RootVolumeSize){
            $BDMs = $Image.BlockDeviceMappings
            ($BDMs | Where-Object {$_.DeviceName -eq $Image.RootDeviceName}).EBS.VolumeSize = $RootVolumeSize
        }
        If ($SecondaryVolumeSize){
            If (-not $BDMs) {$BDMs = $Image.BlockDeviceMappings}
            Try {
                ($BDMs | Where-Object {$_.DeviceName -ne $Image.RootDeviceName -and $null -ne $_.Ebs})[0].EBS.VolumeSize = $SecondaryVolumeSize
            } Catch {
                $SecondaryBdm = [Amazon.EC2.Model.BlockDeviceMapping]@{
                    DeviceName = if ($Image.Platform -eq "Windows"){"xvdb"}
                        Else {"/dev/sdb"}
                    Ebs = [Amazon.EC2.Model.EbsBlockDevice]@{
                        VolumeSize          = $SecondaryVolumeSize
                        DeleteOnTermination = ($TerminateSecondaryVolume -ne $false)
                    }
                }
                $BDMs += $SecondaryBdm
            }
        }
        $InstanceParams    = @{
            MinCount         = $Count
            MaxCount         = $Count
            InstanceType     = $InstanceType
            SecurityGroupId  = $SecurityGroupId
            ImageId          = $ImageId
            SubnetId         = $SubNetId
            KeyName          = $KeyName
        }
            
        if ($Name) {
            $InstanceParams.TagSpecification = @(
                [Amazon.EC2.Model.TagSpecification]@{
                    ResourceType = "instance"
                    Tags         = @(@{ Key="Name"; Value=$Name })    },
                [Amazon.EC2.Model.TagSpecification]@{
                    ResourceType = "volume"
                    Tags         = @(@{ Key="Name"; Value=$Name })    }
            )
        }
        If ($SpotInstance)    { $InstanceParams.InstanceMarketOption = @{MarketType ="Spot"} }
        If ($UserData64)      { $InstanceParams.UserData             = $UserData64 }
        If ($InstanceProfile) { $InstanceParams.InstanceProfile_Name = $InstanceProfile }
        If ($BDMs)            { $InstanceParams.BlockDeviceMapping   = $BDMs }
        
        $InstanceId       = ($NewInstance = (New-EC2Instance @InstanceParams @GeneralParams).Instances).InstanceId
        
        if ($Name -and $Count -eq 1) {
            If ($DomainName) {
                $DNSParams        = @{InstanceId    = $InstanceId}
                if($Region)        {$DNSParams.Add('Region',$Region)}
                $SetDns           = Set-CmEc2DnsName @DNSParams -DomainName $DomainName -InstanceName $Name
                $HostName         = $SetDns.Hostname
            }
        } else {
            $HostName = $RunningInstance.PublicIPAddress
        }
        [PSCustomObject]@{
            InstanceId      = $InstanceId
            Region          = $NewInstance.Placement.AvailabilityZone.Trimend('abcdef')
            Name            = $Name
            Hostname        = $HostName
            InstanceType    = $InstanceType
            ImageName       = $Image.Name
            ImageId         = $ImageId
            KeyName         = $KeyName
            InstanceProfile = $InstanceProfile
        }
    }
}