Trying Out Cross-Account References with CloudFormation's Fn::GetStackOutput

Trying Out Cross-Account References with CloudFormation's Fn::GetStackOutput

I tested cross-account stack references using CloudFormation's new intrinsic function Fn::GetStackOutput. This post walks through the steps I deployed and verified, including IAM role design and CFn execution role settings.
2026.05.04

This page has been translated by machine translation. View original

The previous article verified the basic behavior of Fn::GetStackOutput and cross-region references. I had left cross-account references unverified, and this time I'm presenting the results of that verification.

https://dev.classmethod.jp/articles/cloudformation-fn-getstackoutput-cross-region-reference/

https://dev.classmethod.jp/articles/cloudformation-fn-getstackoutput-ga-known-limitations/

Verification Scenario

For the cross-account reference verification scenario, I chose EventBridge cross-account forwarding.

  • Aggregator account (= Producer): The side that has a custom event bus and receives events. The side that provides the Output via Fn::GetStackOutput
  • Sender account (= Consumer): The side that references the aggregator account's event bus ARN via Fn::GetStackOutput and forwards events

Fn::GetStackOutput is used in two places.

  • eb-role.yaml: Specifies the event bus ARN in the policy resource of the forwarding role
  • eb-consumer.yaml: Specifies the event bus ARN in the target ARN of the forwarding rule

At deploy time (ARN resolution via Fn::GetStackOutput)

At runtime (EventBridge event forwarding)

CloudFormation Templates

eb-producer.yaml (Aggregator account)

This creates a custom event bus and CloudWatch Logs. EventBusPolicy allows events:PutEvents from the sender account. EventBridge cross-account forwarding requires both the EventBusPolicy on the receiving bus side and the forwarding role on the sender side (described later). I plan to touch on this point in another article as well.

AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  ConsumerAccountId:
    Type: String

Resources:
  EventBus:
    Type: AWS::Events::EventBus
    Properties:
      Name: getstackoutput-cross-account-bus

  EventBusPolicy:
    Type: AWS::Events::EventBusPolicy
    Properties:
      EventBusName: !Ref EventBus
      StatementId: AllowCrossAccountPutEvents
      Action: events:PutEvents
      Principal: !Ref ConsumerAccountId

  LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: /aws/events/getstackoutput-cross-account
      RetentionInDays: 7

  LogGroupPolicy:
    Type: AWS::Logs::ResourcePolicy
    Properties:
      PolicyName: EventBridgeToCloudWatchLogs
      PolicyDocument: !Sub |
        {
          "Version": "2012-10-17",
          "Statement": [{
            "Effect": "Allow",
            "Principal": {"Service": "events.amazonaws.com"},
            "Action": ["logs:CreateLogStream", "logs:PutLogEvents"],
            "Resource": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/events/getstackoutput-cross-account:*"
          }]
        }

  Rule:
    Type: AWS::Events::Rule
    Properties:
      EventBusName: !Ref EventBus
      EventPattern:
        source:
          - my.test
      Targets:
        - Id: CloudWatchLogs
          Arn: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/events/getstackoutput-cross-account'

Outputs:
  EventBusArn:
    Value: !GetAtt EventBus.Arn

cfn-role.yaml (Aggregator account)

This is the IAM role used by Fn::GetStackOutput. It trusts roles in the sender account that have the cfn-getstackoutput- prefix via prefix matching, and grants the cloudformation:DescribeStacks permission.

AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  ConsumerAccountId:
    Type: String

Resources:
  GetStackOutputRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: cfn-getstackoutput-cross-account-role
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              AWS: !Sub 'arn:aws:iam::${ConsumerAccountId}:root'
            Action: sts:AssumeRole
            Condition:
              ArnLike:
                aws:PrincipalArn: !Sub 'arn:aws:iam::${ConsumerAccountId}:role/cfn-getstackoutput-*'

  GetStackOutputPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: cfn-getstackoutput-describe-stacks-policy
      Roles:
        - !Ref GetStackOutputRole
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Action: cloudformation:DescribeStacks
            Resource:
              - !Sub 'arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/cfn-getstackoutput-eb-producer/*'

Outputs:
  RoleArn:
    Value: !GetAtt GetStackOutputRole.Arn

About the trust policy design: root (the entire account) is used as the Principal, but the ArnLike condition restricts it to roles with the cfn-getstackoutput- prefix. Specifying a specific role ARN directly would be stricter, but there is a tradeoff: the aggregator account side would need to be updated every time the deployment method changes.

cfn-exec-role.yaml (Sender account)

This is the CFn execution role used in the sender account. By unifying names with the cfn-getstackoutput- prefix, it matches the prefix-matching trust policy of the aggregator account.

cfn-exec-role.yaml (click to expand)
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  AggregatorAccountId:
    Type: String

Resources:
  CfnExecRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: cfn-getstackoutput-cfn-exec-role
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: cloudformation.amazonaws.com
            Action: sts:AssumeRole

  CfnExecPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: cfn-getstackoutput-cfn-exec-policy
      Roles:
        - !Ref CfnExecRole
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Action:
              - cloudformation:CreateStack
              - cloudformation:UpdateStack
              - cloudformation:DeleteStack
              - cloudformation:DescribeStacks
              - cloudformation:DescribeStackEvents
              - cloudformation:CreateChangeSet
              - cloudformation:ExecuteChangeSet
              - cloudformation:DescribeChangeSet
              - cloudformation:DeleteChangeSet
              - cloudformation:GetTemplate
            Resource: '*'
          - Effect: Allow
            Action:
              - iam:CreateRole
              - iam:DeleteRole
              - iam:GetRole
              - iam:PutRolePolicy
              - iam:DeleteRolePolicy
              - iam:GetRolePolicy
              - iam:AttachRolePolicy
              - iam:DetachRolePolicy
              - iam:PassRole
            Resource:
              - !Sub 'arn:aws:iam::${AWS::AccountId}:role/cfn-getstackoutput-*'
              - !Sub 'arn:aws:iam::${AWS::AccountId}:policy/cfn-getstackoutput-*'
          - Effect: Allow
            Action:
              - events:CreateEventBus
              - events:DeleteEventBus
              - events:DescribeEventBus
              - events:PutRule
              - events:DeleteRule
              - events:DescribeRule
              - events:PutTargets
              - events:RemoveTargets
              - events:ListTargetsByRule
              - events:TagResource
              - events:UntagResource
            Resource: '*'
          - Effect: Allow
            Action: sts:AssumeRole
            Resource: !Sub 'arn:aws:iam::${AggregatorAccountId}:role/cfn-getstackoutput-*'

Outputs:
  RoleArn:
    Value: !GetAtt CfnExecRole.Arn

eb-role.yaml (Sender account)

This is the first place Fn::GetStackOutput is used. In the policy resource of the EventBridge forwarding role, the aggregator account's event bus ARN is obtained and specified via Fn::GetStackOutput.

The reason for separating eb-role.yaml and eb-consumer.yaml is to show that Fn::GetStackOutput can be used in different places: the IAM policy Resource and the EventBridge rule target ARN. In actual operation, combining them into one stack also works.

AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  GetStackOutputRoleArn:
    Type: String

Resources:
  EventBridgeForwardRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: cfn-getstackoutput-eb-forward-role
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: events.amazonaws.com
            Action: sts:AssumeRole

  EventBridgeForwardPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: cfn-getstackoutput-eb-forward-policy
      Roles:
        - !Ref EventBridgeForwardRole
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Action: events:PutEvents
            Resource:
              Fn::GetStackOutput:           # ← Get the aggregator account's EventBusArn here
                StackName: cfn-getstackoutput-eb-producer
                OutputName: EventBusArn
                Region: ap-northeast-1
                RoleArn: !Ref GetStackOutputRoleArn

Outputs:
  ForwardRoleArn:
    Value: !GetAtt EventBridgeForwardRole.Arn

eb-consumer.yaml (Sender account)

This is the second place Fn::GetStackOutput is used. In the target ARN of the EventBridge rule, the aggregator account's event bus ARN is obtained and specified via Fn::GetStackOutput.

AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  GetStackOutputRoleArn:
    Type: String
  ForwardRoleArn:
    Type: String

Resources:
  EventBus:
    Type: AWS::Events::EventBus
    Properties:
      Name: getstackoutput-cross-account-consumer-bus

  ForwardRule:
    Type: AWS::Events::Rule
    Properties:
      EventBusName: !Ref EventBus
      EventPattern:
        source:
          - my.test
      Targets:
        - Id: CrossAccountBus
          Arn:
            Fn::GetStackOutput:             # ← Get the aggregator account's EventBusArn here
              StackName: cfn-getstackoutput-eb-producer
              OutputName: EventBusArn
              Region: ap-northeast-1
              RoleArn: !Ref GetStackOutputRoleArn
          RoleArn: !Ref ForwardRoleArn

Outputs:
  EventBusName:
    Value: !Ref EventBus

Deployment Steps

Set the following shell variables before running the commands.

AGGREGATOR_ACCOUNT_ID=111111111111   # Aggregator account ID
CONSUMER_ACCOUNT_ID=222222222222     # Sender account ID

# Verify variables
echo "Aggregator: ${AGGREGATOR_ACCOUNT_ID:?未設定}"
echo "Consumer:   ${CONSUMER_ACCOUNT_ID:?未設定}"

Steps 1 and 2: Deploy to the aggregator account

# After switching to the aggregator account

aws cloudformation deploy \
  --stack-name cfn-getstackoutput-eb-producer \
  --template-file eb-producer.yaml \
  --parameter-overrides ConsumerAccountId=${CONSUMER_ACCOUNT_ID} \
  --region ap-northeast-1

aws cloudformation deploy \
  --stack-name cfn-getstackoutput-cfn-role \
  --template-file cfn-role.yaml \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides ConsumerAccountId=${CONSUMER_ACCOUNT_ID} \
  --region ap-northeast-1

Steps 3, 4, and 5: Deploy to the sender account

# After switching to the sender account

aws cloudformation deploy \
  --stack-name cfn-getstackoutput-exec-role \
  --template-file cfn-exec-role.yaml \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides AggregatorAccountId=${AGGREGATOR_ACCOUNT_ID} \
  --region ap-northeast-1

aws cloudformation deploy \
  --stack-name cfn-getstackoutput-eb-role \
  --template-file eb-role.yaml \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides \
    GetStackOutputRoleArn=arn:aws:iam::${AGGREGATOR_ACCOUNT_ID}:role/cfn-getstackoutput-cross-account-role \
  --role-arn arn:aws:iam::${CONSUMER_ACCOUNT_ID}:role/cfn-getstackoutput-cfn-exec-role \
  --region ap-northeast-1

aws cloudformation deploy \
  --stack-name cfn-getstackoutput-eb-consumer \
  --template-file eb-consumer.yaml \
  --parameter-overrides \
    GetStackOutputRoleArn=arn:aws:iam::${AGGREGATOR_ACCOUNT_ID}:role/cfn-getstackoutput-cross-account-role \
    ForwardRoleArn=arn:aws:iam::${CONSUMER_ACCOUNT_ID}:role/cfn-getstackoutput-eb-forward-role \
  --role-arn arn:aws:iam::${CONSUMER_ACCOUNT_ID}:role/cfn-getstackoutput-cfn-exec-role \
  --region ap-northeast-1

Operation Check

Sending an event (Sender account)

aws events put-events \
  --entries '[{
    "Source": "my.test",
    "DetailType": "TestEvent",
    "Detail": "{\"message\": \"hello from cross-account\"}",
    "EventBusName": "getstackoutput-cross-account-consumer-bus"
  }]' \
  --region ap-northeast-1

Checking receipt in CloudWatch Logs (Aggregator account)

aws logs filter-log-events \
  --log-group-name /aws/events/getstackoutput-cross-account \
  --region ap-northeast-1

Result

{
  "version": "0",
  "id": "9409e3bd-f762-32c4-e773-aa9c78451b39",
  "detail-type": "TestEvent",
  "source": "my.test",
  "account": "222222222222",
  "time": "2026-05-04T10:14:29Z",
  "region": "ap-northeast-1",
  "detail": {"message": "hello from cross-account"}
}

The event sent from the sender account (222222222222) arrived in the CloudWatch Logs of the aggregator account (111111111111). We confirmed that the cross-account EventBusArn was obtained via Fn::GetStackOutput and that EventBridge forwarding works correctly.

Summary

We confirmed that cross-account references with Fn::GetStackOutput work.

There are two key points when using it cross-account.

Aggregator account side (Producer):

  • Create an IAM role for Fn::GetStackOutput and trust the sender account's CFn execution role
  • The only permission is cloudformation:DescribeStacks (limited to the target stack)

Sender account side (Consumer):

  • Explicitly specify the CFn execution role with --role-arn when deploying
  • Match the CFn execution role's name to the condition (prefix match) in the aggregator account's trust policy

Through this verification, we found that the cross-account references left unverified in the previous article can be used. While waiting for the official release of Fn::GetStackOutput, I'd like to consider using it in multi-account setups.

Notes and Limitations

This article covers notes specific to cross-account use. For the behavior of stack deletion (such as what happens when you update the Consumer after deleting the Producer), please refer to the previous article.

  • Specifying the CFn execution role: If you omit --role-arn, the role of the user running the deployment calls sts:AssumeRole, which may not match the trust policy on the aggregator account side. We recommend explicitly specifying a dedicated CFn execution role with --role-arn
  • Narrowing down the allowed targets: If an Output may contain sensitive information, we recommend limiting the Resource in cfn-role.yaml to the target stack (storing sensitive information in Parameter Store or Secrets Manager is the CloudFormation best practice)
  • Visualizing dependencies: When going cross-account, the aggregator account side cannot see "which account's Consumer is referencing my Output." Since this feature does not have the deletion guard that Fn::ImportValue has, be mindful of dependency management in long-term operation

Confused Deputy Protection

EventBridgeForwardRole is a service role that trusts events.amazonaws.com. If configured without conditions, it may be assumed by other event buses or rules within the same AWS account (the Confused Deputy problem). By adding the aws:SourceAccount and aws:SourceArn conditions, you can restrict invocations to only the intended resources.

Condition:
  StringEquals:
    aws:SourceAccount: !Ref AWS::AccountId
  ArnLike:
    aws:SourceArn: !Sub 'arn:aws:events:${AWS::Region}:${AWS::AccountId}:rule/*'

References

Share this article

AWSのお困り事はクラスメソッドへ