packages/aws-cdk-lib/aws-ecs/lib/container-definition.ts (675 lines of code) (raw):

import { Construct } from 'constructs'; import { NetworkMode, TaskDefinition } from './base/task-definition'; import { ContainerImage, ContainerImageConfig } from './container-image'; import { CredentialSpec, CredentialSpecConfig } from './credential-spec'; import { CfnTaskDefinition } from './ecs.generated'; import { EnvironmentFile, EnvironmentFileConfig } from './environment-file'; import { LinuxParameters } from './linux-parameters'; import { LogDriver, LogDriverConfig } from './log-drivers/log-driver'; import * as iam from '../../aws-iam'; import * as secretsmanager from '../../aws-secretsmanager'; import * as ssm from '../../aws-ssm'; import * as cdk from '../../core'; /** * Specify the secret's version id or version stage */ export interface SecretVersionInfo { /** * version id of the secret * * @default - use default version id */ readonly versionId?: string; /** * version stage of the secret * * @default - use default version stage */ readonly versionStage?: string; } /** * A secret environment variable. */ export abstract class Secret { /** * Creates an environment variable value from a parameter stored in AWS * Systems Manager Parameter Store. */ public static fromSsmParameter(parameter: ssm.IParameter): Secret { return { arn: parameter.parameterArn, grantRead: grantee => parameter.grantRead(grantee), }; } /** * Creates a environment variable value from a secret stored in AWS Secrets * Manager. * * @param secret the secret stored in AWS Secrets Manager * @param field the name of the field with the value that you want to set as * the environment variable value. Only values in JSON format are supported. * If you do not specify a JSON field, then the full content of the secret is * used. */ public static fromSecretsManager(secret: secretsmanager.ISecret, field?: string): Secret { return { arn: field ? `${secret.secretArn}:${field}::` : secret.secretArn, hasField: !!field, grantRead: grantee => secret.grantRead(grantee), }; } /** * Creates a environment variable value from a secret stored in AWS Secrets * Manager. * * @param secret the secret stored in AWS Secrets Manager * @param versionInfo the version information to reference the secret * @param field the name of the field with the value that you want to set as * the environment variable value. Only values in JSON format are supported. * If you do not specify a JSON field, then the full content of the secret is * used. */ public static fromSecretsManagerVersion(secret: secretsmanager.ISecret, versionInfo: SecretVersionInfo, field?: string): Secret { return { arn: `${secret.secretArn}:${field ?? ''}:${versionInfo.versionStage ?? ''}:${versionInfo.versionId ?? ''}`, hasField: !!field, grantRead: grantee => secret.grantRead(grantee), }; } /** * The ARN of the secret */ public abstract readonly arn: string; /** * Whether this secret uses a specific JSON field */ public abstract readonly hasField?: boolean; /** * Grants reading the secret to a principal */ public abstract grantRead(grantee: iam.IGrantable): iam.Grant; } /* * The options for creating a container definition. */ export interface ContainerDefinitionOptions { /** * The image used to start a container. * * This string is passed directly to the Docker daemon. * Images in the Docker Hub registry are available by default. * Other repositories are specified with either repository-url/image:tag or repository-url/image@digest. * TODO: Update these to specify using classes of IContainerImage */ readonly image: ContainerImage; /** * The name of the container. * * @default - id of node associated with ContainerDefinition. */ readonly containerName?: string; /** * The command that is passed to the container. * * If you provide a shell command as a single string, you have to quote command-line arguments. * * @default - CMD value built into container image. */ readonly command?: string[]; /** * A list of ARNs in SSM or Amazon S3 to a credential spec (`CredSpec`) file that configures the container for Active Directory authentication. * * We recommend that you use this parameter instead of the `dockerSecurityOptions`. * * Currently, only one credential spec is allowed per container definition. * * @default - No credential specs. */ readonly credentialSpecs?: CredentialSpec[]; /** * The minimum number of CPU units to reserve for the container. * * @default - No minimum CPU units reserved. */ readonly cpu?: number; /** * Specifies whether networking is disabled within the container. * * When this parameter is true, networking is disabled within the container. * * @default false */ readonly disableNetworking?: boolean; /** * A list of DNS search domains that are presented to the container. * * @default - No search domains. */ readonly dnsSearchDomains?: string[]; /** * A list of DNS servers that are presented to the container. * * @default - Default DNS servers. */ readonly dnsServers?: string[]; /** * A key/value map of labels to add to the container. * * @default - No labels. */ readonly dockerLabels?: { [key: string]: string }; /** * A list of strings to provide custom labels for SELinux and AppArmor multi-level security systems. * * @default - No security labels. */ readonly dockerSecurityOptions?: string[]; /** * The ENTRYPOINT value to pass to the container. * * @see https://docs.docker.com/engine/reference/builder/#entrypoint * * @default - Entry point configured in container. */ readonly entryPoint?: string[]; /** * The environment variables to pass to the container. * * @default - No environment variables. */ readonly environment?: { [key: string]: string }; /** * The environment files to pass to the container. * * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/taskdef-envfiles.html * * @default - No environment files. */ readonly environmentFiles?: EnvironmentFile[]; /** * The secret environment variables to pass to the container. * * @default - No secret environment variables. */ readonly secrets?: { [key: string]: Secret }; /** * Time duration (in seconds) to wait before giving up on resolving dependencies for a container. * * @default - none */ readonly startTimeout?: cdk.Duration; /** * Time duration (in seconds) to wait before the container is forcefully killed if it doesn't exit normally on its own. * * @default - none */ readonly stopTimeout?: cdk.Duration; /** * Specifies whether the container is marked essential. * * If the essential parameter of a container is marked as true, and that container fails * or stops for any reason, all other containers that are part of the task are stopped. * If the essential parameter of a container is marked as false, then its failure does not * affect the rest of the containers in a task. All tasks must have at least one essential container. * * If this parameter is omitted, a container is assumed to be essential. * * @default true */ readonly essential?: boolean; /** * A list of hostnames and IP address mappings to append to the /etc/hosts file on the container. * * @default - No extra hosts. */ readonly extraHosts?: { [name: string]: string }; /** * The health check command and associated configuration parameters for the container. * * @default - Health check configuration from container. */ readonly healthCheck?: HealthCheck; /** * The hostname to use for your container. * * @default - Automatic hostname. */ readonly hostname?: string; /** * When this parameter is true, you can deploy containerized applications that require stdin or a tty to be allocated. * * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-taskdefinition-containerdefinition.html#cfn-ecs-taskdefinition-containerdefinition-interactive * @default - false */ readonly interactive?: boolean; /** * The amount (in MiB) of memory to present to the container. * * If your container attempts to exceed the allocated memory, the container * is terminated. * * At least one of memoryLimitMiB and memoryReservationMiB is required for non-Fargate services. * * @default - No memory limit. */ readonly memoryLimitMiB?: number; /** * The soft limit (in MiB) of memory to reserve for the container. * * When system memory is under heavy contention, Docker attempts to keep the * container memory to this soft limit. However, your container can consume more * memory when it needs to, up to either the hard limit specified with the memory * parameter (if applicable), or all of the available memory on the container * instance, whichever comes first. * * At least one of memoryLimitMiB and memoryReservationMiB is required for non-Fargate services. * * @default - No memory reserved. */ readonly memoryReservationMiB?: number; /** * Specifies whether the container is marked as privileged. * When this parameter is true, the container is given elevated privileges on the host container instance (similar to the root user). * * @default false */ readonly privileged?: boolean; /** * When this parameter is true, the container is given read-only access to its root file system. * * @default false */ readonly readonlyRootFilesystem?: boolean; /** * The user to use inside the container. This parameter maps to User in the Create a container section of the Docker Remote API and the --user option to docker run. * * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#ContainerDefinition-user * @default root */ readonly user?: string; /** * Specifies whether Amazon ECS will resolve the container image tag provided * in the container definition to an image digest. * * If you set the value for a container as disabled, Amazon ECS will * not resolve the provided container image tag to a digest and will use the * original image URI specified in the container definition for deployment. * * @default VersionConsistency.DISABLED if `image` is a CDK asset, VersionConsistency.ENABLED otherwise * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-taskdefinition-containerdefinition.html#cfn-ecs-taskdefinition-containerdefinition-versionconsistency */ readonly versionConsistency?: VersionConsistency; /** * The working directory in which to run commands inside the container. * * @default / */ readonly workingDirectory?: string; /** * The log configuration specification for the container. * * @default - Containers use the same logging driver that the Docker daemon uses. */ readonly logging?: LogDriver; /** * Linux-specific modifications that are applied to the container, such as Linux kernel capabilities. * For more information see [KernelCapabilities](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_KernelCapabilities.html). * * @default - No Linux parameters. */ readonly linuxParameters?: LinuxParameters; /** * The number of GPUs assigned to the container. * * @default - No GPUs assigned. */ readonly gpuCount?: number; /** * The port mappings to add to the container definition. * @default - No ports are mapped. */ readonly portMappings?: PortMapping[]; /** * The inference accelerators referenced by the container. * @default - No inference accelerators assigned. */ readonly inferenceAcceleratorResources?: string[]; /** * A list of namespaced kernel parameters to set in the container. * * @default - No system controls are set. * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-taskdefinition-systemcontrol.html * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#container_definition_systemcontrols */ readonly systemControls?: SystemControl[]; /** * When this parameter is true, a TTY is allocated. This parameter maps to Tty in the "Create a container section" of the * Docker Remote API and the --tty option to `docker run`. * * @default - false * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#container_definition_pseudoterminal */ readonly pseudoTerminal?: boolean; /** * An array of ulimits to set in the container. */ readonly ulimits?: Ulimit[]; /** * Enable a restart policy for a container. * * When you set up a restart policy, Amazon ECS can restart the container without needing to replace the task. * * @default - false unless `restartIgnoredExitCodes` or `restartAttemptPeriod` is set. * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/container-restart-policy.html */ readonly enableRestartPolicy?: boolean; /** * A list of exit codes that Amazon ECS will ignore and not attempt a restart on. * * This property can't be used if `enableRestartPolicy` is set to false. * * You can specify a maximum of 50 container exit codes. * * @default - No exit codes are ignored. */ readonly restartIgnoredExitCodes?: number[]; /** * A period of time that the container must run for before a restart can be attempted. * * A container can be restarted only once every `restartAttemptPeriod` seconds. * If a container isn't able to run for this time period and exits early, it will not be restarted. * * This property can't be used if `enableRestartPolicy` is set to false. * * You can set a minimum `restartAttemptPeriod` of 60 seconds and a maximum `restartAttemptPeriod` * of 1800 seconds. * * @default - Duration.seconds(300) if `enableRestartPolicy` is true, otherwise no period. */ readonly restartAttemptPeriod?: cdk.Duration; } /** * The properties in a container definition. */ export interface ContainerDefinitionProps extends ContainerDefinitionOptions { /** * The name of the task definition that includes this container definition. * * [disable-awslint:ref-via-interface] */ readonly taskDefinition: TaskDefinition; } /** * A container definition is used in a task definition to describe the containers that are launched as part of a task. */ export class ContainerDefinition extends Construct { public static readonly CONTAINER_PORT_USE_RANGE = 0; /** * The Linux-specific modifications that are applied to the container, such as Linux kernel capabilities. */ public readonly linuxParameters?: LinuxParameters; /** * The mount points for data volumes in your container. */ public readonly mountPoints = new Array<MountPoint>(); /** * The list of port mappings for the container. Port mappings allow containers to access ports * on the host container instance to send or receive traffic. */ public readonly portMappings = new Array<PortMapping>(); /** * The data volumes to mount from another container in the same task definition. */ public readonly volumesFrom = new Array<VolumeFrom>(); /** * An array of ulimits to set in the container. */ public readonly ulimits = new Array<Ulimit>(); /** * An array dependencies defined for container startup and shutdown. */ public readonly containerDependencies = new Array<ContainerDependency>(); /** * Specifies whether the container will be marked essential. * * If the essential parameter of a container is marked as true, and that container * fails or stops for any reason, all other containers that are part of the task are * stopped. If the essential parameter of a container is marked as false, then its * failure does not affect the rest of the containers in a task. * * If this parameter is omitted, a container is assumed to be essential. */ public readonly essential: boolean; /** * The name of this container */ public readonly containerName: string; /** * Whether there was at least one memory limit specified in this definition */ public readonly memoryLimitSpecified: boolean; /** * The name of the task definition that includes this container definition. */ public readonly taskDefinition: TaskDefinition; /** * The environment files for this container */ public readonly environmentFiles?: EnvironmentFileConfig[]; /** * The log configuration specification for the container. */ public readonly logDriverConfig?: LogDriverConfig; /** * The crdential specifications for this container. */ public readonly credentialSpecs?: CredentialSpecConfig[]; /** * The name of the image referenced by this container. */ public readonly imageName: string; /** * The number of cpu units reserved for the container. */ public readonly cpu?: number; /** * The inference accelerators referenced by this container. */ private readonly inferenceAcceleratorResources: string[] = []; /** * Specifies whether a TTY must be allocated for this container. */ public readonly pseudoTerminal?: boolean; /** * The configured container links */ private readonly links = new Array<string>(); private readonly imageConfig: ContainerImageConfig; private readonly secrets: CfnTaskDefinition.SecretProperty[] = []; private readonly dockerLabels: { [key: string]: string }; private readonly environment: { [key: string]: string }; private _namedPorts: Map<string, PortMapping>; private versionConsistency?: VersionConsistency; /** * Constructs a new instance of the ContainerDefinition class. */ constructor(scope: Construct, id: string, private readonly props: ContainerDefinitionProps) { super(scope, id); if (props.memoryLimitMiB !== undefined && props.memoryReservationMiB !== undefined) { if (props.memoryLimitMiB < props.memoryReservationMiB) { throw new Error('MemoryLimitMiB should not be less than MemoryReservationMiB.'); } } this.essential = props.essential ?? true; this.taskDefinition = props.taskDefinition; this.memoryLimitSpecified = props.memoryLimitMiB !== undefined || props.memoryReservationMiB !== undefined; this.linuxParameters = props.linuxParameters; this.containerName = props.containerName ?? this.node.id; this.imageConfig = props.image.bind(this, this); this.imageName = this.imageConfig.imageName; this._namedPorts = new Map<string, PortMapping>(); this.versionConsistency = props.versionConsistency; if (props.logging) { this.logDriverConfig = props.logging.bind(this, this); } if (props.secrets) { for (const [name, secret] of Object.entries(props.secrets)) { this.addSecret(name, secret); } } this.dockerLabels = { ...props.dockerLabels }; if (props.environment) { this.environment = { ...props.environment }; } else { this.environment = {}; } if (props.environmentFiles) { this.environmentFiles = []; for (const environmentFile of props.environmentFiles) { this.environmentFiles.push(environmentFile.bind(this)); } } if (props.credentialSpecs) { this.credentialSpecs = []; if (props.credentialSpecs.length > 1) { throw new Error('Only one credential spec is allowed per container definition.'); } for (const credSpec of props.credentialSpecs) { this.credentialSpecs.push(credSpec.bind()); } } if (props.cpu) { this.cpu = props.cpu; } props.taskDefinition._linkContainer(this); if (props.portMappings) { this.addPortMappings(...props.portMappings); } if (props.inferenceAcceleratorResources) { this.addInferenceAcceleratorResource(...props.inferenceAcceleratorResources); } this.pseudoTerminal = props.pseudoTerminal; if (props.ulimits) { this.addUlimits(...props.ulimits); } this.validateRestartPolicy(props.enableRestartPolicy, props.restartIgnoredExitCodes, props.restartAttemptPeriod); } /** * This method adds a link which allows containers to communicate with each other without the need for port mappings. * * This parameter is only supported if the task definition is using the bridge network mode. * Warning: The --link flag is a legacy feature of Docker. It may eventually be removed. */ public addLink(container: ContainerDefinition, alias?: string) { if (this.taskDefinition.networkMode !== NetworkMode.BRIDGE) { throw new Error('You must use network mode Bridge to add container links.'); } if (alias !== undefined) { this.links.push(`${container.containerName}:${alias}`); } else { this.links.push(`${container.containerName}`); } } /** * This method adds one or more mount points for data volumes to the container. */ public addMountPoints(...mountPoints: MountPoint[]) { this.mountPoints.push(...mountPoints); } /** * This method mounts temporary disk space to the container. * * This adds the correct container mountPoint and task definition volume. */ public addScratch(scratch: ScratchSpace) { const mountPoint = { containerPath: scratch.containerPath, readOnly: scratch.readOnly, sourceVolume: scratch.name, }; const volume = { host: { sourcePath: scratch.sourcePath, }, name: scratch.name, }; this.taskDefinition.addVolume(volume); this.addMountPoints(mountPoint); } /** * This method adds one or more port mappings to the container. */ public addPortMappings(...portMappings: PortMapping[]) { this.portMappings.push(...portMappings.map(pm => { const portMap = new PortMap(this.taskDefinition.networkMode, pm); portMap.validate(); const serviceConnect = new ServiceConnect(this.taskDefinition.networkMode, pm); if (serviceConnect.isServiceConnect()) { serviceConnect.validate(); this.setNamedPort(pm); } const sanitizedPM = this.addHostPortIfNeeded(pm); return sanitizedPM; })); } /** * This method adds an environment variable to the container. */ public addEnvironment(name: string, value: string) { this.environment[name] = value; } /** * This method adds a Docker label to the container. */ public addDockerLabel(name: string, value: string) { this.dockerLabels[name] = value; } /** * This method adds a secret as environment variable to the container. */ public addSecret(name: string, secret: Secret) { secret.grantRead(this.taskDefinition.obtainExecutionRole()); this.secrets.push({ name, valueFrom: secret.arn, }); } /** * This method adds one or more resources to the container. */ public addInferenceAcceleratorResource(...inferenceAcceleratorResources: string[]) { this.inferenceAcceleratorResources.push(...inferenceAcceleratorResources.map(resource => { for (const inferenceAccelerator of this.taskDefinition.inferenceAccelerators) { if (resource === inferenceAccelerator.deviceName) { return resource; } } throw new Error(`Resource value ${resource} in container definition doesn't match any inference accelerator device name in the task definition.`); })); } /** * This method adds one or more ulimits to the container. */ public addUlimits(...ulimits: Ulimit[]) { this.ulimits.push(...ulimits); } /** * This method adds one or more container dependencies to the container. */ public addContainerDependencies(...containerDependencies: ContainerDependency[]) { this.containerDependencies.push(...containerDependencies); } /** * This method adds one or more volumes to the container. */ public addVolumesFrom(...volumesFrom: VolumeFrom[]) { this.volumesFrom.push(...volumesFrom); } /** * This method adds the specified statement to the IAM task execution policy in the task definition. */ public addToExecutionPolicy(statement: iam.PolicyStatement) { this.taskDefinition.addToExecutionRolePolicy(statement); } /** * Returns the host port for the requested container port if it exists */ public findPortMapping(containerPort: number, protocol: Protocol): PortMapping | undefined { for (const portMapping of this.portMappings) { const p = portMapping.protocol || Protocol.TCP; const c = portMapping.containerPort; if (c === containerPort && p === protocol) { return portMapping; } } return undefined; } /** * Returns the port mapping with the given name, if it exists. */ public findPortMappingByName(name: string): PortMapping | undefined { return this._namedPorts.get(name); } /** * This method adds an namedPort */ private setNamedPort(pm: PortMapping) :void { if (!pm.name) return; if (this._namedPorts.has(pm.name)) { throw new Error(`Port mapping name '${pm.name}' already exists on this container`); } this._namedPorts.set(pm.name, pm); } /** * This method sets the host port to 0 if the network mode is Bridge and neither * the host port nor the container port range is already set. */ private addHostPortIfNeeded(pm: PortMapping) :PortMapping { if (this.taskDefinition.networkMode !== NetworkMode.BRIDGE || pm.hostPort !== undefined || pm.containerPortRange !== undefined) { return pm; } return { ...pm, hostPort: 0, }; } private validateRestartPolicy(enableRestartPolicy?: boolean, restartIgnoredExitCodes?: number[], restartAttemptPeriod?: cdk.Duration) { if (enableRestartPolicy === false && (restartIgnoredExitCodes !== undefined || restartAttemptPeriod !== undefined)) { throw new Error('The restartIgnoredExitCodes and restartAttemptPeriod cannot be specified if enableRestartPolicy is false'); } if (restartIgnoredExitCodes && restartIgnoredExitCodes.length > 50) { throw new Error(`Only up to 50 can be specified for restartIgnoredExitCodes, got: ${restartIgnoredExitCodes.length}`); } if (restartAttemptPeriod && (restartAttemptPeriod.toSeconds() < 60 || restartAttemptPeriod.toSeconds() > 1800)) { throw new Error(`The restartAttemptPeriod must be between 60 seconds and 1800 seconds, got ${restartAttemptPeriod.toSeconds()} seconds`); } } /** * Whether this container definition references a specific JSON field of a secret * stored in Secrets Manager. */ public get referencesSecretJsonField(): boolean | undefined { for (const secret of this.secrets) { if (secret.valueFrom.endsWith('::')) { return true; } } return false; } /** * The inbound rules associated with the security group the task or service will use. * * This property is only used for tasks that use the awsvpc network mode. */ public get ingressPort(): number { if (this.portMappings.length === 0) { throw new Error(`Container ${this.containerName} hasn't defined any ports. Call addPortMappings().`); } const defaultPortMapping = this.portMappings[0]; if (defaultPortMapping.hostPort !== undefined && defaultPortMapping.hostPort !== 0) { return defaultPortMapping.hostPort; } if (this.taskDefinition.networkMode === NetworkMode.BRIDGE) { return 0; } if (defaultPortMapping.containerPortRange !== undefined) { throw new Error(`The first port mapping of the container ${this.containerName} must expose a single port.`); } return defaultPortMapping.containerPort; } /** * The port the container will listen on. */ public get containerPort(): number { if (this.portMappings.length === 0) { throw new Error(`Container ${this.containerName} hasn't defined any ports. Call addPortMappings().`); } const defaultPortMapping = this.portMappings[0]; if (defaultPortMapping.containerPortRange !== undefined) { throw new Error(`The first port mapping of the container ${this.containerName} must expose a single port.`); } return defaultPortMapping.containerPort; } /** * Allows disabling version consistency if the user did not specify a value. * * Intended for CDK asset images, as asset images are tagged based upon a hash * of image inputs, meaning the image won't change if the tag didn't change, * making version consistency for such containers a waste of time. Literally, * as version consistency can only be achieved by slowing down deployments. * * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-type-ecs.html#deployment-container-image-stability * @internal */ public _defaultDisableVersionConsistency() { if (!this.versionConsistency) { this.versionConsistency = VersionConsistency.DISABLED; } } /** * Render this container definition to a CloudFormation object * * @param _taskDefinition [disable-awslint:ref-via-interface] (unused but kept to avoid breaking change) */ public renderContainerDefinition(_taskDefinition?: TaskDefinition): CfnTaskDefinition.ContainerDefinitionProperty { return { command: this.props.command, credentialSpecs: this.credentialSpecs && this.credentialSpecs.map(renderCredentialSpec), cpu: this.props.cpu, disableNetworking: this.props.disableNetworking, dependsOn: cdk.Lazy.any({ produce: () => this.containerDependencies.map(renderContainerDependency) }, { omitEmptyArray: true }), dnsSearchDomains: this.props.dnsSearchDomains, dnsServers: this.props.dnsServers, dockerLabels: Object.keys(this.dockerLabels).length ? this.dockerLabels : undefined, dockerSecurityOptions: this.props.dockerSecurityOptions, entryPoint: this.props.entryPoint, essential: this.essential, hostname: this.props.hostname, image: this.imageConfig.imageName, interactive: this.props.interactive, memory: this.props.memoryLimitMiB, memoryReservation: this.props.memoryReservationMiB, mountPoints: cdk.Lazy.any({ produce: () => this.mountPoints.map(renderMountPoint) }, { omitEmptyArray: true }), name: this.containerName, portMappings: cdk.Lazy.any({ produce: () => this.portMappings.map(renderPortMapping) }, { omitEmptyArray: true }), privileged: this.props.privileged, pseudoTerminal: this.props.pseudoTerminal, readonlyRootFilesystem: this.props.readonlyRootFilesystem, repositoryCredentials: this.imageConfig.repositoryCredentials, startTimeout: this.props.startTimeout && this.props.startTimeout.toSeconds(), stopTimeout: this.props.stopTimeout && this.props.stopTimeout.toSeconds(), ulimits: cdk.Lazy.any({ produce: () => this.ulimits.map(renderUlimit) }, { omitEmptyArray: true }), user: this.props.user, versionConsistency: this.versionConsistency, volumesFrom: cdk.Lazy.any({ produce: () => this.volumesFrom.map(renderVolumeFrom) }, { omitEmptyArray: true }), workingDirectory: this.props.workingDirectory, logConfiguration: this.logDriverConfig, environment: this.environment && Object.keys(this.environment).length ? renderKV(this.environment, 'name', 'value') : undefined, environmentFiles: this.environmentFiles && renderEnvironmentFiles(cdk.Stack.of(this).partition, this.environmentFiles), secrets: this.secrets.length ? this.secrets : undefined, extraHosts: this.props.extraHosts && renderKV(this.props.extraHosts, 'hostname', 'ipAddress'), healthCheck: this.props.healthCheck && renderHealthCheck(this.props.healthCheck), links: cdk.Lazy.list({ produce: () => this.links }, { omitEmpty: true }), linuxParameters: this.linuxParameters && this.linuxParameters.renderLinuxParameters(), resourceRequirements: (!this.props.gpuCount && this.inferenceAcceleratorResources.length == 0 ) ? undefined : renderResourceRequirements(this.props.gpuCount, this.inferenceAcceleratorResources), systemControls: this.props.systemControls && renderSystemControls(this.props.systemControls), restartPolicy: renderRestartPolicy(this.props.enableRestartPolicy, this.props.restartIgnoredExitCodes, this.props.restartAttemptPeriod), }; } } /** * The health check command and associated configuration parameters for the container. */ export interface HealthCheck { /** * A string array representing the command that the container runs to determine if it is healthy. * The string array must start with CMD to execute the command arguments directly, or * CMD-SHELL to run the command with the container's default shell. * * For example: [ "CMD-SHELL", "curl -f http://localhost/ || exit 1" ] */ readonly command: string[]; /** * The time period in seconds between each health check execution. * * You may specify between 5 and 300 seconds. * * @default Duration.seconds(30) */ readonly interval?: cdk.Duration; /** * The number of times to retry a failed health check before the container is considered unhealthy. * * You may specify between 1 and 10 retries. * * @default 3 */ readonly retries?: number; /** * The optional grace period within which to provide containers time to bootstrap before * failed health checks count towards the maximum number of retries. * * You may specify between 0 and 300 seconds. * * @default No start period */ readonly startPeriod?: cdk.Duration; /** * The time period in seconds to wait for a health check to succeed before it is considered a failure. * * You may specify between 2 and 60 seconds. * * @default Duration.seconds(5) */ readonly timeout?: cdk.Duration; } function renderKV(env: { [key: string]: string }, keyName: string, valueName: string): any[] { const ret = []; for (const [key, value] of Object.entries(env)) { ret.push({ [keyName]: key, [valueName]: value }); } return ret; } function renderEnvironmentFiles(partition: string, environmentFiles: EnvironmentFileConfig[]): any[] { const ret = []; for (const environmentFile of environmentFiles) { const s3Location = environmentFile.s3Location; if (!s3Location) { throw Error('Environment file must specify an S3 location'); } ret.push({ type: environmentFile.fileType, value: `arn:${partition}:s3:::${s3Location.bucketName}/${s3Location.objectKey}`, }); } return ret; } function renderCredentialSpec(credSpec: CredentialSpecConfig): string { if (!credSpec.location) { throw Error('CredentialSpec must specify a valid location or ARN'); } return `${credSpec.typePrefix}:${credSpec.location}`; } function renderHealthCheck(hc: HealthCheck): CfnTaskDefinition.HealthCheckProperty { if (hc.interval?.toSeconds() !== undefined) { if (5 > hc.interval?.toSeconds() || hc.interval?.toSeconds() > 300) { throw new Error('Interval must be between 5 seconds and 300 seconds.'); } } if (hc.timeout?.toSeconds() !== undefined) { if (2 > hc.timeout?.toSeconds() || hc.timeout?.toSeconds() > 120) { throw new Error('Timeout must be between 2 seconds and 120 seconds.'); } } if (hc.interval?.toSeconds() !== undefined && hc.timeout?.toSeconds() !== undefined) { if (hc.interval?.toSeconds() < hc.timeout?.toSeconds()) { throw new Error('Health check interval should be longer than timeout.'); } } return { command: getHealthCheckCommand(hc), interval: hc.interval?.toSeconds() ?? 30, retries: hc.retries ?? 3, startPeriod: hc.startPeriod?.toSeconds(), timeout: hc.timeout?.toSeconds() ?? 5, }; } function getHealthCheckCommand(hc: HealthCheck): string[] { const cmd = hc.command; const hcCommand = new Array<string>(); if (cmd.length === 0) { throw new Error('At least one argument must be supplied for health check command.'); } if (cmd.length === 1) { hcCommand.push('CMD-SHELL', cmd[0]); return hcCommand; } if (cmd[0] !== 'CMD' && cmd[0] !== 'CMD-SHELL') { hcCommand.push('CMD'); } return hcCommand.concat(cmd); } function renderResourceRequirements(gpuCount: number = 0, inferenceAcceleratorResources: string[] = []): CfnTaskDefinition.ResourceRequirementProperty[] | undefined { const ret = []; for (const resource of inferenceAcceleratorResources) { ret.push({ type: 'InferenceAccelerator', value: resource, }); } if (gpuCount > 0) { ret.push({ type: 'GPU', value: gpuCount.toString(), }); } return ret; } /** * The ulimit settings to pass to the container. * * NOTE: Does not work for Windows containers. */ export interface Ulimit { /** * The type of the ulimit. * * For more information, see [UlimitName](https://docs.aws.amazon.com/cdk/api/latest/typescript/api/aws-ecs/ulimitname.html#aws_ecs_UlimitName). */ readonly name: UlimitName; /** * The soft limit for the ulimit type. */ readonly softLimit: number; /** * The hard limit for the ulimit type. */ readonly hardLimit: number; } /** * Type of resource to set a limit on */ export enum UlimitName { CORE = 'core', CPU = 'cpu', DATA = 'data', FSIZE = 'fsize', LOCKS = 'locks', MEMLOCK = 'memlock', MSGQUEUE = 'msgqueue', NICE = 'nice', NOFILE = 'nofile', NPROC = 'nproc', RSS = 'rss', RTPRIO = 'rtprio', RTTIME = 'rttime', SIGPENDING = 'sigpending', STACK = 'stack', } function renderUlimit(ulimit: Ulimit): CfnTaskDefinition.UlimitProperty { return { name: ulimit.name, softLimit: ulimit.softLimit, hardLimit: ulimit.hardLimit, }; } /** * The details of a dependency on another container in the task definition. * * @see https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_ContainerDependency.html */ export interface ContainerDependency { /** * The container to depend on. */ readonly container: ContainerDefinition; /** * The state the container needs to be in to satisfy the dependency and proceed with startup. * Valid values are ContainerDependencyCondition.START, ContainerDependencyCondition.COMPLETE, * ContainerDependencyCondition.SUCCESS and ContainerDependencyCondition.HEALTHY. * * @default ContainerDependencyCondition.HEALTHY */ readonly condition?: ContainerDependencyCondition; } export enum ContainerDependencyCondition { /** * This condition emulates the behavior of links and volumes today. * It validates that a dependent container is started before permitting other containers to start. */ START = 'START', /** * This condition validates that a dependent container runs to completion (exits) before permitting other containers to start. * This can be useful for nonessential containers that run a script and then exit. */ COMPLETE = 'COMPLETE', /** * This condition is the same as COMPLETE, but it also requires that the container exits with a zero status. */ SUCCESS = 'SUCCESS', /** * This condition validates that the dependent container passes its Docker health check before permitting other containers to start. * This requires that the dependent container has health checks configured. This condition is confirmed only at task startup. */ HEALTHY = 'HEALTHY', } function renderContainerDependency(containerDependency: ContainerDependency): CfnTaskDefinition.ContainerDependencyProperty { return { containerName: containerDependency.container.containerName, condition: containerDependency.condition || ContainerDependencyCondition.HEALTHY, }; } /** * Port mappings allow containers to access ports on the host container instance to send or receive traffic. */ export interface PortMapping { /** * The port number on the container that is bound to the user-specified or automatically assigned host port. * * If you are using containers in a task with the awsvpc or host network mode, exposed ports should be specified using containerPort. * If you are using containers in a task with the bridge network mode and you specify a container port and not a host port, * your container automatically receives a host port in the ephemeral port range. * * For more information, see hostPort. * Port mappings that are automatically assigned in this way do not count toward the 100 reserved ports limit of a container instance. * * If you want to expose a port range, you must specify `CONTAINER_PORT_USE_RANGE` as container port. */ readonly containerPort: number; /** * The port number range on the container that's bound to the dynamically mapped host port range. * * The following rules apply when you specify a `containerPortRange`: * * - You must specify `CONTAINER_PORT_USE_RANGE` as `containerPort` * - You must use either the `bridge` network mode or the `awsvpc` network mode. * - The container instance must have at least version 1.67.0 of the container agent and at least version 1.67.0-1 of the `ecs-init` package * - You can specify a maximum of 100 port ranges per container. * - A port can only be included in one port mapping per container. * - You cannot specify overlapping port ranges. * - The first port in the range must be less than last port in the range. * * If you want to expose a single port, you must not set a range. */ readonly containerPortRange?: string; /** * The port number on the container instance to reserve for your container. * * If you are using containers in a task with the awsvpc or host network mode, * the hostPort can either be left blank or set to the same value as the containerPort. * * If you are using containers in a task with the bridge network mode, * you can specify a non-reserved host port for your container port mapping, or * you can omit the hostPort (or set it to 0) while specifying a containerPort and * your container automatically receives a port in the ephemeral port range for * your container instance operating system and Docker version. */ readonly hostPort?: number; /** * The protocol used for the port mapping. Valid values are Protocol.TCP and Protocol.UDP. * * @default TCP */ readonly protocol?: Protocol; /** * The name to give the port mapping. * * Name is required in order to use the port mapping with ECS Service Connect. * This field may only be set when the task definition uses Bridge or Awsvpc network modes. * * @default - no port mapping name */ readonly name?: string; /** * The protocol used by Service Connect. Valid values are AppProtocol.http, AppProtocol.http2, and * AppProtocol.grpc. The protocol determines what telemetry will be shown in the ECS Console for * Service Connect services using this port mapping. * * This field may only be set when the task definition uses Bridge or Awsvpc network modes. * * @default - no app protocol */ readonly appProtocol?: AppProtocol; } /** * PortMap ValueObjectClass having by ContainerDefinition */ export class PortMap { /** * The networking mode to use for the containers in the task. */ readonly networkmode: NetworkMode; /** * Port mappings allow containers to access ports on the host container instance to send or receive traffic. */ readonly portmapping: PortMapping; constructor(networkmode: NetworkMode, pm: PortMapping) { this.networkmode = networkmode; this.portmapping = pm; } /** * validate invalid portmapping and networkmode parameters. * throw Error when invalid parameters. */ public validate(): void { if (!this.isvalidPortName()) { throw new Error('Port mapping name cannot be an empty string.'); } if (this.portmapping.containerPort === ContainerDefinition.CONTAINER_PORT_USE_RANGE && this.portmapping.containerPortRange === undefined) { throw new Error(`The containerPortRange must be set when containerPort is equal to ${ContainerDefinition.CONTAINER_PORT_USE_RANGE}`); } if (this.portmapping.containerPort !== ContainerDefinition.CONTAINER_PORT_USE_RANGE && this.portmapping.containerPortRange !== undefined) { throw new Error('Cannot set "containerPort" and "containerPortRange" at the same time.'); } if (this.portmapping.containerPort !== ContainerDefinition.CONTAINER_PORT_USE_RANGE) { if ((this.networkmode === NetworkMode.AWS_VPC || this.networkmode === NetworkMode.HOST) && this.portmapping.hostPort !== undefined && this.portmapping.hostPort !== this.portmapping.containerPort) { throw new Error('The host port must be left out or must be the same as the container port for AwsVpc or Host network mode.'); } } if (this.portmapping.containerPortRange !== undefined) { if (cdk.Token.isUnresolved(this.portmapping.containerPortRange)) { throw new Error('The value of containerPortRange must be concrete (no Tokens)'); } if (this.portmapping.hostPort !== undefined) { throw new Error('Cannot set "hostPort" while using a port range for the container.'); } if (this.networkmode !== NetworkMode.BRIDGE && this.networkmode !== NetworkMode.AWS_VPC) { throw new Error('Either AwsVpc or Bridge network mode is required to set a port range for the container.'); } if (!/^\d+-\d+$/.test(this.portmapping.containerPortRange)) { throw new Error('The containerPortRange must be a string in the format [start port]-[end port].'); } } } private isvalidPortName(): boolean { if (this.portmapping.name === '') { return false; } return true; } } /** * ServiceConnect ValueObjectClass having by ContainerDefinition */ export class ServiceConnect { /** * Port mappings allow containers to access ports on the host container instance to send or receive traffic. */ readonly portmapping: PortMapping; /** * The networking mode to use for the containers in the task. */ readonly networkmode: NetworkMode; constructor(networkmode: NetworkMode, pm: PortMapping) { this.portmapping = pm; this.networkmode = networkmode; } /** * Judge parameters can be serviceconnect logick. * If parameters can be serviceConnect return true. */ public isServiceConnect() :boolean { const hasPortname = this.portmapping.name; const hasAppProtcol = this.portmapping.appProtocol; if (hasPortname || hasAppProtcol) return true; return false; } /** * Judge serviceconnect parametes are valid. * If invalid, throw Error. */ public validate() :void { if (!this.isValidNetworkmode()) { throw new Error(`Service connect related port mapping fields 'name' and 'appProtocol' are not supported for network mode ${this.networkmode}`); } if (!this.isValidPortName()) { throw new Error('Service connect-related port mapping field \'appProtocol\' cannot be set without \'name\''); } } private isValidNetworkmode() :boolean { const isAwsVpcMode = this.networkmode == NetworkMode.AWS_VPC; const isBridgeMode = this.networkmode == NetworkMode.BRIDGE; if (isAwsVpcMode || isBridgeMode) return true; return false; } private isValidPortName() :boolean { if (!this.portmapping.name) return false; return true; } } /** * Network protocol */ export enum Protocol { /** * TCP */ TCP = 'tcp', /** * UDP */ UDP = 'udp', } /** * Service connect app protocol. */ export class AppProtocol { /** * HTTP app protocol. */ public static http = new AppProtocol('http'); /** * HTTP2 app protocol. */ public static http2 = new AppProtocol('http2'); /** * GRPC app protocol. */ public static grpc = new AppProtocol('grpc'); /** * Custom value. */ public readonly value: string; protected constructor(value: string) { this.value = value; } } function renderPortMapping(pm: PortMapping): CfnTaskDefinition.PortMappingProperty { return { containerPort: pm.containerPort !== ContainerDefinition.CONTAINER_PORT_USE_RANGE ? pm.containerPort : undefined, containerPortRange: pm.containerPortRange, hostPort: pm.hostPort, protocol: pm.protocol || Protocol.TCP, appProtocol: pm.appProtocol?.value, name: pm.name ? pm.name : undefined, }; } /** * The temporary disk space mounted to the container. */ export interface ScratchSpace { /** * The path on the container to mount the scratch volume at. */ readonly containerPath: string; /** * Specifies whether to give the container read-only access to the scratch volume. * * If this value is true, the container has read-only access to the scratch volume. * If this value is false, then the container can write to the scratch volume. */ readonly readOnly: boolean; readonly sourcePath: string; /** * The name of the scratch volume to mount. Must be a volume name referenced in the name parameter of task definition volume. */ readonly name: string; } /** * The base details of where a volume will be mounted within a container */ export interface BaseMountPoint { /** * The path on the container to mount the host volume at. */ readonly containerPath: string; /** * Specifies whether to give the container read-only access to the volume. * * If this value is true, the container has read-only access to the volume. * If this value is false, then the container can write to the volume. */ readonly readOnly: boolean; } /** * The details of data volume mount points for a container. */ export interface MountPoint extends BaseMountPoint { /** * The name of the volume to mount. * * Must be a volume name referenced in the name parameter of task definition volume. */ readonly sourceVolume: string; } function renderMountPoint(mp: MountPoint): CfnTaskDefinition.MountPointProperty { return { containerPath: mp.containerPath, readOnly: mp.readOnly, sourceVolume: mp.sourceVolume, }; } /** * State of the container version consistency feature. * * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-taskdefinition-containerdefinition.html#cfn-ecs-taskdefinition-containerdefinition-versionconsistency */ export enum VersionConsistency { /** * The version consistency feature is enabled for this container. */ ENABLED = 'enabled', /** * The version consistency feature is disabled for this container. */ DISABLED = 'disabled', } /** * The details on a data volume from another container in the same task definition. */ export interface VolumeFrom { /** * The name of another container within the same task definition from which to mount volumes. */ readonly sourceContainer: string; /** * Specifies whether the container has read-only access to the volume. * * If this value is true, the container has read-only access to the volume. * If this value is false, then the container can write to the volume. */ readonly readOnly: boolean; } function renderVolumeFrom(vf: VolumeFrom): CfnTaskDefinition.VolumeFromProperty { return { sourceContainer: vf.sourceContainer, readOnly: vf.readOnly, }; } /** * Kernel parameters to set in the container */ export interface SystemControl { /** * The namespaced kernel parameter for which to set a value. */ readonly namespace: string; /** * The value for the namespaced kernel parameter specified in namespace. */ readonly value: string; } function renderSystemControls(systemControls: SystemControl[]): CfnTaskDefinition.SystemControlProperty[] { return systemControls.map(sc => ({ namespace: sc.namespace, value: sc.value, })); } function renderRestartPolicy( enableRestartPolicy?: boolean, restartIgnoredExitCodes?: number[], restartAttemptPeriod?: cdk.Duration, ): CfnTaskDefinition.RestartPolicyProperty | undefined { if (enableRestartPolicy === undefined && restartIgnoredExitCodes === undefined && restartAttemptPeriod === undefined) { return; } return { // If `enableRestartPolicy` is undefined, we know that `restartIgnoredExitCodes` or restartAttemptPeriod is specified // according to the above branch, so we treat `enableRestartPolicy` as true. // The `validateRestartPolicy` function also ensures that `enableRestartPolicy` is not false if `restartIgnoredExitCodes` // or `restartAttemptPeriod` is specified, so there is no conflict. enabled: enableRestartPolicy ?? true, ignoredExitCodes: restartIgnoredExitCodes, // always undefined if `enabled` is false restartAttemptPeriod: restartAttemptPeriod?.toSeconds(), // always undefined if `enabled` is false }; }