Plan AWS Tags with PowerShell
Build a read-only PowerShell planner for safe AWS multi-account tags.
Introduction
30 Second Summary
A familiar label can feel trustworthy even when it points somewhere unexpected. One unchecked assumption can turn a routine update into a change in the wrong place.
In this project, you will build a read-only multi-account tagging planner with PowerShell. It verifies each AWS IAM Identity Center profile with AWS STS before producing a local tag preview.
What You'll Build
Picture running one planner command to reveal each profile's real account before handing a reviewer an approval-ready tag preview without changing a single AWS resource.
By the end of this project, you'll have:
- A verified identity table that shows the actual account ID behind every configured profile plus the caller ARN returned by AWS.
- A blocked-by-default guardrail that catches deliberately swapped account mappings as AccountMismatch before any tag plan rows are produced.
- A reviewable change plan for approved regional resource ARNs with ManagedBy, Project, Owner, and account-specific Environment tags exported to identity-report.csv, tagging-plan.csv, tagging-plan.json, and permission-readiness.md.
- Secret Mission: Add a third account to trigger a Region mismatch before correcting it and regenerating the audit bundle.
Are there any prerequisites?
You need a Windows computer with PowerShell plus AWS CLI v2 already installed. You also need access to at least two AWS accounts through AWS IAM Identity Center plus one approved regional resource ARN for each account.
Have your organization's SSO Start URL plus SSO Region ready if your named profiles still need configuration.
Before We Start
Before any hands-on work, lock in the scope of your two-account pilot. This checkpoint confirms that your different Environment tags use approved regional account-bearing ARNs within a read-only AWS plan.
Verify Your Read-Only Toolchain
The planner's safety checks depend on identity output from your local toolchain. An unknown shell or CLI version could undermine those checks before the script starts.
You will verify Windows PowerShell and AWS CLI v2.
You will finish with two authenticated AWS IAM Identity Center profiles.
In this step, get ready to:
- Verify Windows PowerShell and AWS CLI v2.
- Prepare the local .\multi-account-tagging-planner directory.
- Authenticate two known IAM Identity Center profiles.
Verify PowerShell and AWS CLI
PowerShell runs the planner locally. AWS CLI supplies the authenticated identity information that the planner checks.
- Press the Windows key to open Windows search.
- Type Windows PowerShell into the search field.
- Press Enter to open Windows PowerShell.
- Display the PowerShell version and check the AWS CLI version by running:
$PSVersionTable.PSVersion
aws --version
What Do These Checks Show?
- The first line reads PSVersion from PowerShell's version table.
- The second line reveals the installed AWS CLI major version.
✔️ I see aws-cli/2
Your shell and AWS CLI are ready for the planner.
- Continue to the folder setup below.
ⓧ I see an older major version
The planner requires AWS CLI v2. Update the CLI before continuing.
- Open the official AWS CLI installation guide.
- Follow the Windows installation steps for AWS CLI v2.
- Close the current PowerShell window after installation.
- Press the Windows key to open Windows search.
- Type Windows PowerShell into the search field.
- Press Enter to reopen Windows PowerShell.
- Check the updated CLI version by running:
aws --version
What Should I See?
The output should now contain aws-cli/2.
Still Seeing the Older Version?
Close every PowerShell window before reopening the shell. This gives the updated installation a fresh session.
Use the official installation guide to confirm that you completed the Windows steps for AWS CLI v2.
Ask for help with an AWS CLI version that did not update.
ⓧ Command not found
PowerShell cannot currently find AWS CLI. Install AWS CLI v2 before continuing.
- Open the official AWS CLI installation guide.
- Follow the Windows installation steps for AWS CLI v2.
- Close the current PowerShell window after installation.
- Press the Windows key to open Windows search.
- Type Windows PowerShell into the search field.
- Press Enter to reopen Windows PowerShell.
- Verify the installation by running:
aws --version
What Should I See?
A working AWS CLI v2 installation reports a version containing aws-cli/2.
PowerShell Still Cannot Find AWS CLI?
Restart Windows if a fresh PowerShell session still cannot find the newly installed CLI.
Ask for help with an AWS CLI installation that PowerShell cannot find.
- Create the local project directory and enter it by running these commands:
New-Item -ItemType Directory -Path .\multi-account-tagging-planner -Force
Set-Location .\multi-account-tagging-planner
What Do These Commands Do?
The first line creates the multi-account-tagging-planner directory. The -Force option also allows PowerShell to return an existing directory without deleting its files.
The second line uses Set-Location to make that directory your current location.
- Confirm that the PowerShell prompt shows multi-account-tagging-planner as the current directory.
Did the Directory Setup Fail?
Confirm that both lines were pasted into the same PowerShell session. The second line depends on the directory created by the first line.
Ask for help with creating the local planner directory in PowerShell.
Confirm your IAM Identity Center profiles
A named profile is a local label for an account and permission set. The planner needs two distinct profile names before it can discover the identities behind them.
- List every profile available to AWS CLI by running:
aws configure list-profiles
What Does This Command Do?
AWS CLI prints the names of locally configured profiles. This list lets you confirm that each pilot account has a profile.
✔️ I see both intended profiles
Both intended profile names are available to AWS CLI.
- Continue below to record both profile names.
ⓧ One or both profiles are missing
The IAM Identity Center wizard creates a named profile from your organization's sign-in details. Gather each missing account's details before starting it.
- Get your organization's SSO Start URL from your administrator.
- Confirm your organization's SSO Region.
- Identify the intended AWS account.
- Identify the permission set for that account.
- Choose the default Region for that profile.
- Choose a unique profile name for that account.
- Start the IAM Identity Center profile wizard by running:
aws configure sso
What Does This Wizard Do?
The wizard uses your SSO Start URL and SSO Region to connect AWS CLI to IAM Identity Center. It then creates a named profile for the account and permission set you select.
- Enter the prepared SSO Start URL when the wizard requests it.
- Enter the prepared SSO Region when the wizard requests it.
- Select the intended AWS account.
- Select the intended permission set.
- Enter the default Region for the profile.
- Enter the unique profile name.
- Repeat the wizard for the second account if its profile is also missing.
- List the profiles again by running:
aws configure list-profiles
What Should I See Now?
The refreshed list should contain a unique profile name for each of your two pilot accounts.
Profile Still Missing?
Run the wizard again for the missing account. Confirm that you enter a unique profile name before the wizard finishes.
Ask for help with an IAM Identity Center profile that does not appear.
- Record the first intended profile name here: your first profile name.
- Record the second intended profile name here: your second profile name.
Sign in and run the final check
Each IAM Identity Center login retrieves and caches temporary credentials for one named profile. The planner can use those credentials without performing an AWS tagging operation.
This step touches your organization's sign-in flow. Your project remains inside its read-only boundary.
- Expect each command to start or direct you to your organization's authentication flow.
- Complete any required sign-in or approval request before the next command starts.
- Sign in to both profiles by running these commands one at a time:
aws sso login --profile [[FIRST_PROFILE="your first profile name"]]
aws sso login --profile [[SECOND_PROFILE="your second profile name"]]
What Do These Commands Do?
Each command retrieves and caches IAM Identity Center credentials for the selected profile. The --profile flag keeps the two account sessions distinct.
You should receive a successful sign-in confirmation for each profile.
Could Not Complete a Profile Login?
Compare the profile name in the failed command with the names returned by your profile list. Profile names must match exactly.
Restart the login command if your organization's authentication flow expires before you finish it.
Ask for help with an IAM Identity Center profile login failure.
Execution policy changes can feel risky. The Process scope confines RemoteSigned to the current PowerShell session.
- If PowerShell later restricts local script execution, allow it only in the current process by running:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process
Why Use Process Scope?
This setting lasts only for the current PowerShell session. PowerShell removes it when the session closes.
Execution Policy Change Rejected?
An organization policy can override the setting for your current process. Ask your administrator before attempting any broader policy change.
Ask for help with a restricted PowerShell execution policy.
Before you run the final check, predict what each of its three parts will report.
- Run the complete toolchain check from the multi-account-tagging-planner directory:
$PSVersionTable.PSVersion; aws --version; aws configure list-profiles
What Should I See?
- The PowerShell section contains a version number.
- The AWS CLI section contains aws-cli/2.
- The profile list contains both intended profile names.
That foundation is secure: both named profiles are authenticated through the verified CLI.
Next, you will ask AWS which account and caller ARN each profile actually reaches.
Discover Profile Identities
Your PowerShell toolchain is ready. Your two AWS IAM Identity Center profiles are signed in.
A profile name is only a local label. It cannot prove which AWS account the cached credentials reach.
This step asks AWS STS for the actual account ID behind each profile. It also captures the caller ARN as evidence of the identity in use.
In this step, get ready to:
- Create a two-account configuration with distinct environment values.
- Call AWS STS through each signed-in profile.
- Record the observed account IDs for the next step.
Create the account map
The JSON configuration gives the planner one entry for each profile. Distinct environment values let you tell the two accounts apart in later plan rows.
- Create accounts.json inside the current .\multi-account-tagging-planner folder by pasting this configuration into a new file in your text editor:
{
"Accounts": [
{
"Profile": "replace-with-first-profile",
"Region": "replace-with-first-region",
"Environment": "Development"
},
{
"Profile": "replace-with-second-profile",
"Region": "replace-with-second-region",
"Environment": "Production"
}
]
}
What does this configuration control?
- The Accounts array keeps the two profile configurations together.
- Each Profile value identifies the signed-in profile that the planner queries.
- Each Region value declares the intended AWS Region for that account.
- Each Environment value gives the account a distinct planning label.
- Save accounts.json in the current project folder.
- Confirm your editor lists accounts.json with two account objects inside it.
Seeing invalid JSON?
- Check that the comma remains between the two account objects.
- Check that every property name stays inside double quotation marks.
- Check that the final account object has no trailing comma.
Help me fix my account configuration.
✔️ Awesome, I've got everything!
Your accounts.json template is complete. Keep the file open for the profile values you add before the run.
ⓧ I'd like to double check the full code
The complete configuration template should match this structure.
{
"Accounts": [
{
"Profile": "replace-with-first-profile",
"Region": "replace-with-first-region",
"Environment": "Development"
},
{
"Profile": "replace-with-second-profile",
"Region": "replace-with-second-region",
"Environment": "Production"
}
]
}
Build identity discovery
The planner reads the whole configuration before querying either profile. It converts the configuration into PowerShell objects so each account entry can drive one identity request.
- Create tagging-planner.ps1 inside the current .\multi-account-tagging-planner folder by pasting this script into a new file in your text editor:
$config = Get-Content -Path ".\accounts.json" -Raw | ConvertFrom-Json
foreach ($account in $config.Accounts) {
$identity = & aws sts get-caller-identity --profile $account.Profile --output json --no-cli-pager | ConvertFrom-Json
[pscustomobject]@{
Profile = $account.Profile
ActualAccountId = $identity.Account
CallerArn = $identity.Arn
Status = "IdentityDiscovered"
}
} | Format-Table -AutoSize
What does this code do?
- The Get-Content -Raw pipeline reads accounts.json as one complete value.
- The ConvertFrom-Json command turns that value into objects that PowerShell can inspect.
- The foreach loop processes one account configuration at a time.
- The AWS CLI call retrieves the account ID plus caller ARN for the selected profile.
- The IdentityDiscovered status confirms that AWS returned identity data for that row.
The script contains no AWS tagging write operation. Its only AWS request is the identity lookup.
- Save tagging-planner.ps1 in the current project folder.
- Confirm your editor lists both accounts.json and tagging-planner.ps1 in .\multi-account-tagging-planner.
Does the script look incomplete?
- Check that the closing brace after the custom object also closes the loop.
- Check that Format-Table -AutoSize remains after the loop.
- Check that the file extension is .ps1.
Help me check the identity script.
✔️ Awesome, I've got everything!
Your identity discovery script is saved. It is ready to query the two profiles after you personalize the configuration.
ⓧ I'd like to double check the full code
The complete identity discovery script should match this file.
$config = Get-Content -Path ".\accounts.json" -Raw | ConvertFrom-Json
foreach ($account in $config.Accounts) {
$identity = & aws sts get-caller-identity --profile $account.Profile --output json --no-cli-pager | ConvertFrom-Json
[pscustomobject]@{
Profile = $account.Profile
ActualAccountId = $identity.Account
CallerArn = $identity.Arn
Status = "IdentityDiscovered"
}
} | Format-Table -AutoSize
Run the planner and record the identities
The template now needs the real profile names plus Regions from your signed-in setup. These values select the identities that the planner asks AWS to verify.
- In the accounts.json tab from earlier, replace all four profile and Region placeholders with the corresponding values for your two signed-in profiles.
- Save accounts.json.
Before you run the planner, do you think the friendly profile names alone can prove which AWS accounts you reached?
- Run the planner from the current PowerShell session with this command:
.\tagging-planner.ps1
What should you see?
You should see one IdentityDiscovered row per profile. Each row should contain a populated ActualAccountId plus CallerArn value.
This output proves which identity each profile actually reached. No AWS tagging API was called.
Don't see identity rows?
- Check that each Profile value exactly matches a signed-in profile name from the earlier profile list.
- Sign in again with the affected IAM Identity Center profile if its cached session has expired.
- Rerun tagging-planner.ps1 after restoring the session.
Help me troubleshoot missing identity rows.
- Record the first profile's observed account ID here: your first observed account ID.
- Record the second profile's observed account ID here: your second observed account ID.
- Crop your checkpoint screenshot to the identity table so unrelated terminal history stays out of view.
That is the first safety proof complete. Your planner now knows the real account behind each profile label.
Your identity baseline is ready. Next, you will test whether the planner blocks an account map that disagrees with those observed IDs.
Trigger the Account Guardrail
Your PowerShell planner now reveals the account behind each AWS IAM Identity Center profile. The next safety test asks whether a friendly profile name can override that evidence.
A profile name is only a label. You will deliberately assign each profile the other account's ID before testing the guardrail.
In this step, get ready to:
- Deliberately swap the two observed account IDs in accounts.json.
- Add account identity validation to tagging-planner.ps1.
- Run the planner against the swapped account mapping.
Swap the expected account IDs
The ExpectedAccountId field declares where each profile is allowed to land. Swapping these values creates a controlled mismatch using the identities discovered in the previous step.
- Return to accounts.json in your text editor.
- Add an ExpectedAccountId field below the first Profile field using the second profile's recorded 12-digit account ID.
- Add an ExpectedAccountId field below the second Profile field using the first profile's recorded 12-digit account ID.
- Save accounts.json.
- Compare both ExpectedAccountId values with your safe working note.
The first profile now declares the second account as its expected destination. The second profile declares the first account as its expected destination.
Add the account comparison guardrail
The guardrail classifies an account ID before trusting it. It checks for a placeholder value before checking the 12-digit format or comparing the expected account with the identity returned by AWS.
- In tagging-planner.ps1, replace the opening configuration line with this setup code:
param([string]$ConfigPath = ".\accounts.json")
$config = Get-Content -Path $ConfigPath -Raw | ConvertFrom-Json
$identityRows = @()
What Does This Setup Do?
- The ConfigPath parameter keeps accounts.json as the default configuration file.
- The identityRows array collects every profile result before PowerShell displays the table.
- Save tagging-planner.ps1.
- Confirm the top of the file declares ConfigPath before initializing identityRows.
Seeing Two Configuration Lines?
- Keep only the configuration line that uses $ConfigPath.
- Place $identityRows = @() immediately before the foreach loop.
Help me fix the setup section in my PowerShell planner.
- Replace the existing foreach pipeline in tagging-planner.ps1 with this guarded identity loop:
foreach ($account in $config.Accounts) {
$identity = & aws sts get-caller-identity --profile $account.Profile --output json --no-cli-pager | ConvertFrom-Json
$status = "IdentityVerified"
$detail = "The profile reached the expected account."
if ([string]::IsNullOrWhiteSpace($account.ExpectedAccountId) -or $account.ExpectedAccountId.StartsWith("replace-with-")) {
$status = "IdentityDiscovered"
$detail = "Copy the actual account ID into ExpectedAccountId."
}
elseif ($account.ExpectedAccountId -notmatch "^\d{12}$") {
$status = "ConfigRequired"
$detail = "ExpectedAccountId must contain 12 digits."
}
elseif ($identity.Account -ne $account.ExpectedAccountId) {
$status = "AccountMismatch"
$detail = "The profile reached a different account than configured."
}
$identityRows += [pscustomobject]@{
Profile = $account.Profile
ExpectedAccountId = $account.ExpectedAccountId
ActualAccountId = $identity.Account
CallerArn = $identity.Arn
Status = $status
Detail = $detail
}
}
$identityRows | Format-Table -AutoSize
Write-Host "No tag plan rows were produced. Verify every expected account ID first."
How Does the Guardrail Decide?
- The default IdentityVerified status represents a matching 12-digit account ID.
- A missing value or placeholder produces IdentityDiscovered.
- A value that fails ^\d{12}$ produces ConfigRequired.
- A valid ID that differs from the AWS result produces AccountMismatch.
- The final message makes the absence of tag plan rows explicit.
- Save tagging-planner.ps1.
- Confirm the old } | Format-Table -AutoSize pipeline ending has been removed.
Seeing a PowerShell Syntax Error?
- Check that the foreach loop has its closing brace before the table output.
- Check that each account condition has its own closing brace.
- Remove the previous identity object block if it still appears below the new loop.
Help me find the syntax problem in the account guardrail.
✔️ Awesome, I've got everything!
Both files now contain the deliberately swapped account map and the comparison guardrail.
ⓧ I'd like to double check the full code
- Compare your saved files with these complete references.
{
"Accounts": [
{
"Profile": "replace-with-first-profile",
"ExpectedAccountId": "second-observed-12-digit-account-id",
"Region": "replace-with-first-region",
"Environment": "Development"
},
{
"Profile": "replace-with-second-profile",
"ExpectedAccountId": "first-observed-12-digit-account-id",
"Region": "replace-with-second-region",
"Environment": "Production"
}
]
}
param([string]$ConfigPath = ".\accounts.json")
$config = Get-Content -Path $ConfigPath -Raw | ConvertFrom-Json
$identityRows = @()
foreach ($account in $config.Accounts) {
$identity = & aws sts get-caller-identity --profile $account.Profile --output json --no-cli-pager | ConvertFrom-Json
$status = "IdentityVerified"
$detail = "The profile reached the expected account."
if ([string]::IsNullOrWhiteSpace($account.ExpectedAccountId) -or $account.ExpectedAccountId.StartsWith("replace-with-")) {
$status = "IdentityDiscovered"
$detail = "Copy the actual account ID into ExpectedAccountId."
}
elseif ($account.ExpectedAccountId -notmatch "^\d{12}$") {
$status = "ConfigRequired"
$detail = "ExpectedAccountId must contain 12 digits."
}
elseif ($identity.Account -ne $account.ExpectedAccountId) {
$status = "AccountMismatch"
$detail = "The profile reached a different account than configured."
}
$identityRows += [pscustomobject]@{
Profile = $account.Profile
ExpectedAccountId = $account.ExpectedAccountId
ActualAccountId = $identity.Account
CallerArn = $identity.Arn
Status = $status
Detail = $detail
}
}
$identityRows | Format-Table -AutoSize
Write-Host "No tag plan rows were produced. Verify every expected account ID first."
Run the deliberate mismatch test
This run is the evidence point for the account boundary. The planner compares each AWS-reported identity with the deliberately swapped expectation.
Before you run it, predict whether either profile can pass when its expected ID belongs to the other account.
- Test the swapped account mapping by running this command:
.\tagging-planner.ps1
The Guardrail Blocked Both Profiles
You will see two AccountMismatch rows. You will also see No tag plan rows were produced. Verify every expected account ID first.
This failure is intentional. The planner queried AWS STS through GetCallerIdentity.
That is the guardrail doing its job: both mislabeled profiles are blocked before a change plan exists. No AWS tagging API was called.
Do the Rows Show a Different Status?
- Replace placeholder values if either row shows IdentityDiscovered.
- Enter exactly 12 digits if either row shows ConfigRequired.
- Check the swap if either row shows IdentityVerified.
Help me diagnose why the swapped accounts are not both blocked.
Your planner now rejects a profile-to-account mismatch before planning any tag changes. Next, you will correct the account map and extend the same protection to regional resource ARNs.
Build the Regional ARN Tag Plan
Your deliberate account swap proved that a friendly profile name cannot establish account identity. The account guardrail blocked both profiles before the planner produced any tag rows.
Now you will correct the account map. Your PowerShell planner will parse each ARN before generating a desired tag plan.
In this step, get ready to:
- Correct each profile-to-account mapping.
- Validate each approved regional ARN against its authenticated account and configured Region.
- Generate the four desired tag rows for every valid target.
Correct the account map
The declared account map becomes the source for every local safety check. Each approved resource ARN must contain the same account ID and Region as its account entry.
- Switch back to accounts.json from the previous step.
- Replace its current contents with this configuration scaffold:
{
"CommonTags": {
"ManagedBy": "PowerShell",
"Project": "MultiAccountTaggingPilot",
"Owner": "replace-with-non-sensitive-owner"
},
"Accounts": [
{
"Profile": "replace-with-first-profile",
"ExpectedAccountId": "replace-with-first-12-digit-account-id",
"Region": "replace-with-first-region",
"Environment": "Development",
"ResourceArns": [
"replace-with-first-regional-account-bearing-resource-arn"
]
},
{
"Profile": "replace-with-second-profile",
"ExpectedAccountId": "replace-with-second-12-digit-account-id",
"Region": "replace-with-second-region",
"Environment": "Production",
"ResourceArns": [
"replace-with-second-regional-account-bearing-resource-arn"
]
}
]
}
What does this configuration control?
- The CommonTags object declares the tags shared by every approved target.
- Each ExpectedAccountId value declares which account its profile must reach.
- Each Region value declares the allowed Region for that account's targets.
- Each ResourceArns list limits the plan to explicitly approved resources.
- Each Environment value produces an account-specific desired tag.
- Replace replace-with-first-profile with the first signed-in profile name.
- Replace replace-with-first-12-digit-account-id with that profile's observed account ID from your safe working note.
- Replace replace-with-first-region with the Region of the first approved resource.
- Replace replace-with-first-regional-account-bearing-resource-arn with the first approved ARN.
Keep Tag Values Non-Sensitive
AWS resource tags can become visible through consoles and reports. Use a team label or owner label that contains no personal or confidential information.
This keeps the plan useful without placing sensitive data in resource metadata.
- Replace replace-with-second-profile with the second signed-in profile name.
- Replace replace-with-second-12-digit-account-id with that profile's observed account ID from your safe working note.
- Replace replace-with-second-region with the Region of the second approved resource.
- Replace replace-with-second-regional-account-bearing-resource-arn with the second approved ARN.
- Replace replace-with-non-sensitive-owner with a non-sensitive team or owner label.
- Save accounts.json.
Use these tabs to confirm the complete configuration before testing the corrected account map.
✔️ Awesome, I've got everything!
Your configuration now declares shared tags and two approved regional targets. Confirm that every placeholder has been replaced before you continue.
ⓧ I'd like to double check the full code
{
"CommonTags": {
"ManagedBy": "PowerShell",
"Project": "MultiAccountTaggingPilot",
"Owner": "replace-with-non-sensitive-owner"
},
"Accounts": [
{
"Profile": "replace-with-first-profile",
"ExpectedAccountId": "replace-with-first-12-digit-account-id",
"Region": "replace-with-first-region",
"Environment": "Development",
"ResourceArns": [
"replace-with-first-regional-account-bearing-resource-arn"
]
},
{
"Profile": "replace-with-second-profile",
"ExpectedAccountId": "replace-with-second-12-digit-account-id",
"Region": "replace-with-second-region",
"Environment": "Production",
"ResourceArns": [
"replace-with-second-regional-account-bearing-resource-arn"
]
}
]
}
How should I use this reference?
Compare the structure with your saved accounts.json file. Keep your real profile names and approved AWS values in place of the scaffold placeholders.
- Predict whether the corrected expected account IDs will produce verified identities.
- Test the corrected mapping by running this command:
.\tagging-planner.ps1
What should I see?
You should see two IdentityVerified rows. The message about no tag plan rows remains because the current script still contains only identity planning logic.
That result proves both profiles now reach their declared accounts.
Still seeing an account mismatch?
- Compare each ExpectedAccountId with the ActualAccountId shown for the same profile.
- Check that each account ID contains exactly 12 digits.
- Confirm that the two expected account IDs are no longer swapped.
Help me diagnose the account mapping.
Add ARN validation and desired tags
A regional account-bearing ARN exposes the target service, Region, and account ID in six general fields. The planner can compare those fields locally before it creates any proposed tag rows.
Why limit the pilot to these ARNs?
Some AWS resource ARNs omit the Region or account field. This pilot reports those targets as UnsupportedArnScope because it cannot validate missing identity boundaries safely.
Explicit scope keeps every Ready row tied to an authenticated account and configured Region.
- Switch back to tagging-planner.ps1 from the previous step.
- Select all of the existing script contents.
- Replace the selected contents with the cumulative script in the double-check tab below.
✔️ Awesome, I've got everything!
Your script now validates identity before it parses approved resource ARNs. It produces desired tag rows only after every guardrail passes.
ⓧ I'd like to double check the full code
param(
[string]$ConfigPath = ".\accounts.json",
[string]$OutputDirectory = ".\output",
[switch]$Export
)
Set-StrictMode -Version 2.0
$ErrorActionPreference = "Stop"
function Get-ConfigValue {
param(
[object]$InputObject,
[string]$Name
)
if ($null -eq $InputObject) {
return $null
}
$property = $InputObject.PSObject.Properties[$Name]
if ($null -eq $property) {
return $null
}
return $property.Value
}
function Test-Placeholder {
param([string]$Value)
return [string]::IsNullOrWhiteSpace($Value) -or
$Value.StartsWith("replace-with-", [System.StringComparison]::OrdinalIgnoreCase)
}
function Invoke-AwsJson {
param([string[]]$Arguments)
$result = & aws @Arguments 2>&1
$exitCode = $LASTEXITCODE
$resultText = ($result | Out-String).Trim()
if ($exitCode -ne 0) {
throw "AWS CLI failed with exit code $exitCode. $resultText"
}
if ([string]::IsNullOrWhiteSpace($resultText)) {
throw "AWS CLI returned no JSON output."
}
return $resultText | ConvertFrom-Json
}
$config = Get-Content -Path $ConfigPath -Raw | ConvertFrom-Json
$accounts = @(Get-ConfigValue -InputObject $config -Name "Accounts")
$commonTags = Get-ConfigValue -InputObject $config -Name "CommonTags"
if ($accounts.Count -lt 1) {
throw "The configuration must contain at least one account."
}
$identityRows = @()
$planRows = @()
foreach ($account in $accounts) {
$profile = [string](Get-ConfigValue -InputObject $account -Name "Profile")
$expectedAccountId = [string](Get-ConfigValue -InputObject $account -Name "ExpectedAccountId")
$region = [string](Get-ConfigValue -InputObject $account -Name "Region")
$environment = [string](Get-ConfigValue -InputObject $account -Name "Environment")
$actualAccountId = ""
$callerArn = ""
$identityStatus = "ConfigRequired"
$identityDetail = "Replace the profile placeholder."
if (-not (Test-Placeholder -Value $profile)) {
try {
$identity = Invoke-AwsJson -Arguments @(
"sts",
"get-caller-identity",
"--profile",
$profile,
"--output",
"json",
"--no-cli-pager"
)
$actualAccountId = [string]$identity.Account
$callerArn = [string]$identity.Arn
if (Test-Placeholder -Value $expectedAccountId) {
$identityStatus = "IdentityDiscovered"
$identityDetail = "Copy the actual account ID into ExpectedAccountId."
}
elseif ($expectedAccountId -notmatch "^\d{12}$") {
$identityStatus = "ConfigRequired"
$identityDetail = "ExpectedAccountId must contain 12 digits."
}
elseif ($actualAccountId -ne $expectedAccountId) {
$identityStatus = "AccountMismatch"
$identityDetail = "The profile reached a different account than configured."
}
else {
$identityStatus = "IdentityVerified"
$identityDetail = "The profile reached the expected account."
}
}
catch {
$identityStatus = "AuthenticationFailed"
$identityDetail = $_.Exception.Message
}
}
$identityRows += [pscustomobject][ordered]@{
Profile = $profile
ExpectedAccountId = $expectedAccountId
ActualAccountId = $actualAccountId
CallerArn = $callerArn
Status = $identityStatus
Detail = $identityDetail
}
if ($identityStatus -ne "IdentityVerified") {
continue
}
$desiredTags = [ordered]@{}
if ($null -ne $commonTags) {
foreach ($tagProperty in $commonTags.PSObject.Properties) {
$desiredTags[$tagProperty.Name] = [string]$tagProperty.Value
}
}
$desiredTags["Environment"] = $environment
$resourceArns = @(Get-ConfigValue -InputObject $account -Name "ResourceArns")
foreach ($resourceArnValue in $resourceArns) {
$resourceArn = [string]$resourceArnValue
$service = ""
$targetStatus = "Ready"
$targetDetail = "Identity, account, and Region checks passed."
if (Test-Placeholder -Value $region) {
$targetStatus = "ConfigRequired"
$targetDetail = "Replace the Region placeholder."
}
elseif (Test-Placeholder -Value $resourceArn) {
$targetStatus = "ConfigRequired"
$targetDetail = "Replace the resource ARN placeholder."
}
else {
$arnParts = $resourceArn -split ":", 6
if ($arnParts.Count -ne 6 -or $arnParts[0] -ne "arn") {
$targetStatus = "InvalidArn"
$targetDetail = "The value does not contain the six general ARN fields."
}
else {
$service = $arnParts[2]
$arnRegion = $arnParts[3]
$arnAccountId = $arnParts[4]
if ([string]::IsNullOrWhiteSpace($arnRegion) -or
[string]::IsNullOrWhiteSpace($arnAccountId)) {
$targetStatus = "UnsupportedArnScope"
$targetDetail = "This pilot accepts only ARNs with Region and account fields."
}
elseif ($arnAccountId -ne $actualAccountId) {
$targetStatus = "ResourceAccountMismatch"
$targetDetail = "The ARN account does not match the authenticated account."
}
elseif ($arnRegion -ne $region) {
$targetStatus = "RegionMismatch"
$targetDetail = "The ARN Region does not match the configured Region."
}
}
}
foreach ($tagKey in $desiredTags.Keys) {
$tagValue = [string]$desiredTags[$tagKey]
$rowStatus = $targetStatus
$rowDetail = $targetDetail
if ($rowStatus -eq "Ready" -and (Test-Placeholder -Value $tagValue)) {
$rowStatus = "ConfigRequired"
$rowDetail = "Replace the placeholder tag value."
}
$planRows += [pscustomobject][ordered]@{
Profile = $profile
AccountId = $actualAccountId
Region = $region
Environment = $environment
Service = $service
ResourceArn = $resourceArn
TagKey = $tagKey
TagValue = $tagValue
Status = $rowStatus
Detail = $rowDetail
}
}
}
}
Write-Host "`nIdentity checks"
$identityRows | Format-Table Profile, ExpectedAccountId, ActualAccountId, Status, Detail -AutoSize
Write-Host "`nTag plan"
if ($planRows.Count -eq 0) {
Write-Host "No tag plan rows were produced. Verify every expected account ID first."
}
else {
$planRows | Format-Table Profile, Environment, Service, TagKey, TagValue, Status -AutoSize
}
$readyCount = @($planRows | Where-Object { $_.Status -eq "Ready" }).Count
$blockedCount = @($planRows | Where-Object { $_.Status -ne "Ready" }).Count
Write-Host "`nReady tag rows: $readyCount"
Write-Host "Blocked tag rows: $blockedCount"
Write-Host "No AWS tagging API was called."
What does this script do?
- The helper functions read optional configuration properties and detect untouched placeholders.
- The AWS helper runs GetCallerIdentity for each profile and converts its JSON response into a PowerShell object.
- The identity guardrail skips resource planning unless the authenticated account matches ExpectedAccountId.
- The ARN parser splits each approved target into six general fields.
- The target guardrails compare the ARN account and Region with the verified configuration.
- The desired-tag loop creates ManagedBy, Project, Owner, and Environment rows for each target.
- The final summary states that no AWS tagging API was called.
- Save tagging-planner.ps1.
Verify the regional tag plan
The completed planner has enough information to distinguish approved targets from blocked targets. Its output remains a local plan because the only AWS operation retrieves caller identity through AWS STS.
- Predict how many Ready rows each approved resource will produce.
- Generate the regional tag plan by running this command:
.\tagging-planner.ps1
What should I see?
You should see two IdentityVerified rows. Each approved target should produce four Ready rows.
The four tag keys should be ManagedBy, Project, Owner, and Environment.
The two profiles should show their distinct environment values. The final line should confirm that no AWS tagging API was called.
Seeing blocked plan rows?
- Replace any remaining configuration value that begins with replace-with-.
- Compare the account field inside each resource ARN with the corresponding ActualAccountId.
- Compare the Region field inside each resource ARN with the configured Region.
- Use an approved ARN that contains non-empty account and Region fields.
Help me diagnose the blocked row.
Your planner now turns authenticated identities and approved ARNs into a blocked-by-default tag preview. Next, you will export that evidence as a reviewable audit bundle.
Export the Audit Bundle
Your PowerShell planner now verifies identities before it produces any Ready tag rows. Those guardrails keep every proposed change tied to an authenticated account.
Console output disappears when the session closes. This step turns the results into durable CSV, JSON, and Markdown evidence that a reviewer can inspect before any future write workflow is approved.
In this step, get ready to:
- Add the export path for the identity report and tagging plan.
- Generate the JSON plan and permission-readiness report.
- Verify the completed audit bundle.
Add the export switch and CSV reports
The export switch keeps normal console runs unchanged. Supplying -Export opts into creating the local audit files.
- In tagging-planner.ps1, locate the param( block at the top of the file.
- Replace that parameter block with this final version:
param(
[string]$ConfigPath = ".\accounts.json",
[string]$OutputDirectory = ".\output",
[switch]$Export
)
What Do These Parameters Control?
- $ConfigPath keeps .\accounts.json as the default configuration file.
- $OutputDirectory sets .\output as the local audit destination.
- $Export records whether the export switch was included in the command.
- Save tagging-planner.ps1.
Before you run the planner, consider whether PowerShell now accepts the new switch.
- Test the new parameter by running this command:
.\tagging-planner.ps1 -Export
What Does This Check Prove?
PowerShell should accept -Export without reporting an unknown parameter. You should still see the identity checks and Ready tag rows from the existing planner.
The script now accepts an explicit export request while preserving its read-only planning behavior.
- Scroll to the final Write-Host "No AWS tagging API was called." line in tagging-planner.ps1.
- Add this export block directly below that line:
if ($Export) {
New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null
$identityCsvPath = Join-Path -Path $OutputDirectory -ChildPath "identity-report.csv"
$planCsvPath = Join-Path -Path $OutputDirectory -ChildPath "tagging-plan.csv"
$planJsonPath = Join-Path -Path $OutputDirectory -ChildPath "tagging-plan.json"
$permissionPath = Join-Path -Path $OutputDirectory -ChildPath "permission-readiness.md"
$identityRows | Export-Csv -Path $identityCsvPath -NoTypeInformation -Encoding UTF8
$planRows | Export-Csv -Path $planCsvPath -NoTypeInformation -Encoding UTF8
}
How Do the CSV Exports Work?
- New-Item creates the .\output directory when the export switch is present.
- Join-Path builds each output path with the correct Windows separator.
- Export-Csv writes the identity rows and plan rows as reviewable tables.
- UTF8 gives both files an explicit encoding.
- Save tagging-planner.ps1.
Before you run the exporter, predict how many CSV files should appear in the output directory.
- Create the first two audit files by running these commands:
.\tagging-planner.ps1 -Export
Get-ChildItem .\output
What Should You See?
The directory listing should include identity-report.csv and tagging-plan.csv. The first report captures caller evidence while the second captures the proposed tags.
Missing the CSV Files?
- Check that the export block sits after the console-reporting lines in tagging-planner.ps1.
- Check that the closing brace appears after both Export-Csv commands.
- Confirm that you included -Export when you ran the planner.
Help me diagnose missing CSV files from my PowerShell audit export.
Add JSON and permission readiness
The JSON file preserves the cumulative plan rows for automated review. The service list also tells an administrator which service-specific tagging permissions need investigation.
- In the final if ($Export) block, locate its closing brace.
- Insert this code directly above that closing brace:
$planJson = ConvertTo-Json -InputObject @($planRows) -Depth 6
Set-Content -Path $planJsonPath -Value $planJson -Encoding UTF8
$serviceList = @(
$planRows |
Where-Object { -not [string]::IsNullOrWhiteSpace($_.Service) } |
Select-Object -ExpandProperty Service -Unique
) -join ", "
if ([string]::IsNullOrWhiteSpace($serviceList)) {
$serviceList = "none from ready rows"
}
What Does the JSON Export Preserve?
- ConvertTo-Json serializes every cumulative plan row.
- Set-Content writes the serialized array to tagging-plan.json with explicit UTF-8 encoding.
- $serviceList collects the unique services represented by parsed resource ARNs.
- Save tagging-planner.ps1.
- Create the JSON plan by running these commands:
.\tagging-planner.ps1 -Export
Get-ChildItem .\output
What Should the Directory Contain?
You should now see tagging-plan.json beside the two CSV reports. This file contains the same plan rows in a machine-readable format.
The final report documents the boundary between this read-only planner and a future approved writer. It gives an administrator a precise permission review checklist.
- Return to the closing brace of the final if ($Export) block.
- Insert this final section directly above that closing brace:
$permissionText = @"
# Permission readiness
This planner calls AWS STS GetCallerIdentity only. It does not call an AWS tagging write operation.
A future approved apply workflow must be reviewed for:
- tag:TagResources
- The service-specific tagging permission for every target resource type
- tag:GetResources if post-change verification will use the Resource Groups Tagging API
- Additional service-specific requirements documented by the target service
Services represented in this plan: $serviceList
"@
Set-Content -Path $permissionPath -Value $permissionText -Encoding UTF8
Write-Host "`nExported audit bundle to $OutputDirectory"
Get-ChildItem -Path $OutputDirectory
What Does the Permission Report Explain?
- AWS STS GetCallerIdentity is the only AWS operation called by the planner.
- tag:TagResources is required for a future approved Resource Groups Tagging API writer.
- Service-specific tagging permissions are also required for each resource type.
- tag:GetResources is optional when post-change verification uses the AWS Resource Groups Tagging API.
- Save tagging-planner.ps1.
Before you run the completed exporter, predict the fourth filename that should join the three existing reports.
- Generate the completed audit bundle by running this command:
.\tagging-planner.ps1 -Export
What Should the Exporter Report?
The planner should print the export destination followed by all four output files. It should also repeat that no AWS tagging API was called.
That is the durable safety record complete. Your identity evidence and proposed tag changes now survive the PowerShell session.
Seeing an Export Error?
- Check that each new section remains inside the final if ($Export) block.
- Check that the opening @" and closing "@ markers for $permissionText each start on their own line.
- Check that $planJsonPath and $permissionPath are defined before they are used.
Help me fix my PowerShell audit export error.
✔️ Awesome, I've got everything!
Great work. Save tagging-planner.ps1 before the final inspection.
ⓧ I'd like to double check the full code
param(
[string]$ConfigPath = ".\accounts.json",
[string]$OutputDirectory = ".\output",
[switch]$Export
)
Set-StrictMode -Version 2.0
$ErrorActionPreference = "Stop"
function Get-ConfigValue {
param(
[object]$InputObject,
[string]$Name
)
if ($null -eq $InputObject) {
return $null
}
$property = $InputObject.PSObject.Properties[$Name]
if ($null -eq $property) {
return $null
}
return $property.Value
}
function Test-Placeholder {
param([string]$Value)
return [string]::IsNullOrWhiteSpace($Value) -or
$Value.StartsWith("replace-with-", [System.StringComparison]::OrdinalIgnoreCase)
}
function Invoke-AwsJson {
param([string[]]$Arguments)
$result = & aws @Arguments 2>&1
$exitCode = $LASTEXITCODE
$resultText = ($result | Out-String).Trim()
if ($exitCode -ne 0) {
throw "AWS CLI failed with exit code $exitCode. $resultText"
}
if ([string]::IsNullOrWhiteSpace($resultText)) {
throw "AWS CLI returned no JSON output."
}
return $resultText | ConvertFrom-Json
}
$config = Get-Content -Path $ConfigPath -Raw | ConvertFrom-Json
$accounts = @(Get-ConfigValue -InputObject $config -Name "Accounts")
$commonTags = Get-ConfigValue -InputObject $config -Name "CommonTags"
if ($accounts.Count -lt 1) {
throw "The configuration must contain at least one account."
}
$identityRows = @()
$planRows = @()
foreach ($account in $accounts) {
$profile = [string](Get-ConfigValue -InputObject $account -Name "Profile")
$expectedAccountId = [string](Get-ConfigValue -InputObject $account -Name "ExpectedAccountId")
$region = [string](Get-ConfigValue -InputObject $account -Name "Region")
$environment = [string](Get-ConfigValue -InputObject $account -Name "Environment")
$actualAccountId = ""
$callerArn = ""
$identityStatus = "ConfigRequired"
$identityDetail = "Replace the profile placeholder."
if (-not (Test-Placeholder -Value $profile)) {
try {
$identity = Invoke-AwsJson -Arguments @(
"sts",
"get-caller-identity",
"--profile",
$profile,
"--output",
"json",
"--no-cli-pager"
)
$actualAccountId = [string]$identity.Account
$callerArn = [string]$identity.Arn
if (Test-Placeholder -Value $expectedAccountId) {
$identityStatus = "IdentityDiscovered"
$identityDetail = "Copy the actual account ID into ExpectedAccountId."
}
elseif ($expectedAccountId -notmatch "^\d{12}$") {
$identityStatus = "ConfigRequired"
$identityDetail = "ExpectedAccountId must contain 12 digits."
}
elseif ($actualAccountId -ne $expectedAccountId) {
$identityStatus = "AccountMismatch"
$identityDetail = "The profile reached a different account than configured."
}
else {
$identityStatus = "IdentityVerified"
$identityDetail = "The profile reached the expected account."
}
}
catch {
$identityStatus = "AuthenticationFailed"
$identityDetail = $_.Exception.Message
}
}
$identityRows += [pscustomobject][ordered]@{
Profile = $profile
ExpectedAccountId = $expectedAccountId
ActualAccountId = $actualAccountId
CallerArn = $callerArn
Status = $identityStatus
Detail = $identityDetail
}
if ($identityStatus -ne "IdentityVerified") {
continue
}
$desiredTags = [ordered]@{}
if ($null -ne $commonTags) {
foreach ($tagProperty in $commonTags.PSObject.Properties) {
$desiredTags[$tagProperty.Name] = [string]$tagProperty.Value
}
}
$desiredTags["Environment"] = $environment
$resourceArns = @(Get-ConfigValue -InputObject $account -Name "ResourceArns")
foreach ($resourceArnValue in $resourceArns) {
$resourceArn = [string]$resourceArnValue
$service = ""
$targetStatus = "Ready"
$targetDetail = "Identity, account, and Region checks passed."
if (Test-Placeholder -Value $region) {
$targetStatus = "ConfigRequired"
$targetDetail = "Replace the Region placeholder."
}
elseif (Test-Placeholder -Value $resourceArn) {
$targetStatus = "ConfigRequired"
$targetDetail = "Replace the resource ARN placeholder."
}
else {
$arnParts = $resourceArn -split ":", 6
if ($arnParts.Count -ne 6 -or $arnParts[0] -ne "arn") {
$targetStatus = "InvalidArn"
$targetDetail = "The value does not contain the six general ARN fields."
}
else {
$service = $arnParts[2]
$arnRegion = $arnParts[3]
$arnAccountId = $arnParts[4]
if ([string]::IsNullOrWhiteSpace($arnRegion) -or
[string]::IsNullOrWhiteSpace($arnAccountId)) {
$targetStatus = "UnsupportedArnScope"
$targetDetail = "This pilot accepts only ARNs with Region and account fields."
}
elseif ($arnAccountId -ne $actualAccountId) {
$targetStatus = "ResourceAccountMismatch"
$targetDetail = "The ARN account does not match the authenticated account."
}
elseif ($arnRegion -ne $region) {
$targetStatus = "RegionMismatch"
$targetDetail = "The ARN Region does not match the configured Region."
}
}
}
foreach ($tagKey in $desiredTags.Keys) {
$tagValue = [string]$desiredTags[$tagKey]
$rowStatus = $targetStatus
$rowDetail = $targetDetail
if ($rowStatus -eq "Ready" -and (Test-Placeholder -Value $tagValue)) {
$rowStatus = "ConfigRequired"
$rowDetail = "Replace the placeholder tag value."
}
$planRows += [pscustomobject][ordered]@{
Profile = $profile
AccountId = $actualAccountId
Region = $region
Environment = $environment
Service = $service
ResourceArn = $resourceArn
TagKey = $tagKey
TagValue = $tagValue
Status = $rowStatus
Detail = $rowDetail
}
}
}
}
Write-Host "`nIdentity checks"
$identityRows | Format-Table Profile, ExpectedAccountId, ActualAccountId, Status, Detail -AutoSize
Write-Host "`nTag plan"
if ($planRows.Count -eq 0) {
Write-Host "No tag plan rows were produced. Verify every expected account ID first."
}
else {
$planRows | Format-Table Profile, Environment, Service, TagKey, TagValue, Status -AutoSize
}
$readyCount = @($planRows | Where-Object { $_.Status -eq "Ready" }).Count
$blockedCount = @($planRows | Where-Object { $_.Status -ne "Ready" }).Count
Write-Host "`nReady tag rows: $readyCount"
Write-Host "Blocked tag rows: $blockedCount"
Write-Host "No AWS tagging API was called."
if ($Export) {
New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null
$identityCsvPath = Join-Path -Path $OutputDirectory -ChildPath "identity-report.csv"
$planCsvPath = Join-Path -Path $OutputDirectory -ChildPath "tagging-plan.csv"
$planJsonPath = Join-Path -Path $OutputDirectory -ChildPath "tagging-plan.json"
$permissionPath = Join-Path -Path $OutputDirectory -ChildPath "permission-readiness.md"
$identityRows | Export-Csv -Path $identityCsvPath -NoTypeInformation -Encoding UTF8
$planRows | Export-Csv -Path $planCsvPath -NoTypeInformation -Encoding UTF8
$planJson = ConvertTo-Json -InputObject @($planRows) -Depth 6
Set-Content -Path $planJsonPath -Value $planJson -Encoding UTF8
$serviceList = @(
$planRows |
Where-Object { -not [string]::IsNullOrWhiteSpace($_.Service) } |
Select-Object -ExpandProperty Service -Unique
) -join ", "
if ([string]::IsNullOrWhiteSpace($serviceList)) {
$serviceList = "none from ready rows"
}
$permissionText = @"
# Permission readiness
This planner calls AWS STS GetCallerIdentity only. It does not call an AWS tagging write operation.
A future approved apply workflow must be reviewed for:
- tag:TagResources
- The service-specific tagging permission for every target resource type
- tag:GetResources if post-change verification will use the Resource Groups Tagging API
- Additional service-specific requirements documented by the target service
Services represented in this plan: $serviceList
"@
Set-Content -Path $permissionPath -Value $permissionText -Encoding UTF8
Write-Host "`nExported audit bundle to $OutputDirectory"
Get-ChildItem -Path $OutputDirectory
}
How Should You Use This Reference?
Compare this reference with your saved tagging-planner.ps1 file. The export block belongs after the existing console report.
Generate and inspect the audit bundle
The final check covers both file creation and report quality. Each file answers a different review question about identity, intended changes, or permission readiness.
Before you run the check, predict the four filenames that should appear.
- Regenerate the bundle and list its files by running these commands:
.\tagging-planner.ps1 -Export
Get-ChildItem .\output
What Does Success Look Like?
You should see identity-report.csv, tagging-plan.csv, tagging-plan.json, and permission-readiness.md in the listing.
The planner should still report only STS identity calls. No AWS tagging write operation should appear.
- Open the local output directory with its default Windows action by running this command:
Invoke-Item .\output
What Does This Command Do?
Invoke-Item performs the default action for the local directory. Windows opens .\output in File Explorer.
- Open identity-report.csv.
- Confirm that its columns include Profile, ExpectedAccountId, ActualAccountId, CallerArn, Status, and Detail.
- Open tagging-plan.csv or tagging-plan.json.
- Spot-check the profile, account, Region, ARN, tag key, tag value, and status fields.
- Open permission-readiness.md.
- Confirm that it names tag:TagResources for a future writer.
- Confirm that it requires service-specific tagging permissions.
- Confirm that it presents tag:GetResources as an optional verification permission.
Well done. You now have an approval-ready audit bundle that proves identity, preserves the complete tag plan, and documents the permissions a future writer needs.
Secret mission
Onboard a Third Account Safely
Extend the planner to a third AWS account without changing the script. You will diagnose a deliberate RegionMismatch. Then you will regenerate the audit bundle with Ready rows for all three accounts.
Clean Up Your Resources
Clean Up Your Resources
This project created only local files inside multi-account-tagging-planner. AWS IAM Identity Center and AWS STS add no charge.
No cloud resources were created or modified. Your existing AWS resources keep their normal costs.
Choose whether to keep the local planner available, pause your work for later, or delete every project file.
Resources you used:
- The local multi-account-tagging-planner folder containing accounts.json and tagging-planner.ps1.
- The output folder containing identity-report.csv, tagging-plan.csv, tagging-plan.json, and permission-readiness.md.
Keep everything running
No action needed. Choose this if you plan to keep reviewing or extending the tagging plan.
- Keep the multi-account-tagging-planner folder in its current location.
- Use the files in output when you need to review the verified identities or proposed tags.
- Retain accounts.json when you want to regenerate the plan for the same three accounts.
Pause - I'll come back to this later
There is no planner process to stop. Closing your PowerShell session pauses your local work while preserving the configuration and audit files.
- Save any changes to accounts.json or tagging-planner.ps1.
- Close the current Windows PowerShell window.
- Leave the multi-account-tagging-planner folder in place.
- Return to the same folder when you are ready to regenerate the audit bundle.
Your configuration and all four exported audit files remain available without creating an ongoing project cost.
Delete - I don't want to use this again
Deleting the folder is permanent. Your AWS resources remain unchanged because the planner never called an AWS tagging write operation.
- Copy any audit files you want to retain from output to a folder outside multi-account-tagging-planner.
- Close every open file from multi-account-tagging-planner.
- Close the current Windows PowerShell window.
The project folder is now free from the PowerShell session that was using it.
- Press the Windows key to open Windows search.
- Type File Explorer into the search field.
- Press Enter to open File Explorer.
- Browse to the location that contains the multi-account-tagging-planner folder.
- Select the multi-account-tagging-planner folder.
- Press Shift+Delete to remove the selected folder permanently.
- Confirm the permanent deletion in the dialog.
- Verify that multi-account-tagging-planner no longer appears in its previous location.
Cleanup is complete. Your local planner folder is gone.
Nice Work!
Nice Work!
You made it! Your read-only PowerShell planner now verifies three accounts before producing an approval-ready audit bundle without changing AWS resources.
You've learned how to:
- Authenticate through AWS IAM Identity Center profiles. Use AWS STS to record the account ID and caller ARN behind each profile.
- Trigger the AccountMismatch guardrail with deliberately swapped IDs. Correct the account map before producing Ready rows for validated regional ARNs.
- Preview a read-only tag plan with common tags plus a distinct Environment value for each account. Export a reviewable audit bundle that documents permission readiness for a future approved writer.
- Secret Mission: Onboarded a third account. The RegionMismatch guardrail caught an incorrect Region before the audit bundle was regenerated.
Ready to quiz yourself?