Skip to content

!Ref versus !GetAtt

CloudFormation provides two intrinsic functions for referring to something declared elsewhere in a template. !Ref returns a resource’s default value; !GetAtt returns a named attribute of it. Choosing correctly is the difference between a template that wires itself together and one that fails at create time with an unhelpful type error.

Using either of them also creates an implicit dependency, so CloudFormation orders resource creation correctly without a DependsOn.

!Ref takes a logical name and returns that resource’s default value, which differs by resource type:

Resource!Ref returns
ParameterThe parameter’s value
AWS::SNS::TopicThe topic ARN
AWS::SQS::QueueThe queue URL
AWS::S3::BucketThe bucket name
AWS::EC2::InstanceThe instance ID
AWS::EC2::VPCThe VPC ID
AWS::IAM::RoleThe role name
AWS::Lambda::FunctionThe function name

The resource reference in the CloudFormation documentation lists the return value for every type; guessing is not worth the deployment cycle.

Parameters:
EnvironmentName:
Type: String
Default: dev
Resources:
MyTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: my-notification-topic
MyQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: my-queue
MyBucket:
Type: AWS::S3::Bucket
Properties:
# Bucket names are globally unique, so the environment alone is not enough
BucketName: !Sub '${EnvironmentName}-${AWS::AccountId}-assets'
MyLambdaFunction:
Type: AWS::Lambda::Function
Properties:
Runtime: nodejs24.x
Handler: index.handler
Role: !GetAtt LambdaExecutionRole.Arn
Environment:
Variables:
TOPIC_ARN: !Ref MyTopic # SNS topics return their ARN
QUEUE_URL: !Ref MyQueue # SQS queues return their URL
BUCKET_NAME: !Ref MyBucket # S3 buckets return their name

Note the placement: MyBucket is a resource and belongs under Resources. Declaring it alongside a parameter under Parameters is a common transcription error and CloudFormation rejects the template outright.

!GetAtt reads a named attribute of a resource, written as !GetAtt LogicalName.AttributeName. Nested attributes are addressed with a dot:

LambdaArn: !GetAtt MyLambdaFunction.Arn
BucketDomainName: !GetAtt MyBucket.DomainName
StreamArn: !GetAtt MyTable.StreamArn
DbEndpoint: !GetAtt MyDbInstance.Endpoint.Address
DbPort: !GetAtt MyDbInstance.Endpoint.Port
AlbDnsName: !GetAtt MyLoadBalancer.DNSName
LatestVersion: !GetAtt MyLaunchTemplate.LatestVersionNumber

The distinction in one line:

TopicArn: !Ref MyTopic # the default value, which happens to be the ARN
TopicName: !GetAtt MyTopic.TopicName # a specific named attribute

Worked example: EventBridge to Lambda and SNS

Section titled “Worked example: EventBridge to Lambda and SNS”

The template below matches EC2 instance state changes and fans them out to a Lambda function and an SNS topic. The part that is easy to get wrong is the permission model: the rule will be created successfully whether or not the targets allow EventBridge to invoke them, and a rule that cannot invoke its targets fails silently.

For Lambda, SNS and SQS targets, EventBridge can use either an IAM execution role specified on the target, or a resource-based policy on the target itself. If neither is present, nothing happens. The example uses resource-based policies, which is the simpler of the two.

Resources:
NotificationFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: ec2-state-change-handler
Runtime: nodejs24.x
Handler: index.handler
Code:
ZipFile: |
exports.handler = async (event) => {
console.log('Received EC2 state change:', JSON.stringify(event, null, 2));
return { statusCode: 200 };
};
Role: !GetAtt LambdaExecutionRole.Arn
Timeout: 30
MemorySize: 128
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
NotificationTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: ec2-state-change-notifications
Ec2StateChangeRule:
Type: AWS::Events::Rule
Properties:
Name: ec2-state-change-rule
Description: Notify on EC2 instance state changes
EventPattern:
source:
- aws.ec2
detail-type:
- EC2 Instance State-change Notification
detail:
state:
- stopped
- running
State: ENABLED
Targets:
- Arn: !GetAtt NotificationFunction.Arn # Lambda targets need the ARN
Id: ProcessEc2StateChangeLambda
- Arn: !Ref NotificationTopic # SNS topics return their ARN from !Ref
Id: NotifyEc2StateChangeSns
# Without this, EventBridge cannot invoke the function and the rule fails silently
InvokeFunctionPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !GetAtt NotificationFunction.Arn
Action: lambda:InvokeFunction
Principal: events.amazonaws.com
SourceArn: !GetAtt Ec2StateChangeRule.Arn
# Without this, EventBridge cannot publish to the topic
NotificationTopicPolicy:
Type: AWS::SNS::TopicPolicy
Properties:
Topics:
- !Ref NotificationTopic
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: events.amazonaws.com
Action: sns:Publish
Resource: !Ref NotificationTopic
Condition:
ArnEquals:
aws:SourceArn: !GetAtt Ec2StateChangeRule.Arn

Both target entries in the rule illustrate the difference between the two functions: the Lambda target needs !GetAtt NotificationFunction.Arn because a Lambda function’s !Ref returns its name, while the SNS target can use !Ref NotificationTopic because an SNS topic’s !Ref already returns the ARN.

The SourceArn conditions matter as well: without them the permission allows any EventBridge rule in any account to invoke the target, which is the confused-deputy problem.

  • No values are hardcoded, so the same template deploys to any account or Region.
  • CloudFormation resolves the actual value at deployment time, after the referenced resource exists.
  • The reference is what creates the dependency graph, so creation and deletion happen in the right order without hand-written DependsOn entries.
  • Refactoring a resource does not require finding every place its identifier was pasted.

Runtime identifiers such as nodejs24.x are the exception — they are pinned deliberately, and they expire. Lambda deprecates runtimes on a published schedule and eventually blocks creating and updating functions that use a deprecated one, so check the Lambda runtime support table rather than copying a runtime string from an old template.