Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Registers an Amazon ECS task definition and deploys it to an ECS service.
- [Credentials and Region](#credentials-and-region)
- [Permissions](#permissions)
- [AWS CodeDeploy Support](#aws-codedeploy-support)
- [Polling Configuration](#polling-configuration)
- [Troubleshooting](#troubleshooting)
- [License Summary](#license-summary)
- [Security Disclosures](#security-disclosures)
Expand Down Expand Up @@ -388,6 +389,25 @@ This is particularly useful in cases where ECS or a previous task definition app
wait-for-service-stability: true
```

## Polling Configuration

By default when waiting for service stability or task completion, the AWS SDK uses exponential backoff which can result in delays up to 120 seconds between polling attempts. This means even after your service becomes stable, you may wait up to 2 minutes before the next poll detects it.

To use consistent polling intervals instead, set `wait-max-delay-seconds`:

```yaml
- name: Deploy to Amazon ECS
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: task-definition.json
service: my-service
cluster: my-cluster
wait-for-service-stability: true
wait-max-delay-seconds: 15
```

This configuration polls every 15 seconds instead of using exponential backoff.

## Retries

To automatically retry a failed task definition deployment, use the max-retries input. This controls how many times the action will attempt to register and deploy the task definition before failing.
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ inputs:
wait-for-minutes:
description: 'How long to wait for the ECS service to reach stable state, in minutes (default: 30 minutes, max: 6 hours). For CodeDeploy deployments, any wait time configured in the CodeDeploy deployment group will be added to this value.'
required: false
wait-max-delay-seconds:
description: 'Maximum delay in seconds between polling attempts when waiting for service stability or task completion. If not set, AWS SDK uses exponential backoff up to 120 seconds. Set to 15 for consistent 15-second polling intervals.'
required: false
codedeploy-appspec:
description: "The path to the AWS CodeDeploy AppSpec file, if the ECS service uses the CODE_DEPLOY deployment controller. Will default to 'appspec.yaml'."
required: false
Expand Down
51 changes: 37 additions & 14 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 = [
];

// Method to run a stand-alone task with desired inputs
async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSManagedTags) {
async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSManagedTags, waitMaxDelaySeconds) {
core.info('Running task')

const waitForTask = core.getInput('wait-for-task-stopped', { required: false }) || 'false';
Expand Down Expand Up @@ -102,7 +102,7 @@ async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSMa

// Wait for task to end
if (waitForTask && waitForTask.toLowerCase() === "true") {
await waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes)
await waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes, waitMaxDelaySeconds)
await tasksExitCode(ecs, clusterName, taskArns)
} else {
core.debug('Not waiting for the task to stop');
Expand Down Expand Up @@ -151,18 +151,24 @@ function convertToManagedEbsVolumeObject(managedEbsVolume) {
}

// Poll tasks until they enter a stopped state
async function waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes) {
async function waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes, waitMaxDelaySeconds) {
if (waitForMinutes > MAX_WAIT_MINUTES) {
waitForMinutes = MAX_WAIT_MINUTES;
}

core.info(`Waiting for tasks to stop. Will wait for ${waitForMinutes} minutes`);

const waitTaskResponse = await waitUntilTasksStopped({
const waiterConfig = {
client: ecs,
minDelay: WAIT_DEFAULT_DELAY_SEC,
maxWaitTime: waitForMinutes * 60,
}, {
};

if (waitMaxDelaySeconds) {
waiterConfig.maxDelay = waitMaxDelaySeconds;
}

const waitTaskResponse = await waitUntilTasksStopped(waiterConfig, {
cluster: clusterName,
tasks: taskArns,
});
Expand Down Expand Up @@ -197,7 +203,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, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags, waitMaxDelaySeconds) {
core.debug('Updating the service');

const serviceManagedEBSVolumeName = core.getInput('service-managed-ebs-volume-name', { required: false }) || '';
Expand Down Expand Up @@ -242,11 +248,18 @@ async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForSe
// Wait for service stability
if (waitForService && waitForService.toLowerCase() === 'true') {
core.debug(`Waiting for the service to become stable. Will wait for ${waitForMinutes} minutes`);
await waitUntilServicesStable({

const waiterConfig = {
client: ecs,
minDelay: WAIT_DEFAULT_DELAY_SEC,
maxWaitTime: waitForMinutes * 60
}, {
};

if (waitMaxDelaySeconds) {
waiterConfig.maxDelay = waitMaxDelaySeconds;
}

await waitUntilServicesStable(waiterConfig, {
services: [service],
cluster: clusterName
});
Expand Down Expand Up @@ -381,7 +394,7 @@ function validateProxyConfigurations(taskDef){
}

// Deploy to a service that uses the 'CODE_DEPLOY' deployment controller
async function createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService, waitForMinutes) {
async function createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService, waitForMinutes, waitMaxDelaySeconds) {
core.debug('Updating AppSpec file with new task definition ARN');

let codeDeployAppSpecFile = core.getInput('codedeploy-appspec', { required : false });
Expand Down Expand Up @@ -460,11 +473,18 @@ async function createCodeDeployDeployment(codedeploy, clusterName, service, task
totalWaitMin = MAX_WAIT_MINUTES;
}
core.debug(`Waiting for the deployment to complete. Will wait for ${totalWaitMin} minutes`);
await waitUntilDeploymentSuccessful({

const waiterConfig = {
client: codedeploy,
minDelay: WAIT_DEFAULT_DELAY_SEC,
maxWaitTime: totalWaitMin * 60
}, {
};

if (waitMaxDelaySeconds) {
waiterConfig.maxDelay = waitMaxDelaySeconds;
}

await waitUntilDeploymentSuccessful(waiterConfig, {
deploymentId: createDeployResponse.deploymentId
});
} else {
Expand All @@ -486,6 +506,9 @@ async function run() {
waitForMinutes = MAX_WAIT_MINUTES;
}

const waitMaxDelaySecondsInput = core.getInput('wait-max-delay-seconds', { required: false });
const waitMaxDelaySeconds = waitMaxDelaySecondsInput ? parseInt(waitMaxDelaySecondsInput) : null;

const forceNewDeployInput = core.getInput('force-new-deployment', { required: false }) || 'false';
const forceNewDeployment = forceNewDeployInput.toLowerCase() === 'true';
const desiredCount = parseInt((core.getInput('desired-count', {required: false})));
Expand Down Expand Up @@ -545,7 +568,7 @@ async function run() {
core.debug(`shouldRunTask: ${shouldRunTask}`);
if (shouldRunTask) {
core.debug("Running ad-hoc task...");
await runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSManagedTags);
await runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSManagedTags, waitMaxDelaySeconds);
}

// Update the service with the new task definition
Expand All @@ -569,12 +592,12 @@ 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, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags, waitMaxDelaySeconds);

} else if (serviceResponse.deploymentController.type === 'CODE_DEPLOY') {
// Service uses CodeDeploy, so we should start a CodeDeploy deployment
core.debug('Deploying service in the default cluster');
await createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService, waitForMinutes);
await createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService, waitForMinutes, waitMaxDelaySeconds);
} else {
throw new Error(`Unsupported deployment controller: ${serviceResponse.deploymentController.type}`);
}
Expand Down
51 changes: 37 additions & 14 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const IGNORED_TASK_DEFINITION_ATTRIBUTES = [
];

// Method to run a stand-alone task with desired inputs
async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSManagedTags) {
async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSManagedTags, waitMaxDelaySeconds) {
core.info('Running task')

const waitForTask = core.getInput('wait-for-task-stopped', { required: false }) || 'false';
Expand Down Expand Up @@ -96,7 +96,7 @@ async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSMa

// Wait for task to end
if (waitForTask && waitForTask.toLowerCase() === "true") {
await waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes)
await waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes, waitMaxDelaySeconds)
await tasksExitCode(ecs, clusterName, taskArns)
} else {
core.debug('Not waiting for the task to stop');
Expand Down Expand Up @@ -145,18 +145,24 @@ function convertToManagedEbsVolumeObject(managedEbsVolume) {
}

// Poll tasks until they enter a stopped state
async function waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes) {
async function waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes, waitMaxDelaySeconds) {
if (waitForMinutes > MAX_WAIT_MINUTES) {
waitForMinutes = MAX_WAIT_MINUTES;
}

core.info(`Waiting for tasks to stop. Will wait for ${waitForMinutes} minutes`);

const waitTaskResponse = await waitUntilTasksStopped({
const waiterConfig = {
client: ecs,
minDelay: WAIT_DEFAULT_DELAY_SEC,
maxWaitTime: waitForMinutes * 60,
}, {
};

if (waitMaxDelaySeconds) {
waiterConfig.maxDelay = waitMaxDelaySeconds;
}

const waitTaskResponse = await waitUntilTasksStopped(waiterConfig, {
cluster: clusterName,
tasks: taskArns,
});
Expand Down Expand Up @@ -191,7 +197,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, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags, waitMaxDelaySeconds) {
core.debug('Updating the service');

const serviceManagedEBSVolumeName = core.getInput('service-managed-ebs-volume-name', { required: false }) || '';
Expand Down Expand Up @@ -236,11 +242,18 @@ async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForSe
// Wait for service stability
if (waitForService && waitForService.toLowerCase() === 'true') {
core.debug(`Waiting for the service to become stable. Will wait for ${waitForMinutes} minutes`);
await waitUntilServicesStable({

const waiterConfig = {
client: ecs,
minDelay: WAIT_DEFAULT_DELAY_SEC,
maxWaitTime: waitForMinutes * 60
}, {
};

if (waitMaxDelaySeconds) {
waiterConfig.maxDelay = waitMaxDelaySeconds;
}

await waitUntilServicesStable(waiterConfig, {
services: [service],
cluster: clusterName
});
Expand Down Expand Up @@ -375,7 +388,7 @@ function validateProxyConfigurations(taskDef){
}

// Deploy to a service that uses the 'CODE_DEPLOY' deployment controller
async function createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService, waitForMinutes) {
async function createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService, waitForMinutes, waitMaxDelaySeconds) {
core.debug('Updating AppSpec file with new task definition ARN');

let codeDeployAppSpecFile = core.getInput('codedeploy-appspec', { required : false });
Expand Down Expand Up @@ -454,11 +467,18 @@ async function createCodeDeployDeployment(codedeploy, clusterName, service, task
totalWaitMin = MAX_WAIT_MINUTES;
}
core.debug(`Waiting for the deployment to complete. Will wait for ${totalWaitMin} minutes`);
await waitUntilDeploymentSuccessful({

const waiterConfig = {
client: codedeploy,
minDelay: WAIT_DEFAULT_DELAY_SEC,
maxWaitTime: totalWaitMin * 60
}, {
};

if (waitMaxDelaySeconds) {
waiterConfig.maxDelay = waitMaxDelaySeconds;
}

await waitUntilDeploymentSuccessful(waiterConfig, {
deploymentId: createDeployResponse.deploymentId
});
} else {
Expand All @@ -480,6 +500,9 @@ async function run() {
waitForMinutes = MAX_WAIT_MINUTES;
}

const waitMaxDelaySecondsInput = core.getInput('wait-max-delay-seconds', { required: false });
const waitMaxDelaySeconds = waitMaxDelaySecondsInput ? parseInt(waitMaxDelaySecondsInput) : null;

const forceNewDeployInput = core.getInput('force-new-deployment', { required: false }) || 'false';
const forceNewDeployment = forceNewDeployInput.toLowerCase() === 'true';
const desiredCount = parseInt((core.getInput('desired-count', {required: false})));
Expand Down Expand Up @@ -539,7 +562,7 @@ async function run() {
core.debug(`shouldRunTask: ${shouldRunTask}`);
if (shouldRunTask) {
core.debug("Running ad-hoc task...");
await runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSManagedTags);
await runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSManagedTags, waitMaxDelaySeconds);
}

// Update the service with the new task definition
Expand All @@ -563,12 +586,12 @@ 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, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags, waitMaxDelaySeconds);

} else if (serviceResponse.deploymentController.type === 'CODE_DEPLOY') {
// Service uses CodeDeploy, so we should start a CodeDeploy deployment
core.debug('Deploying service in the default cluster');
await createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService, waitForMinutes);
await createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService, waitForMinutes, waitMaxDelaySeconds);
} else {
throw new Error(`Unsupported deployment controller: ${serviceResponse.deploymentController.type}`);
}
Expand Down
Loading
Loading