Skip to content

Deploy: With Separate Functional Accounts

Four phases, in order. Use this page when you are bringing existing accounts and FinOps, audit, or the log archive lives on an account of its own.

Which template and flags for your edition

Every command below uses the Governance template. If you bought Automate, substitute the Admin template and its flags throughout:

Edition Template Flags
Observe RosettaOpsGovernance* EnablePermissions=false
Govern RosettaOpsGovernance* EnablePermissions=true
Automate RosettaOpsAdmin* EnableDestroy, EnableFederation, EnableMetacloud, EnableLifecycle -- EnablePermissions does not exist, it is always on

Before starting you need your UserUid and ApiKey from the RosettaHub onboarding email. See Onboarding Your Organization if you do not have them yet.

flowchart TD
    P1["1 &nbsp; Control role<br/><i>payer</i>"] --> P2["2 &nbsp; Functional accounts<br/><i>FinOps, audit, log archive</i>"]
    P2 --> P3["3 &nbsp; Management template<br/><i>payer</i>"]
    P3 --> P4["4 &nbsp; Member accounts<br/><i>StackSet</i>"]

    style P1 fill:#e3f2fd,stroke:#1565c0,color:#000
    style P2 fill:#e3f2fd,stroke:#1565c0,color:#000
    style P3 fill:#fff9c4,stroke:#f9a825,color:#000
    style P4 fill:#e8f5e9,stroke:#2e7d32,color:#000

The order is not arbitrary. Each phase depends on something the previous one created, and phase 1 exists only on this path: the functional templates trust the control role by ARN, and IAM validates a trust principal at creation time, so the control role must exist before they deploy. It therefore cannot wait for the management template in phase 3 to create it.

Set your variables

BASE=https://com-rosettahub-public-code.s3.eu-west-1.amazonaws.com/cloudformation/2.0.0
PAYER=111111111111          # your management account number
FINOPS=222222222222
AUDIT=333333333333
LOGARCHIVE=444444444444
ROOT_OU=r-xxxx              # any OU the functional accounts already sit in
MEMBER_OU=ou-xxxx-xxxxxxxx  # the OU holding the accounts you want governed
USER_UID=<from your email>
API_KEY=<from your email>
REGION=eu-west-1            # must be one of the four -- see Regions below

1. The control role, on the payer

aws cloudformation create-stack \
    --stack-name RosettaOpsControlRole \
    --template-url $BASE/RosettaOpsControlRole.yaml \
    --parameters ParameterKey=UserUid,ParameterValue=$USER_UID \
    --capabilities CAPABILITY_NAMED_IAM \
    --region $REGION

aws cloudformation wait stack-create-complete \
    --stack-name RosettaOpsControlRole --region $REGION

Creates the role RosettaHub assumes to reach everything else. Every other template names it in a trust policy, so anything deployed before this fails outright with Invalid principal in policy.

2. The functional accounts

One stack set, three instances. The three functional accounts take identical values for ControlRoleName, ManagementAccountId and the Enable* flags. They differ in exactly one thing each: which role they claim. So the shared values go on the stack set and the rest are per-instance overrides.

These templates carry no Lambda, which is what lets them run this early: there is nothing to call the platform before the organization is registered.

aws cloudformation create-stack-set \
    --stack-set-name RosettaOpsGovernanceFunctional \
    --template-url $BASE/RosettaOpsGovernanceFunctional.yaml \
    --permission-model SERVICE_MANAGED \
    --auto-deployment Enabled=false \
    --parameters ParameterKey=ManagementAccountId,ParameterValue=$PAYER \
                 ParameterKey=EnablePermissions,ParameterValue=true \
    --capabilities CAPABILITY_NAMED_IAM \
    --region $REGION

Then once per account, naming an OU they already sit in and narrowing to the one account:

# FinOps account
aws cloudformation create-stack-instances \
    --stack-set-name RosettaOpsGovernanceFunctional \
    --deployment-targets OrganizationalUnitIds=$ROOT_OU,Accounts=$FINOPS,AccountFilterType=INTERSECTION \
    --regions $REGION --region $REGION \
    --parameter-overrides ParameterKey=FinopsAccountId,ParameterValue=self \
                          ParameterKey=FinopsBucketName,ParameterValue=my-cur-bucket

# Audit account
aws cloudformation create-stack-instances \
    --stack-set-name RosettaOpsGovernanceFunctional \
    --deployment-targets OrganizationalUnitIds=$ROOT_OU,Accounts=$AUDIT,AccountFilterType=INTERSECTION \
    --regions $REGION --region $REGION \
    --parameter-overrides ParameterKey=AuditAccountId,ParameterValue=self

# Log archive account
aws cloudformation create-stack-instances \
    --stack-set-name RosettaOpsGovernanceFunctional \
    --deployment-targets OrganizationalUnitIds=$ROOT_OU,Accounts=$LOGARCHIVE,AccountFilterType=INTERSECTION \
    --regions $REGION --region $REGION \
    --parameter-overrides ParameterKey=LogArchiveAccountId,ParameterValue=self

Each account sets its own role parameter to self and leaves the other two at their default. Two of the three roles can also share one account: set both to self in the same override, and skip the instance for the account you no longer need.

Three details that decide this shape

AccountFilterType=INTERSECTION means you do not need an OU per account. Name an OU the accounts already sit in -- the root OU is fine -- and filter to one account. Without it you would be creating organizational units purely to express "this one account", which is org structure bending to a deployment mechanism rather than the other way round.

Use SERVICE_MANAGED, not self-managed. Self-managed targets bare account IDs with no OU, which sounds like exactly what you want here, but it requires you to pre-create AWSCloudFormationStackSetExecutionRole in every target account -- and AWS's own sample template for that role grants administrator access. You would be creating and owning that privilege by hand, in accounts you are setting up for the first time, which is the bootstrap problem again. Service-managed needs no pre-created role: CloudFormation provisions what it needs through Organizations trusted access.

--auto-deployment Enabled=false matters on this one. With it on, any new account landing in that OU would pick up the stack set's defaults -- all three role IDs empty -- and quietly acquire a functional stack it should not have. An estate must not grow a second FinOps account by accident.

The member stack set in phase 4 is the opposite case: identical parameters everywhere, so it targets an OU directly with auto-deployment on.

3. The management template, on the payer

aws cloudformation create-stack \
    --stack-name RosettaOpsManagement \
    --template-url $BASE/RosettaOpsGovernanceManagement.yaml \
    --parameters ParameterKey=UserUid,ParameterValue=$USER_UID \
                 ParameterKey=ApiKey,ParameterValue=$API_KEY \
                 ParameterKey=EnablePermissions,ParameterValue=true \
                 ParameterKey=CreateControlRole,ParameterValue=false \
                 ParameterKey=FinopsAccountId,ParameterValue=$FINOPS \
                 ParameterKey=AuditAccountId,ParameterValue=$AUDIT \
                 ParameterKey=LogArchiveAccountId,ParameterValue=$LOGARCHIVE \
    --capabilities CAPABILITY_NAMED_IAM \
    --region $REGION

aws cloudformation wait stack-create-complete \
    --stack-name RosettaOpsManagement --region $REGION

This is the template that registers your organization with the platform.

Parameter Notes
CreateControlRole false, because phase 1 already created the role. Leaving it true fails with "already exists"
FinopsAccountId, AuditAccountId, LogArchiveAccountId The account number of whichever account plays that role, or self for any that stayed on the payer
EnablePermissions true for Govern, false for Observe
CreateOrganizationIfMissing true only if this account is not yet in an AWS Organization
BillingBucketName Optional. Empty means the platform creates the CUR bucket

Do not leave the role IDs empty

An empty role account ID asks RosettaHub to create that account, which needs the Lifecycle grant and is therefore Automate only. On Observe or Govern, name an account you already own.

4. The member accounts

aws cloudformation create-stack-set \
    --stack-set-name RosettaOpsMembers \
    --template-url $BASE/RosettaOpsGovernance.yaml \
    --parameters ParameterKey=ManagementAccountId,ParameterValue=$PAYER \
                 ParameterKey=ApiKey,ParameterValue=$API_KEY \
                 ParameterKey=EnablePermissions,ParameterValue=true \
    --permission-model SERVICE_MANAGED \
    --auto-deployment Enabled=true,RetainStacksOnAccountRemoval=false \
    --capabilities CAPABILITY_NAMED_IAM \
    --region $REGION

aws cloudformation create-stack-instances \
    --stack-set-name RosettaOpsMembers \
    --deployment-targets OrganizationalUnitIds=$MEMBER_OU \
    --regions $REGION \
    --region $REGION

Every member account takes the same parameters, so this targets the OU directly with no per-instance overrides and no account filter. Each instance registers its own account against the organization created in phase 3, and with --auto-deployment Enabled=true, accounts added to that OU later onboard themselves.

Keep the functional accounts out of this OU

The member template carries no role parameters, so a functional account that lands in the member StackSet's target gets under-provisioned rather than misconfigured. That is the safe failure, but it is still a failure.

Optional -- narrow the bootstrap trust

Only for accounts RosettaHub created for you, and only once setup on each is verified. It narrows the trust on AWS's own OrganizationAccountAccessRole so RosettaHub can no longer assume it.

aws cloudformation create-stack \
    --stack-name RosettaOpsOrgAccessRoleTrust \
    --template-url $BASE/RosettaOpsOrgAccessRoleTrust.yaml \
    --capabilities CAPABILITY_NAMED_IAM \
    --region $REGION

Applied before RosettaHubAccessRole exists on that account, it leaves the account unreachable by RosettaHub until recovered. See Account Access and Scopes for the caveats and the StackSet recovery route.

Regions

The management and member templates deploy a setup Lambda whose code bucket must live in the function's own region, so both are limited to:

us-east-1    us-west-2    eu-west-1    ap-southeast-1

Anywhere else fails at mapping lookup. This constrains the member StackSet's --regions exactly as it constrains the payer's stack.

The control role and the functional templates create no Lambda and deploy anywhere. RosettaOpsOrgAccessRoleTrust does create one, but its code is inline in the template rather than fetched from a regional bucket, so it is unaffected.

Next