-
Notifications
You must be signed in to change notification settings - Fork 4k
/
volume.ts
734 lines (652 loc) · 26.6 KB
/
volume.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
import * as crypto from 'crypto';
import { AccountRootPrincipal, Grant, IGrantable } from '@aws-cdk/aws-iam';
import { IKey, ViaServicePrincipal } from '@aws-cdk/aws-kms';
import { IResource, Resource, Size, SizeRoundingBehavior, Stack, Token, Tags, Names, RemovalPolicy } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CfnVolume } from './ec2.generated';
import { IInstance } from './instance';
// v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch.
// eslint-disable-next-line
import { Construct as CoreConstruct } from '@aws-cdk/core';
/**
* Block device
*/
export interface BlockDevice {
/**
* The device name exposed to the EC2 instance
*
* @example '/dev/sdh', 'xvdh'
*
* @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/device_naming.html
*/
readonly deviceName: string;
/**
* Defines the block device volume, to be either an Amazon EBS volume or an ephemeral instance store volume
*
* @example BlockDeviceVolume.ebs(15), BlockDeviceVolume.ephemeral(0)
*
*/
readonly volume: BlockDeviceVolume;
/**
* If false, the device mapping will be suppressed.
* If set to false for the root device, the instance might fail the Amazon EC2 health check.
* Amazon EC2 Auto Scaling launches a replacement instance if the instance fails the health check.
*
* @default true - device mapping is left untouched
*/
readonly mappingEnabled?: boolean;
}
/**
* Base block device options for an EBS volume
*/
export interface EbsDeviceOptionsBase {
/**
* Indicates whether to delete the volume when the instance is terminated.
*
* @default - true for Amazon EC2 Auto Scaling, false otherwise (e.g. EBS)
*/
readonly deleteOnTermination?: boolean;
/**
* The number of I/O operations per second (IOPS) to provision for the volume.
*
* Must only be set for {@link volumeType}: {@link EbsDeviceVolumeType.IO1}
*
* The maximum ratio of IOPS to volume size (in GiB) is 50:1, so for 5,000 provisioned IOPS,
* you need at least 100 GiB storage on the volume.
*
* @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html
*
* @default - none, required for {@link EbsDeviceVolumeType.IO1}
*/
readonly iops?: number;
/**
* The EBS volume type
* @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html
*
* @default {@link EbsDeviceVolumeType.GP2}
*/
readonly volumeType?: EbsDeviceVolumeType;
}
/**
* Block device options for an EBS volume
*/
export interface EbsDeviceOptions extends EbsDeviceOptionsBase {
/**
* Specifies whether the EBS volume is encrypted.
* Encrypted EBS volumes can only be attached to instances that support Amazon EBS encryption
*
* @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html#EBSEncryption_supported_instances
*
* @default false
*/
readonly encrypted?: boolean;
}
/**
* Block device options for an EBS volume created from a snapshot
*/
export interface EbsDeviceSnapshotOptions extends EbsDeviceOptionsBase {
/**
* The volume size, in Gibibytes (GiB)
*
* If you specify volumeSize, it must be equal or greater than the size of the snapshot.
*
* @default - The snapshot size
*/
readonly volumeSize?: number;
}
/**
* Properties of an EBS block device
*/
export interface EbsDeviceProps extends EbsDeviceSnapshotOptions {
/**
* The snapshot ID of the volume to use
*
* @default - No snapshot will be used
*/
readonly snapshotId?: string;
}
/**
* Describes a block device mapping for an EC2 instance or Auto Scaling group.
*/
export class BlockDeviceVolume {
/**
* Creates a new Elastic Block Storage device
*
* @param volumeSize The volume size, in Gibibytes (GiB)
* @param options additional device options
*/
public static ebs(volumeSize: number, options: EbsDeviceOptions = {}): BlockDeviceVolume {
return new this({ ...options, volumeSize });
}
/**
* Creates a new Elastic Block Storage device from an existing snapshot
*
* @param snapshotId The snapshot ID of the volume to use
* @param options additional device options
*/
public static ebsFromSnapshot(snapshotId: string, options: EbsDeviceSnapshotOptions = {}): BlockDeviceVolume {
return new this({ ...options, snapshotId });
}
/**
* Creates a virtual, ephemeral device.
* The name will be in the form ephemeral{volumeIndex}.
*
* @param volumeIndex the volume index. Must be equal or greater than 0
*/
public static ephemeral(volumeIndex: number) {
if (volumeIndex < 0) {
throw new Error(`volumeIndex must be a number starting from 0, got "${volumeIndex}"`);
}
return new this(undefined, `ephemeral${volumeIndex}`);
}
/**
* @param ebsDevice EBS device info
* @param virtualName Virtual device name
*/
protected constructor(public readonly ebsDevice?: EbsDeviceProps, public readonly virtualName?: string) {
}
}
/**
* Supported EBS volume types for blockDevices
*/
export enum EbsDeviceVolumeType {
/**
* Magnetic
*/
STANDARD = 'standard',
/**
* Provisioned IOPS SSD - IO1
*/
IO1 = 'io1',
/**
* Provisioned IOPS SSD - IO2
*/
IO2 = 'io2',
/**
* General Purpose SSD - GP2
*/
GP2 = 'gp2',
/**
* General Purpose SSD - GP3
*/
GP3 = 'gp3',
/**
* Throughput Optimized HDD
*/
ST1 = 'st1',
/**
* Cold HDD
*/
SC1 = 'sc1',
/**
* General purpose SSD volume (GP2) that balances price and performance for a wide variety of workloads.
*/
GENERAL_PURPOSE_SSD = GP2,
/**
* General purpose SSD volume (GP3) that balances price and performance for a wide variety of workloads.
*/
GENERAL_PURPOSE_SSD_GP3 = GP3,
/**
* Highest-performance SSD volume (IO1) for mission-critical low-latency or high-throughput workloads.
*/
PROVISIONED_IOPS_SSD = IO1,
/**
* Highest-performance SSD volume (IO2) for mission-critical low-latency or high-throughput workloads.
*/
PROVISIONED_IOPS_SSD_IO2 = IO2,
/**
* Low-cost HDD volume designed for frequently accessed, throughput-intensive workloads.
*/
THROUGHPUT_OPTIMIZED_HDD = ST1,
/**
* Lowest cost HDD volume designed for less frequently accessed workloads.
*/
COLD_HDD = SC1,
/**
* Magnetic volumes are backed by magnetic drives and are suited for workloads where data is accessed infrequently, and scenarios where low-cost
* storage for small volume sizes is important.
*/
MAGNETIC = STANDARD,
}
/**
* An EBS Volume in AWS EC2.
*/
export interface IVolume extends IResource {
/**
* The EBS Volume's ID
*
* @attribute
*/
readonly volumeId: string;
/**
* The availability zone that the EBS Volume is contained within (ex: us-west-2a)
*/
readonly availabilityZone: string;
/**
* The customer-managed encryption key that is used to encrypt the Volume.
*
* @attribute
*/
readonly encryptionKey?: IKey;
/**
* Grants permission to attach this Volume to an instance.
* CAUTION: Granting an instance permission to attach to itself using this method will lead to
* an unresolvable circular reference between the instance role and the instance.
* Use {@link IVolume.grantAttachVolumeToSelf} to grant an instance permission to attach this
* volume to itself.
*
* @param grantee the principal being granted permission.
* @param instances the instances to which permission is being granted to attach this
* volume to. If not specified, then permission is granted to attach
* to all instances in this account.
*/
grantAttachVolume(grantee: IGrantable, instances?: IInstance[]): Grant;
/**
* Grants permission to attach the Volume by a ResourceTag condition. If you are looking to
* grant an Instance, AutoScalingGroup, EC2-Fleet, SpotFleet, ECS host, etc the ability to attach
* this volume to **itself** then this is the method you want to use.
*
* This is implemented by adding a Tag with key `VolumeGrantAttach-<suffix>` to the given
* constructs and this Volume, and then conditioning the Grant such that the grantee is only
* given the ability to AttachVolume if both the Volume and the destination Instance have that
* tag applied to them.
*
* @param grantee the principal being granted permission.
* @param constructs The list of constructs that will have the generated resource tag applied to them.
* @param tagKeySuffix A suffix to use on the generated Tag key in place of the generated hash value.
* Defaults to a hash calculated from this volume and list of constructs. (DEPRECATED)
*/
grantAttachVolumeByResourceTag(grantee: IGrantable, constructs: Construct[], tagKeySuffix?: string): Grant;
/**
* Grants permission to detach this Volume from an instance
* CAUTION: Granting an instance permission to detach from itself using this method will lead to
* an unresolvable circular reference between the instance role and the instance.
* Use {@link IVolume.grantDetachVolumeFromSelf} to grant an instance permission to detach this
* volume from itself.
*
* @param grantee the principal being granted permission.
* @param instances the instances to which permission is being granted to detach this
* volume from. If not specified, then permission is granted to detach
* from all instances in this account.
*/
grantDetachVolume(grantee: IGrantable, instances?: IInstance[]): Grant;
/**
* Grants permission to detach the Volume by a ResourceTag condition.
*
* This is implemented via the same mechanism as {@link IVolume.grantAttachVolumeByResourceTag},
* and is subject to the same conditions.
*
* @param grantee the principal being granted permission.
* @param constructs The list of constructs that will have the generated resource tag applied to them.
* @param tagKeySuffix A suffix to use on the generated Tag key in place of the generated hash value.
* Defaults to a hash calculated from this volume and list of constructs. (DEPRECATED)
*/
grantDetachVolumeByResourceTag(grantee: IGrantable, constructs: Construct[], tagKeySuffix?: string): Grant;
}
/**
* Properties of an EBS Volume
*/
export interface VolumeProps {
/**
* The value of the physicalName property of this resource.
*
* @default The physical name will be allocated by CloudFormation at deployment time
*/
readonly volumeName?: string;
/**
* The Availability Zone in which to create the volume.
*/
readonly availabilityZone: string;
/**
* The size of the volume, in GiBs. You must specify either a snapshot ID or a volume size.
* See {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-ebs-volume.html}
* for details on the allowable size for each type of volume.
*
* @default If you're creating the volume from a snapshot and don't specify a volume size, the default is the snapshot size.
*/
readonly size?: Size;
/**
* The snapshot from which to create the volume. You must specify either a snapshot ID or a volume size.
*
* @default The EBS volume is not created from a snapshot.
*/
readonly snapshotId?: string;
/**
* Indicates whether Amazon EBS Multi-Attach is enabled.
* See {@link https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volumes-multi.html#considerations|Considerations and limitations}
* for the constraints of multi-attach.
*
* @default false
*/
readonly enableMultiAttach?: boolean;
/**
* Specifies whether the volume should be encrypted. The effect of setting the encryption state to true depends on the volume origin
* (new or from a snapshot), starting encryption state, ownership, and whether encryption by default is enabled. For more information,
* see {@link https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html#encryption-by-default|Encryption by Default}
* in the Amazon Elastic Compute Cloud User Guide.
*
* Encrypted Amazon EBS volumes must be attached to instances that support Amazon EBS encryption. For more information, see
* {@link https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html#EBSEncryption_supported_instances|Supported Instance Types.}
*
* @default false
*/
readonly encrypted?: boolean;
/**
* The customer-managed encryption key that is used to encrypt the Volume. The encrypted property must
* be true if this is provided.
*
* Note: If using an {@link aws-kms.IKey} created from a {@link aws-kms.Key.fromKeyArn()} here,
* then the KMS key **must** have the following in its Key policy; otherwise, the Volume
* will fail to create.
*
* {
* "Effect": "Allow",
* "Principal": { "AWS": "<arn for your account-user> ex: arn:aws:iam::00000000000:root" },
* "Resource": "*",
* "Action": [
* "kms:DescribeKey",
* "kms:GenerateDataKeyWithoutPlainText",
* ],
* "Condition": {
* "StringEquals": {
* "kms:ViaService": "ec2.<Region>.amazonaws.com", (eg: ec2.us-east-1.amazonaws.com)
* "kms:CallerAccount": "0000000000" (your account ID)
* }
* }
* }
*
* @default The default KMS key for the account, region, and EC2 service is used.
*/
readonly encryptionKey?: IKey;
/**
* Indicates whether the volume is auto-enabled for I/O operations. By default, Amazon EBS disables I/O to the volume from attached EC2
* instances when it determines that a volume's data is potentially inconsistent. If the consistency of the volume is not a concern, and
* you prefer that the volume be made available immediately if it's impaired, you can configure the volume to automatically enable I/O.
*
* @default false
*/
readonly autoEnableIo?: boolean;
/**
* The type of the volume; what type of storage to use to form the EBS Volume.
*
* @default {@link EbsDeviceVolumeType.GENERAL_PURPOSE_SSD}
*/
readonly volumeType?: EbsDeviceVolumeType;
/**
* The number of I/O operations per second (IOPS) to provision for the volume. The maximum ratio is 50 IOPS/GiB for PROVISIONED_IOPS_SSD,
* and 500 IOPS/GiB for both PROVISIONED_IOPS_SSD_IO2 and GENERAL_PURPOSE_SSD_GP3.
* See {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-ebs-volume.html}
* for more information.
*
* This parameter is valid only for PROVISIONED_IOPS_SSD, PROVISIONED_IOPS_SSD_IO2 and GENERAL_PURPOSE_SSD_GP3 volumes.
*
* @default None -- Required for io1 and io2 volumes. The default for gp3 volumes is 3,000 IOPS if omitted.
*/
readonly iops?: number;
/**
* Policy to apply when the volume is removed from the stack
*
* @default RemovalPolicy.RETAIN
*/
readonly removalPolicy?: RemovalPolicy;
}
/**
* Attributes required to import an existing EBS Volume into the Stack.
*/
export interface VolumeAttributes {
/**
* The EBS Volume's ID
*/
readonly volumeId: string;
/**
* The availability zone that the EBS Volume is contained within (ex: us-west-2a)
*/
readonly availabilityZone: string;
/**
* The customer-managed encryption key that is used to encrypt the Volume.
*
* @default None -- The EBS Volume is not using a customer-managed KMS key for encryption.
*/
readonly encryptionKey?: IKey;
}
/**
* Common behavior of Volumes. Users should not use this class directly, and instead use ``Volume``.
*/
abstract class VolumeBase extends Resource implements IVolume {
public abstract readonly volumeId: string;
public abstract readonly availabilityZone: string;
public abstract readonly encryptionKey?: IKey;
public grantAttachVolume(grantee: IGrantable, instances?: IInstance[]): Grant {
const result = Grant.addToPrincipal({
grantee,
actions: ['ec2:AttachVolume'],
resourceArns: this.collectGrantResourceArns(instances),
});
if (this.encryptionKey) {
// When attaching a volume, the EC2 Service will need to grant to itself permission
// to be able to decrypt the encryption key. We restrict the CreateGrant for principle
// of least privilege, in accordance with best practices.
// See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html#ebs-encryption-permissions
const kmsGrant: Grant = this.encryptionKey.grant(grantee, 'kms:CreateGrant');
kmsGrant.principalStatement!.addConditions(
{
Bool: { 'kms:GrantIsForAWSResource': true },
StringEquals: {
'kms:ViaService': `ec2.${Stack.of(this).region}.amazonaws.com`,
'kms:GrantConstraintType': 'EncryptionContextSubset',
},
},
);
}
return result;
}
public grantAttachVolumeByResourceTag(grantee: IGrantable, constructs: Construct[], tagKeySuffix?: string): Grant {
const tagValue = this.calculateResourceTagValue([this, ...constructs]);
const tagKey = `VolumeGrantAttach-${tagKeySuffix ?? tagValue.slice(0, 10).toUpperCase()}`;
const grantCondition: { [key: string]: string } = {};
grantCondition[`ec2:ResourceTag/${tagKey}`] = tagValue;
const result = this.grantAttachVolume(grantee);
result.principalStatement!.addCondition(
'ForAnyValue:StringEquals', grantCondition,
);
// The ResourceTag condition requires that all resources involved in the operation have
// the given tag, so we tag this and all constructs given.
Tags.of(this).add(tagKey, tagValue);
constructs.forEach(construct => Tags.of(construct as CoreConstruct).add(tagKey, tagValue));
return result;
}
public grantDetachVolume(grantee: IGrantable, instances?: IInstance[]): Grant {
const result = Grant.addToPrincipal({
grantee,
actions: ['ec2:DetachVolume'],
resourceArns: this.collectGrantResourceArns(instances),
});
// Note: No encryption key permissions are required to detach an encrypted volume.
return result;
}
public grantDetachVolumeByResourceTag(grantee: IGrantable, constructs: Construct[], tagKeySuffix?: string): Grant {
const tagValue = this.calculateResourceTagValue([this, ...constructs]);
const tagKey = `VolumeGrantDetach-${tagKeySuffix ?? tagValue.slice(0, 10).toUpperCase()}`;
const grantCondition: { [key: string]: string } = {};
grantCondition[`ec2:ResourceTag/${tagKey}`] = tagValue;
const result = this.grantDetachVolume(grantee);
result.principalStatement!.addCondition(
'ForAnyValue:StringEquals', grantCondition,
);
// The ResourceTag condition requires that all resources involved in the operation have
// the given tag, so we tag this and all constructs given.
Tags.of(this).add(tagKey, tagValue);
constructs.forEach(construct => Tags.of(construct as CoreConstruct).add(tagKey, tagValue));
return result;
}
private collectGrantResourceArns(instances?: IInstance[]): string[] {
const stack = Stack.of(this);
const resourceArns: string[] = [
`arn:${stack.partition}:ec2:${stack.region}:${stack.account}:volume/${this.volumeId}`,
];
const instanceArnPrefix = `arn:${stack.partition}:ec2:${stack.region}:${stack.account}:instance`;
if (instances) {
instances.forEach(instance => resourceArns.push(`${instanceArnPrefix}/${instance?.instanceId}`));
} else {
resourceArns.push(`${instanceArnPrefix}/*`);
}
return resourceArns;
}
private calculateResourceTagValue(constructs: Construct[]): string {
const md5 = crypto.createHash('md5');
constructs.forEach(construct => md5.update(Names.uniqueId(construct)));
return md5.digest('hex');
}
}
/**
* Creates a new EBS Volume in AWS EC2.
*/
export class Volume extends VolumeBase {
/**
* Import an existing EBS Volume into the Stack.
*
* @param scope the scope of the import.
* @param id the ID of the imported Volume in the construct tree.
* @param attrs the attributes of the imported Volume
*/
public static fromVolumeAttributes(scope: Construct, id: string, attrs: VolumeAttributes): IVolume {
class Import extends VolumeBase {
public readonly volumeId = attrs.volumeId;
public readonly availabilityZone = attrs.availabilityZone;
public readonly encryptionKey = attrs.encryptionKey;
}
// Check that the provided volumeId looks like it could be valid.
if (!Token.isUnresolved(attrs.volumeId) && !/^vol-[0-9a-fA-F]+$/.test(attrs.volumeId)) {
throw new Error('`volumeId` does not match expected pattern. Expected `vol-<hexadecmial value>` (ex: `vol-05abe246af`) or a Token');
}
return new Import(scope, id);
}
public readonly volumeId: string;
public readonly availabilityZone: string;
public readonly encryptionKey?: IKey;
constructor(scope: Construct, id: string, props: VolumeProps) {
super(scope, id, {
physicalName: props.volumeName,
});
this.validateProps(props);
const resource = new CfnVolume(this, 'Resource', {
availabilityZone: props.availabilityZone,
autoEnableIo: props.autoEnableIo,
encrypted: props.encrypted,
kmsKeyId: props.encryptionKey?.keyArn,
iops: props.iops,
multiAttachEnabled: props.enableMultiAttach ?? false,
size: props.size?.toGibibytes({ rounding: SizeRoundingBehavior.FAIL }),
snapshotId: props.snapshotId,
volumeType: props.volumeType ?? EbsDeviceVolumeType.GENERAL_PURPOSE_SSD,
});
resource.applyRemovalPolicy(props.removalPolicy);
this.volumeId = resource.ref;
this.availabilityZone = props.availabilityZone;
this.encryptionKey = props.encryptionKey;
if (this.encryptionKey) {
// Per: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html#ebs-encryption-requirements
const principal =
new ViaServicePrincipal(`ec2.${Stack.of(this).region}.amazonaws.com`, new AccountRootPrincipal()).withConditions({
StringEquals: {
'kms:CallerAccount': Stack.of(this).account,
},
});
const grant = this.encryptionKey.grant(principal,
// Describe & Generate are required to be able to create the CMK-encrypted Volume.
'kms:DescribeKey',
'kms:GenerateDataKeyWithoutPlainText',
);
if (props.snapshotId) {
// ReEncrypt is required for when re-encrypting from an encrypted snapshot.
grant.principalStatement?.addActions('kms:ReEncrypt*');
}
}
}
protected validateProps(props: VolumeProps) {
if (!(props.size || props.snapshotId)) {
throw new Error('Must provide at least one of `size` or `snapshotId`');
}
if (props.snapshotId && !Token.isUnresolved(props.snapshotId) && !/^snap-[0-9a-fA-F]+$/.test(props.snapshotId)) {
throw new Error('`snapshotId` does match expected pattern. Expected `snap-<hexadecmial value>` (ex: `snap-05abe246af`) or Token');
}
if (props.encryptionKey && !props.encrypted) {
throw new Error('`encrypted` must be true when providing an `encryptionKey`.');
}
if (
props.volumeType &&
[
EbsDeviceVolumeType.PROVISIONED_IOPS_SSD,
EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2,
].includes(props.volumeType) &&
!props.iops
) {
throw new Error(
'`iops` must be specified if the `volumeType` is `PROVISIONED_IOPS_SSD` or `PROVISIONED_IOPS_SSD_IO2`.',
);
}
if (props.iops) {
const volumeType = props.volumeType ?? EbsDeviceVolumeType.GENERAL_PURPOSE_SSD;
if (
![
EbsDeviceVolumeType.PROVISIONED_IOPS_SSD,
EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2,
EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3,
].includes(volumeType)
) {
throw new Error(
'`iops` may only be specified if the `volumeType` is `PROVISIONED_IOPS_SSD`, `PROVISIONED_IOPS_SSD_IO2` or `GENERAL_PURPOSE_SSD_GP3`.',
);
}
// Enforce minimum & maximum IOPS:
// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-ebs-volume.html
const iopsRanges: { [key: string]: { Min: number, Max: number } } = {};
iopsRanges[EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3] = { Min: 3000, Max: 16000 };
iopsRanges[EbsDeviceVolumeType.PROVISIONED_IOPS_SSD] = { Min: 100, Max: 64000 };
iopsRanges[EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2] = { Min: 100, Max: 64000 };
const { Min, Max } = iopsRanges[volumeType];
if (props.iops < Min || props.iops > Max) {
throw new Error(`\`${volumeType}\` volumes iops must be between ${Min} and ${Max}.`);
}
// Enforce maximum ratio of IOPS/GiB:
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html
const maximumRatios: { [key: string]: number } = {};
maximumRatios[EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3] = 500;
maximumRatios[EbsDeviceVolumeType.PROVISIONED_IOPS_SSD] = 50;
maximumRatios[EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2] = 500;
const maximumRatio = maximumRatios[volumeType];
if (props.size && (props.iops > maximumRatio * props.size.toGibibytes({ rounding: SizeRoundingBehavior.FAIL }))) {
throw new Error(`\`${volumeType}\` volumes iops has a maximum ratio of ${maximumRatio} IOPS/GiB.`);
}
}
if (props.enableMultiAttach) {
const volumeType = props.volumeType ?? EbsDeviceVolumeType.GENERAL_PURPOSE_SSD;
if (
![
EbsDeviceVolumeType.PROVISIONED_IOPS_SSD,
EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2,
].includes(volumeType)
) {
throw new Error('multi-attach is supported exclusively on `PROVISIONED_IOPS_SSD` and `PROVISIONED_IOPS_SSD_IO2` volumes.');
}
}
if (props.size) {
const size = props.size.toGibibytes({ rounding: SizeRoundingBehavior.FAIL });
// Enforce minimum & maximum volume size:
// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-ebs-volume.html
const sizeRanges: { [key: string]: { Min: number, Max: number } } = {};
sizeRanges[EbsDeviceVolumeType.GENERAL_PURPOSE_SSD] = { Min: 1, Max: 16384 };
sizeRanges[EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3] = { Min: 1, Max: 16384 };
sizeRanges[EbsDeviceVolumeType.PROVISIONED_IOPS_SSD] = { Min: 4, Max: 16384 };
sizeRanges[EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2] = { Min: 4, Max: 16384 };
sizeRanges[EbsDeviceVolumeType.THROUGHPUT_OPTIMIZED_HDD] = { Min: 125, Max: 16384 };
sizeRanges[EbsDeviceVolumeType.COLD_HDD] = { Min: 125, Max: 16384 };
sizeRanges[EbsDeviceVolumeType.MAGNETIC] = { Min: 1, Max: 1024 };
const volumeType = props.volumeType ?? EbsDeviceVolumeType.GENERAL_PURPOSE_SSD;
const { Min, Max } = sizeRanges[volumeType];
if (size < Min || size > Max) {
throw new Error(`\`${volumeType}\` volumes must be between ${Min} GiB and ${Max} GiB in size.`);
}
}
}
}