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 authored and Felipe Macias committed Oct 2, 2024
1 parent 88fc08e commit 61fc2fd
Showing 5 changed files with 376 additions and 15 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -326,6 +326,8 @@ To tag your tasks:

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.
6 changes: 6 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -16,6 +16,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
94 changes: 88 additions & 6 deletions dist/index.js
Original file line number Diff line number Diff line change
@@ -137,7 +137,7 @@ async function tasksExitCode(ecs, clusterName, taskArns) {
}

// Deploy to a service that uses the 'ECS' deployment controller
async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags) {
async function updateEcsService(ecs, clusterName, service, taskDefArn, showServiceEvents, showServiceEventsFrequency, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags) {
core.debug('Updating the service');

let params = {
@@ -155,10 +155,65 @@ async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForSe
}
await ecs.updateService(params);

const region = await ecs.config.region();
const consoleHostname = 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
});
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
});

core.info(`Deployment started. Watch this deployment's progress in the Amazon ECS console: https://${region}.${consoleHostname}/ecs/v2/clusters/${clusterName}/services/${service}/events?region=${region}`);
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'
});
let describeTaskResponse = await ecs.describeTasks({
tasks: [tasksList.taskArns[0]],
cluster: clusterName,
});
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 region = await ecs.config.region();
const consoleHostname = 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://${region}.${consoleHostname}/ecs/v2/clusters/${clusterName}/services/${service}/events?region=${region}`);
}

// Wait for service stability
if (waitForService && waitForService.toLowerCase() === 'true') {
@@ -176,6 +231,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)];
@@ -392,7 +470,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) {
@@ -406,6 +485,9 @@ async function run() {
const enableECSManagedTags = enableECSManagedTagsInput.toLowerCase() === 'true';
const propagateTags = core.getInput('propagate-tags', { required: false }) || 'NONE';

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) ?
@@ -456,7 +538,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
core.debug('Updating service...');
await updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags);
await updateEcsService(ecs, clusterName, service, taskDefArn, showServiceEvents, showServiceEventsFrequency, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags);

} else if (serviceResponse.deploymentController.type === 'CODE_DEPLOY') {
// Service uses CodeDeploy, so we should start a CodeDeploy deployment
94 changes: 88 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
@@ -131,7 +131,7 @@ async function tasksExitCode(ecs, clusterName, taskArns) {
}

// Deploy to a service that uses the 'ECS' deployment controller
async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags) {
async function updateEcsService(ecs, clusterName, service, taskDefArn, showServiceEvents, showServiceEventsFrequency, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags) {
core.debug('Updating the service');

let params = {
@@ -149,10 +149,65 @@ async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForSe
}
await ecs.updateService(params);

const region = await ecs.config.region();
const consoleHostname = 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
});
const deployTime = describeResponse.services[0].events[0].createdAt
let newEvents = [];

core.info(`Deployment started. Watch this deployment's progress in the Amazon ECS console: https://${region}.${consoleHostname}/ecs/v2/clusters/${clusterName}/services/${service}/events?region=${region}`);
while (initialState == 'IN_PROGRESS') {
const showServiceEventsFrequencyMilisec = (showServiceEventsFrequency * 1000)
await delay(showServiceEventsFrequencyMilisec);
let describeResponse = await ecs.describeServices({
services: [service],
cluster: clusterName
});

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'
});
let describeTaskResponse = await ecs.describeTasks({
tasks: [tasksList.taskArns[0]],
cluster: clusterName,
});
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 region = await ecs.config.region();
const consoleHostname = 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://${region}.${consoleHostname}/ecs/v2/clusters/${clusterName}/services/${service}/events?region=${region}`);
}

// Wait for service stability
if (waitForService && waitForService.toLowerCase() === 'true') {
@@ -170,6 +225,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)];
@@ -386,7 +464,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) {
@@ -400,6 +479,9 @@ async function run() {
const enableECSManagedTags = enableECSManagedTagsInput.toLowerCase() === 'true';
const propagateTags = core.getInput('propagate-tags', { required: false }) || 'NONE';

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) ?
@@ -450,7 +532,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
core.debug('Updating service...');
await updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags);
await updateEcsService(ecs, clusterName, service, taskDefArn, showServiceEvents, showServiceEventsFrequency, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags);

} else if (serviceResponse.deploymentController.type === 'CODE_DEPLOY') {
// Service uses CodeDeploy, so we should start a CodeDeploy deployment
Loading

0 comments on commit 61fc2fd

Please sign in to comment.