!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 |
|---|---|
| Parameter | The parameter’s value |
AWS::SNS::Topic | The topic ARN |
AWS::SQS::Queue | The queue URL |
AWS::S3::Bucket | The bucket name |
AWS::EC2::Instance | The instance ID |
AWS::EC2::VPC | The VPC ID |
AWS::IAM::Role | The role name |
AWS::Lambda::Function | The 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 nameNote 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
Section titled “!GetAtt”!GetAtt reads a named attribute of a resource, written as
!GetAtt LogicalName.AttributeName. Nested attributes are addressed with a dot:
LambdaArn: !GetAtt MyLambdaFunction.ArnBucketDomainName: !GetAtt MyBucket.DomainNameStreamArn: !GetAtt MyTable.StreamArnDbEndpoint: !GetAtt MyDbInstance.Endpoint.AddressDbPort: !GetAtt MyDbInstance.Endpoint.PortAlbDnsName: !GetAtt MyLoadBalancer.DNSNameLatestVersion: !GetAtt MyLaunchTemplate.LatestVersionNumberThe distinction in one line:
TopicArn: !Ref MyTopic # the default value, which happens to be the ARNTopicName: !GetAtt MyTopic.TopicName # a specific named attributeWorked 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.ArnBoth 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.
Why these functions rather than literals
Section titled “Why these functions rather than literals”- 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
DependsOnentries. - 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.