Skip to content

Commit

Permalink
feat(ecs): add fargate ephemeral storage encryption to cluster settings
Browse files Browse the repository at this point in the history
  • Loading branch information
jaecktec committed Jul 8, 2024
1 parent 5864782 commit dab4a7c
Show file tree
Hide file tree
Showing 2 changed files with 200 additions and 15 deletions.
111 changes: 96 additions & 15 deletions packages/aws-cdk-lib/aws-ecs/lib/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import * as autoscaling from '../../aws-autoscaling';
import * as cloudwatch from '../../aws-cloudwatch';
import * as ec2 from '../../aws-ec2';
import * as iam from '../../aws-iam';
import { PolicyStatement, PolicyStatementProps, ServicePrincipal } from '../../aws-iam';
import * as kms from '../../aws-kms';
import { IKey } from '../../aws-kms';
import * as logs from '../../aws-logs';
import * as s3 from '../../aws-s3';
import * as cloudmap from '../../aws-servicediscovery';
import { Duration, IResource, Resource, Stack, Aspects, ArnFormat, IAspect, Token, Names } from '../../core';
import { Aws, Duration, IResource, Resource, Stack, Aspects, ArnFormat, IAspect, Token, Names } from '../../core';

const CLUSTER_SYMBOL = Symbol.for('@aws-cdk/aws-ecs/lib/cluster.Cluster');

Expand Down Expand Up @@ -76,6 +78,13 @@ export interface ClusterProps {
* @default - no configuration will be provided.
*/
readonly executeCommandConfiguration?: ExecuteCommandConfiguration;

/**
* Encryption configuration for ECS Managed storage
*
* @default - no encryption will be applied.
*/
readonly managedStorageConfiguration?: ManagedStorageConfiguration;
}

/**
Expand All @@ -98,10 +107,10 @@ export enum MachineImageType {
export class Cluster extends Resource implements ICluster {

/**
* Return whether the given object is a Cluster
* Return whether the given object is a Cluster
*/
public static isCluster(x: any) : x is Cluster {
return x !== null && typeof(x) === 'object' && CLUSTER_SYMBOL in x;
public static isCluster(x: any): x is Cluster {
return x !== null && typeof (x) === 'object' && CLUSTER_SYMBOL in x;
}

/**
Expand Down Expand Up @@ -130,12 +139,15 @@ export class Cluster extends Resource implements ICluster {
class Import extends Resource implements ICluster {
public readonly clusterArn = clusterArn;
public readonly clusterName = clusterName!;

get hasEc2Capacity(): boolean {
throw new Error(`hasEc2Capacity ${errorSuffix}`);
}

get connections(): ec2.Connections {
throw new Error(`connections ${errorSuffix}`);
}

get vpc(): ec2.IVpc {
throw new Error(`vpc ${errorSuffix}`);
}
Expand Down Expand Up @@ -196,6 +208,8 @@ export class Cluster extends Resource implements ICluster {
*/
private _executeCommandConfiguration?: ExecuteCommandConfiguration;

private _managedStorageConfiguration?: ManagedStorageConfiguration;

/**
* CfnCluster instance
*/
Expand All @@ -213,10 +227,13 @@ export class Cluster extends Resource implements ICluster {
* clusterSettings needs to be undefined if containerInsights is not explicitly set in order to allow any
* containerInsights settings on the account to apply. See:
* https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-clustersettings.html#cfn-ecs-cluster-clustersettings-value
*/
*/
let clusterSettings = undefined;
if (props.containerInsights !== undefined) {
clusterSettings = [{ name: 'containerInsights', value: props.containerInsights ? ContainerInsights.ENABLED : ContainerInsights.DISABLED }];
clusterSettings = [{
name: 'containerInsights',
value: props.containerInsights ? ContainerInsights.ENABLED : ContainerInsights.DISABLED,
}];
}

this._capacityProviderNames = props.capacityProviders ?? [];
Expand All @@ -232,10 +249,15 @@ export class Cluster extends Resource implements ICluster {
this._executeCommandConfiguration = props.executeCommandConfiguration;
}

if (props.managedStorageConfiguration) {
this._managedStorageConfiguration = props.managedStorageConfiguration;
}

this._cfnCluster = new CfnCluster(this, 'Resource', {
clusterName: this.physicalName,
clusterSettings,
configuration: this._executeCommandConfiguration && this.renderExecuteCommandConfiguration(),
configuration: (this._executeCommandConfiguration && this.renderExecuteCommandConfiguration())
|| (this._managedStorageConfiguration && this.renderManagedStorageConfigurationConfiguration()),
});

this.clusterArn = this.getResourceArnAttribute(this._cfnCluster.attrArn, {
Expand All @@ -255,6 +277,11 @@ export class Cluster extends Resource implements ICluster {
? this.addCapacity('DefaultAutoScalingGroup', props.capacity)
: undefined;

if (this._managedStorageConfiguration?.fargateEphemeralStorageKmsKey) {
const key = this._managedStorageConfiguration.fargateEphemeralStorageKmsKey;
this.updateKeyPolicyForEphemeralStorageConfiguration(key, props.clusterName);
}

// Only create cluster capacity provider associations if there are any EC2
// capacity providers. Ordinarily we'd just add the construct to the tree
// since it's harmless, but we'd prefer not to add unexpected new
Expand All @@ -263,6 +290,37 @@ export class Cluster extends Resource implements ICluster {
Aspects.of(this).add(new MaybeCreateCapacityProviderAssociations(this, id));
}

private updateKeyPolicyForEphemeralStorageConfiguration(key: IKey, clusterName?: string) {
const clusterConditions: PolicyStatementProps['conditions'] = {
StringEquals: {
'kms:EncryptionContext:aws:ecs:clusterAccount': [Aws.ACCOUNT_ID],
},
};
if (clusterName) {
clusterConditions.StringEquals['kms:EncryptionContext:aws:ecs:clusterName'] = [clusterName];
}

key.addToResourcePolicy(new PolicyStatement({
sid: 'Allow generate data key access for Fargate tasks.',
principals: [new ServicePrincipal('fargate.amazonaws.com')],
resources: ['*'],
actions: ['kms:GenerateDataKeyWithoutPlaintext'],
conditions: clusterConditions,
}));
key.addToResourcePolicy(new PolicyStatement({
sid: 'Allow grant creation permission for Fargate tasks.',
principals: [new ServicePrincipal('fargate.amazonaws.com')],
resources: ['*'],
actions: ['kms:CreateGrant'],
conditions: {
...clusterConditions,
'ForAllValues:StringEquals': {
'kms:GrantOperations': ['Decrypt'],
},
},
}));
}

/**
* Enable the Fargate capacity providers for this cluster.
*/
Expand Down Expand Up @@ -319,6 +377,13 @@ export class Cluster extends Resource implements ICluster {
},
};
}
private renderManagedStorageConfigurationConfiguration(): CfnCluster.ClusterConfigurationProperty {
return {
managedStorageConfiguration: {
fargateEphemeralStorageKmsKeyId: this._managedStorageConfiguration?.fargateEphemeralStorageKmsKey?.keyId,
},
};
}

private renderExecuteCommandLogConfiguration(): CfnCluster.ExecuteCommandLogConfigurationProperty {
const logConfiguration = this._executeCommandConfiguration?.logConfiguration;
Expand Down Expand Up @@ -452,7 +517,7 @@ export class Cluster extends Resource implements ICluster {
...options,
machineImageType: provider.machineImageType,
// Don't enable the instance-draining lifecycle hook if managed termination protection or managed draining is enabled
taskDrainTime: (provider.enableManagedTerminationProtection || provider.enableManagedDraining)? Duration.seconds(0) : options.taskDrainTime,
taskDrainTime: (provider.enableManagedTerminationProtection || provider.enableManagedDraining) ? Duration.seconds(0) : options.taskDrainTime,
canContainersAccessInstanceRole: options.canContainersAccessInstanceRole ?? provider.canContainersAccessInstanceRole,
});

Expand Down Expand Up @@ -602,12 +667,12 @@ export class Cluster extends Resource implements ICluster {
}

/**
* Grants an ECS Task Protection API permission to the specified grantee.
* This method provides a streamlined way to assign the 'ecs:UpdateTaskProtection'
* permission, enabling the grantee to manage task protection in the ECS cluster.
*
* @param grantee The entity (e.g., IAM role or user) to grant the permissions to.
*/
* Grants an ECS Task Protection API permission to the specified grantee.
* This method provides a streamlined way to assign the 'ecs:UpdateTaskProtection'
* permission, enabling the grantee to manage task protection in the ECS cluster.
*
* @param grantee The entity (e.g., IAM role or user) to grant the permissions to.
*/
public grantTaskProtection(grantee: iam.IGrantable): iam.Grant {
return iam.Grant.addToPrincipal({
grantee,
Expand Down Expand Up @@ -1068,7 +1133,7 @@ export interface CapacityProviderStrategy {
/**
* The weight value designates the relative percentage of the total number of tasks launched that should use the
* specified
capacity provider. The weight value is taken into consideration after the base value, if defined, is satisfied.
capacity provider. The weight value is taken into consideration after the base value, if defined, is satisfied.
*
* @default - 0
*/
Expand Down Expand Up @@ -1254,6 +1319,22 @@ export interface AsgCapacityProviderProps extends AddAutoScalingGroupCapacityOpt
readonly instanceWarmupPeriod?: number;
}

/**
* Kms Keys for encryption ECS managed storage
*/
export interface ManagedStorageConfiguration {

/**
* KMS Key used to encrypt ECS Fargate ephemeral Storage.
* The configured KMS Key's policy will be modified to allow ECS to use the Key to encrypt the ephemeral Storage for this cluster.
*
* For more information, see [Customer managed keys for AWS Fargate ephemeral storage](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/fargate-storage-encryption.html) in the ECS Developer Guide.
*
* @default No encryption will be applied
*/
readonly fargateEphemeralStorageKmsKey?: IKey;
}

/**
* An Auto Scaling Group Capacity Provider. This allows an ECS cluster to target
* a specific EC2 Auto Scaling Group for the placement of tasks. Optionally (and
Expand Down
104 changes: 104 additions & 0 deletions packages/aws-cdk-lib/aws-ecs/test/cluster.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1763,6 +1763,110 @@ describe('cluster', () => {

});

test('enable fargate ephemeral storage encryption on cluster with random name', () => {
// GIVEN
const app = new cdk.App();
const stack = new cdk.Stack(app, 'test');
const key = new kms.Key(stack, 'key', { policy: new iam.PolicyDocument() });
new ecs.Cluster(stack, 'EcsCluster', { managedStorageConfiguration: { fargateEphemeralStorageKmsKey: key } });

// THEN
const output = Template.fromStack(stack);
output.hasResourceProperties('AWS::ECS::Cluster', {
Configuration: {
ManagedStorageConfiguration: {
FargateEphemeralStorageKmsKeyId: {
Ref: 'keyFEDD6EC0',
},
},
},
});
output.hasResourceProperties('AWS::KMS::Key', {
KeyPolicy: {
Statement: [
{
Resource: '*',
Effect: 'Allow',
Action: 'kms:GenerateDataKeyWithoutPlaintext',
Principal: { Service: 'fargate.amazonaws.com' },
Condition: {
StringEquals: {
'kms:EncryptionContext:aws:ecs:clusterAccount': [{ Ref: 'AWS::AccountId' }],
},
},
},
{
Resource: '*',
Effect: 'Allow',
Action: 'kms:CreateGrant',
Principal: { Service: 'fargate.amazonaws.com' },
Condition: {
'StringEquals': {
'kms:EncryptionContext:aws:ecs:clusterAccount': [{ Ref: 'AWS::AccountId' }],
},
'ForAllValues:StringEquals': {
'kms:GrantOperations': ['Decrypt'],
},
},
},
],
},
});
});

test('enable fargate ephemeral storage encryption on cluster with defined name', () => {
// GIVEN
const app = new cdk.App();
const stack = new cdk.Stack(app, 'test');
const key = new kms.Key(stack, 'key', { policy: new iam.PolicyDocument() });
new ecs.Cluster(stack, 'EcsCluster', { clusterName: 'cluster-name', managedStorageConfiguration: { fargateEphemeralStorageKmsKey: key } });

// THEN
const output = Template.fromStack(stack);
output.hasResourceProperties('AWS::ECS::Cluster', {
Configuration: {
ManagedStorageConfiguration: {
FargateEphemeralStorageKmsKeyId: {
Ref: 'keyFEDD6EC0',
},
},
},
});
output.hasResourceProperties('AWS::KMS::Key', {
KeyPolicy: {
Statement: [
{
Resource: '*',
Effect: 'Allow',
Action: 'kms:GenerateDataKeyWithoutPlaintext',
Principal: { Service: 'fargate.amazonaws.com' },
Condition: {
StringEquals: {
'kms:EncryptionContext:aws:ecs:clusterAccount': [{ Ref: 'AWS::AccountId' }],
'kms:EncryptionContext:aws:ecs:clusterName': ['cluster-name'],
},
},
},
{
Resource: '*',
Effect: 'Allow',
Action: 'kms:CreateGrant',
Principal: { Service: 'fargate.amazonaws.com' },
Condition: {
'StringEquals': {
'kms:EncryptionContext:aws:ecs:clusterAccount': [{ Ref: 'AWS::AccountId' }],
'kms:EncryptionContext:aws:ecs:clusterName': ['cluster-name'],
},
'ForAllValues:StringEquals': {
'kms:GrantOperations': ['Decrypt'],
},
},
},
],
},
});
});

test('BottleRocketImage() returns correct AMI', () => {
// GIVEN
const app = new cdk.App();
Expand Down

0 comments on commit dab4a7c

Please sign in to comment.