Skip to content

Commit

Permalink
Check region when using CloudWatchAgent construct on Windows (#1452)
Browse files Browse the repository at this point in the history
* fix(deadline): spelling of "instance" in comments

* fix(core): verify that CloudWatchAgent on Windows is used in a supported region
  • Loading branch information
rondeau-aws authored Feb 7, 2025
1 parent c3457aa commit af2a300
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 44 deletions.
61 changes: 58 additions & 3 deletions packages/aws-rfdk/lib/core/lib/cloudwatch-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@

import * as path from 'path';

import { Stack } from 'aws-cdk-lib';
import { IGrantable } from 'aws-cdk-lib/aws-iam';
import { Stack, Token } from 'aws-cdk-lib';
import { OperatingSystemType } from 'aws-cdk-lib/aws-ec2';
import { IGrantable, PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { StringParameter } from 'aws-cdk-lib/aws-ssm';
import { Construct } from 'constructs';
Expand Down Expand Up @@ -128,6 +129,45 @@ export class CloudWatchAgent extends Construct {
this.ssmParameterForConfig.grantRead(grantee);
}

/**
* Return true if the RFDK-hosted resources required to install the
* CloudWatch Agent are available for the specified osType and region.
*/
private canInstallAgent(osType: OperatingSystemType, region: string) {
if (osType === OperatingSystemType.LINUX) {
// We don't use any RFDK-hosted dependencies on Linux.
return true;
} else {
// The RFDK service has an S3 bucket serving dependencies for Windows
// in these regions.
const REGION_ALLOW_LIST = [
'ap-northeast-1',
'ap-northeast-2',
'ap-northeast-3',
'ap-south-1',
'ap-southeast-1',
'ap-southeast-2',
'ca-central-1',
'eu-central-1',
'eu-north-1',
'eu-west-1',
'eu-west-2',
'eu-west-3',
'sa-east-1',
'us-east-1',
'us-east-2',
'us-west-1',
'us-west-2',
];

if (Token.isUnresolved(region)) {
throw new Error('Region must be set at synth time');
}

return REGION_ALLOW_LIST.includes(region);
}
}

/**
* Configures the CloudWatch Agent on the target host.
*
Expand All @@ -144,11 +184,26 @@ export class CloudWatchAgent extends Construct {
) {
const region = Stack.of(this).region;
if (shouldInstallAgent) {
if (!this.canInstallAgent(host.osType, region)) {
throw new Error(`Cannot install CloudWatch Agent in region "${region}" ` +
`for OS "${OperatingSystemType[host.osType]}" ` +
'because RFDK hosted files are not available in that region.');
}

// Grant access to the required CloudWatch Agent and GPG installer files.
const cloudWatchAgentBucket = Bucket.fromBucketArn(this, 'CloudWatchAgentBucket', `arn:aws:s3:::amazoncloudwatch-agent-${region}`);
cloudWatchAgentBucket.grantRead(host);
const gpgBucket = Bucket.fromBucketArn(this, 'GpgBucket', `arn:aws:s3:::rfdk-external-dependencies-${region}`);
gpgBucket.grantRead(host);
host.grantPrincipal.addToPrincipalPolicy(
new PolicyStatement({
actions: ['s3:GetObject'],
resources: [gpgBucket.bucketArn, gpgBucket.arnForObjects('*')],
conditions: { StringEquals: {
// Download from bucket in RFDK service account
's3:ResourceAccount': '224375009292',
} },
}),
);
}

const scriptArgs = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,29 @@ function Install-CloudWatchAgent($region) {
$gpg_keyring = "$env:temp\keyring.gpg"

# Download GPG
$gpg_bucket_name = "rfdk-external-dependencies-$region"
$gpg_key = "gnupg-w32-2.2.27_20210111.exe"
$gpg_expected_bucket_owner = "224375009292"
$gpg_installer = "$env:temp\gnupg-w32-2.2.27_20210111.exe"
try {
Read-S3Object -BucketName rfdk-external-dependencies-$region -Key gnupg-w32-2.2.27_20210111.exe -File $gpg_installer -Region $region
# Check if the Read-S3Object call below will download from a bucket owned
# by the RFDK service.
# This is a separate call because Read-S3Object doesn't yet support the
# ExpectedBucketOwner parameter.
Get-S3ObjectMetadata -BucketName $gpg_bucket_name -Key $gpg_key -Region $region -ExpectedBucketOwner $gpg_expected_bucket_owner | Out-Null
} catch {
$ex = $PSItem.Exception.GetBaseException()
if ($ex.Response.StatusCode -eq 403) {
Write-Output ("Got Forbidden error when verifying owner of S3 bucket containing GPG installer. " +
"This may be caused by attempting to access a bucket that is not owned by the RFDK service.")
} else {
Write-Output "Failed to verify owner of bucket containing GPG installer."
}
return
}

try {
Read-S3Object -BucketName $gpg_bucket_name -Key $gpg_key -File $gpg_installer -Region $region
} catch {
Write-Output "Failed downloading GPG to verify CloudWatch agent."
Remove-Item -Path $cwa_installer -Force
Expand Down
2 changes: 1 addition & 1 deletion packages/aws-rfdk/lib/core/test/asset-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const CWA_ASSET_LINUX = {
// ConfigureCloudWatchAgent.ps1
export const CWA_ASSET_WINDOWS = {
Bucket: 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}',
Key: 'b3a03a74afa8a045b35e08f11a719544622172869cc031787f580407d665ee36',
Key: 'ea268a603f4cce783c290fc755e99c9d8c127224c1be30d6158aed70e533c730',
};

// mountEbsBlockVolume.sh + metadataUtilities.sh + ec2-certificates.crt
Expand Down
87 changes: 73 additions & 14 deletions packages/aws-rfdk/lib/core/test/cloudwatch-agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@
* SPDX-License-Identifier: Apache-2.0
*/

import {Stack} from 'aws-cdk-lib';
import {App, Stack} from 'aws-cdk-lib';
import {
Template,
} from 'aws-cdk-lib/assertions';
import {
AmazonLinuxGeneration,
AmazonLinuxImage,
GenericWindowsImage,
Instance,
InstanceClass,
InstanceSize,
InstanceType,
OperatingSystemType,
Vpc,
WindowsImage,
WindowsVersion,
Expand Down Expand Up @@ -171,11 +173,12 @@ describe('CloudWatchAgent', () => {
],
},
{
Action: [
's3:GetObject*',
's3:GetBucket*',
's3:List*',
],
Action: 's3:GetObject',
Condition: {
StringEquals: {
's3:ResourceAccount': '224375009292',
},
},
Effect: 'Allow',
Resource: [
{
Expand Down Expand Up @@ -251,34 +254,90 @@ describe('CloudWatchAgent', () => {
['', false],
])('adds user data commands to fetch and execute the script (windows). installFlag: %s shouldInstallAgent: %p', (installFlag: string, shouldInstallAgent?: boolean) => {
// GIVEN
const host = new Instance(stack, 'Instance', {
const region = 'ap-southeast-1';
const app = new App();
const regionalStack = new Stack(app, 'stack', {env: {region}});
const regionalVpc = new Vpc(regionalStack, 'vpc');

const host = new Instance(regionalStack, 'Instance', {
instanceType: InstanceType.of(InstanceClass.T2, InstanceSize.LARGE),
machineImage: new WindowsImage(WindowsVersion.WINDOWS_SERVER_2019_ENGLISH_FULL_BASE),
vpc,
vpc: regionalVpc,
});

// WHEN
new CloudWatchAgent(stack, 'testResource', {
new CloudWatchAgent(regionalStack, 'testResource', {
cloudWatchConfig,
host,
shouldInstallAgent,
});

// THEN
const userData = stack.resolve(host.userData.render());
const userData = regionalStack.resolve(host.userData.render());
expect(userData).toStrictEqual({
'Fn::Join': [
'',
[
`<powershell>mkdir (Split-Path -Path 'C:/temp/${CWA_ASSET_WINDOWS.Key}.ps1' ) -ea 0\nRead-S3Object -BucketName '`,
{ 'Fn::Sub': CWA_ASSET_WINDOWS.Bucket },
`' -key '${CWA_ASSET_WINDOWS.Key}.ps1' -file 'C:/temp/${CWA_ASSET_WINDOWS.Key}.ps1' -ErrorAction Stop\n&'C:/temp/${CWA_ASSET_WINDOWS.Key}.ps1'${installFlag} `,
{ Ref: 'AWS::Region' },
' ',
{ 'Fn::Sub': CWA_ASSET_WINDOWS.Bucket.replace('${AWS::Region}', region) },
`' -key '${CWA_ASSET_WINDOWS.Key}.ps1' -file 'C:/temp/${CWA_ASSET_WINDOWS.Key}.ps1' -ErrorAction Stop\n&'C:/temp/${CWA_ASSET_WINDOWS.Key}.ps1'${installFlag} ${region} `,
{ Ref: 'StringParameter472EED0E' },
`\nif (!$?) { Write-Error 'Failed to execute the file \"C:/temp/${CWA_ASSET_WINDOWS.Key}.ps1\"' -ErrorAction Stop }</powershell>`,
],
],
});
});
});

describe('CloudWatchAgentRegionSupport', () => {
const availableRegion = 'eu-north-1';
const optInRegion = 'ap-east-1';

// Generate CloudWatch Agent configuration JSON
const configBuilder = new CloudWatchConfigBuilder();
const cloudWatchConfig = configBuilder.generateCloudWatchConfiguration();

test.each([
['Linux Available Region', OperatingSystemType.LINUX, availableRegion, true],
['Linux Opt-In Region', OperatingSystemType.LINUX, optInRegion, true],
['Windows Available Region', OperatingSystemType.WINDOWS, availableRegion, true ],
['Windows Opt-In Region', OperatingSystemType.WINDOWS, optInRegion, false],
])('CloudWatchAgent support for %s', (_scenarioName, osType, region, expectSuccess) => {
const app = new App();
const stack = new Stack(app, 'stack', {env: {region: region}});
const vpc = new Vpc(stack, 'vpc');

let machineImage;
if (osType == OperatingSystemType.LINUX) {
machineImage = new AmazonLinuxImage({
generation: AmazonLinuxGeneration.AMAZON_LINUX_2023,
});
} else {
machineImage = new GenericWindowsImage({
[availableRegion]: 'ami-aaaaaaaaaaaaaaaaa',
[optInRegion]: 'ami-bbbbbbbbbbbbbbbbb',
});
}

const host = new Instance(stack, 'instance', {
instanceType: InstanceType.of(InstanceClass.T2, InstanceSize.LARGE),
machineImage: machineImage,
vpc,
});

// WHEN
function createCloudWatchAgent() {
new CloudWatchAgent(stack, 'cloudWatchAgent', {
cloudWatchConfig,
host,
});
}

// THEN
if (expectSuccess) {
expect(createCloudWatchAgent).not.toThrow(); // eslint-disable-line jest/no-conditional-expect
} else {
expect(createCloudWatchAgent).toThrow('Cannot install CloudWatch Agent'); // eslint-disable-line jest/no-conditional-expect
}
});
});
11 changes: 6 additions & 5 deletions packages/aws-rfdk/lib/core/test/deployment-instance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,11 +402,12 @@ describe('DeploymentInstance', () => {
PolicyDocument: {
Statement: Match.arrayWith([
{
Action: [
's3:GetObject*',
's3:GetBucket*',
's3:List*',
],
Action: 's3:GetObject',
Condition: {
StringEquals: {
's3:ResourceAccount': '224375009292',
},
},
Effect: 'Allow',
Resource: stack.resolve([
rfdkExternalDepsBucket.bucketArn,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export enum AwsCustomerAgreementAndIpLicenseAcceptance {
}

/**
* Interface to specify the properties when instantiating a {@link ThinkboxDockerImages} instnace.
* Interface to specify the properties when instantiating a {@link ThinkboxDockerImages} instance.
*/
export interface ThinkboxDockerImagesProps {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export enum ThinkboxManagedDeadlineDockerRecipes {
}

/**
* Interface to specify the properties when instantiating a {@link ThinkboxDockerRecipes} instnace.
* Interface to specify the properties when instantiating a {@link ThinkboxDockerRecipes} instance.
*/
export interface ThinkboxDockerRecipesProps {
/**
Expand Down
Loading

0 comments on commit af2a300

Please sign in to comment.