Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,8 @@ Really happy to implement this based on someone elses use-case.
- [Automating deployments](docs/task-files.md)
- [Custom Account Creation Workflow](examples/automation/create-account/readme.md)
- [CLI reference](docs/cli-reference.md)
- [AWS GovCloud Partition Support](docs/us-govcloud-partition.md)
- [AWS European Sovereign Cloud (EUSC) Partition Support](docs/aws-eusc-partition.md)
- [Changelog](CHANGELOG.md)
- [Contributing](CONTRIBUTING.md)

Expand Down
192 changes: 192 additions & 0 deletions docs/aws-eusc-partition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# AWS European Sovereign Cloud (EUSC) Partition

The AWS European Sovereign Cloud (EUSC) is a new AWS partition designed to meet the digital sovereignty requirements of European customers. Similar to GovCloud, EUSC operates as a separate partition with its own domain and regions.

> **important**: Like GovCloud, EUSC accounts may be tied to commercial accounts for billing purposes. This means that Org Formation may need to manage both partitions simultaneously. Org Formation does this by "mirroring" these accounts. The `organization.yml` file looks the same as you would expect, with some slight differences.

## Key Characteristics

- **Partition Name**: `aws-eusc`
- **Domain**: `amazonaws.eu` (instead of `amazonaws.com`)
- **Initial Region**: `eusc-de-east-1` (Brandenburg, Germany)
- **ARN Format**: `arn:aws-eusc:service:region:account-id:resource`

## Organization Configuration

To enable partition mirroring for EUSC, configure your `organization.yml` with the `MirrorInPartition` attribute:

```yaml
OrganizationRoot:
Type: OC::ORG::OrganizationRoot
Properties:
MirrorInPartition: True
DefaultOrganizationAccessRoleName: OrganizationAccountAccessRole
```

The `MirrorInPartition` attribute on the `OC::ORG::OrganizationRoot` resource indicates to org formation that when accounts are created or modified to do so in both the commercial and aws-eusc partitions. Currently this is a boolean value, however, in the future it should indicate which partition to mirror in.

## Account Configuration

When creating accounts that exist in both partitions, specify different aliases:

```yaml
EUSCAccount:
Type: OC::ORG::Account
Properties:
AccountName: EUSC Test Account
RootEmail: email@example.com
Alias: test-commercial
PartitionAlias: test-eusc
```

> **note:** `Alias` and `PartitionAlias` must be **different** values

The `PartitionAlias` attribute on the `OC::ORG::Account` resource type indicates the account alias for the mirrored partition account. To prevent confusion this alias should be different than the `Alias` attribute.

> **important**: Currently organizational units are **not** supported in non-commercial partition accounts and cannot be present in your `organization.yml` if you are mirroring across a different partition.

## Authentication

Org Formation supports multiple methods for authenticating to the EUSC partition:

### Option 1: Environment Variables

```bash
export EUSC_AWS_ACCESS_KEY_ID=your-access-key
export EUSC_AWS_SECRET_ACCESS_KEY=your-secret-key
```

### Option 2: AWS Profile

Use the `--partition-profile` flag to specify a named AWS profile configured for EUSC access.

## Commands

### `org-formation init`

Creates a local organization formation file that contains all organization resources. Running this command will create an S3 Bucket (hence the region) in your account that contains a state file which is used to track differences when updating your resources.

```bash
org-formation init organization.yml \
--region eu-central-1 \
--partition-region eusc-de-east-1 \
--partition-profile eusc-profile
```

Or with environment variables:

```bash
org-formation init organization.yml \
--region eu-central-1 \
--partition-region eusc-de-east-1 \
--partition-keys
```

A few new attributes are required for org formation to work properly across partitions:
- `--partition-region` string indicating which region to target in the partition. For EUSC, use `eusc-de-east-1`.
- `--partition-profile` is an optional string argument indicating where org formation can find credentials to access the partition.
- `--partition-keys` is an optional boolean argument indicating org formation to look for partition credentials as environment variables (`EUSC_AWS_ACCESS_KEY_ID` and `EUSC_AWS_SECRET_ACCESS_KEY`).

> **important**: This command must be executed from a terminal session with active AWS credentials to the commercial management account. One of the `--partition-profile` or `--partition-keys` arguments must be passed.

### `org-formation update`

Updates organizational resources specified in templateFile.

```bash
org-formation update organization.yml \
--partition-region eusc-de-east-1 \
--partition-profile eusc-profile
```

Again, when running org-formation update partition arguments are required for org formation to have proper access to both the commercial and mirrored partition. The Update command will "mirror" the organization on both sides of the partition.

> **note**: There are org formation state files on both sides of the partition. Meaning when you create an organization an s3 bucket is created in both commercial and EUSC master accounts.

## Running Tasks

Tasks can only be ran on specific partitions (commercial or mirrored partition i.e. aws-eusc).

**Commercial partition tasks:**
```bash
org-formation perform-tasks commercial-tasks.yml --profile my-aws-profile
```

**EUSC partition tasks:**
```bash
org-formation perform-tasks eusc-tasks.yml \
--is-partition \
--partition-region eusc-de-east-1 \
--partition-profile eusc-profile
```

> **note**: Currently `update-organization` tasks types are not supported within your task files.

## Important Considerations

1. **State Files**: Org Formation creates separate S3 buckets and state files in both the commercial and EUSC partitions.

2. **Organizational Units**: Currently, organizational units are not supported in non-commercial partition accounts and cannot be present in your `organization.yml` if you are mirroring across partitions.

3. **Service Availability**: Not all AWS services are available in the EUSC partition. Verify service availability before deploying resources.

4. **Domain Differences**: The EUSC partition uses `amazonaws.eu` instead of `amazonaws.com` for service endpoints and S3 URLs.

5. **Region Availability**: Currently, only `eusc-de-east-1` is available in the EUSC partition. Additional regions may be added in the future.

## CloudFormation Templates

When writing CloudFormation templates that need to work across partitions, use the `AWS::Partition` pseudo-parameter:

```yaml
Resources:
MyRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: !Sub 'lambda.${AWS::Partition}.amazonaws.eu'
Action: sts:AssumeRole
```

Or use the `ORG::IsPartition` parameter provided by Org Formation:

```yaml
Conditions:
IsEUSC: !Ref ORG::IsPartition

Resources:
MyBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !If
- IsEUSC
- my-eusc-bucket
- my-commercial-bucket
```

## Troubleshooting

### Authentication Issues

If you encounter authentication errors:

1. Verify your credentials are correctly set in environment variables or AWS profile
2. Ensure the profile or credentials have access to the EUSC partition
3. Check that `--partition-region` is set to `eusc-de-east-1`

### Service Endpoint Issues

If services fail to connect:

1. Verify the service is available in the EUSC partition
2. Check that the correct domain (`amazonaws.eu`) is being used
3. Ensure the region `eusc-de-east-1` is specified correctly

## See Also

- [GovCloud Partition Documentation](./us-govcloud-partition.md)
- [Task Files Documentation](./task-files.md)
- [Organization Resources](./organization-resources.md)
2 changes: 1 addition & 1 deletion src/parser/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export class Validator {

}
}
private static knownRegions = ['us-east-2', 'us-east-1', 'us-west-1', 'us-west-2', 'ap-east-1', 'ap-south-1', 'ap-northeast-3', 'ap-northeast-2', 'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1', 'ca-central-1', 'eu-central-1', 'eu-west-1', 'eu-west-2', 'eu-west-3', 'eu-north-1', 'me-south-1', 'sa-east-1', 'us-gov-west-1', 'us-gov-east-1'];
private static knownRegions = ['us-east-2', 'us-east-1', 'us-west-1', 'us-west-2', 'ap-east-1', 'ap-south-1', 'ap-northeast-3', 'ap-northeast-2', 'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1', 'ca-central-1', 'eu-central-1', 'eu-west-1', 'eu-west-2', 'eu-west-3', 'eu-north-1', 'me-south-1', 'sa-east-1', 'us-gov-west-1', 'us-gov-east-1', 'eusc-de-east-1'];

private static validateReferenceToAccount(resourceRefs: IResourceRef | IResourceRef[], id: string): void {
if (resourceRefs === undefined) { return; }
Expand Down
35 changes: 32 additions & 3 deletions src/plugin/impl/rp-build-task-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,39 @@
}

private getCatalogBucket(isPartition: boolean): Catalog {
const partition = AwsUtil.partition || 'aws';

Check failure on line 185 in src/plugin/impl/rp-build-task-plugin.ts

View workflow job for this annotation

GitHub Actions / test

Trailing spaces not allowed
if (isPartition) {
// Determine S3 domain and region based on partition
let s3Domain = 'amazonaws.com';
let s3Region = 'us-gov-west-1';

Check failure on line 190 in src/plugin/impl/rp-build-task-plugin.ts

View workflow job for this annotation

GitHub Actions / test

Trailing spaces not allowed
switch (partition) {
case 'aws-us-gov':
s3Domain = 'amazonaws.com';
s3Region = 'us-gov-west-1';
break;
case 'aws-eusc':
s3Domain = 'amazonaws.eu';
s3Region = 'eusc-de-east-1';
break;
case 'aws-cn':
s3Domain = 'amazonaws.com.cn';
s3Region = 'cn-north-1';
break;
}

Check failure on line 205 in src/plugin/impl/rp-build-task-plugin.ts

View workflow job for this annotation

GitHub Actions / test

Trailing spaces not allowed
return {
bucket: communityResourceProviderCatalogGov,
uri: `s3://${communityResourceProviderCatalogGov}/`,
path: `https://${communityResourceProviderCatalogGov}.s3-${s3Region}.${s3Domain}/`,
};
}

Check failure on line 212 in src/plugin/impl/rp-build-task-plugin.ts

View workflow job for this annotation

GitHub Actions / test

Trailing spaces not allowed
return {
bucket: (isPartition) ? communityResourceProviderCatalogGov : communityResourceProviderCatalog,
uri: (isPartition) ? `s3://${communityResourceProviderCatalogGov}/` : `s3://${communityResourceProviderCatalog}/`,
path: (isPartition) ? `https://${communityResourceProviderCatalogGov}.s3-us-gov-west-1.amazonaws.com/` : `https://${communityResourceProviderCatalog}.s3.amazonaws.com/`,
bucket: communityResourceProviderCatalog,
uri: `s3://${communityResourceProviderCatalog}/`,
path: `https://${communityResourceProviderCatalog}.s3.amazonaws.com/`,
};
}

Expand Down
47 changes: 35 additions & 12 deletions src/util/aws-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,20 +175,24 @@
}

/**
* Look for GOV_AWS_ACCESS_KEY_ID and GOV_AWS_SECRET_ACCESS_KEY from the environment to set
* the credentials for the GovCloud partition.
* Look for GOV_AWS_ACCESS_KEY_ID/GOV_AWS_SECRET_ACCESS_KEY or

Check failure on line 178 in src/util/aws-util.ts

View workflow job for this annotation

GitHub Actions / test

Trailing spaces not allowed
* EUSC_AWS_ACCESS_KEY_ID/EUSC_AWS_SECRET_ACCESS_KEY from the environment to set
* the credentials for the GovCloud or EUSC partition.
*/
public static SetPartitionCredentials(): void {
if (process.env.GOV_AWS_ACCESS_KEY_ID && process.env.GOV_AWS_SECRET_ACCESS_KEY) {
const accessKeyId = process.env.GOV_AWS_ACCESS_KEY_ID || process.env.EUSC_AWS_ACCESS_KEY_ID;
const secretAccessKey = process.env.GOV_AWS_SECRET_ACCESS_KEY || process.env.EUSC_AWS_SECRET_ACCESS_KEY;

Check failure on line 185 in src/util/aws-util.ts

View workflow job for this annotation

GitHub Actions / test

Trailing spaces not allowed
if (accessKeyId && secretAccessKey) {
AwsUtil.partitionCredentials = {
accessKeyId: process.env.GOV_AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.GOV_AWS_SECRET_ACCESS_KEY,
accessKeyId,
secretAccessKey,
};
// this is a bit of a hack - leaving it up to the SDK to pick these up and give precedence
process.env.AWS_ACCESS_KEY_ID = process.env.GOV_AWS_ACCESS_KEY_ID;
process.env.AWS_SECRET_ACCESS_KEY = process.env.GOV_AWS_SECRET_ACCESS_KEY;
process.env.AWS_ACCESS_KEY_ID = accessKeyId;
process.env.AWS_SECRET_ACCESS_KEY = secretAccessKey;
} else {
throw new OrgFormationError('Expected GOV_AWS_ACCESS_KEY_ID and GOV_AWS_SECRET_ACCESS_KEY to be set on the environment');
throw new OrgFormationError('Expected GOV_AWS_ACCESS_KEY_ID/GOV_AWS_SECRET_ACCESS_KEY or EUSC_AWS_ACCESS_KEY_ID/EUSC_AWS_SECRET_ACCESS_KEY to be set on the environment');
}
}

Expand All @@ -202,8 +206,11 @@

public static SetIsPartition(isPartition: boolean, partitionProfile?: string): void {
if (isPartition === true && !partitionProfile && !AwsUtil.GetPartitionProfile()) {
if (!process.env.GOV_AWS_ACCESS_KEY_ID || !process.env.GOV_AWS_SECRET_ACCESS_KEY) {
throw new OrgFormationError('GOV_AWS_ACCESS_KEY_ID and GOV_AWS_SECRET_ACCESS_KEY must be set on the environment or a `partitionProfile` must be provided');
const hasGovCreds = process.env.GOV_AWS_ACCESS_KEY_ID && process.env.GOV_AWS_SECRET_ACCESS_KEY;
const hasEuscCreds = process.env.EUSC_AWS_ACCESS_KEY_ID && process.env.EUSC_AWS_SECRET_ACCESS_KEY;

Check failure on line 211 in src/util/aws-util.ts

View workflow job for this annotation

GitHub Actions / test

Trailing spaces not allowed
if (!hasGovCreds && !hasEuscCreds) {
throw new OrgFormationError('GOV_AWS_ACCESS_KEY_ID/GOV_AWS_SECRET_ACCESS_KEY or EUSC_AWS_ACCESS_KEY_ID/EUSC_AWS_SECRET_ACCESS_KEY must be set on the environment or a `partitionProfile` must be provided');
}
}
AwsUtil.isPartition = isPartition;
Expand All @@ -225,6 +232,20 @@
AwsUtil.partitionRegion = partitionRegion;
}

public static GetS3DomainForPartition(): string {
const partition = AwsUtil.partition || 'aws';
switch (partition) {
case 'aws-cn':
return 'amazonaws.com.cn';
case 'aws-us-gov':
return 'amazonaws.com';
case 'aws-eusc':
return 'amazonaws.eu';
default:
return 'amazonaws.com';
}
}

public static async GetMasterAccountId(): Promise<string> {
if (AwsUtil.masterAccountId !== undefined) {
return AwsUtil.masterAccountId;
Expand Down Expand Up @@ -280,7 +301,8 @@
}

private static GetPartitionRoleArn(accountId: string, roleInTargetAccount: string): string {
return 'arn:aws-us-gov:iam::' + accountId + ':role/' + roleInTargetAccount;
const partition = AwsUtil.partition || 'aws-us-gov';
return `arn:${partition}:iam::${accountId}:role/${roleInTargetAccount}`;
}

private static throwIfNowInitiazized() {
Expand Down Expand Up @@ -745,7 +767,8 @@
const bucketRegion: string = largeTemplateBucketRegion.LocationConstraint ?? 'us-east-1';
const putObjectRequest: PutObjectCommandInput = { Bucket: bucketName, Key: `${stackName}-${templateHash}.json`, Body: stackInput.TemplateBody, ACL: 'bucket-owner-full-control' };
await s3Service.send(new PutObjectCommand(putObjectRequest));
stackInput.TemplateURL = `https://${bucketName}.s3.${bucketRegion}.amazonaws.com/${putObjectRequest.Key}`;
const s3Domain = AwsUtil.GetS3DomainForPartition();
stackInput.TemplateURL = `https://${bucketName}.s3.${bucketRegion}.${s3Domain}/${putObjectRequest.Key}`;
delete stackInput.TemplateBody;
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/util/credentials-provider-partition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { AwsCredentialIdentity, AwsCredentialIdentityProvider } from '@smithy/ty

export function partitionFromEnv(isPartition: boolean): AwsCredentialIdentityProvider {
return async () => {
const accessKeyId = process.env.GOV_AWS_ACCESS_KEY_ID;
const secretAccessKey = process.env.GOV_AWS_SECRET_ACCESS_KEY;
const accessKeyId = process.env.GOV_AWS_ACCESS_KEY_ID || process.env.EUSC_AWS_ACCESS_KEY_ID;
const secretAccessKey = process.env.GOV_AWS_SECRET_ACCESS_KEY || process.env.EUSC_AWS_SECRET_ACCESS_KEY;

if (isPartition && accessKeyId && secretAccessKey) {
const identity: AwsCredentialIdentity = {
Expand All @@ -13,6 +13,7 @@ export function partitionFromEnv(isPartition: boolean): AwsCredentialIdentityPro
};
return identity;
}
throw new CredentialsProviderError('GOV_AWS_ACCESS_KEY_ID or GOV_AWS_SECRET_ACCESS_KEY missing', true);
throw new CredentialsProviderError('GOV_AWS_ACCESS_KEY_ID/GOV_AWS_SECRET_ACCESS_KEY or EUSC_AWS_ACCESS_KEY_ID/EUSC_AWS_SECRET_ACCESS_KEY missing', true);
};
}

Loading