packages/constructs/L3/utility/ec2-l3-construct/lib/ec2-l3-construct.ts (598 lines of code) (raw):
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import {
BlockDeviceProps,
MdaaEC2Instance,
MdaaEC2InstanceProps,
MdaaEC2SecretKeyPair,
MdaaEC2SecretKeyPairProps,
MdaaSecurityGroup,
MdaaSecurityGroupProps,
MdaaSecurityGroupRuleProps,
} from '@aws-mdaa/ec2-constructs';
import { MdaaRole } from '@aws-mdaa/iam-constructs';
import { MdaaResolvableRole, MdaaRoleRef } from '@aws-mdaa/iam-role-helper';
import { DECRYPT_ACTIONS, ENCRYPT_ACTIONS, MdaaKmsKey } from '@aws-mdaa/kms-constructs';
import { MdaaL3Construct, MdaaL3ConstructProps } from '@aws-mdaa/l3-construct';
import {
ApplyCloudFormationInitOptions,
CfnInstance,
CloudFormationInit,
ConfigSetProps,
IMachineImage,
InitCommand,
InitCommandOptions,
InitCommandWaitDuration,
InitConfig,
InitElement,
InitFile,
InitFileOptions,
InitPackage,
InitService,
InitServiceOptions,
InitServiceRestartHandle,
Instance,
InstanceType,
ISecurityGroup,
LocationPackageOptions,
MachineImageConfig,
NamedPackageOptions,
OperatingSystemType,
SecurityGroup,
Subnet,
UserData,
Vpc,
} from 'aws-cdk-lib/aws-ec2';
import { ArnPrincipal, Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { IKey, Key } from 'aws-cdk-lib/aws-kms';
import { Construct } from 'constructs';
import { readFileSync } from 'fs';
import { MdaaNagSuppressions } from '@aws-mdaa/construct'; //NOSONAR
import { Duration } from 'aws-cdk-lib';
import { MdaaConfigRefValueTransformer, MdaaConfigRefValueTransformerProps } from '@aws-mdaa/config';
export interface NamedSecurityGroupProps {
/** @jsii ignore */
readonly [name: string]: SecurityGroupProps;
}
export interface SecurityGroupProps {
/**
* id of VPC to launch the instance in.
*/
readonly vpcId: string;
/**
* List of ingress rules to be added to the function SG
*/
readonly ingressRules?: MdaaSecurityGroupRuleProps;
/**
* List of egress rules to be added to the function SG
*/
readonly egressRules?: MdaaSecurityGroupRuleProps;
/**
* If true, the SG will allow traffic to and from itself
*/
readonly addSelfReferenceRule?: boolean;
}
export interface KeyPairProps {
readonly kmsKeyArn?: string;
}
export interface NamedKeyPairProps {
/** @jsii ignore */
readonly [name: string]: KeyPairProps;
}
export interface NamedInitProps {
/** @jsii ignore */
readonly [name: string]: InitProps;
}
export interface InitProps {
/**
* Set of configs in order they need to run
*/
// readonly configSets: { [configSetName:string]: string[] };
readonly configSets: NamedConfigSetsProps;
/**
* list of configs
*/
readonly configs: NamedConfigProps;
}
export interface NamedConfigSetsProps {
/** @jsii ignore */
readonly [name: string]: ConfigSetsProps;
}
export interface ConfigSetsProps {
readonly configs: string[];
}
export interface NamedConfigProps {
/** @jsii ignore */
readonly [name: string]: ConfigProps;
}
export interface ConfigProps {
/**
* You can use the packages key to download and install pre-packaged applications and components. On Windows systems, the packages key supports only the MSI installer.
* The cfn-init script currently supports the following package formats: apt, msi, python, rpm, rubygems, yum, and Zypper.
*/
readonly packages?: NamedPackageProps;
/**
* You can use the groups key to create Linux/UNIX groups and to assign group IDs. The groups key isn't supported for Windows systems.
*/
readonly groups?: NamedGroupProps;
/**
* You can use the users key to create Linux/UNIX users on the EC2 instance. The users key isn't supported for Windows systems.
*/
readonly users?: NamedUserProps;
/**
* You can use the sources key to download an archive file and unpack it in a target directory on the EC2 instance.
* This key is fully supported for both Linux and Windows systems.
*/
readonly sources?: NamedSourceProps;
/**
* You can use the files key to create files on the EC2 instance.
* Content is pulled from a given file
*/
readonly files?: NamedFileProps;
/**
* You can use the commands key to run commands on the EC2 instance.
* The commands are processed in alphabetical order by name.
*/
readonly commands?: NamedCommandProps;
/**
* You can use the services key to define which services should be enabled or disabled when the instance is launched.
* On Linux systems, this key is supported by using sysvinit or systemd.
* On Windows systems, it's supported by using the Windows service manager.
*/
readonly services?: NamedServiceProps;
}
export interface NamedPackageProps {
/**
* Refers to package to be installed
* key could be any string, and is just a reference, not used for package itself.
*/
/** @jsii ignore */
readonly [name: string]: PackageProps;
}
export interface PackageProps {
/**
* Package Manager to be used
* Available package manager values: msi, rpm, gem, yum, python, apt
*/
readonly packageManager: string;
/**
* Package location
* to be provided for msi & rpm packages
*/
readonly packageLocation?: string;
/**
* Package name
* to be provided for gem, yum, python, apt packages
*/
readonly packageName?: string;
/**
* Empty list if latest version is required
* default is latest
*/
readonly packageVersions?: string[];
/**
* Identifier key for this package. part of LocationPackageOptions, for msi and rpm packages
*/
readonly key?: string;
/**
* Restart the given service after this package is installed.
*/
readonly restartRequired?: boolean;
}
export interface NamedGroupProps {
/** @jsii ignore */
readonly [name: string]: GroupProps;
}
export interface GroupProps {
/**
*
* A group ID number
* If a group ID is specified, and the group already exists by name, the group creation will fail.
* If another group has the specified group ID, the OS may reject the group creation.
*/
readonly gid?: string;
}
export interface NamedUserProps {
/** @jsii ignore */
readonly [name: string]: UserProps;
}
export interface UserProps {
/**
* A user ID.
* The creation process fails if the user name exists with a different user ID.
* If the user ID is already assigned to an existing user the operating system may reject the creation request.
*/
readonly uid?: string;
/**
* A list of group names. The user will be added to each group in the list.
*/
readonly groups: string[];
/**
* The user's home directory.
*/
readonly homeDir: string;
}
export interface NamedSourceProps {
/**
* Key is the directory where sources file needs to be stored.
*/
/** @jsii ignore */
readonly [name: string]: SourceProps;
}
export interface SourceProps {
/**
* source location url
*/
readonly source: string;
}
export interface NamedFileProps {
/**
* Key is the directory where sources file needs to be stored.
*/
/** @jsii ignore */
readonly [name: string]: FileProps;
}
export interface FileProps {
/**
* source file path
*/
readonly filePath: string;
/**
* Restart the given service(s) after this command has run, default: Do not restart any service
*/
readonly restartRequired?: boolean;
}
export interface NamedCommandProps {
/**
* Identifier key for this command.
* Commands are executed in lexicographical order of their key names.
*/
/** @jsii ignore */
readonly [name: string]: CommandProps;
}
export interface CommandProps {
// readonly key?: string;
/**
* Shell command that needs to be run, either shell command or argvs should be provided.
*/
readonly shellCommand?: string;
/**
* list of args that needs to be run as argvs, either shell command or argvs should be provided.
*/
readonly argvs?: string[];
/**
* Sets environment variables for the command.
* This property overwrites, rather than appends, the existing environment.
*/
readonly env?: NamedEnvProps;
/**
* dir where command needs to be run.
*/
readonly workingDir?: string;
/**
* Command to determine whether this command should be run.
* If the test passes (exits with error code of 0), the command is run.
*/
readonly testCommand?: string;
/**
* Continue running if this command fails. default is false
*/
readonly ignoreErrors?: boolean;
/**
* The duration in minutes to wait after a command has finished in case the command causes a reboot.
* Set this value to InitCommandWaitDuration.none() if you do not want to wait for every command; InitCommandWaitDuration.forever() directs cfn-init to exit and resume only after the reboot is complete.
* For Windows systems only.
* Default is 1 minute
*/
readonly waitAfterCompletion?: number;
/**
* cfn-init will exit and resume only after a reboot.
* Choose either waitAfterCompletion waitForever or waitNone, If choose none of these >> default wait time will be 1 minute
*/
readonly waitForever?: boolean;
/**
* Do not wait for this command.
* Choose either waitAfterCompletion waitForever or waitNone, If choose none of these >> default wait time will be 1 minute
*/
readonly waitNone?: boolean;
/**
* Restart the given service(s) after this command has run, default: Do not restart any service
*/
readonly restartRequired?: boolean;
}
export interface NamedEnvProps {
/** @jsii ignore */
readonly [name: string]: string;
}
export interface NamedServiceProps {
/**
* Identifier key for this service.
* key should be the name of the service.
* For Windows can be retrieved using Get-Service powershell command
* https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/get-service?view=powershell-7.3
*/
/** @jsii ignore */
readonly [name: string]: ServiceProps;
}
export interface ServiceProps {
/**
* Set to true to ensure that the service is running after cfn-init finishes.
* Set to false to ensure that the service isn't running after cfn-init finishes.
* Omit this key to make no changes to the service state.
*/
readonly ensureRunning?: boolean;
/**
* Set to true to ensure that the service will be started automatically upon boot.
* Set to false to ensure that the service won't be started automatically upon boot.
* Omit this key to make no changes to this property.
*/
readonly enabled?: boolean;
/**
* Disable and stop the given service
*/
readonly disabled?: boolean;
/**
* Restart the given service(s) after this command has run, default: Do not restart any service
*/
readonly restartRequired?: boolean;
//following params are Utilized in a later release of aws-cdk-lib, with service manager explicitly declared in a prop, and option to choose systemd for AL2
// Need to upgrade from cdk 2.54.0 for the same
//which introduces breaking changes requiring update to remaining constructs as well
//While using current setup for 2.54.0, restart can still be triggered by declaring initrestarthandle in remaining init props
// /**
// * A list of files. If cfn-init changes one directly through the files block, this service will be restarted.
// */
// readonly files?: string[];
// /**
// * A list of directories. If cfn-init expands an archive into one of these directories, this service will be restarted.
// */
// readonly sources?: string[];
// /**
// * A map of package manager to list of package names. If cfn-init installs or updates one of these packages, this service will be restarted.
// * e.g. { "yum" : ["php", "spawn-fcgi"] }
// */
// readonly packages?: {[name:string]:string[]};
// /**
// * A list of command names. If cfn-init runs the specified command, this service will be restarted.
// */
// readonly commands?: string[]
}
export interface InitOptionsProps {
/**
* ConfigSet to activate.
* default value is ['default']
*/
readonly configSets?: string[];
/**
* Force instance replacement by embedding a config fingerprint.
* If true (the default), a hash of the config will be embedded into the UserData, so that if the config changes, the UserData changes.
* If the EC2 instance is instance-store backed or userDataCausesReplacement is set, this will cause the instance to be replaced and the new configuration to be applied.
* If the instance is EBS-backed and userDataCausesReplacement is not set, the change of UserData will make the instance restart but not be replaced, and the configuration will not be applied automatically.
* If false, no hash will be embedded, and if the CloudFormation Init config changes nothing will happen to the running instance. If a config update introduces errors, you will not notice until after the CloudFormation deployment successfully finishes and the next instance fails to launch.
*/
readonly embedFingerprint?: boolean;
/**
* Don't fail the instance creation when cfn-init fails.
* You can use this to prevent CloudFormation from rolling back when instances fail to start up, to help in debugging.
*/
readonly ignoreFailures?: boolean;
/**
* Include --role argument when running cfn-init and cfn-signal commands.
* This will be the IAM instance profile attached to the EC2 instance
*/
readonly includeRole?: boolean;
/**
* Include --url argument when running cfn-init and cfn-signal commands.
* This will be the cloudformation endpoint in the deployed region
*/
readonly includeUrl?: boolean;
/**
* Print the results of running cfn-init to the Instance System Log.
* By default, the output of running cfn-init is written to a log file on the instance.
* Set this to true to print it to the System Log (visible from the EC2 Console), false to not print it.
* (Be aware that the system log is refreshed at certain points in time of the instance life cycle, and successful execution may not always show up).
*/
readonly printLog?: boolean;
/**
* Timeout waiting for the configuration to be applied.
* in minutes
* default is 5 mins
*/
readonly timeout?: number;
}
export interface NamedInstanceProps {
/** @jsii ignore */
readonly [name: string]: InstanceProps;
}
export interface InstanceProps {
readonly securityGroup?: string;
readonly securityGroupId?: string;
/**
* Type of instance to launch.
*/
readonly instanceType: string;
/**
* AMI to launch.
*/
readonly amiId: string;
/**
* id of VPC to launch the instance in.
*/
readonly vpcId: string;
/**
* Where to place the instance within the VPC.
*/
readonly subnetId: string;
/**
* list of configs for block device to be mapped to instance
*/
readonly blockDevices: BlockDeviceProps[];
/**
* Role used by instance
*/
readonly instanceRole: MdaaRoleRef;
/**
* Specific key to use.
*/
readonly kmsKeyArn?: string;
/**
* In which AZ to place the instance within the VPC.
*/
readonly availabilityZone: string;
/**
* Type of OS for the AMI.
*/
readonly osType: 'linux' | 'windows' | 'unknown';
/**
* Specific UserData to use.
*/
readonly userDataScriptPath?: string;
/**
* Changes to the UserData force replacement.
* Depending the EC2 instance type, changing UserData either restarts the instance or replaces the instance.
* Instance store-backed instances are replaced.
* EBS-backed instances are restarted.
* By default, restarting does not execute the new UserData so you will need a different mechanism to ensure the instance is restarted.
* Setting this to true will make the instance's Logical ID depend on the UserData, which will cause CloudFormation to replace it if the UserData changes.
* default: true iff initOptions is specified, false otherwise.
*/
readonly userDataCausesReplacement?: boolean;
/**
* Apply the given CloudFormation Init configuration to the instance at startup.
* For Linux
* @link https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html
* For Windows
* @link https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/ec2-windows-user-data.html
*/
// readonly init?: CloudFormationInit;
readonly init?: InitProps;
/**
* Name of init to be implemented , name can be referred from init object in config
*/
readonly initName?: string;
/**
* Use the given options for applying CloudFormation Init.
*
* @link https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.ApplyCloudFormationInitOptions.html
*/
// readonly initOptions?: ApplyCloudFormationInitOptions;
readonly initOptions?: InitOptionsProps;
/**
* count of Successful signals required for creation policy .
*/
readonly signalCount?: number;
/**
* Timeout for creation policy .
*/
readonly creationTimeOut?: string;
/**
* Specifies whether to enable an instance launched in a VPC to perform NAT.
*/
readonly sourceDestCheck?: boolean;
/**
* Name of SSH keypair (created by this construct) to grant access to instance.
*/
readonly keyPairName?: string;
/**
* Name of existing SSH keypair to grant access to instance.
*/
readonly existingKeyPairName?: string;
}
export interface Ec2L3ConstructProps extends MdaaL3ConstructProps {
/**
* Roles which will be provided Admin access to the
* KMS key, and KeyPair secrets.
*/
readonly adminRoles: MdaaRoleRef[];
/**
* List of security groups to be created.
*/
readonly securityGroups?: NamedSecurityGroupProps;
/**
* List of key pairs to be created.
*/
readonly keyPairs?: NamedKeyPairProps;
/**
* List of init objects to be created.
*/
readonly cfnInit?: NamedInitProps;
/**
* List of instances to be launched.
*/
readonly instances?: NamedInstanceProps;
}
//This stack creates and manages an EC2 instance
export class Ec2L3Construct extends MdaaL3Construct {
protected readonly props: Ec2L3ConstructProps;
private static osTypeMap: { [key: string]: OperatingSystemType } = {
linux: OperatingSystemType.LINUX,
windows: OperatingSystemType.WINDOWS,
unknown: OperatingSystemType.UNKNOWN,
};
private readonly adminRoles: MdaaResolvableRole[];
private kmsKey?: Key;
initServiceRestartHandle = new InitServiceRestartHandle();
public readonly keyPairs: { [key: string]: MdaaEC2SecretKeyPair } = {};
public readonly securityGroups: { [key: string]: MdaaSecurityGroup } = {};
public readonly instances: { [key: string]: Instance } = {};
public readonly cfnInit: { [key: string]: CloudFormationInit } = {};
constructor(scope: Construct, id: string, props: Ec2L3ConstructProps) {
super(scope, id, props);
this.props = props;
this.adminRoles = props.roleHelper.resolveRoleRefsWithOrdinals(props.adminRoles, 'admin');
this.createKeyPairs(props.keyPairs || {});
this.createSecurityGroups(props.securityGroups || {});
this.cfnInit = this.createInit(props.cfnInit || {});
this.createInstances(props.instances || {});
}
private createKeyPairs(namedKeyPairProps: NamedKeyPairProps) {
Object.entries(namedKeyPairProps).forEach(entry => {
const keyPairName = entry[0];
const keyPairProps = entry[1];
const kmsKey = keyPairProps.kmsKeyArn
? Key.fromKeyArn(this, `kms-keypair-${keyPairName}`, keyPairProps.kmsKeyArn)
: this.getKmsKey();
const createKeyPairProps: MdaaEC2SecretKeyPairProps = {
name: keyPairName,
kmsKey: kmsKey,
naming: this.props.naming,
readPrincipals: this.adminRoles.map(x => new ArnPrincipal(x.arn())),
};
this.keyPairs[keyPairName] = new MdaaEC2SecretKeyPair(this, `key-pair-${keyPairName}`, createKeyPairProps);
});
}
private createConfigSet(namedConfigSetsProps: NamedConfigSetsProps) {
/** @jsii ignore */
const configSetMap: { [name: string]: string[] } = {};
Object.entries(namedConfigSetsProps).forEach(entry => {
const configSetName = entry[0];
const configSetProps = entry[1];
configSetMap[configSetName] = configSetProps.configs;
});
return configSetMap;
}
private createConfig(namedConfigProps: NamedConfigProps) {
/** @jsii ignore */
const configMap: { [name: string]: InitConfig } = {};
Object.entries(namedConfigProps).forEach(entry => {
const configName = entry[0];
const configProps = entry[1];
const configList: InitElement[] = [];
if (configProps.packages) {
configList.push(...this.createPackages(configProps.packages));
}
if (configProps.commands) {
configList.push(...this.createCommands(configProps.commands));
}
if (configProps.files) {
configList.push(...this.createFiles(configProps.files));
}
if (configProps.services) {
configList.push(...this.createServices(configProps.services));
}
configMap[configName] = new InitConfig(configList);
});
return configMap;
}
private createPackages(namedPackageProps: NamedPackageProps) {
const packageList: InitElement[] = [];
Object.entries(namedPackageProps).forEach(entry => {
const packageProps = entry[1];
const namedPackageOptions: NamedPackageOptions = packageProps.restartRequired
? {
serviceRestartHandles: [this.initServiceRestartHandle],
version: packageProps.packageVersions,
}
: {
version: packageProps.packageVersions,
};
const locationPackageOptions: LocationPackageOptions = packageProps.restartRequired
? {
serviceRestartHandles: [this.initServiceRestartHandle],
key: packageProps.key,
}
: {
key: packageProps.key,
};
if (packageProps.packageManager == 'yum') {
packageList.push(InitPackage.yum(packageProps.packageName!, namedPackageOptions));
}
if (packageProps.packageManager == 'apt') {
packageList.push(InitPackage.apt(packageProps.packageName!, namedPackageOptions));
}
if (packageProps.packageManager == 'python') {
packageList.push(InitPackage.python(packageProps.packageName!, namedPackageOptions));
}
if (packageProps.packageManager == 'rubyGem') {
packageList.push(InitPackage.rubyGem(packageProps.packageName!, namedPackageOptions));
}
if (packageProps.packageManager == 'msi') {
packageList.push(InitPackage.msi(packageProps.packageLocation!, locationPackageOptions));
}
if (packageProps.packageManager == 'rpm') {
packageList.push(InitPackage.rpm(packageProps.packageLocation!, locationPackageOptions));
}
});
return packageList;
}
private toWaitOrNotToWait(duration?: Duration, waitForever?: boolean, waitNone?: boolean) {
if (duration) return InitCommandWaitDuration.of(duration);
if (waitForever) return InitCommandWaitDuration.forever();
if (waitNone) return InitCommandWaitDuration.none();
else {
return undefined;
}
}
private createCommands(namedCommandProps: NamedCommandProps) {
const commandList: InitElement[] = [];
Object.entries(namedCommandProps).forEach(entry => {
const commandKey = entry[0];
const commandProps = entry[1];
const duration = commandProps.waitAfterCompletion
? Duration.minutes(commandProps.waitAfterCompletion)
: undefined;
const waitAfterCompletion = this.toWaitOrNotToWait(duration, commandProps.waitForever, commandProps.waitNone);
const commandOptions: InitCommandOptions = {
cwd: commandProps.workingDir,
env: commandProps.env,
ignoreErrors: commandProps.ignoreErrors,
key: commandKey,
serviceRestartHandles: commandProps.restartRequired ? [this.initServiceRestartHandle] : undefined,
testCmd: commandProps.testCommand,
waitAfterCompletion: waitAfterCompletion,
};
if (commandProps.shellCommand) {
commandList.push(InitCommand.shellCommand(commandProps.shellCommand, commandOptions));
}
if (commandProps.argvs) {
commandList.push(InitCommand.argvCommand(commandProps.argvs, commandOptions));
}
});
return commandList;
}
private createFiles(namedFileProps: NamedFileProps) {
const fileList: InitElement[] = [];
Object.entries(namedFileProps).forEach(entry => {
const fileName = entry[0];
const fileProps = entry[1];
const initFileOptions: InitFileOptions = {
// not supported for windows , to be added later
// group: fileProps,
// mode: fileProps,
// owner: fileProps,
serviceRestartHandles: fileProps.restartRequired ? [this.initServiceRestartHandle] : undefined,
};
// fileList.push( InitFile.fromAsset( fileName, fileProps.filePath, initFileAssetOptions ) )
// fromAsset creates a construct to store file in s3 with id `${targetFileName}Asset`.
// Thus if more than one instance using the same target file name in stack, it will cause name collision.
//Open Issue: https://github.com/aws/aws-cdk/issues/16891
fileList.push(InitFile.fromFileInline(fileName, fileProps.filePath, initFileOptions));
});
return fileList;
}
private createServices(namedServiceProps: NamedServiceProps) {
const serviceList: InitElement[] = [];
Object.entries(namedServiceProps).forEach(entry => {
const serviceName = entry[0];
const serviceProps = entry[1];
const serviceInitOptions: InitServiceOptions = {
enabled: serviceProps.enabled,
ensureRunning: serviceProps.ensureRunning,
serviceRestartHandle: serviceProps.restartRequired ? this.initServiceRestartHandle : undefined,
};
if (serviceProps.enabled) {
serviceList.push(InitService.enable(serviceName, serviceInitOptions));
}
if (serviceProps.disabled) {
serviceList.push(InitService.disable(serviceName));
}
});
return serviceList;
}
private createInit(namedInitProps: NamedInitProps) {
/** @jsii ignore */
const initMap: { [name: string]: CloudFormationInit } = {};
Object.entries(namedInitProps).forEach(entry => {
const initName = entry[0];
const initProps = entry[1];
const configMap = this.createConfig(initProps.configs);
const configSetMap = this.createConfigSet(initProps.configSets);
const cfnconfigSets: ConfigSetProps = {
configSets: configSetMap,
configs: configMap,
};
initMap[initName] = CloudFormationInit.fromConfigSets(cfnconfigSets);
});
return initMap;
}
private createInstances(namedInstanceProps: NamedInstanceProps) {
Object.entries(namedInstanceProps).forEach(entry => {
const instanceName = entry[0];
const instanceProps = entry[1];
const resolvedInstanceRole = this.props.roleHelper.resolveRoleRefWithRefId(
instanceProps.instanceRole,
'instanceRole',
);
const roleArn = resolvedInstanceRole.arn();
const instanceRole = MdaaRole.fromRoleArn(this, 'role for' + instanceName, roleArn);
const kmsKey = instanceProps.kmsKeyArn
? MdaaKmsKey.fromKeyArn(this, 'key for' + instanceName, instanceProps.kmsKeyArn)
: this.getKmsKey();
if (!instanceProps.kmsKeyArn) {
this.addRoleToKmsKey(roleArn);
}
const machineImage: IMachineImage = this.getMachineImage(instanceProps);
const vpc = Vpc.fromVpcAttributes(this, 'vpc of' + instanceName, {
availabilityZones: ['dummy'],
vpcId: instanceProps.vpcId,
});
const instanceType = new InstanceType(instanceProps.instanceType);
const instanceSubnet = Subnet.fromSubnetAttributes(this, 'Subnet for' + instanceName, {
subnetId: instanceProps.subnetId,
availabilityZone: instanceProps.availabilityZone,
});
const securityGroup = this.getInstanceSecurityGroup(instanceName, instanceProps);
const keyPairName = this.getInstanceKeyPairName(instanceProps);
const cfnInitNew = instanceProps.initName ? this.cfnInit[instanceProps.initName] : undefined;
const initDuration = instanceProps.initOptions?.timeout
? Duration.minutes(instanceProps.initOptions.timeout)
: undefined;
const initOptions: ApplyCloudFormationInitOptions | undefined = instanceProps.initOptions
? {
configSets: instanceProps.initOptions.configSets,
embedFingerprint: instanceProps.initOptions.embedFingerprint,
ignoreFailures: instanceProps.initOptions.ignoreFailures,
includeRole: instanceProps.initOptions.includeRole,
includeUrl: instanceProps.initOptions.includeUrl,
printLog: instanceProps.initOptions.printLog,
timeout: initDuration,
}
: undefined;
const createInstanceProps: MdaaEC2InstanceProps = {
role: instanceRole,
securityGroup: securityGroup,
instanceType: instanceType,
machineImage: machineImage,
vpc: vpc,
instanceSubnet: instanceSubnet,
instanceName: instanceName,
userDataCausesReplacement: instanceProps.userDataCausesReplacement,
init: cfnInitNew,
initOptions: initOptions,
sourceDestCheck: instanceProps.sourceDestCheck,
kmsKey: kmsKey,
blockDeviceProps: instanceProps.blockDevices,
keyName: keyPairName,
naming: this.props.naming,
};
this.instances[instanceName] = new MdaaEC2Instance(this, instanceName + 'instance', createInstanceProps);
MdaaNagSuppressions.addCodeResourceSuppressions(
this.instances[instanceName].role,
[
{
id: 'NIST.800.53.R5-IAMNoInlinePolicy',
reason: 'Adding cfn init adds inline policy to instance role to describe stack',
},
{
id: 'HIPAA.Security-IAMNoInlinePolicy',
reason: 'Adding cfn init adds inline policy to instance role to describe stack',
},
{
id: 'PCI.DSS.321-IAMNoInlinePolicy',
reason: 'Adding cfn init adds inline policy to instance role to describe stack',
},
{
id: 'AwsSolutions-IAM5',
reason:
'Adding files section for cfn init, adds permission for cdk bootstrap bucket with wildcard to store the file',
},
],
true,
);
const cfnInstance = this.instances[instanceName].node.defaultChild as CfnInstance;
if (instanceProps.signalCount || instanceProps.creationTimeOut) {
cfnInstance.cfnOptions.creationPolicy = {
resourceSignal: {
count: instanceProps.signalCount,
timeout: instanceProps.creationTimeOut,
},
};
}
});
}
private getMachineImage(instanceProps: InstanceProps): IMachineImage {
const osType = Ec2L3Construct.osTypeMap[instanceProps.osType];
const userDataScript = instanceProps.userDataScriptPath
? readFileSync(instanceProps.userDataScriptPath, 'utf8')
: undefined;
const configRefValueTranformerProps: MdaaConfigRefValueTransformerProps = {
org: this.node.tryGetContext('org'),
domain: this.node.tryGetContext('domain'),
env: this.node.tryGetContext('env'),
module_name: this.node.tryGetContext('module_name'),
scope: this,
};
const transformedUserDataScript = userDataScript
? new MdaaConfigRefValueTransformer(configRefValueTranformerProps).transformValue(userDataScript)
: undefined;
return {
getImage: function (): MachineImageConfig {
const userData: UserData = UserData.forOperatingSystem(osType);
if (transformedUserDataScript) {
userData.addCommands(transformedUserDataScript.toString());
}
return {
imageId: instanceProps.amiId,
osType: osType,
userData: userData,
};
},
};
}
private getInstanceKeyPairName(instanceProps: InstanceProps): string | undefined {
if (instanceProps.keyPairName && instanceProps.existingKeyPairName) {
throw new Error('At most one of keyPairName or existingKeyPairName must be specified');
} else if (instanceProps.keyPairName) {
const keyPairName = this.keyPairs[instanceProps.keyPairName].name;
if (!keyPairName) {
throw new Error(`Non-existent key pair name specified: ${instanceProps.keyPairName}`);
}
return keyPairName;
} else if (instanceProps.existingKeyPairName) {
return instanceProps.existingKeyPairName;
}
return undefined;
}
private getInstanceSecurityGroup(instanceName: string, instanceProps: InstanceProps): ISecurityGroup {
if (
(!instanceProps.securityGroup && !instanceProps.securityGroupId) ||
(instanceProps.securityGroup && instanceProps.securityGroupId)
) {
throw new Error('Exactly one of securityGroup or securityGroupId must be specified');
} else {
if (instanceProps.securityGroup) {
const sg = this.securityGroups[instanceProps.securityGroup];
if (!sg) {
throw new Error(`Security Group ${instanceProps.securityGroup} is not known to this module.`);
}
return sg;
} else {
return SecurityGroup.fromSecurityGroupId(this, 'SG for' + instanceName, instanceProps.securityGroupId || '');
}
}
}
private createSecurityGroups(securityGroups: NamedSecurityGroupProps) {
Object.entries(securityGroups).forEach(entry => {
const securityGroupName = entry[0];
const securityGroupProps = entry[1];
const vpc = Vpc.fromVpcAttributes(this, 'vpc of' + securityGroupName, {
availabilityZones: ['dummy'],
vpcId: securityGroupProps.vpcId,
});
const customEgress: boolean =
(securityGroupProps.egressRules?.ipv4 && securityGroupProps.egressRules?.ipv4.length > 0) ||
(securityGroupProps.egressRules?.prefixList && securityGroupProps.egressRules?.prefixList.length > 0) ||
(securityGroupProps.egressRules?.sg && securityGroupProps.egressRules?.sg.length > 0) ||
false;
const securityGroupCreateProps: MdaaSecurityGroupProps = {
securityGroupName: securityGroupName,
vpc: vpc,
naming: this.props.naming,
ingressRules: securityGroupProps.ingressRules,
egressRules: securityGroupProps.egressRules,
allowAllOutbound: !customEgress,
addSelfReferenceRule: securityGroupProps.addSelfReferenceRule,
};
this.securityGroups[securityGroupName] = new MdaaSecurityGroup(this, securityGroupName, securityGroupCreateProps);
});
}
private getKmsKey(): IKey {
const kmsKey = this.kmsKey
? this.kmsKey
: new MdaaKmsKey(this, 'kms-key', {
naming: this.props.naming,
keyAdminRoleIds: this.adminRoles.map(x => x.id()),
keyUserRoleIds: this.adminRoles.map(x => x.id()),
});
this.kmsKey = kmsKey;
return kmsKey;
}
private addRoleToKmsKey(roleArn: string) {
// Allow execution role to use the key
const kmsEncryptDecryptPolicy = new PolicyStatement({
effect: Effect.ALLOW,
// Use of * mirrors what is done in the CDK methods for adding policy helpers.
resources: ['*'],
actions: [
...DECRYPT_ACTIONS,
...ENCRYPT_ACTIONS,
'kms:GenerateDataKeyWithoutPlaintext',
'kms:CreateGrant',
'kms:DescribeKey',
'kms:ListAliases',
],
});
kmsEncryptDecryptPolicy.addArnPrincipal(roleArn);
this.getKmsKey().addToResourcePolicy(kmsEncryptDecryptPolicy);
}
}