Table of Contents

CloudFormation

Why is Terraform better?

Deploying with cloudformation

If using nested-stacks first you need a bucket, into which you will package nested stacks.

AWSTemplateFormatVersion: '2010-09-09'
Description: AWS template for SiteWise demo
Resources:

  MyS3SubstackBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: my-alf-s3-package-bucket-2023-12-05
      AccessControl: Private
      Tags:
        - Key: Purpose
          Value: CF stacks bucket


  MyBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref MyS3SubstackBucket
      PolicyDocument:
        Statement:
          - Sid: AllowCloudFormationAccess
            Effect: Allow
            Principal:
              Service: cloudformation.amazonaws.com
            Action: s3:*
            Resource: !Join
              - ''
              - - 'arn:aws:s3:::'
                - !Ref MyS3SubstackBucket
                - /*

now deploy the bucket


FILENAME="seed.cloudformation.yaml"
STACKNAME="MySeedStackName"
REGION="eu-west-1"

# validate
aws cloudformation validate-template --template-body file://$FILENAME


# check the change set
aws cloudformation deploy --stack-name $STACKNAME --template-file $FILENAME --region $REGION --no-execute-changeset


# execute via "deploy" which automatically creates / updates stack
aws cloudformation deploy --stack-name $STACKNAME --template-file $FILENAME --region $REGION


# delete stack
# aws cloudformation delete-stack --stack-name $STACKNAME

parent1.cloudformation.yaml

AWSTemplateFormatVersion: "2010-09-09"
Description: Provision a SG

Parameters:

  VpcIdParameter:
    Type: String
    Default: "vpc-01eb7fd6f29cea57b"
    
  packageBucket:
    Type: String
    Default: "my-alf-s3-package-bucket-2023-12-05"

Resources:

  SubStack1:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: "substack.helloworld.cloudformation.yaml"
      Parameters:
        VpcId: !Ref VpcIdParameter

substack.helloworld.cloudformation.yaml

AWSTemplateFormatVersion: "2010-09-09"
Description: Provision a SG

Parameters:
  VpcId:
    Type: String


Resources:

  MySecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: MySecurityGroup
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0 # Example: Allowing HTTP traffic from anywhere (Please adjust for your use case)
      Tags:
        - Key: Name
          Value: MySecurityGroup

now you can package the stack

set -e

FILENAME="parent1.cloudformation.yaml"
STACKNAME="MyParentStackName"
REGION="eu-west-1"
PACKAGEBUCKET="my-alf-s3-package-bucket-2023-12-05"


# validate
# aws cloudformation validate-template --template-body file://$FILENAME


# package uploading substacks
rm packaged-root-template.yaml
aws cloudformation package --template-file $FILENAME --s3-bucket $PACKAGEBUCKET  --output-template-file packaged-root-template.yaml --region $REGION 



# check the change set, dont execute :  "no-execute-changeset"
# aws cloudformation deploy --stack-name $STACKNAME --template-file $FILENAME --region $REGION --no-execute-changeset

# arn of change set is printed, here arn:aws:cloudformation:eu-west-1:913372342854:changeSet/awscli-cloudformation-package-deploy-1701783364/21bb1e0c-a0ea-41ca-9edd-5a7ab989b3a5

# can see change-set
# aws cloudformation describe-change-set --change-set-name arn:aws:cloudformation:eu-west-1:913372342854:changeSet/awscli-cloudformation-package-deploy-1701783364/21bb1e0c-a0ea-41ca-9edd-5a7ab989b3a5  --region $REGION

# can continue via 
# aws cloudformation execute-change-set --change-set-name arn:aws:cloudformation:eu-west-1:913372342854:changeSet/awscli-cloudformation-package-deploy-1701783364/21bb1e0c-a0ea-41ca-9edd-5a7ab989b3a5  --region $REGION





# execute via "deploy" which automatically creates / updates stack
aws cloudformation deploy --stack-name $STACKNAME --template-file packaged-root-template.yaml --region $REGION  --parameter-overrides VpcIdParameter="vpc-01eb7fd6f29cea57b"


# delete stack
# aws cloudformation delete-stack --stack-name $STACKNAME

Structure

Parameters

User parameters.

Resources

The resources, which should be created, like EC2 Instances, SecurityGroups.

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html

Outputs

What is printed inside the AWS console, after the cloud formation script is completed.

Instance Initiation

Initiation is described inside of the following block: AWS::CloudFormation::Init

This block is NOT executed on the instance automatgically. Instead a special script must be executed within the UserData block.

"UserData": {
				  "Fn::Base64": { "Fn::Join":["", [
					"#!/bin/bash -ex\n",
					"\n",
					"# Install the cfn-init and cfn-signal scripts \n",
					"apt-get update\n",
					"apt-get -y install python-setuptools\n",
					"mkdir aws-cfn-bootstrap-latest\n",
					"curl https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz | tar xz -C aws-cfn-bootstrap-latest --strip-components 1\n",
					"easy_install aws-cfn-bootstrap-latest\n",
					"\n",
					"# Trigger the default configset described in AWS::CloudFormation::Init \n",
					"/usr/local/bin/cfn-init --stack ", { "Ref":"AWS::StackName" }, " --resource WebServerInstance" ," --region ", { "Ref": "AWS::Region" },"  --configset InstallAndRun" , "\n",
					"\n",
					"# Signal the status from cfn-init to release the CreationPolicy \n",
					"/usr/local/bin/cfn-signal --stack ", { "Ref":"AWS::StackName" }, " --resource WebServerInstance" ," --region ", { "Ref": "AWS::Region" }," --exit-code $? \n"
				  ]]}

For details see: https://gist.github.com/skipidar/a3966bbaf733de429c676f6e910b3bc4

cfn-init

This is an amazon provided script, which triggers the “AWS::CloudFormation::Init” block. https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-init.html

"/usr/local/bin/cfn-init --stack ", { "Ref":"AWS::StackName" }, " --resource WebServerInstance" ," --region ", { "Ref": "AWS::Region" },"  --configset InstallAndRun"

The most interesting usage of this script is, that it may be triggered directly on the named instance, to do the debugging.

cfn-signal

This script sends a signal to a WaitCondition or to a CreationPolicy.

It in pair with the cfn-init it signals the completion of an instance creation.

/usr/local/bin/cfn-signal --stack ", { "Ref":"AWS::StackName" }, " --resource WebServerInstance" ," --region ", { "Ref": "AWS::Region" }," --exit-code $?

Execution Order

Different Resources

The creation of the resources may happen in parallel. Cloud formation waits if resources are marked as a dependent.

You can easily define the order, by specifying the DependsOn attribute.

Instance initiation

Instance initiation is triggered from UserData. So UserData comes first.

      "AWS::CloudFormation::Init" : {
        "config" : {
          "packages" : {
            :
          },
          "groups" : {
            :
          },
          "users" : {
            :
          },
          "sources" : {
            :
          },
          "files" : {
            :
          },
          "commands" : {
            :
          },
          "services" : {
            :
          }

Download from S3

A lot is necessary to download from S3 using a role autehnticaiton.

A role (here alf-digital-s3) is required with following settings:

Here are the details: https://aws.amazon.com/blogs/devops/authenticated-file-downloads-with-cloudformation/

{
	"Parameters": {
		...
		"RoleS3ReaderName": {
			"Description": "IAM role for S3 access",
			"Type": "String",
			"Default": "myIamS3RoleWithTrustToEc2Service",
			"ConstraintDescription": "Must be a valid IAM role, with read access to S3 files."
		}
	},
	"Resources": {
		"WebServerInstance": {
			"Type": "AWS::EC2::Instance",
			"Metadata": {
				"AWS::CloudFormation::Authentication": {
					"default": {
						"type": "S3",
						"buckets": ["mybucketname"],
						"roleName": {
							"Ref": "RoleS3ReaderName"
						}
					}
				},
				"AWS::CloudFormation::Init": {
					"configSets": {
						"myConfigSetsHere": ["myconfigset"]
					},
					"myconfigset": {
						"files": {
							"/root/alf.digital.seed.zip": {
								"source": "http://mybucketname.s3-eu-central-1.amazonaws.com/path/to/myarchive.zip",
								"authentication": "default",
								"mode": "000644",
								"owner": "root",
								"group": "root"
							}
						}
					}
				}
			},
			"CreationPolicy": {
				"ResourceSignal": {
					"Count": "1",
					"Timeout": "PT15M"
				}
			},
			"Properties": {
				...
				"IamInstanceProfile": {
					"Ref": "InstanceProfileS3"
				}
			}
		},
		"InstanceProfileS3": {
			"Type": "AWS::IAM::InstanceProfile",
			"Properties": {
				"Path": "/",
				"Roles": [{
					"Ref": "RoleS3ReaderName"
				}]
			}
		}
	},
	
}

Use !Sub in commands

To reference cloudformation arguments in commands - use the “env”

Resources:
Parameters:
  S3Prefix:
    AllowedPattern: >-
      ^s3://[/a-zA-Z0-9_-]*[a-zA-Z0-9_-]$
    Description: The s3://..... prefix to the bucket and folder, where the temp files are located.
    Type: String

...
            04_execute:
              command: !Sub |
                ansible-playbook /ssh-jump-host/sshJumpServer.ansible.yml -v --extra-vars "host=local" --extra-vars s3prefix="${S3Prefix}" >> "/ssh-jump-host/sshJumpServer.ansible.yml.log"

Domain Join PowerShell Script

When creating a windows-machine - it is possible to join the domain via a PowerShell script, instead of using the wizard inside the Amazon console.

It is useful if some initiation of the domain must be done. Since the domain-joining via the Wizard seems to happen AFTER the user-data are executed - to be able to modify the Active Directory from the user-data we must manually join the domain.

For that we do the following:

  1. Create a Microsoft Domain
  2. Retrieve the Microsoft Domain DNS-Server Ips from the Microsoft Domain (“21.1.2.28”,“21.1.3.84”). To find these addresses, go to the AWS Directory Service console navigation pane, choose Directories, choose the applicable directory ID, and on the Details page use the IPs that are displayed in DNS address.
  3. reconfigure the new instance's Ethernet adapter with the Domain DNS-Server Ips
  4. now, with the new DNS the domain is resolvable - join the domain
  5. do the modifications: create an organizational unit, create users etc.
  6. finally undo the <persistent> - disable the task scheduler task “Amazon Ec2 Launch - Userdata Execution”, responsible for triggering UserData. Use SCHTASKS for that.
Restart duing the powershell userdata script

During the script, after the domain join the machine will be restarted. So the execution of the script will be stopped. In order to go on with the userdata script execution after the restart one should use <persist>true</persist>. This will make the script persistent and executed after the restart again.


<powershell>

echo "START" | Out-File c:\Windows\Temp\logs-joindomain.txt -Append

#install snapin
Install-WindowsFeature -Name AD-Domain-Services,GPMC -IncludeManagementTools -Restart

##### JOIN THE DOMAIN #####

#Retrieve the AWS instance ID, keep trying until the metadata is available
$instanceID = "null"
while ($instanceID -NotLike "i-*") {
 Start-Sleep -s 3
 $instanceID = invoke-restmethod -uri http://169.254.169.254/latest/meta-data/instance-id
 echo "Waiting until the instance metadata is available" | Out-File c:\Windows\Temp\logs-joindomain.txt -Append
}
echo "Instance metadata is available" | Out-File c:\Windows\Temp\logs-joindomain.txt -Append


# set the domain-own DNS servers to the network adapter, so that the domain name can be resolved and we can join
echo "Setting DNS for the Ethernet adapter" | Out-File c:\Windows\Temp\logs-joindomain.txt -Append
$adapter = Get-NetAdapter -Name "Ethernet 2"
Set-DnsClientServerAddress -InterfaceAlias $adapter.Name -ServerAddresses ("21.1.2.28","21.1.3.84")
echo "Setting DNS done" | Out-File c:\Windows\Temp\logs-joindomain.txt -Append


# join the domain now
echo "Joining the domain block start" | Out-File c:\Windows\Temp\logs-joindomain.txt -Append
$domain = "basicm.local"
$username = "$domain\admin" 
$password = "TonoGada65" | ConvertTo-SecureString -AsPlainText -Force
$cred = New-Object -typename System.Management.Automation.PSCredential($username, $password)

Try {
	$partOfDomain = (Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain
    if ( $partOfDomain ) {
        echo "Already part of the domain. Wont trigger domain join" | Out-File c:\Windows\Temp\logs-joindomain.txt -Append
    }
	Else {
		echo "Not yet part of the domain - start joining" | Out-File c:\Windows\Temp\logs-joindomain.txt -Append
		Rename-Computer -NewName $instanceID -Force
		Start-Sleep -s 5
		Add-Computer -DomainName basicm.local -OUPath "OU=basicm,dc=basicm,dc=local" -Options JoinWithNewName,AccountCreate -Credential $cred -Force -Restart -erroraction 'stop'
	}
	
}
Catch{
	echo $_.Exception | Out-File c:\Windows\Temp\error-joindomain.txt -Append
}
echo "Joining the domain block done" | Out-File c:\Windows\Temp\logs-joindomain.txt -Append



# wait until the machine joins the domain
echo "Now waiting for the instance to be part of the domain" | Out-File c:\Windows\Temp\logs-joindomain.txt -Append
$partOfDomain = $False
while ( -not $partOfDomain ) {
 Start-Sleep -s 3
 $partOfDomain = (Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain
 echo "Waiting for the instance to join the domain" | Out-File c:\Windows\Temp\logs-joindomain.txt -Append
}
echo "Instance joined the domain '$env:userdomain'" | Out-File c:\Windows\Temp\logs-joindomain.txt -Append

				
# add a basic user
echo "Now modify the Active Directory" | Out-File c:\Windows\Temp\logs-joindomain.txt -Append
Try {
	echo "Adding new OU" | Out-File c:\Windows\Temp\logs-joindomain.txt -Append
	New-ADOrganizationalUnit -Name Groups -Path "OU=basicm,dc=basicm,dc=local" -Credential $cred
	NEW-ADGroup –name "OpenVpnUsers" –groupscope Global –path "OU=Groups,OU=basicm,dc=basicm,dc=local" -Credential $cred	
	
	
	
	echo "Adding new user" | Out-File c:\Windows\Temp\logs-joindomain.txt -Append	
	New-ADUser -GivenName First -Surname User -Name first.user -Path "ou=Users,ou=basicm,dc=basicm,dc=local" -PasswordNeverExpires $True -ChangePasswordAtLogon $False -Description "The initial OpenVPN user"  -Credential $cred
	$user = Get-ADUser "CN=first.user,ou=Users,ou=basicm,dc=basicm,dc=local"  -Credential $cred
	
	echo "Setting a new password" | Out-File c:\Windows\Temp\logs-joindomain.txt -Append		
	Set-ADAccountPassword $user -NewPassword (ConvertTo-SecureString "123AbC!!!!" -AsPlainText -force) -Reset  -Credential $cred
	Enable-ADAccount -Identity $user  -Credential $cred
	
	echo "Adding new member to the groups" | Out-File c:\Windows\Temp\logs-joindomain.txt -Append			
	$group = Get-ADGroup "CN=OpenVpnUsers,OU=Groups,OU=basicm,dc=basicm,dc=local"  -Credential $cred
	Add-ADGroupMember $group -Members $user  -Credential $cred
	$group = Get-ADGroup "CN=Admins,OU=AWS Delegated Groups,dc=basicm,dc=local"  -Credential $cred
	Add-ADGroupMember $group -Members $user  -Credential $cred
	
	echo "Adding a system user" | Out-File c:\Windows\Temp\logs-joindomain.txt -Append			
	New-ADUser -Name s000001 -Path "ou=Users,ou=basicm,dc=basicm,dc=local" -PasswordNeverExpires $True -ChangePasswordAtLogon $False -Description "System user for LDAP connections"  -Credential $cred
	$user = Get-ADUser "CN=s000001,ou=Users,ou=basicm,dc=basicm,dc=local"  -Credential $cred
	
	echo "Set the password and enable" | Out-File c:\Windows\Temp\logs-joindomain.txt -Append				
	Set-ADAccountPassword $user -NewPassword (ConvertTo-SecureString "komumisa76!" -AsPlainText -force) -Reset  -Credential $cred
	Enable-ADAccount -Identity $user  -Credential $cred
}
Catch{
	echo $_.Exception | Out-File c:\Windows\Temp\error-joindomain.txt -Append
}




# disable the userdata running task scheduler task, now when we are done
echo "Now Disable the task" | Out-File c:\Windows\Temp\logs-joindomain.txt -Append
Try {
	& SCHTASKS /Change /DISABLE /TN "\Amazon Ec2 Launch - Userdata Execution"
}
Catch{
	echo $_.Exception | Out-File c:\Windows\Temp\error-joindomain.txt -Append
}


echo "Script done" | Out-File c:\Windows\Temp\logs-joindomain.txt -Append

</powershell>
<persist>true</persist>

To convert the above powershell script to a cloudformation capable UserData block do:

Result:

{
	"AWSTemplateFormatVersion": "2010-09-09",
	"Description" : "Creates a Windows server, joins it to the domain 'basic.local' using the domain admin, creates an initial directory users and some directory groups.",
	"Parameters": {
		"Subnet": {
			"Type": "AWS::EC2::Subnet::Id",
			"Description": "A private, shared subnet."
		},
		"SecGroup": {
			"Type": "AWS::EC2::SecurityGroup::Id",
			"Description": "A private, shared security group."
		},
		"KeyName": {
			"Description": "Name of an existing EC2 KeyPair to enable SSH access to the instances",
			"Type": "AWS::EC2::KeyPair::KeyName",
			"ConstraintDescription": "must be the name of an existing EC2 KeyPair."
		},
		"DomainDnsServer1": {
			"Type": "String",
			"Description": "The DNS Server 1 of the directory. Check the directory service > Directory > DNS Address "
		},
		"DomainDnsServer2": {
			"Type": "String",
			"Description": "The DNS Server 2 of the directory. Check the directory service > Directory > DNS Address"
		},
		"DomainAdminPassword": {
			"Type": "String",
			"Description": "The password of the existing domain user 'admin', which was defined during the directory creation.",
			"Default": "TonoGada65"
		},
		"FirstUserPassword": {
			"Type": "String",
			"Description": "The password for the example domain user 'first.user', which will be created. Use him e.g. for RDP connection. ",
			"Default": "123AbC!!!!"
		},
		"S000001UserPassword": {
			"Type": "String",
			"Description": "The password for the system domain user 'S000001', which will be created. Use him e.g. for LDAP/AD joining.",
			"Default": "komumisa76!"
		}
	},
	"Resources": {
		"MicrosoftWindowsAdManager": {
			"Type": "AWS::EC2::Instance",
			"Properties": {
				"ImageId": "ami-96b824ef",
				"UserData": {
					"Fn::Base64": {
						"Fn::Join": ["",
                        [" <powershell> \n ",
"  \n ",
" echo \"START\" | Out-File c:\\Windows\\Temp\\logs-joindomain.txt -Append \n ",
"  \n ",
" #install snapin \n ",
" Install-WindowsFeature -Name AD-Domain-Services,GPMC -IncludeManagementTools -Restart \n ",
"  \n ",
" ##### JOIN THE DOMAIN ##### \n ",
"  \n ",
" #Retrieve the AWS instance ID, keep trying until the metadata is available \n ",
" $instanceID = \"null\" \n ",
" while ($instanceID -NotLike \"i-*\") { \n ",
"  Start-Sleep -s 3 \n ",
"  $instanceID = invoke-restmethod -uri http://169.254.169.254/latest/meta-data/instance-id \n ",
"  echo \"Waiting until the instance metadata is available\" | Out-File c:\\Windows\\Temp\\logs-joindomain.txt -Append \n ",
" } \n ",
" echo \"Instance metadata is available\" | Out-File c:\\Windows\\Temp\\logs-joindomain.txt -Append \n ",
"  \n ",
"  \n ",
" # set the domain-own DNS servers to the network adapter, so that the domain name can be resolved and we can join \n ",
" echo \"Setting DNS for the Ethernet adapter\" | Out-File c:\\Windows\\Temp\\logs-joindomain.txt -Append \n ",
" $adapter = Get-NetAdapter -Name \"Ethernet 2\" \n ",
" Set-DnsClientServerAddress -InterfaceAlias $adapter.Name -ServerAddresses (\"",{ "Ref" : "DomainDnsServer1" },"\",\"",{ "Ref" : "DomainDnsServer2" },"\") \n ", 
" echo \"Setting DNS done\" | Out-File c:\\Windows\\Temp\\logs-joindomain.txt -Append \n ",
"  \n ",
"  \n ",
" # join the domain now \n ",
" echo \"Joining the domain block start\" | Out-File c:\\Windows\\Temp\\logs-joindomain.txt -Append \n ",
" $domain = \"basic.local\" \n ",
" $username = \"$domain\\admin\"  \n ",
" $password = \"",{ "Ref" : "DomainAdminPassword" },"\" | ConvertTo-SecureString -AsPlainText -Force \n ",
" $cred = New-Object -typename System.Management.Automation.PSCredential($username, $password) \n ",
"  \n ",
" Try { \n ",
"   $partOfDomain = (Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain \n ",
"     if ( $partOfDomain ) { \n ",
"         echo \"Already part of the domain. Wont trigger domain join\" | Out-File c:\\Windows\\Temp\\logs-joindomain.txt -Append \n ",
"     } \n ",
"   Else { \n ",
"       echo \"Not yet part of the domain - start joining\" | Out-File c:\\Windows\\Temp\\logs-joindomain.txt -Append \n ",
"       Rename-Computer -NewName $instanceID -Force \n ",
"       Start-Sleep -s 5 \n ",
"       Add-Computer -DomainName basic.local -OUPath \"OU=basic,dc=basic,dc=local\" -Options JoinWithNewName,AccountCreate -Credential $cred -Force -Restart -erroraction 'stop' \n ",
"   } \n ",
"    \n ",
" } \n ",
" Catch{ \n ",
"   echo $_.Exception | Out-File c:\\Windows\\Temp\\error-joindomain.txt -Append \n ",
" } \n ",
" echo \"Joining the domain block done\" | Out-File c:\\Windows\\Temp\\logs-joindomain.txt -Append \n ",
"  \n ",
"  \n ",
"  \n ",
" # wait until the machine joins the domain \n ",
" echo \"Now waiting for the instance to be part of the domain\" | Out-File c:\\Windows\\Temp\\logs-joindomain.txt -Append \n ",
" $partOfDomain = $False \n ",
" while ( -not $partOfDomain ) { \n ",
"  Start-Sleep -s 3 \n ",
"  $partOfDomain = (Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain \n ",
"  echo \"Waiting for the instance to join the domain\" | Out-File c:\\Windows\\Temp\\logs-joindomain.txt -Append \n ",
" } \n ",
" echo \"Instance joined the domain '$env:userdomain'\" | Out-File c:\\Windows\\Temp\\logs-joindomain.txt -Append \n ",
"  \n ",
"                \n ",
" # add an initial user \n ",
" echo \"Now modify the Active Directory\" | Out-File c:\\Windows\\Temp\\logs-joindomain.txt -Append \n ",
" Try { \n ",
"   echo \"Adding new OU\" | Out-File c:\\Windows\\Temp\\logs-joindomain.txt -Append \n ",
"   New-ADOrganizationalUnit -Name Groups -Path \"OU=basic,dc=basic,dc=local\" -Credential $cred \n ",
"   NEW-ADGroup –name \"OpenVpnUsers\" –groupscope Global –path \"OU=Groups,OU=basic,dc=basic,dc=local\" -Credential $cred   \n ",
"    \n ",
"    \n ",
"    \n ",
"   echo \"Adding new user\" | Out-File c:\\Windows\\Temp\\logs-joindomain.txt -Append   \n ",
"   New-ADUser -GivenName First -Surname User -Name first.user -Path \"ou=Users,ou=basic,dc=basic,dc=local\" -PasswordNeverExpires $True -ChangePasswordAtLogon $False -Description \"The initial OpenVPN user\"  -Credential $cred \n ",
"   $user = Get-ADUser \"CN=first.user,ou=Users,ou=basic,dc=basic,dc=local\"  -Credential $cred \n ",
"    \n ",
"   echo \"Setting a new password\" | Out-File c:\\Windows\\Temp\\logs-joindomain.txt -Append        \n ",
"   Set-ADAccountPassword $user -NewPassword (ConvertTo-SecureString \"",{ "Ref" : "FirstUserPassword" },"\" -AsPlainText -force) -Reset  -Credential $cred \n ",
"   Enable-ADAccount -Identity $user  -Credential $cred \n ",
"    \n ",
"   echo \"Adding new member to the groups\" | Out-File c:\\Windows\\Temp\\logs-joindomain.txt -Append           \n ",
"   $group = Get-ADGroup \"CN=OpenVpnUsers,OU=Groups,OU=basic,dc=basic,dc=local\"  -Credential $cred \n ",
"   Add-ADGroupMember $group -Members $user  -Credential $cred \n ",
"   $group = Get-ADGroup \"CN=Admins,OU=AWS Delegated Groups,dc=basic,dc=local\"  -Credential $cred \n ",
"   Add-ADGroupMember $group -Members $user  -Credential $cred \n ",
"    \n ",
"   echo \"Adding a system user\" | Out-File c:\\Windows\\Temp\\logs-joindomain.txt -Append          \n ",
"   New-ADUser -Name s000001 -Path \"ou=Users,ou=basic,dc=basic,dc=local\" -PasswordNeverExpires $True -ChangePasswordAtLogon $False -Description \"System user for LDAP connections\"  -Credential $cred \n ",
"   $user = Get-ADUser \"CN=s000001,ou=Users,ou=basic,dc=basic,dc=local\"  -Credential $cred \n ",
"    \n ",
"   echo \"Set the password and enable\" | Out-File c:\\Windows\\Temp\\logs-joindomain.txt -Append               \n ",
"   Set-ADAccountPassword $user -NewPassword (ConvertTo-SecureString \"",{ "Ref" : "S000001UserPassword" },"\" -AsPlainText -force) -Reset  -Credential $cred \n ",
"   Enable-ADAccount -Identity $user  -Credential $cred \n ",
" } \n ",
" Catch{ \n ",
"   echo $_.Exception | Out-File c:\\Windows\\Temp\\error-joindomain.txt -Append \n ",
" } \n ",
"  \n ",
"  \n ",
"  \n ",
"  \n ",
" # disable the userdata running task scheduler task, now when we are done \n ",
" echo \"Now Disable the task\" | Out-File c:\\Windows\\Temp\\logs-joindomain.txt -Append \n ",
" Try { \n ",
"   & SCHTASKS /Change /DISABLE /TN \"\\Amazon Ec2 Launch - Userdata Execution\" \n ",
" } \n ",
" Catch{ \n ",
"   echo $_.Exception | Out-File c:\\Windows\\Temp\\error-joindomain.txt -Append \n ",
" } \n ",
"  \n ",
"  \n ",
" echo \"Script done\" | Out-File c:\\Windows\\Temp\\logs-joindomain.txt -Append \n ",
"  \n ",
" </powershell> \n ",
" <persist>true</persist> "]]
					}
				},
				"NetworkInterfaces": [{
					"AssociatePublicIpAddress": "true",
					"DeviceIndex": "0",
					"SubnetId": {
						"Ref": "Subnet"
					},
					"GroupSet": [{
						"Ref": "SecGroup"
					}]
				}],
				"KeyName": { "Ref" : "KeyName" },
				"Tags": [{
					"Key": "Name",
					"Value": "AMicrosoftAdManager"
				},
				{
					"Key": "Purpose",
					"Value": "To manage the Active Directory Users. Should be stopped the most of the time."
				}],
				"InstanceType": "t2.small",
				"BlockDeviceMappings": [{
					"DeviceName": "/dev/sda1",
					"Ebs": {
						"VolumeSize": "30",
						"VolumeType": "gp2"
					}
				}]
			}
		}
	}
}

Debugging

Validate Template

To validate the local template use the aws command. The validation includes simple dependency checks too.

aws cloudformation validate-template --template-body "file:////mnt/d/1PROJEKTE/AWS/alf.digital/cloudFormation/alf.digital.template"

Initiation Logs of an Instance

The logs on an Instance are here: /var/log/

Human readable logs of the whole instance initiation /var/log/cloud-init-output.log
Human readable logs of the cfn-init call /var/log/cfn-init.log
Code from AWS::CloudFormation::Init block of a Cloud Formation template on Linux systems /var/lib/cfn-init/data/metadata.json

Debug the initiation of the instance

You can repeat the initiation part of the instance by triggering the cfn-init script.

/usr/local/bin/cfn-init -v --stack  WebS<erver1 --resource WebServerInstance --region eu-central-1 --configset InstallAndRun && cat /var/log/cfn-init.log

Debug User Data

The user data are located under /var/lib/cloud/instance

Here is the complete troubleshooting guide: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/troubleshooting.html

The scripts, created by userData are located under: /var/lib/cloud/instance/scripts

You can try to re run them.

Examples

Here is an example creates a security group, an EC2 Instance and associate the group with the instance. It has parameters: - the instance type - VPC Id - Server subnet

https://gist.github.com/skipidar/a8e86fbeccd51e6fb96f720ddd3885f1

Here are some examples by AWS

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/sample-templates-appframeworks-us-west-2.html

Code Style

Exporting Stack Output Values vs. Using Nested Stacks:

Stack A Export:

"Outputs" : {
  "PublicSubnet" : {
    "Description" : "The subnet ID to use for public web servers",
    "Value" :  { "Ref" : "PublicSubnet" },
    "Export" : { "Name" : {"Fn::Sub": "${AWS::StackName}-SubnetID" }}
  },
  "WebServerSecurityGroup" : {
    "Description" : "The security group ID to use for public web servers",
    "Value" :  { "Fn::GetAtt" : ["WebServerSecurityGroup", "GroupId"] },
    "Export" : { "Name" : {"Fn::Sub": "${AWS::StackName}-SecurityGroupID" }}
  }
}

Stack B Import

"Resources" : {
  "WebServerInstance" : {
    "Type" : "AWS::EC2::Instance",
    "Properties" : {
      "InstanceType" : "t2.micro",
      "ImageId" : "ami-a1b23456",
      "NetworkInterfaces" : [{
        "GroupSet" : [{"Fn::ImportValue" : {"Fn::Sub" : "${NetworkStackNameParameter}-SecurityGroupID"}}],
        "AssociatePublicIpAddress" : "true",
        "DeviceIndex" : "0",
        "DeleteOnTermination" : "true",
        "SubnetId" : {"Fn::ImportValue" : {"Fn::Sub" : "${NetworkStackNameParameter}-SubnetID"}}
      }]
    }
  }
}

AWS specific parameters:

Structure

Example of a custom definition in CLoudFormation. EFS with Fargate in this example

  CustomTaskDefinition:
    Type: 'Custom::TaskDefinition'
    Version: '1.0'
    Properties:
      ServiceToken: !GetAtt 'CustomResourceFunction.Arn'
      TaskDefinition: {
        executionRoleArn: !Ref CustomTaskDefinitionRole,
        containerDefinitions: [
          {
            name: "navvis",
            image: !Ref NavvisDockerImage,
            memoryReservation: 2000,
            logConfiguration: {
              logDriver: "awslogs",
              options: {
                awslogs-group:  !Ref CustomLogsGroup,
                awslogs-datetime-format: "%Y-%m-%d %H:%M:%S.%L",
                awslogs-region: !Ref 'AWS::Region',
                awslogs-stream-prefix:  !Sub '${ProjectName}'
              }
            },
            portMappings: [
              {
                hostPort: 8080,
                protocol: "tcp",
                containerPort: 8080
              }
            ],
            command: [],
            "environment": [
              {
                "name": "INSTANCE_NAME",
                "value": !Ref 'BuildingID'
              },
              {
                "name": "INSTANCE_PORT",
                "value": "8080"
              },
              {
                "name": "STORAGE_PATH",
                "value": "/mnt"
              }
            ]
           ,
           mountPoints: [
             {sourceVolume: "myefs", containerPath: "/mnt"}
           ]
          }
        ],
        family: "navvis",
        taskRoleArn: !Ref CustomTaskDefinitionRole, #required for EFS permissions
        requiresCompatibilities: ["FARGATE"],
        cpu: "256",
        memory: "2048",
        networkMode: "awsvpc",
        volumes: [
          {
            name: "myefs",
            efsVolumeConfiguration: {
              fileSystemId: {'Fn::ImportValue': !Sub '${ProjectName}-arc-${TargetAwsAccount}-efsid-for-${BuildingID}----${ScanID}'}
            }
          },
        ]
      }
  CustomResourceFunction:
    Type: 'AWS::Lambda::Function'
    Properties:
      Code:
        ZipFile: |
          const aws = require('aws-sdk')
          const response = require('cfn-response')
          const ecs = new aws.ECS({apiVersion: '2014-11-13'})
          exports.handler = function(event, context) {
            console.log("REQUEST RECEIVED:\n" + JSON.stringify(event))
            if (event.RequestType === 'Create' || event.RequestType === 'Update') {
              ecs.registerTaskDefinition(event.ResourceProperties.TaskDefinition, function(err, data) {
                if (err) {
                  console.error(err);
                  response.send(event, context, response.FAILED)
                } else {
                  console.log(`Created/Updated task definition ${data.taskDefinition.taskDefinitionArn}`)
                  response.send(event, context, response.SUCCESS, {}, data.taskDefinition.taskDefinitionArn)
                }
              })
            } else if (event.RequestType === 'Delete') {
              ecs.deregisterTaskDefinition({taskDefinition: event.PhysicalResourceId}, function(err) {
                if (err) {
                  if (err.code === 'InvalidParameterException') {
                    console.log(`Task definition: ${event.PhysicalResourceId} does not exist. Skipping deletion.`)
                    response.send(event, context, response.SUCCESS)
                  } else {
                    console.error(err)
                    response.send(event, context, response.FAILED)
                  }
                } else {
                  console.log(`Removed task definition ${event.PhysicalResourceId}`)
                  response.send(event, context, response.SUCCESS)
                }
              })
            } else {
              console.error(`Unsupported request type: ${event.RequestType}`)
              response.send(event, context, response.FAILED)
            }
          }
      Handler: 'index.handler'
      MemorySize: 128
      Role: !GetAtt 'CustomResourceRole.Arn'
      Runtime: 'nodejs10.x'
      Timeout: 30
  CustomResourceRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service: 'lambda.amazonaws.com'
          Action: 'sts:AssumeRole'
      Policies:
      - PolicyName: 'customresource'
        PolicyDocument:
          Statement:
          - Effect: Allow
            Action:
            - 'ecs:DeregisterTaskDefinition'
            - 'ecs:RegisterTaskDefinition'
            Resource: '*'
          - Effect: Allow
            Action:
            - 'logs:CreateLogGroup'
            - 'logs:CreateLogStream'
            - 'logs:PutLogEvents'
            Resource: '*'
          - Effect: Allow
            Action:
            - 'iam:PassRole'
            Resource: '*' # replace with value of taskRoleArn
  CustomLogsGroup:
    Type: 'AWS::Logs::LogGroup'
    Properties:
      LogGroupName: !Sub "${ProjectName}-custom"
      RetentionInDays: 14


  NavvisServiceFargate:
    Type: 'AWS::ECS::Service'
    Properties:
      Cluster: {'Fn::ImportValue': !Sub '${ProjectName}-arc-${TargetAwsAccount}-ecscluster'}
      LaunchType: FARGATE
      DesiredCount: 1
      PlatformVersion: "1.4.0" # by default on 30th June 2020 still pick up automatically the version 1.3.0 and not really the latest (same behaviour on web console) but to have the EFS we need the 1.4.0
      DeploymentConfiguration:
        MinimumHealthyPercent: 0 # setting to 0 as otherwise, if there are not enough resources (e.g. eni per instance) - the service will not be able to tear down the old container to replace it by the new one
        MaximumPercent: 250
      TaskDefinition: !Ref CustomTaskDefinition
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: DISABLED
          SecurityGroups:
            - {'Fn::ImportValue': !Sub '${ProjectName}-arc-${TargetAwsAccount}-codebuildfargate-sg'}
          Subnets:
            - {'Fn::ImportValue': !Sub '${PlatformPrefix}-PrivateSubnet1AID'}
            - {'Fn::ImportValue': !Sub '${PlatformPrefix}-PrivateSubnet1BID'}
            - {'Fn::ImportValue': !Sub '${PlatformPrefix}-PrivateSubnet1CID'}
      LoadBalancers:
        - ContainerName: 'navvis'
          ContainerPort: 8080
          TargetGroupArn: !Ref TargetGroupNavvisHTTP
      SchedulingStrategy: "REPLICA"