CloudFormation – Create and customize a windows server using user data

This blog demonstrates how to

  • create a windows server via CloudFormation
  • create a user directory
  • install the aws cli
  • add the aws cli to the windows path
  • Create a log file to track the output of the commands executed in the user data section
  • Download a file from an s3 bucket

By using these commands, you can download and install software after the Windows instance is created.

The IAM profile assigned to the EC2 instance should have access to the s3 bucket used.

The code is below:

AWSTemplateFormatVersion: 2010-09-09
Description: Innovate - Create App and Web Servers
Resources:
  rMyBastionInstance:
    Type: 'AWS::EC2::Instance'
    Properties:
      ImageId: 'ami-I-want-to-use'
      KeyName: 'my_windows_key'
      IamInstanceProfile: 'My-IAM-profile'
      InstanceType: 'm4.2xlarge'
      SecurityGroupIds: 
        - sg-security-group-id
      SubnetId: 'my-subnet-id'
      BlockDeviceMappings:
        - DeviceName: /dev/sda1
          Ebs:
                VolumeType: gp2
                DeleteOnTermination: true
                VolumeSize: 100
                Encrypted: true
      UserData:
        'Fn::Base64': !Sub |
          <script>
              cd \
              mkdir tempdc
              msiexec.exe /i https://awscli.amazonaws.com/AWSCLIV2.msi /qn
              cd \tempdc
              echo Current date and time > c:\tempdc\s3_output.txt
              echo %DATE% %TIME% >> c:\tempdc\s3_output.txt
              SET PATH=%PATH%;C:\Program Files\Amazon\AWSCLIV2
              echo %PATH% >> c:\tempdc\s3_output.txt
              aws s3 cp s3://mybucket/my-file.exe c:\tempdc\my-file.exe >> c:\tempdc\s3_output.txt
          </script>
      Tags:
        - Key: Name
          Value: MyWindowsInstance

Creating an EC2 in AWS via Cloud Formation

This blog post explains how to create (β€œspin up” in the cool lingo πŸ™‚ ) an EC2 in AWS using Cloud Formation. I will attempt to explain each line or section of code before the code itself. The complete cloud formation template is available at the end of this post. This template is written in JSON. It could also have been done in YAML.

This is the opening curly brace. Just make sure you have one at the end to close it out. Strongly suggest using a JSON formatter and syntax checker.

{

These lines identify the AWS template format version and a description tag for documentation purposes

    "AWSTemplateFormatVersion":"2010-09-09",
    "Description":"Linux EC2 to test with",

The parameters section contains the parameters that will be passed from my Jenkins job to this cloud formation template. Each of these can have a type, default value, and description.

    "Parameters":{
        "CFstackname":{
            "Type":"String"
        },
        "AppID":{
            "Type":"String",
            "Default":"ABC",
            "Description":"Name of the application. Default=ABC"
        },
        "Role":{
            "Type":"String",
            "Default":"App",
            "Description":"Enter the Role name. Default=App"
        },
        "UserID":{
            "Type":"String",
            "Description":"Enter the userid of the owner(s)"
        }
    },

This next section specifies the resources that are to be created by this cloud formation template.

    "Resources":{

The type of the resource to be created is an EC2 instance

        "EC2Instance":{
            "Type":"AWS::EC2::Instance",

This section contains the properties to describe the resource.

            "Properties":{

This parameter specifies the instance type which in this case is a t3 small

                "InstanceType":"t3.small",

The DisableApiTermination attribute controls whether the instance can be terminated using the console, CLI, or API. While writing this blog post I did learn that the DisableApiTermination attribute does not prevent Amazon EC2 Auto Scaling from terminating an instance. Just a useful piece of information.

                "DisableApiTermination":"false",

This specifies the network interfaces. The AssociatePublicIpAddress is set to false in this case. The subnet ID is your subnet i.e. logical subdivision of an IP network. The group set contains the security groups that you want to be attached to this EC2 instance. These need to be predefined.

                "NetworkInterfaces":[
                    {
                        "AssociatePublicIpAddress":"false",
                        "DeviceIndex":"0",
                        "SubnetId":"my-subnet-01",
                        "GroupSet":[
                            "sg-for-me-01",
                            "sg-for-me-02"
                        ]
                    }
                ],

This next parameter specifies the Amazon machine image (AMI) that you wish to use to create this instance. In my case, I had a pre-created AMI that I was using as it was already encrypted with my KMS key

                "ImageId":"ami-dean-01",

Here I’m specifying the block devices (disks) to be associated with this machine. The volume size is specified in gigabytes. Storage is deleted when the EC2 is terminated. The disk is to be encrypted with the KMS key specified

                "BlockDeviceMappings":[
                    {
                        "DeviceName":"/dev/sda1",
                        "Ebs":{
                            "VolumeSize":"100",
                            "DeleteOnTermination":true,
                            "VolumeType":"gp2",
                            "Encrypted":"true",
                            "KmsKeyId":"my magical KMS key"
                        }
                    }
                ],

These are the tags that are to be associated with this EC2. Some of them are from the parameters of the top of the template. This is a free form to the extent that you can specify any tag string as the key and any value as the value.

                "Tags":[
                    {
                        "Key":"UserID",
                        "Value":"xyz01"
                    },
                    {
                        "Key":"Name",
                        "Value":"My Linux Machine"
                    },
                    {
                        "Key":"Role",
                        "Value":"App"
                    }

Close all the curly braces or else bad things will happen πŸ™‚

 
                ]
            }
        }
    },

This section is the outputs that I am expecting. The first is the instance id, followed by the Availability Zone (AZ) and the private IP

    "Outputs":{
        "InstanceId":{
            "Description":"InstanceId of the newly created EC2 instance",
            "Value":{
                "Ref":"EC2Instance"
            }
        },
        "AZ":{
            "Description":"Availability Zone of the newly created EC2 instance",
            "Value":{
                "Fn::GetAtt":[
                    "EC2Instance",
                    "AvailabilityZone"
                ]
            }
        },
        "PrivateIP":{
            "Description":"PrivateIP of the newly created EC2 instance",
            "Value":{
                "Fn::GetAtt":[
                    "EC2Instance",
                    "PrivateIp"
                ]
            }
        }
    }
}

Complete cloud formation template

{
    "AWSTemplateFormatVersion":"2010-09-09",
    "Description":"Linux EC2 to test with",
    "Parameters":{
        "CFstackname":{
            "Type":"String"
        },
        "AppID":{
            "Type":"String",
            "Default":"ABC",
            "Description":"Name of the application. Default=ABC"
        },
        "Role":{
            "Type":"String",
            "Default":"App",
            "Description":"Enter the Role name. Default=App"
        },
        "UserID":{
            "Type":"String",
            "Description":"Enter the userid of the owner(s)"
        }
    },
    "Resources":{
        "EC2Instance":{
            "Type":"AWS::EC2::Instance",
            "Properties":{
                "InstanceType":"t3.small",
                "DisableApiTermination":"false",
                "NetworkInterfaces":[
                    {
                        "AssociatePublicIpAddress":"false",
                        "DeviceIndex":"0",
                        "SubnetId":"my-subnet-01",
                        "GroupSet":[
                            "sg-for-me-01",
                            "sg-for-me-02"
                        ]
                    }
                ],
                "ImageId":"ami-dean-01",
                "BlockDeviceMappings":[
                    {
                        "DeviceName":"/dev/sda1",
                        "Ebs":{
                            "VolumeSize":"100",
                            "DeleteOnTermination":true,
                            "VolumeType":"gp2",
                            "Encrypted":"true",
                            "KmsKeyId":"my magical KMS key"
                        }
                    }
                ],
                "Tags":[
                    {
                        "Key":"UserID",
                        "Value":"xyz01"
                    },
                    {
                        "Key":"Name",
                        "Value":"My Linux Machine"
                    },
                    {
                        "Key":"Role",
                        "Value":"App"
                    }
 
                ]
            }
        }
    },
    "Outputs":{
        "InstanceId":{
            "Description":"InstanceId of the newly created EC2 instance",
            "Value":{
                "Ref":"EC2Instance"
            }
        },
        "AZ":{
            "Description":"Availability Zone of the newly created EC2 instance",
            "Value":{
                "Fn::GetAtt":[
                    "EC2Instance",
                    "AvailabilityZone"
                ]
            }
        },
        "PrivateIP":{
            "Description":"PrivateIP of the newly created EC2 instance",
            "Value":{
                "Fn::GetAtt":[
                    "EC2Instance",
                    "PrivateIp"
                ]
            }
        }
    }
}

ORA-15032, ORA-15017 and ORA-15040 issues with an ASM disk group

While creating a new Oracle database with ASM on an AWS EC2 platform I encountered the below errors while attempting to mount one of the ASM disk groups:

ALTER DISKGROUP VOTE_DISK mount
*
ERROR at line 1:
ORA-15032: not all alterations performed
ORA-15017: diskgroup "VOTE_DISK" cannot be mounted
ORA-15040: diskgroup is incomplete

When I checked the v$asm_diskgroup, I saw

SQL> select NAME,TOTAL_MB,FREE_MB from v$asm_diskgroup;  

NAME                             TOTAL_MB    FREE_MB
------------------------------ ---------- ----------
ASMDATA1                            716796     716726
DBVOT                                    0          0
ASMDATA2                            716796     716726
ASMARCH                             716796     716726

The goal was to create the VOTE_DISK diskgroup on the DBVOT volume that was in turn mapped to the /dev/bqdx disk. The AWS Console indicated that the /dev/bqdx was attached to the EC2 but was not visible in the OS for some reason. I issued the command:

/usr/bin/echo -e "o\nn\np\n1\n\n\nt\n8e\nw" | /usr/sbin/fdisk /dev/bqdx

Which resulted in:

Welcome to fdisk (util-linux 2.23.2).

Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.


Command (m for help): Building a new DOS disklabel with disk identifier 0x29ba6b8d.

Command (m for help): Partition type:
   p   primary (0 primary, 0 extended, 4 free)
   e   extended
Select (default p): Partition number (1-4, default 1): First sector (2048-104857599, default 2048): Using default value 2048
Last sector, +sectors or +size{K,M,G} (2048-104857599, default 104857599): Using default value 104857599
Partition 1 of type Linux and of size 50 GiB is set

Command (m for help): Selected partition 1
Hex code (type L to list all codes): Changed type of partition 'Linux' to 'Linux LVM'

Command (m for help): The partition table has been altered!

Calling ioctl() to re-read partition table.
Syncing disks.

However, when I attempted to create the disk I got the error:

root@10-118-134-71:/root # /usr/sbin/oracleasm createdisk DBVOT /dev/bqdx1
Device "/dev/bqdx1" is already labeled for ASM disk "DBVOT"

I then attempted to drop the disk group

SQL> drop diskgroup DBVOT;
drop diskgroup DBVOT
*
ERROR at line 1:
ORA-15039: diskgroup not dropped
ORA-15001: diskgroup "DBVOT" does not exist or is not mounted

I then tried with the FORCE option and succeeded

SQL> drop diskgroup DBVOT force including contents;

Diskgroup dropped.

I then deleted the disk

root@10-118-134-71:/root # /usr/sbin/oracleasm deletedisk DBVOT 
Clearing disk header: done
Dropping disk: done

and checked to make sure it was no longer visible

[oracle@10-118-134-71 ~]$ /usr/sbin/oracleasm listdisks
DB1
DG10
DG11
DG12
DG2
DG3
DG4
DG5
DG6
DG7
DG8
DG9

Then I recreated the disk

root@10-118-134-71:/root # /usr/sbin/oracleasm createdisk DBVOT /dev/bqdx1
Writing disk header: done
Instantiating disk: done

and listed the disks to confirm

root@10-118-134-71:/root # /usr/sbin/oracleasm listdisks
DG1
DG10
DG11
DG12
DG2
DG3
DG4
DG5
DG6
DG7
DG8
DG9
DBVOT

Then I created the disk in ASM

SQL> CREATE DISKGROUP DBVOT EXTERNAL REDUNDANCY DISK '/dev/oracleasm/disks/DBVOT';

Diskgroup created.

Installing the Oracle Client 12c on an AWS Linux EC2 – Silent Mode

After the EC2 is built, run the below commands as root. Depending on the network security, you may have to either set or unset proxy variables.

/usr/bin/yum install -y binutils.x86_64 compat-libcap1.x86_64 compat-libstdc++-33.i686 \
compat-libstdc++-33.x86_64 gcc.x86_64 gcc-c++.x86_64 glibc-devel.i686 glibc-devel.x86_64 glibc.i686 \
glibc.x86_64 libXtst.x86_64 libXtst.i686 ksh.x86_64 libaio.x86_64 libaio.i686 libaio-devel.i686 \
libaio-devel.x86_64 libgcc.i686 libgcc.x86_64 libstdc++.x86_64 libstdc++.i686 libstdc++-devel.i686 \
libstdc++-devel.x86_64 libXi.i686 libXi.x86_64 make.x86_64 sysstat.x86_6

The above command will install all of the prerequisites required for the Oracle Client 12c .

Create the install group

/usr/sbin/groupadd oinstall

Create the dba group

/usr/sbin/groupadd dba

Create the oracle user and add to the groups

/usr/sbin/useradd -g oinstall -G dba oracle

Set the oracle password

passwd oracle

Respond to the above prompt with the oracle password that you want to use.

Make a directory for the software

mkdir /apps/opt/oracle

Change ownership of the directory to the oracle user and the oinstall group

chown oracle:oinstall /apps/opt/oracle

Change to oracle user

su - oracle

Copy the software into the above directory via ftp, sftp, scp etc. The directory will contain file

linuxamd64_12102_client.zip

Unzip the foftware

unzip linuxamd64_12102_client.zip

Change directory to the client directory that was unzipped.

cd /apps/opt/oracle/client

Run the installer in silent mode

./runInstaller -silent \
 -responseFile /apps/opt/oracle/client/response/client_install.rsp    \
   oracle.install.client.installType=Runtime           \
   UNIX_GROUP_NAME=dba                                 \
   INVENTORY_LOCATION=/apps/opt/oracle/oraInventory            \
   SELECTED_LANGUAGES=en                               \
   ORACLE_HOME=/apps/opt/oracle/base/rtclient64            \
   ORACLE_BASE=/apps/opt/oracle/base                           \
   waitForCompletion=true

At the end of the installation, you should see the message

The installation of Oracle Client 12c was successful.
Please check '/apps/opt/oracle/oraInventory/logs/silentInstall2017-08-30_06-00-25PM.log' for more details

Exit from oracl back to root

exit

As a root user, execute the following script

/apps/opt/oracle/oraInventory/orainstRoot.sh

The above script will display messages similar to

Changing permissions of /apps/opt/oracle/oraInventory.
Adding read,write permissions for group.
Removing read,write,execute permissions for world.

Changing groupname of /apps/opt/oracle/oraInventory to dba.
The execution of the script is complete.

Set up environment variables for the oracle user. Go to the oracle user’s home

su - oracle
cd

Edit the oracle users

vi .bash_profile

Setup the below variable to make it easier for the users to access sqlplus etc.

# Oracle client variables
export ORACLE_HOME=/apps/opt/oracle/base/rtclient64
export LD_LIBRARY_PATH=$ORACLE_HOME/lib:$LD_LIBRARY_PATH
PATH=$ORACLE_HOME/bin:$PATH
which sqlplus

Test sqlplus

sqlplus user_id

SQL*Plus: Release 12.1.0.2.0 Production on Wed Aug 30 18:23:27 2017

Copyright (c) 1982, 2014, Oracle.  All rights reserved.

Enter password: 

Connected to:
Oracle Database 11g Enterprise Edition Release 11.2.0.4.0 - 64bit Production
With the Partitioning, Real Application Clusters, Automatic Storage Management, Oracle Label Security,
OLAP, Data Mining and Real Application Testing options