From 17dbe5f476ac1ccc0c0e6a0905b0de5ae6186704 Mon Sep 17 00:00:00 2001 From: markussiebert Date: Tue, 1 Feb 2022 14:05:57 +0100 Subject: [PATCH] feat(ec2): support KMS keys for block device mappings for both instances and launch templates (#18326) This pullrequest will add the optional ```kmsKeyId``` property to the ```EbsDeviceOptions``` Interface. Whit this, it will be possible to specify the kmsKeyId used for encrypting the ebs volumes when launching instances. At the moment I already use this via an escape hatch in my projects, but it's not that handy as the block device mapping is an array. I don't like to only specify a kmsKeyId (= ARN) but accepting an ```kms.IKey``` in the properties would be a bigger change. fixes: #18309 *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-ec2/README.md | 31 +++++++++++++++++++ .../@aws-cdk/aws-ec2/lib/private/ebs-util.ts | 23 ++++++++++++-- packages/@aws-cdk/aws-ec2/lib/volume.ts | 13 +++++++- .../@aws-cdk/aws-ec2/test/instance.test.ts | 28 +++++++++++++++++ .../aws-ec2/test/launch-template.test.ts | 28 +++++++++++++++++ 5 files changed, 120 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index c40b8e6879709..fb2db769617e7 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -1051,6 +1051,37 @@ new ec2.Instance(this, 'Instance', { ``` +It is also possible to encrypt the block devices. In this example we will create an customer managed key encrypted EBS-backed root device: + +```ts +import { Key } from '@aws-cdk/aws-kms'; + +declare const vpc: ec2.Vpc; +declare const instanceType: ec2.InstanceType; +declare const machineImage: ec2.IMachineImage; + +const kmsKey = new Key(this, 'KmsKey') + +new ec2.Instance(this, 'Instance', { + vpc, + instanceType, + machineImage, + + // ... + + blockDevices: [ + { + deviceName: '/dev/sda1', + volume: ec2.BlockDeviceVolume.ebs(50, { + encrypted: true, + kmsKey: kmsKey, + }), + }, + ], +}); + +``` + ### Volumes Whereas a `BlockDeviceVolume` is an EBS volume that is created and destroyed as part of the creation and destruction of a specific instance. A `Volume` is for when you want an EBS volume separate from any particular instance. A `Volume` is an EBS block device that can be attached to, or detached from, any instance at any time. Some types of `Volume`s can also be attached to multiple instances at the same time to allow you to have shared storage between those instances. diff --git a/packages/@aws-cdk/aws-ec2/lib/private/ebs-util.ts b/packages/@aws-cdk/aws-ec2/lib/private/ebs-util.ts index dc91f6d795011..52c7738afdbef 100644 --- a/packages/@aws-cdk/aws-ec2/lib/private/ebs-util.ts +++ b/packages/@aws-cdk/aws-ec2/lib/private/ebs-util.ts @@ -24,8 +24,11 @@ function synthesizeBlockDeviceMappings(construct: Construct, blockDevic return blockDevices.map(({ deviceName, volume, mappingEnabled }): RT => { const { virtualName, ebsDevice: ebs } = volume; + let finalEbs: CfnLaunchTemplate.EbsProperty | CfnInstance.EbsProperty | undefined; + if (ebs) { - const { iops, volumeType } = ebs; + + const { iops, volumeType, kmsKey, ...rest } = ebs; if (!iops) { if (volumeType === EbsDeviceVolumeType.IO1) { @@ -34,9 +37,25 @@ function synthesizeBlockDeviceMappings(construct: Construct, blockDevic } else if (volumeType !== EbsDeviceVolumeType.IO1) { Annotations.of(construct).addWarning('iops will be ignored without volumeType: EbsDeviceVolumeType.IO1'); } + + /** + * Because the Ebs properties of the L2 Constructs do not match the Ebs properties of the Cfn Constructs, + * we have to do some transformation and handle all destructed properties + */ + + finalEbs = { + ...rest, + iops, + volumeType, + kmsKeyId: kmsKey?.keyArn, + }; + + } else { + finalEbs = undefined; } + const noDevice = mappingEnabled === false ? noDeviceValue : undefined; - return { deviceName, ebs, virtualName, noDevice } as any; + return { deviceName, ebs: finalEbs, virtualName, noDevice } as any; }); } diff --git a/packages/@aws-cdk/aws-ec2/lib/volume.ts b/packages/@aws-cdk/aws-ec2/lib/volume.ts index 87e92b3f5006a..723c0f91e602e 100644 --- a/packages/@aws-cdk/aws-ec2/lib/volume.ts +++ b/packages/@aws-cdk/aws-ec2/lib/volume.ts @@ -89,6 +89,17 @@ export interface EbsDeviceOptions extends EbsDeviceOptionsBase { * @default false */ readonly encrypted?: boolean; + + /** + * The ARN of the AWS Key Management Service (AWS KMS) CMK used for encryption. + * + * You have to ensure that the KMS CMK has the correct permissions to be used by the service launching the ec2 instances. + * + * @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html#ebs-encryption-requirements + * + * @default - If encrypted is true, the default aws/ebs KMS key will be used. + */ + readonly kmsKey?: IKey; } /** @@ -108,7 +119,7 @@ export interface EbsDeviceSnapshotOptions extends EbsDeviceOptionsBase { /** * Properties of an EBS block device */ -export interface EbsDeviceProps extends EbsDeviceSnapshotOptions { +export interface EbsDeviceProps extends EbsDeviceSnapshotOptions, EbsDeviceOptions { /** * The snapshot ID of the volume to use * diff --git a/packages/@aws-cdk/aws-ec2/test/instance.test.ts b/packages/@aws-cdk/aws-ec2/test/instance.test.ts index e2d4e47e78fde..7f40cd8f202a0 100644 --- a/packages/@aws-cdk/aws-ec2/test/instance.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/instance.test.ts @@ -1,5 +1,6 @@ import * as path from 'path'; import { Match, Template } from '@aws-cdk/assertions'; +import { Key } from '@aws-cdk/aws-kms'; import { Asset } from '@aws-cdk/aws-s3-assets'; import { StringParameter } from '@aws-cdk/aws-ssm'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; @@ -184,6 +185,7 @@ describe('instance', () => { describe('blockDeviceMappings', () => { test('can set blockDeviceMappings', () => { // WHEN + const kmsKey = new Key(stack, 'EbsKey'); new Instance(stack, 'Instance', { vpc, machineImage: new AmazonLinuxImage(), @@ -197,6 +199,16 @@ describe('instance', () => { volumeType: EbsDeviceVolumeType.IO1, iops: 5000, }), + }, { + deviceName: 'ebs-cmk', + mappingEnabled: true, + volume: BlockDeviceVolume.ebs(15, { + deleteOnTermination: true, + encrypted: true, + kmsKey: kmsKey, + volumeType: EbsDeviceVolumeType.IO1, + iops: 5000, + }), }, { deviceName: 'ebs-snapshot', mappingEnabled: false, @@ -224,6 +236,22 @@ describe('instance', () => { VolumeType: 'io1', }, }, + { + DeviceName: 'ebs-cmk', + Ebs: { + DeleteOnTermination: true, + Encrypted: true, + KmsKeyId: { + 'Fn::GetAtt': [ + 'EbsKeyD3FEE551', + 'Arn', + ], + }, + Iops: 5000, + VolumeSize: 15, + VolumeType: 'io1', + }, + }, { DeviceName: 'ebs-snapshot', Ebs: { diff --git a/packages/@aws-cdk/aws-ec2/test/launch-template.test.ts b/packages/@aws-cdk/aws-ec2/test/launch-template.test.ts index 499368608e7a6..49b979ca7084d 100644 --- a/packages/@aws-cdk/aws-ec2/test/launch-template.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/launch-template.test.ts @@ -4,6 +4,7 @@ import { Role, ServicePrincipal, } from '@aws-cdk/aws-iam'; +import { Key } from '@aws-cdk/aws-kms'; import { App, Duration, @@ -254,6 +255,7 @@ describe('LaunchTemplate', () => { test('Given blockDeviceMapping', () => { // GIVEN + const kmsKey = new Key(stack, 'EbsKey'); const blockDevices: BlockDevice[] = [ { deviceName: 'ebs', @@ -264,6 +266,16 @@ describe('LaunchTemplate', () => { volumeType: EbsDeviceVolumeType.IO1, iops: 5000, }), + }, { + deviceName: 'ebs-cmk', + mappingEnabled: true, + volume: BlockDeviceVolume.ebs(15, { + deleteOnTermination: true, + encrypted: true, + kmsKey: kmsKey, + volumeType: EbsDeviceVolumeType.IO1, + iops: 5000, + }), }, { deviceName: 'ebs-snapshot', mappingEnabled: false, @@ -297,6 +309,22 @@ describe('LaunchTemplate', () => { VolumeType: 'io1', }, }, + { + DeviceName: 'ebs-cmk', + Ebs: { + DeleteOnTermination: true, + Encrypted: true, + KmsKeyId: { + 'Fn::GetAtt': [ + 'EbsKeyD3FEE551', + 'Arn', + ], + }, + Iops: 5000, + VolumeSize: 15, + VolumeType: 'io1', + }, + }, { DeviceName: 'ebs-snapshot', Ebs: {