cf-custom-resources/lib/desired-count-delegation.js (132 lines of code) (raw):

// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 "use strict"; const { ECS, DescribeServicesCommand } = require('@aws-sdk/client-ecs'); const { ResourceGroupsTaggingAPI, GetResourcesCommand } = require('@aws-sdk/client-resource-groups-tagging-api'); // These are used for test purposes only let defaultResponseURL; /** * Upload a CloudFormation response object to S3. * * @param {object} event the Lambda event payload received by the handler function * @param {object} context the Lambda context received by the handler function * @param {string} responseStatus the response status, either 'SUCCESS' or 'FAILED' * @param {string} physicalResourceId CloudFormation physical resource ID * @param {object} [responseData] arbitrary response data object * @param {string} [reason] reason for failure, if any, to convey to the user * @returns {Promise} Promise that is resolved on success, or rejected on connection error or HTTP error response */ let report = function ( event, context, responseStatus, physicalResourceId, responseData, reason ) { return new Promise((resolve, reject) => { const https = require("https"); const { URL } = require("url"); var responseBody = JSON.stringify({ Status: responseStatus, Reason: reason, PhysicalResourceId: physicalResourceId || context.logStreamName, StackId: event.StackId, RequestId: event.RequestId, LogicalResourceId: event.LogicalResourceId, Data: responseData, }); const parsedUrl = new URL(event.ResponseURL || defaultResponseURL); const options = { hostname: parsedUrl.hostname, port: 443, path: parsedUrl.pathname + parsedUrl.search, method: "PUT", headers: { "Content-Type": "", "Content-Length": responseBody.length, }, }; https .request(options) .on("error", reject) .on("response", (res) => { res.resume(); if (res.statusCode >= 400) { reject(new Error(`Error ${res.statusCode}: ${res.statusMessage}`)); } else { resolve(); } }) .end(responseBody, "utf8"); }); }; /** * Get the current running task number for a specific task definition. * * @param {string} defaultDesiredCount Default desired count number. * @param {string} cluster Name of the ECS cluster. * @param {string} app Name of the copilot application. * @param {string} env Name of the copilot environment. * @param {string} svc Name of the copilot service. * * @returns {number} The running task number. */ const getRunningTaskCount = async function ( defaultDesiredCount, cluster, app, env, svc ) { var resourcegroupstaggingapi = new ResourceGroupsTaggingAPI(); const rgResp = await resourcegroupstaggingapi .send(new GetResourcesCommand({ ResourceTypeFilters: ["ecs:service"], TagFilters: [ { Key: "copilot-application", Values: [app], }, { Key: "copilot-environment", Values: [env], }, { Key: "copilot-service", Values: [svc], }, ], })); const resources = rgResp.ResourceTagMappingList; if (resources.length !== 1) { return defaultDesiredCount; } const serviceARN = resources[0].ResourceARN; var ecs = new ECS(); const resp = await ecs .send(new DescribeServicesCommand({ cluster: cluster, services: [serviceARN], })); if (resp.services.length !== 1) { return defaultDesiredCount; } return resp.services[0].desiredCount; }; /** * Correct desired count handler, invoked by Lambda. */ exports.handler = async function (event, context) { var responseData = {}; const props = event.ResourceProperties; const physicalResourceId = event.PhysicalResourceId || `copilot/apps/${props.App}/envs/${props.Env}/services/${props.Svc}/autoscaling`; try { switch (event.RequestType) { case "Create": responseData.DesiredCount = await getRunningTaskCount( props.DefaultDesiredCount, props.Cluster, props.App, props.Env, props.Svc ); break; case "Update": responseData.DesiredCount = await getRunningTaskCount( props.DefaultDesiredCount, props.Cluster, props.App, props.Env, props.Svc ); break; case "Delete": break; default: throw new Error(`Unsupported request type ${event.RequestType}`); } await report(event, context, "SUCCESS", physicalResourceId, responseData); } catch (err) { // If it fails, just set to be desired count and return. responseData.DesiredCount = props.DefaultDesiredCount; console.log( `Caught error ${err}. Set back desired count to ${responseData.DesiredCount}` ); await report(event, context, "SUCCESS", physicalResourceId, responseData); } }; /** * @private */ exports.withDefaultResponseURL = function (url) { defaultResponseURL = url; };