diff --git a/README.md b/README.md index 38bef920..5fb9c300 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/docs/aws-eusc-partition.md b/docs/aws-eusc-partition.md new file mode 100644 index 00000000..a16d39af --- /dev/null +++ b/docs/aws-eusc-partition.md @@ -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) diff --git a/src/parser/validator.ts b/src/parser/validator.ts index 528676c6..d1a26da6 100644 --- a/src/parser/validator.ts +++ b/src/parser/validator.ts @@ -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; } diff --git a/src/plugin/impl/rp-build-task-plugin.ts b/src/plugin/impl/rp-build-task-plugin.ts index 14f83555..60cb5a15 100644 --- a/src/plugin/impl/rp-build-task-plugin.ts +++ b/src/plugin/impl/rp-build-task-plugin.ts @@ -181,10 +181,39 @@ export class RpBuildTaskPlugin implements IBuildTaskPlugin { if (AwsUtil.masterAccountId !== undefined) { return AwsUtil.masterAccountId; @@ -280,7 +301,8 @@ export class AwsUtil { } 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() { @@ -745,7 +767,8 @@ export class CfnUtil { 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; } } diff --git a/src/util/credentials-provider-partition.ts b/src/util/credentials-provider-partition.ts index 9757a87e..ddd4b825 100644 --- a/src/util/credentials-provider-partition.ts +++ b/src/util/credentials-provider-partition.ts @@ -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 = { @@ -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); }; } +