Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for 'VolumeConfigurations' property on both UpdateService and RunTask API call #721

Merged
merged 16 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from 15 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
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,50 @@ You can propagate your custom tags from your existing service using `propagate-t
propagate-tags: SERVICE
```

### EBS Volume Configuration
This action supports configuring Amazon EBS volumes for both services and standalone tasks.

For Services (Update Service):

```yaml
- name: Deploy to Amazon ECS with EBS Volume
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
service-managed-ebs-volume-name: "ebs1"
service-managed-ebs-volume: '{"sizeInGiB": 30, "volumeType": "gp3", "encrypted": true, "roleArn":"arn:aws:iam::<account-id>:role/ebs-role"}'
```

Note: Your task definition must include a volume that is configuredAtLaunch:

```json
...
"volumes": [
{
"name": "ebs1",
"configuredAtLaunch": true
}
],
...
```

For Standalone Tasks (RunTask):

```yaml
- name: Deploy to Amazon ECS
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: task-definition.json
cluster: my-cluster
run-task: true
run-task-launch-type: EC2
run-task-managed-ebs-volume-name: "ebs1"
run-task-managed-ebs-volume: '{"filesystemType":"xfs", "roleArn":"arn:aws:iam::<account-id>:role/github-actions-setup-stack-EBSRole-YwVmgS4g7gQE", "encrypted":false, "sizeInGiB":30}'
```

## Credentials and Region

This action relies on the [default behavior of the AWS SDK for Javascript](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/setting-credentials-node.html) to determine AWS credentials and region.
Expand Down
12 changes: 12 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ inputs:
force-new-deployment:
description: 'Whether to force a new deployment of the service. Valid value is "true". Will default to not force a new deployment.'
required: false
service-managed-ebs-volume-name:
description: "The name of the volume, to be manage in the ECS service. This value must match the volume name from the Volume object in the task definition, that was configuredAtLaunch."
required: false
service-managed-ebs-volume:
description: "A JSON object defining the configuration settings for the EBS Service volume that was ConfiguredAtLaunch. You can configure size, volumeType, IOPS, throughput, snapshot and encryption in ServiceManagedEBSVolumeConfiguration. Currently, the only supported volume type is an Amazon EBS volume."
required: false
run-task:
description: 'A boolean indicating whether to run a stand-alone task in a ECS cluster. Task will run before the service is updated if both are provided. Default value is false .'
required: false
Expand Down Expand Up @@ -67,6 +73,12 @@ inputs:
run-task-tags:
description: 'A JSON array of tags.'
required: false
run-task-managed-ebs-volume-name:
description: "The name of the volume. This value must match the volume name from the Volume object in the task definition, that was configuredAtLaunch."
required: false
run-task-managed-ebs-volume:
description: "A JSON object defining the configuration settings for the Amazon EBS task volume that was configuredAtLaunch. These settings are used to create each Amazon EBS volume, with one volume created for each task in the service. The Amazon EBS volumes are visible in your account in the Amazon EC2 console once they are created."
required: false
wait-for-task-stopped:
description: 'Whether to wait for the task to stop when running it outside of a service. Will default to not wait.'
required: false
Expand Down
81 changes: 79 additions & 2 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSMa
const assignPublicIP = core.getInput('run-task-assign-public-IP', { required: false }) || 'DISABLED';
const tags = JSON.parse(core.getInput('run-task-tags', { required: false }) || '[]');
const capacityProviderStrategy = JSON.parse(core.getInput('run-task-capacity-provider-strategy', { required: false }) || '[]');
const runTaskManagedEBSVolumeName = core.getInput('run-task-managed-ebs-volume-name', { required: false }) || '';
const runTaskManagedEBSVolume = core.getInput('run-task-managed-ebs-volume', { required: false }) || '{}';

let awsvpcConfiguration = {}

Expand All @@ -53,6 +55,20 @@ async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSMa
if(assignPublicIP != "" && (subnetIds != "" || securityGroupIds != "")){
awsvpcConfiguration["assignPublicIp"] = assignPublicIP
}
let volumeConfigurations = [];
let taskManagedEBSVolumeObject;

if (runTaskManagedEBSVolumeName != '') {
if (runTaskManagedEBSVolume != '{}') {
taskManagedEBSVolumeObject = convertToManagedEbsVolumeObject(runTaskManagedEBSVolume);
volumeConfigurations = [{
name: runTaskManagedEBSVolumeName,
managedEBSVolume: taskManagedEBSVolumeObject
}];
} else {
core.warning(`run-task-managed-ebs-volume-name provided without run-task-managed-ebs-volume value. Ignoring run-task-managed-ebs-volume property`);
kg-aws marked this conversation as resolved.
Show resolved Hide resolved
}
}

const runTaskResponse = await ecs.runTask({
startedBy: startedBy,
Expand All @@ -65,7 +81,8 @@ async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSMa
launchType: capacityProviderStrategy.length === 0 ? launchType : null,
networkConfiguration: Object.keys(awsvpcConfiguration).length === 0 ? null : { awsvpcConfiguration: awsvpcConfiguration },
enableECSManagedTags: enableECSManagedTags,
tags: tags
tags: tags,
volumeConfigurations: volumeConfigurations
});

core.debug(`Run task response ${JSON.stringify(runTaskResponse)}`)
Expand All @@ -92,6 +109,47 @@ async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSMa
}
}

function convertToManagedEbsVolumeObject(managedEbsVolume) {
managedEbsVolumeObject = {}
const ebsVolumeObject = JSON.parse(managedEbsVolume);
if ('roleArn' in ebsVolumeObject){ // required property
managedEbsVolumeObject.roleArn = ebsVolumeObject.roleArn;
core.debug(`Found RoleArn ${ebsVolumeObject['roleArn']}`);
} else {
throw new Error('managed-ebs-volume must provide "role-arn" to associate with the EBS volume')
}

if ('encrypted' in ebsVolumeObject) {
managedEbsVolumeObject.encrypted = ebsVolumeObject.encrypted;
}
if ('filesystemType' in ebsVolumeObject) {
managedEbsVolumeObject.filesystemType = ebsVolumeObject.filesystemType;
}
if ('iops' in ebsVolumeObject) {
managedEbsVolumeObject.iops = ebsVolumeObject.iops;
}
if ('kmsKeyId' in ebsVolumeObject) {
managedEbsVolumeObject.kmsKeyId = ebsVolumeObject.kmsKeyId;
}
if ('sizeInGiB' in ebsVolumeObject) {
managedEbsVolumeObject.sizeInGiB = ebsVolumeObject.sizeInGiB;
}
if ('snapshotId' in ebsVolumeObject) {
managedEbsVolumeObject.snapshotId = ebsVolumeObject.snapshotId;
}
if ('tagSpecifications' in ebsVolumeObject) {
managedEbsVolumeObject.tagSpecifications = ebsVolumeObject.tagSpecifications;
}
if (('throughput' in ebsVolumeObject) && (('volumeType' in ebsVolumeObject) && (ebsVolumeObject.volumeType == 'gp3'))){
managedEbsVolumeObject.throughput = ebsVolumeObject.throughput;
}
if ('volumeType' in ebsVolumeObject) {
managedEbsVolumeObject.volumeType = ebsVolumeObject.volumeType;
}
core.debug(`Created managedEbsVolumeObject: ${JSON.stringify(managedEbsVolumeObject)}`);
return managedEbsVolumeObject;
}

// Poll tasks until they enter a stopped state
async function waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes) {
if (waitForMinutes > MAX_WAIT_MINUTES) {
Expand Down Expand Up @@ -142,13 +200,32 @@ async function tasksExitCode(ecs, clusterName, taskArns) {
async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags) {
core.debug('Updating the service');

const serviceManagedEBSVolumeName = core.getInput('service-managed-ebs-volume-name', { required: false }) || '';
const serviceManagedEBSVolume = core.getInput('service-managed-ebs-volume', { required: false }) || '{}';

let volumeConfigurations = [];
let serviceManagedEbsVolumeObject;

if (serviceManagedEBSVolumeName != '') {
if (serviceManagedEBSVolume != '{}') {
serviceManagedEbsVolumeObject = convertToManagedEbsVolumeObject(serviceManagedEBSVolume);
volumeConfigurations = [{
name: serviceManagedEBSVolumeName,
managedEBSVolume: serviceManagedEbsVolumeObject
}];
} else {
core.warning('service-managed-ebs-volume-name provided without service-managed-ebs-volume value. Ignoring service-managed-ebs-volume property');
}
}

let params = {
cluster: clusterName,
service: service,
taskDefinition: taskDefArn,
forceNewDeployment: forceNewDeployment,
enableECSManagedTags: enableECSManagedTags,
propagateTags: propagateTags
propagateTags: propagateTags,
volumeConfigurations: volumeConfigurations
};

// Add the desiredCount property only if it is defined and a number.
Expand Down
81 changes: 79 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSMa
const assignPublicIP = core.getInput('run-task-assign-public-IP', { required: false }) || 'DISABLED';
const tags = JSON.parse(core.getInput('run-task-tags', { required: false }) || '[]');
const capacityProviderStrategy = JSON.parse(core.getInput('run-task-capacity-provider-strategy', { required: false }) || '[]');
const runTaskManagedEBSVolumeName = core.getInput('run-task-managed-ebs-volume-name', { required: false }) || '';
Copy link
Contributor

@kg-aws kg-aws Jan 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we thought about auto populating this property for customers from the task-definition.json file provided in the input? Also, would auto-populate break any existing customers or in other words would it be backwards compatible?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, That is a good point. I think it would be possible to auto-populate the volume name from task-definition if, the task-definition contains a volume with configuredAtLaunch set to true. We could parse the Volume property from a pre-existingTaskDefinitionContents variable.

Backward-compatibility wise - it should be fine, because:

  1. Task definition MUST have a volume with configuredAtLaunch: true - in-order to configure a volume property with ECS
  2. This volume MUST match the name used in VolumeConfigurations

Pros of this auto-population I think would be:

  • less configuration on user side
  • can't break the volume name matching (since it uses exact name from task definition)

Cons might be:

  • a bit of a magic/implicit behavior, but can be called-out in the README

const runTaskManagedEBSVolume = core.getInput('run-task-managed-ebs-volume', { required: false }) || '{}';

let awsvpcConfiguration = {}

Expand All @@ -47,6 +49,20 @@ async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSMa
if(assignPublicIP != "" && (subnetIds != "" || securityGroupIds != "")){
awsvpcConfiguration["assignPublicIp"] = assignPublicIP
}
let volumeConfigurations = [];
let taskManagedEBSVolumeObject;

if (runTaskManagedEBSVolumeName != '') {
if (runTaskManagedEBSVolume != '{}') {
taskManagedEBSVolumeObject = convertToManagedEbsVolumeObject(runTaskManagedEBSVolume);
volumeConfigurations = [{
name: runTaskManagedEBSVolumeName,
managedEBSVolume: taskManagedEBSVolumeObject
}];
} else {
core.warning(`run-task-managed-ebs-volume-name provided without run-task-managed-ebs-volume value. Ignoring run-task-managed-ebs-volume property`);
}
}

const runTaskResponse = await ecs.runTask({
startedBy: startedBy,
Expand All @@ -59,7 +75,8 @@ async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSMa
launchType: capacityProviderStrategy.length === 0 ? launchType : null,
networkConfiguration: Object.keys(awsvpcConfiguration).length === 0 ? null : { awsvpcConfiguration: awsvpcConfiguration },
enableECSManagedTags: enableECSManagedTags,
tags: tags
tags: tags,
volumeConfigurations: volumeConfigurations
});

core.debug(`Run task response ${JSON.stringify(runTaskResponse)}`)
Expand All @@ -86,6 +103,47 @@ async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSMa
}
}

function convertToManagedEbsVolumeObject(managedEbsVolume) {
managedEbsVolumeObject = {}
const ebsVolumeObject = JSON.parse(managedEbsVolume);
if ('roleArn' in ebsVolumeObject){ // required property
managedEbsVolumeObject.roleArn = ebsVolumeObject.roleArn;
core.debug(`Found RoleArn ${ebsVolumeObject['roleArn']}`);
} else {
throw new Error('managed-ebs-volume must provide "role-arn" to associate with the EBS volume')
}

if ('encrypted' in ebsVolumeObject) {
managedEbsVolumeObject.encrypted = ebsVolumeObject.encrypted;
}
if ('filesystemType' in ebsVolumeObject) {
managedEbsVolumeObject.filesystemType = ebsVolumeObject.filesystemType;
}
if ('iops' in ebsVolumeObject) {
managedEbsVolumeObject.iops = ebsVolumeObject.iops;
}
if ('kmsKeyId' in ebsVolumeObject) {
managedEbsVolumeObject.kmsKeyId = ebsVolumeObject.kmsKeyId;
}
if ('sizeInGiB' in ebsVolumeObject) {
managedEbsVolumeObject.sizeInGiB = ebsVolumeObject.sizeInGiB;
}
if ('snapshotId' in ebsVolumeObject) {
managedEbsVolumeObject.snapshotId = ebsVolumeObject.snapshotId;
}
if ('tagSpecifications' in ebsVolumeObject) {
managedEbsVolumeObject.tagSpecifications = ebsVolumeObject.tagSpecifications;
}
if (('throughput' in ebsVolumeObject) && (('volumeType' in ebsVolumeObject) && (ebsVolumeObject.volumeType == 'gp3'))){
managedEbsVolumeObject.throughput = ebsVolumeObject.throughput;
}
if ('volumeType' in ebsVolumeObject) {
managedEbsVolumeObject.volumeType = ebsVolumeObject.volumeType;
}
core.debug(`Created managedEbsVolumeObject: ${JSON.stringify(managedEbsVolumeObject)}`);
return managedEbsVolumeObject;
}

// Poll tasks until they enter a stopped state
async function waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes) {
if (waitForMinutes > MAX_WAIT_MINUTES) {
Expand Down Expand Up @@ -136,13 +194,32 @@ async function tasksExitCode(ecs, clusterName, taskArns) {
async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags) {
core.debug('Updating the service');

const serviceManagedEBSVolumeName = core.getInput('service-managed-ebs-volume-name', { required: false }) || '';
const serviceManagedEBSVolume = core.getInput('service-managed-ebs-volume', { required: false }) || '{}';

let volumeConfigurations = [];
let serviceManagedEbsVolumeObject;

if (serviceManagedEBSVolumeName != '') {
if (serviceManagedEBSVolume != '{}') {
serviceManagedEbsVolumeObject = convertToManagedEbsVolumeObject(serviceManagedEBSVolume);
volumeConfigurations = [{
name: serviceManagedEBSVolumeName,
managedEBSVolume: serviceManagedEbsVolumeObject
}];
} else {
core.warning('service-managed-ebs-volume-name provided without service-managed-ebs-volume value. Ignoring service-managed-ebs-volume property');
}
}

let params = {
cluster: clusterName,
service: service,
taskDefinition: taskDefArn,
forceNewDeployment: forceNewDeployment,
enableECSManagedTags: enableECSManagedTags,
propagateTags: propagateTags
propagateTags: propagateTags,
volumeConfigurations: volumeConfigurations
};

// Add the desiredCount property only if it is defined and a number.
Expand Down
Loading
Loading