in packages/aws-rfdk/lib/deadline/lib/render-queue.ts [383:652]
constructor(scope: Construct, id: string, private readonly props: RenderQueueProps) {
super(scope, id);
this.repository = props.repository;
this.renderQueueSize = props?.renderQueueSize ?? {min: 1, max: 1};
if (props.version.isLessThan(RenderQueue.MINIMUM_LOAD_BALANCING_VERSION)) {
// Deadline versions earlier than 10.1.10 do not support horizontal scaling behind a load-balancer, so we limit to at most one instance
if ((this.renderQueueSize.min ?? 0) > 1) {
throw new Error(`renderQueueSize.min for Deadline version less than ${RenderQueue.MINIMUM_LOAD_BALANCING_VERSION.toString()} cannot be greater than 1 - got ${this.renderQueueSize.min}`);
}
if ((this.renderQueueSize.desired ?? 0) > 1) {
throw new Error(`renderQueueSize.desired for Deadline version less than ${RenderQueue.MINIMUM_LOAD_BALANCING_VERSION.toString()} cannot be greater than 1 - got ${this.renderQueueSize.desired}`);
}
if ((this.renderQueueSize.max ?? 0) > 1) {
throw new Error(`renderQueueSize.max for Deadline version less than ${RenderQueue.MINIMUM_LOAD_BALANCING_VERSION.toString()} cannot be greater than 1 - got ${this.renderQueueSize.max}`);
}
}
this.version = props?.version;
const externalProtocol = props.trafficEncryption?.externalTLS?.enabled === false ? ApplicationProtocol.HTTP : ApplicationProtocol.HTTPS;
let loadBalancerFQDN: string | undefined;
let domainZone: IHostedZone | undefined;
if ( externalProtocol === ApplicationProtocol.HTTPS ) {
const tlsInfo = this.getOrCreateTlsInfo(props);
this.certChain = tlsInfo.certChain;
this.clientCert = tlsInfo.serverCert;
loadBalancerFQDN = tlsInfo.fullyQualifiedDomainName;
domainZone = tlsInfo.domainZone;
} else {
if (props.hostname) {
loadBalancerFQDN = this.generateFullyQualifiedDomainName(props.hostname.zone, props.hostname.hostname);
domainZone = props.hostname.zone;
}
}
this.version = props.version;
const internalProtocol = props.trafficEncryption?.internalProtocol ?? ApplicationProtocol.HTTPS;
this.cluster = new Cluster(this, 'Cluster', {
vpc: props.vpc,
});
const minCapacity = props.renderQueueSize?.min ?? 1;
if (minCapacity < 1) {
throw new Error(`renderQueueSize.min capacity must be at least 1: got ${minCapacity}`);
}
const maxCapacity = this.renderQueueSize.max ?? this.renderQueueSize?.desired;
if (this.renderQueueSize?.desired && maxCapacity && this.renderQueueSize?.desired > maxCapacity) {
throw new Error(`renderQueueSize.desired capacity cannot be more than ${maxCapacity}: got ${this.renderQueueSize.desired}`);
}
this.asg = this.cluster.addCapacity('RCS Capacity', {
vpcSubnets: props.vpcSubnets ?? RenderQueue.DEFAULT_VPC_SUBNETS_OTHER,
instanceType: props.instanceType ?? new InstanceType('c5.large'),
minCapacity,
desiredCapacity: this.renderQueueSize?.desired,
maxCapacity,
blockDevices: [{
deviceName: '/dev/xvda',
// See: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-ami-storage-config.html
// We want the volume to be encrypted. The default AMI size is 30-GiB.
volume: BlockDeviceVolume.ebs(30, { encrypted: true }),
}],
...{ updateType: undefined }, // Workaround -- See: https://github.com/aws/aws-cdk/issues/11581
updatePolicy: UpdatePolicy.rollingUpdate(),
// addCapacity doesn't specifically take a securityGroup, but it passes on its properties to the ASG it creates,
// so this security group will get applied there
// @ts-ignore
securityGroup: props.securityGroups?.backend,
machineImage: EcsOptimizedImage.amazonLinux2023(),
});
this.backendConnections = this.asg.connections;
/**
* The ECS-optimized AMI that is defaulted to when adding capacity to a cluster does not include the awscli or unzip
* packages as is the case with the standard Amazon Linux AMI. These are required by RFDK scripts to configure the
* direct connection on the host container instances.
*/
this.asg.userData.addCommands(
'yum install -yq awscli unzip',
);
if (props.enableLocalFileCaching ?? false) {
// Has to be done before any filesystems mount.
this.enableFilecaching(this.asg);
}
const externalPortNumber = RenderQueue.RCS_PROTO_PORTS[externalProtocol];
const internalPortNumber = RenderQueue.RCS_PROTO_PORTS[internalProtocol];
this.logGroup = LogGroupFactory.createOrFetch(this, 'LogGroupWrapper', id, {
logGroupPrefix: '/renderfarm/',
...props.logGroupProps,
});
this.logGroup.grantWrite(this.asg);
if (props.repository.secretsManagementSettings.enabled) {
const errors = [];
if (props.version.isLessThan(Version.MINIMUM_SECRETS_MANAGEMENT_VERSION)) {
errors.push(`The supplied Deadline version (${props.version.versionString}) does not support Deadline Secrets Management in RFDK. Either upgrade Deadline to the minimum required version (${Version.MINIMUM_SECRETS_MANAGEMENT_VERSION.versionString}) or disable the feature in the Repository's construct properties.`);
}
if (props.repository.secretsManagementSettings.credentials === undefined) {
errors.push('The Repository does not have Secrets Management credentials');
}
if (internalProtocol !== ApplicationProtocol.HTTPS) {
errors.push('The internal protocol on the Render Queue is not HTTPS.');
}
if (externalProtocol !== ApplicationProtocol.HTTPS) {
errors.push('External TLS on the Render Queue is not enabled.');
}
if (errors.length > 0) {
throw new Error(`Deadline Secrets Management is enabled on the supplied Repository but cannot be enabled on the Render Queue for the following reasons:\n${errors.join('\n')}`);
}
}
const taskDefinition = this.createTaskDefinition({
image: props.images.remoteConnectionServer,
portNumber: internalPortNumber,
protocol: internalProtocol,
repository: props.repository,
runAsUser: RenderQueue.RCS_USER,
secretsManagementOptions: props.repository.secretsManagementSettings.enabled ? {
credentials: props.repository.secretsManagementSettings.credentials!,
posixUsername: RenderQueue.RCS_USER.username,
} : undefined,
});
this.taskDefinition = taskDefinition;
// The fully-qualified domain name to use for the ALB
const loadBalancer = new ApplicationLoadBalancer(this, 'LB', {
vpc: this.cluster.vpc,
vpcSubnets: props.vpcSubnetsAlb ?? RenderQueue.DEFAULT_VPC_SUBNETS_ALB,
internetFacing: false,
deletionProtection: props.deletionProtection ?? true,
securityGroup: props.securityGroups?.frontend,
});
this.pattern = new ApplicationLoadBalancedEc2Service(this, 'AlbEc2ServicePattern', {
certificate: this.clientCert,
cluster: this.cluster,
desiredCount: this.renderQueueSize?.desired ?? minCapacity,
domainZone,
domainName: loadBalancerFQDN,
listenerPort: externalPortNumber,
loadBalancer,
protocol: externalProtocol,
taskDefinition,
// This is required to right-size our host capacity and not have the ECS service block on updates. We set a memory
// reservation, but no memory limit on the container. This allows the container's memory usage to grow unbounded.
// We want 1:1 container to container instances to not over-spend, but this comes at the price of down-time during
// cloudformation updates.
minHealthyPercent: 0,
maxHealthyPercent: 100,
// This is required to ensure that the ALB listener's security group does not allow any ingress by default.
openListener: false,
});
// An explicit dependency is required from the Service to the Client certificate
// Otherwise cloud formation will try to remove the cert before the ALB using it is disposed.
if (this.clientCert) {
this.pattern.node.addDependency(this.clientCert);
}
// An explicit dependency is required from the service to the ASG providing its capacity.
// See: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-dependson.html
this.pattern.service.node.addDependency(this.asg);
this.loadBalancer = this.pattern.loadBalancer;
// Enabling dropping of invalid HTTP header fields on the load balancer to prevent http smuggling attacks.
this.loadBalancer.setAttribute('routing.http.drop_invalid_header_fields.enabled', 'true');
if (props.accessLogs) {
const accessLogsBucket = props.accessLogs.destinationBucket;
// Policies are applied according to
// https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-access-logs.html
accessLogsBucket.addToResourcePolicy( new PolicyStatement({
actions: ['s3:PutObject'],
principals: [new ServicePrincipal('delivery.logs.amazonaws.com')],
resources: [`${accessLogsBucket.bucketArn}/*`],
conditions: {
StringEquals: {
's3:x-amz-acl': 'bucket-owner-full-control',
},
},
}));
accessLogsBucket.addToResourcePolicy(new PolicyStatement({
actions: [ 's3:GetBucketAcl' ],
principals: [ new ServicePrincipal('delivery.logs.amazonaws.com')],
resources: [ accessLogsBucket.bucketArn ],
}));
this.loadBalancer.logAccessLogs(
accessLogsBucket,
props.accessLogs.prefix);
}
// Ensure tasks are run on separate container instances
this.pattern.service.addPlacementConstraints(PlacementConstraint.distinctInstances());
/**
* Uses an escape-hatch to set the target group protocol to HTTPS. We cannot configure server certificate
* validation, but at least traffic is encrypted and terminated at the application layer.
*/
const listener = this.loadBalancer.node.findChild('PublicListener');
this.listener = listener as ApplicationListener;
const targetGroup = listener.node.findChild('ECSGroup') as ApplicationTargetGroup;
const targetGroupResource = targetGroup.node.defaultChild as CfnTargetGroup;
targetGroupResource.protocol = ApplicationProtocol[internalProtocol];
targetGroupResource.port = internalPortNumber;
this.grantPrincipal = taskDefinition.taskRole;
this.connections = new Connections({
defaultPort: Port.tcp(externalPortNumber),
securityGroups: this.pattern.loadBalancer.connections.securityGroups,
});
this.endpoint = new ConnectableApplicationEndpoint({
address: loadBalancerFQDN ?? this.pattern.loadBalancer.loadBalancerDnsName,
port: externalPortNumber,
connections: this.connections,
protocol: externalProtocol,
});
if ( externalProtocol === ApplicationProtocol.HTTP ) {
this.rqConnection = RenderQueueConnection.forHttp({
endpoint: this.endpoint,
});
} else {
this.rqConnection = RenderQueueConnection.forHttps({
endpoint: this.endpoint,
caCert: this.certChain!,
});
}
props.repository.secretsManagementSettings.credentials?.grantRead(this);
this.ecsServiceStabilized = new WaitForStableService(this, 'WaitForStableService', {
service: this.pattern.service,
});
this.node.defaultChild = taskDefinition;
// Tag deployed resources with RFDK meta-data
tagConstruct(this);
const thisConstruct = this;
this.node.addValidation({
validate(): string[] {
const validationErrors = [];
// Using the output of VersionQuery across stacks can cause issues. CloudFormation stack outputs cannot change if
// a resource in another stack is referencing it.
if (thisConstruct.version instanceof VersionQuery) {
const versionStack = Stack.of(thisConstruct.version);
const thisStack = Stack.of(thisConstruct);
if (versionStack != thisStack) {
validationErrors.push('A VersionQuery can not be supplied from a different stack');
}
}
return validationErrors;
},
});
}