Skip to content

Commit

Permalink
feat: adds code for showing service event info when using ecs deploy …
Browse files Browse the repository at this point in the history
…controller
  • Loading branch information
felipem1210 committed Feb 6, 2023
1 parent df96430 commit 8b66948
Show file tree
Hide file tree
Showing 6 changed files with 352 additions and 14 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ The minimal permissions require access to CodeDeploy:

This action emits debug logs to help troubleshoot deployment failures. To see the debug logs, create a secret named `ACTIONS_STEP_DEBUG` with value `true` in your repository.

The input `show-service-events` helps you to check logs from the service deployment events without going to the AWS console. This is just for the `ECS deployment controller`. Is desirable to configure [deployment circuit breaker](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-circuit-breaker.html) to get a 'FAILED' rolloutState.

## License Summary

This code is made available under the MIT license.
Expand Down
6 changes: 6 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ inputs:
cluster:
description: "The name of the ECS service's cluster. Will default to the 'default' cluster"
required: false
show-service-events:
description: "Whether to see or not the service deployment events when deployment rolloutState is 'FAILED'. Useful to see errors when a deployment fails."
required: false
show-service-events-frequency:
description: "The frequency for showing a log line of the service events (default: 15 seconds)."
required: false
wait-for-service-stability:
description: 'Whether to wait for the ECS service to reach stable state after deploying the new task definition. Valid value is "true". Will default to not waiting.'
required: false
Expand Down
88 changes: 83 additions & 5 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const IGNORED_TASK_DEFINITION_ATTRIBUTES = [
];

// Deploy to a service that uses the 'ECS' deployment controller
async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment) {
async function updateEcsService(ecs, clusterName, service, taskDefArn, showServiceEvents, showServiceEventsFrequency, waitForService, waitForMinutes, forceNewDeployment) {
core.debug('Updating the service');
await ecs.updateService({
cluster: clusterName,
Expand All @@ -36,9 +36,60 @@ async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForSe
forceNewDeployment: forceNewDeployment
}).promise();

const consoleHostname = aws.config.region.startsWith('cn') ? 'console.amazonaws.cn' : 'console.aws.amazon.com';
// Create a while loop to print the events of the service if deployment rollout state is failed
// or if there are failed tasks but rollout state is still in progress
if (showServiceEvents && showServiceEvents.toLowerCase() === 'true') {
core.debug(`Deployment started. The option show-service-events is set to true. Showing logs each ${showServiceEventsFrequency} seconds.`);
const initialState = 'IN_PROGRESS';
let describeResponse = await ecs.describeServices({
services: [service],
cluster: clusterName
}).promise();
const deployTime = describeResponse.services[0].events[0].createdAt
let newEvents = [];

while (initialState == 'IN_PROGRESS') {
const showServiceEventsFrequencyMilisec = (showServiceEventsFrequency * 1000)
await delay(showServiceEventsFrequencyMilisec);
let describeResponse = await ecs.describeServices({
services: [service],
cluster: clusterName
}).promise();

core.info(`Deployment started. Watch this deployment's progress in the Amazon ECS console: https://${consoleHostname}/ecs/home?region=${aws.config.region}#/clusters/${clusterName}/services/${service}/events`);
let serviceResponse = describeResponse.services[0];
let rolloutState = serviceResponse.deployments[0].rolloutState;
let rolloutStateReason = serviceResponse.deployments[0].rolloutStateReason;
let failedTasksCount = serviceResponse.deployments[0].failedTasks;
let indexEventContainDeployDate = getPosition(deployTime.toString(), serviceResponse.events);
newEvents = serviceResponse.events.slice(0, indexEventContainDeployDate);

if (rolloutState == 'COMPLETED') {
printEvents(newEvents.reverse());
break;
} else if (rolloutState == 'FAILED') {
printEvents(newEvents.reverse());
throw new Error(`Rollout state is ${rolloutState}. Reason: ${rolloutStateReason}.`);
} else if (rolloutState == 'IN_PROGRESS' && failedTasksCount > 0) {
printEvents(newEvents.reverse());
let tasksList = await ecs.listTasks({
serviceName: service,
cluster: clusterName,
desiredStatus: 'STOPPED'
}).promise();
let describeTaskResponse = await ecs.describeTasks({
tasks: [tasksList.taskArns[0]],
cluster: clusterName,
}).promise();
let stopCode = describeTaskResponse.tasks[0].stopCode;
let stoppedReason = describeTaskResponse.tasks[0].stoppedReason;
core.info(`Task status: ${stopCode}. The reason is: ${stoppedReason}.`);
throw new Error(`There are failed tasks. This means the deployment didn't go well. Please check the logs of task, service or container for more information.`);
}
}
} else {
const consoleHostname = aws.config.region.startsWith('cn') ? 'console.amazonaws.cn' : 'console.aws.amazon.com';
core.info(`Deployment started. Watch this deployment's progress in the Amazon ECS console: https://${consoleHostname}/ecs/home?region=${aws.config.region}#/clusters/${clusterName}/services/${service}/events`);
}

// Wait for service stability
if (waitForService && waitForService.toLowerCase() === 'true') {
Expand All @@ -57,6 +108,29 @@ async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForSe
}
}

// Function to print the events of the service
function printEvents(events) {
core.debug('Showing the service events:')
for (let i = 0; i < events.length; i++) {
core.info(events[i].createdAt.toString().split('(')[0]);
core.info(events[i].message);
}
}

// Function to get the position of an element in an array
function getPosition(elementToFind, arrayElements) {
for (let i = 0; i < arrayElements.length; i += 1) {
if (arrayElements[i].createdAt.toString().includes(elementToFind)) {
return i;
}
}
}

// Fuction to create a delay
function delay(time) {
return new Promise(resolve => setTimeout(resolve, time));
}

// Find value in a CodeDeploy AppSpec file with a case-insensitive key
function findAppSpecValue(obj, keyName) {
return obj[findAppSpecKey(obj, keyName)];
Expand Down Expand Up @@ -266,7 +340,8 @@ async function run() {
const taskDefinitionFile = core.getInput('task-definition', { required: true });
const service = core.getInput('service', { required: false });
const cluster = core.getInput('cluster', { required: false });
const waitForService = core.getInput('wait-for-service-stability', { required: false });
const waitForService = core.getInput('wait-for-service-stability', { required: false });

let waitForMinutes = parseInt(core.getInput('wait-for-minutes', { required: false })) || 30;
if (waitForMinutes > MAX_WAIT_MINUTES) {
waitForMinutes = MAX_WAIT_MINUTES;
Expand All @@ -275,6 +350,9 @@ async function run() {
const forceNewDeployInput = core.getInput('force-new-deployment', { required: false }) || 'false';
const forceNewDeployment = forceNewDeployInput.toLowerCase() === 'true';

const showServiceEvents = core.getInput('show-service-events', { required: false });
let showServiceEventsFrequency = core.getInput('show-service-events-frequency', { required: false }) || 15;

// Register the task definition
core.debug('Registering the task definition');
const taskDefPath = path.isAbsolute(taskDefinitionFile) ?
Expand Down Expand Up @@ -316,7 +394,7 @@ async function run() {

if (!serviceResponse.deploymentController || !serviceResponse.deploymentController.type || serviceResponse.deploymentController.type === 'ECS') {
// Service uses the 'ECS' deployment controller, so we can call UpdateService
await updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment);
await updateEcsService(ecs, clusterName, service, taskDefArn, showServiceEvents, showServiceEventsFrequency, waitForService, waitForMinutes, forceNewDeployment);
} else if (serviceResponse.deploymentController.type === 'CODE_DEPLOY') {
// Service uses CodeDeploy, so we should start a CodeDeploy deployment
await createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService, waitForMinutes);
Expand Down
92 changes: 87 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const IGNORED_TASK_DEFINITION_ATTRIBUTES = [
];

// Deploy to a service that uses the 'ECS' deployment controller
async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment) {
async function updateEcsService(ecs, clusterName, service, taskDefArn, showServiceEvents, showServiceEventsFrequency, waitForService, waitForMinutes, forceNewDeployment) {
core.debug('Updating the service');
await ecs.updateService({
cluster: clusterName,
Expand All @@ -30,9 +30,64 @@ async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForSe
forceNewDeployment: forceNewDeployment
}).promise();

const consoleHostname = aws.config.region.startsWith('cn') ? 'console.amazonaws.cn' : 'console.aws.amazon.com';
// Create a while loop to print the events of the service if deployment rollout state is failed
// or if there are failed tasks but rollout state is still in progress
if (showServiceEvents && showServiceEvents.toLowerCase() === 'true') {
core.debug(`Deployment started. The option show-service-events is set to true. Showing logs each ${showServiceEventsFrequency} seconds.`);
const initialState = 'IN_PROGRESS';
let describeResponse = await ecs.describeServices({
services: [service],
cluster: clusterName
}).promise();
const deployTime = describeResponse.services[0].events[0].createdAt
let newEvents = [];

while (initialState == 'IN_PROGRESS') {
const showServiceEventsFrequencyMilisec = (showServiceEventsFrequency * 1000)
await delay(showServiceEventsFrequencyMilisec);
let describeResponse = await ecs.describeServices({
services: [service],
cluster: clusterName
}).promise();

core.info(`Deployment started. Watch this deployment's progress in the Amazon ECS console: https://${consoleHostname}/ecs/home?region=${aws.config.region}#/clusters/${clusterName}/services/${service}/events`);
let serviceResponse = describeResponse.services[0];
let rolloutState = serviceResponse.deployments[0].rolloutState;
let rolloutStateReason = serviceResponse.deployments[0].rolloutStateReason;
let failedTasksCount = serviceResponse.deployments[0].failedTasks;
let indexEventContainDeployDate = getPosition(deployTime.toString(), serviceResponse.events);
newEvents = serviceResponse.events.slice(0, indexEventContainDeployDate);

if (rolloutState == 'COMPLETED') {
printEvents(newEvents.reverse());
break;
} else if (rolloutState == 'FAILED') {
printEvents(newEvents.reverse());
throw new Error(`Rollout state is ${rolloutState}. Reason: ${rolloutStateReason}.`);
} else if (rolloutState == 'IN_PROGRESS' && failedTasksCount > 0) {
printEvents(newEvents.reverse());
let tasksList = await ecs.listTasks({
serviceName: service,
cluster: clusterName,
desiredStatus: 'STOPPED'
}).promise();
let describeTaskResponse = await ecs.describeTasks({
tasks: [tasksList.taskArns[0]],
cluster: clusterName,
}).promise();
let stopCode = describeTaskResponse.tasks[0].stopCode;
let stoppedReason = describeTaskResponse.tasks[0].stoppedReason;
let containerLastStatus = describeTaskResponse.tasks[0].containers[0].lastStatus;
core.info(`Task status: ${stopCode}. The reason is: ${stoppedReason}.`);
if (containerLastStatus == 'STOPPED') {
core.info(`Container status: ${containerLastStatus}. The reason is: ${describeTaskResponse.tasks[0].containers[0].reason}.`);
}
throw new Error(`There are failed tasks. This means the deployment didn't go well. Please check the logs of task, service or container for more information.`);
}
}
} else {
const consoleHostname = aws.config.region.startsWith('cn') ? 'console.amazonaws.cn' : 'console.aws.amazon.com';
core.info(`Deployment started. Watch this deployment's progress in the Amazon ECS console: https://${consoleHostname}/ecs/home?region=${aws.config.region}#/clusters/${clusterName}/services/${service}/events`);
}

// Wait for service stability
if (waitForService && waitForService.toLowerCase() === 'true') {
Expand All @@ -51,6 +106,29 @@ async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForSe
}
}

// Function to print the events of the service
function printEvents(events) {
core.debug('Showing the service events:')
for (let i = 0; i < events.length; i++) {
core.info(events[i].createdAt.toString().split('(')[0]);
core.info(events[i].message);
}
}

// Function to get the position of an element in an array
function getPosition(elementToFind, arrayElements) {
for (let i = 0; i < arrayElements.length; i += 1) {
if (arrayElements[i].createdAt.toString().includes(elementToFind)) {
return i;
}
}
}

// Fuction to create a delay
function delay(time) {
return new Promise(resolve => setTimeout(resolve, time));
}

// Find value in a CodeDeploy AppSpec file with a case-insensitive key
function findAppSpecValue(obj, keyName) {
return obj[findAppSpecKey(obj, keyName)];
Expand Down Expand Up @@ -260,7 +338,8 @@ async function run() {
const taskDefinitionFile = core.getInput('task-definition', { required: true });
const service = core.getInput('service', { required: false });
const cluster = core.getInput('cluster', { required: false });
const waitForService = core.getInput('wait-for-service-stability', { required: false });
const waitForService = core.getInput('wait-for-service-stability', { required: false });

let waitForMinutes = parseInt(core.getInput('wait-for-minutes', { required: false })) || 30;
if (waitForMinutes > MAX_WAIT_MINUTES) {
waitForMinutes = MAX_WAIT_MINUTES;
Expand All @@ -269,6 +348,9 @@ async function run() {
const forceNewDeployInput = core.getInput('force-new-deployment', { required: false }) || 'false';
const forceNewDeployment = forceNewDeployInput.toLowerCase() === 'true';

const showServiceEvents = core.getInput('show-service-events', { required: false });
let showServiceEventsFrequency = core.getInput('show-service-events-frequency', { required: false }) || 15;

// Register the task definition
core.debug('Registering the task definition');
const taskDefPath = path.isAbsolute(taskDefinitionFile) ?
Expand Down Expand Up @@ -310,7 +392,7 @@ async function run() {

if (!serviceResponse.deploymentController || !serviceResponse.deploymentController.type || serviceResponse.deploymentController.type === 'ECS') {
// Service uses the 'ECS' deployment controller, so we can call UpdateService
await updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment);
await updateEcsService(ecs, clusterName, service, taskDefArn, showServiceEvents, showServiceEventsFrequency, waitForService, waitForMinutes, forceNewDeployment);
} else if (serviceResponse.deploymentController.type === 'CODE_DEPLOY') {
// Service uses CodeDeploy, so we should start a CodeDeploy deployment
await createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService, waitForMinutes);
Expand Down

0 comments on commit 8b66948

Please sign in to comment.