This is an old revision of the document!
Table of Contents
CloudFormation
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:
- read access to S3
- trust relationsship to the ec2 service
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" }] } } }, }
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:
- Create a Microsoft Domain
- 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.
- reconfigure the new instance's Ethernet adapter with the Domain DNS-Server Ips
- now, with the new DNS the domain is resolvable - join the domain
- do the modifications: create an organizational unit, create users etc.
- 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:
- escape all backslashes with an additional backslash:
- escape all doublequotes with a backslash: \“
- prepend at the beginning of every line a doublequote: ”
- append at the end of every line a newline, doublequote, comma: \n “,
- replace all tabs by spaces
- remove the comma from the last line
- replace the parts, which should be passed as parameters to the template by references:
”…RefHere…\n“,
to
”…“, { “Ref” : “AWS::Region” }, ”… \n“,
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 |
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
Code Style
Exporting Stack Output Values vs. Using Nested Stacks:
- https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-stack-exports.html#output-vs-nested * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-importvalue.html * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/walkthrough-crossstackref.html Stack A Export: <code> “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” }} } } </code> Stack B Import <code> “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”}} }] } } } </code> AWS specific parameters: * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html#aws-specific-parameter-types