Skip to content

Commit b4361d4

Browse files
Enable spawning multiple EC2 instances in a single call (#4)
* Enable spawning multiple EC2 instances in a single action call
1 parent fb3a577 commit b4361d4

File tree

6 files changed

+101
-71
lines changed

6 files changed

+101
-71
lines changed

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -193,14 +193,15 @@ Now you're ready to go!
193193
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
194194
| `mode` | Always required. | Specify here which mode you want to use: <br> - `start` - to start a new runner; <br> - `stop` - to stop the previously created runner. |
195195
| `github-token` | Always required. | GitHub Personal Access Token with the `repo` scope assigned. |
196-
| `ec2-image-id` | Required if you use the `start` mode. | EC2 Image Id (AMI). <br><br> The new runner will be launched from this image. <br><br> The action is compatible with Amazon Linux 2 images. |
196+
| `ec2-image-id` | Required if you use the `start` mode. | EC2 Image Id (AMI). <br><br> The new runners will be launched from this image. <br><br> The action is compatible with Amazon Linux 2 images. |
197197
| `ec2-instance-type` | Required if you use the `start` mode. | EC2 Instance Type. |
198+
| `ec2-instance-count` | Number of EC2 instances to create, defaults to 1. | EC2 Instance Count. |
198199
| `subnet-id` | Required if you use the `start` mode. | VPC Subnet Id. <br><br> The subnet should belong to the same VPC as the specified security group. |
199200
| `security-group-id` | Required if you use the `start` mode. | EC2 Security Group Id. <br><br> The security group should belong to the same VPC as the specified subnet. <br><br> Only the outbound traffic for port 443 should be allowed. No inbound traffic is required. |
200-
| `label` | Required if you use the `stop` mode. | Name of the unique label assigned to the runner. <br><br> The label is provided by the output of the action in the `start` mode. <br><br> The label is used to remove the runner from GitHub when the runner is not needed anymore. |
201-
| `ec2-instance-id` | Required if you use the `stop` mode. | EC2 Instance Id of the created runner. <br><br> The id is provided by the output of the action in the `start` mode. <br><br> The id is used to terminate the EC2 instance when the runner is not needed anymore. |
202-
| `iam-role-name` | Optional. Used only with the `start` mode. | IAM role name to attach to the created EC2 runner. <br><br> This allows the runner to have permissions to run additional actions within the AWS account, without having to manage additional GitHub secrets and AWS users. <br><br> Setting this requires additional AWS permissions for the role launching the instance (see above). |
203-
| `aws-resource-tags` | Optional. Used only with the `start` mode. | Specifies tags to add to the EC2 instance and any attached storage. <br><br> This field is a stringified JSON array of tag objects, each containing a `Key` and `Value` field (see example below). <br><br> Setting this requires additional AWS permissions for the role launching the instance (see above). |
201+
| `label` | Required if you use the `stop` mode. | Name of the unique label assigned to the runners. <br><br> The label is provided by the output of the action in the `start` mode. <br><br> The label is used to remove the runners from GitHub when the runners are not needed anymore. |
202+
| `ec2-instance-id` | Required if you use the `stop` mode. | EC2 Instance Ids of the created runners. <br><br> The ids are provided by the output of the action in the `start` mode. <br><br> The ids are used to terminate the EC2 instances when the runners are not needed anymore. |
203+
| `iam-role-name` | Optional. Used only with the `start` mode. | IAM role name to attach to the created EC2 runners. <br><br> This allows the runners to have permissions to run additional actions within the AWS account, without having to manage additional GitHub secrets and AWS users. <br><br> Setting this requires additional AWS permissions for the role launching the instances (see above). |
204+
| `aws-resource-tags` | Optional. Used only with the `start` mode. | Specifies tags to add to the EC2 instances and any attached storage. <br><br> This field is a stringified JSON array of tag objects, each containing a `Key` and `Value` field (see example below). <br><br> Setting this requires additional AWS permissions for the role launching the instance (see above). |
204205
| `runner-home-dir` | Optional. Used only with the `start` mode. | Specifies a directory where pre-installed actions-runner software and scripts are located.<br><br> |
205206

206207
### Environment variables

action.yml

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
name: On-demand self-hosted AWS EC2 runner for GitHub Actions
2-
description: GitHub Action for automatic creation and registration AWS EC2 instance as a GitHub Actions self-hosted runner.
1+
name: On-demand self-hosted AWS EC2 runners for GitHub Actions
2+
description: GitHub Action for automatic creation and registration AWS EC2 instances as GitHub Actions self-hosted runners.
33
author: Volodymyr Machula
44
branding:
55
icon: 'box'
@@ -8,28 +8,33 @@ inputs:
88
mode:
99
description: >-
1010
Specify here which mode you want to use:
11-
- 'start' - to start a new runner;
12-
- 'stop' - to stop the previously created runner.
11+
- 'start' - to start new runners;
12+
- 'stop' - to stop the previously created runners.
1313
required: true
1414
github-token:
1515
description: >-
1616
GitHub Personal Access Token with the 'repo' scope assigned.
1717
required: true
1818
key-pair-name:
1919
description: >-
20-
Key pair name to use when creating the runner instance.
20+
Key pair name to use when creating the runner instances.
2121
This input is required if you use the 'start' mode.
2222
required: false
2323
ec2-image-id:
2424
description: >-
25-
EC2 Image Id (AMI). The new runner will be launched from this image.
25+
EC2 Image Id (AMI). The new runners will be launched from this image.
2626
This input is required if you use the 'start' mode.
2727
required: false
2828
ec2-instance-type:
2929
description: >-
3030
EC2 Instance Type.
3131
This input is required if you use the 'start' mode.
3232
required: false
33+
ec2-instance-count:
34+
description: >-
35+
Number of EC2 instances to create.
36+
required: false
37+
default: 1
3338
subnet-id:
3439
description: >-
3540
VPC Subnet Id. The subnet should belong to the same VPC as the specified security group.
@@ -39,29 +44,32 @@ inputs:
3944
description: >-
4045
EC2 Security Group Id.
4146
The security group should belong to the same VPC as the specified subnet.
42-
The runner doesn't require any inbound traffic. However, outbound traffic should be allowed.
47+
The runners don't require any inbound traffic. However, outbound traffic should be allowed.
4348
This input is required if you use the 'start' mode.
4449
required: false
4550
label:
4651
description: >-
47-
Name of the unique label assigned to the runner.
48-
The label is used to remove the runner from GitHub when the runner is not needed anymore.
52+
Name of the unique label assigned to the runners.
53+
The label is used to remove the runners from GitHub when the runners are not needed anymore.
4954
This input is required if you use the 'stop' mode.
5055
required: false
56+
# This input's name is in the singular form for backwards compatibility
5157
ec2-instance-id:
5258
description: >-
53-
EC2 Instance Id of the created runner.
54-
The id is used to terminate the EC2 instance when the runner is not needed anymore.
55-
This input is required if you use the 'stop' mode.
59+
EC2 Instance Ids of the created runners.
60+
The ids are used to terminate the EC2 instances when the runners are not needed anymore.
61+
This input is required if you use the 'stop' mode. The value can either be in the form of
62+
a single raw string containing a single EC2 instance id, or a JSON-encoded string representing
63+
an array of id strings.
5664
required: false
5765
iam-role-name:
5866
description: >-
59-
IAM Role Name to attach to the created EC2 instance.
67+
IAM Role Name to attach to the created EC2 instances.
6068
This requires additional permissions on the AWS role used to launch instances.
6169
required: false
6270
aws-resource-tags:
6371
description: >-
64-
Tags to attach to the launched EC2 instance and volume.
72+
Tags to attach to the launched EC2 instances and volumes.
6573
This must be a stringified array of AWS Tag objects, with both Key and Value fields,
6674
for example: '[{"Key": "TagKey1", "Value": "TagValue1"}, {"Key": "TagKey2", "Value": "TagValue2"}]'
6775
required: false
@@ -73,14 +81,15 @@ inputs:
7381
outputs:
7482
label:
7583
description: >-
76-
Name of the unique label assigned to the runner.
84+
Name of the unique label assigned to the runners.
7785
The label is used in two cases:
7886
- to use as the input of 'runs-on' property for the following jobs;
79-
- to remove the runner from GitHub when it is not needed anymore.
87+
- to remove the runners from GitHub when they are not needed anymore.
88+
# This output's name is in the singular form for backwards compatibility
8089
ec2-instance-id:
8190
description: >-
82-
EC2 Instance Id of the created runner.
83-
The id is used to terminate the EC2 instance when the runner is not needed anymore.
91+
EC2 Instance Ids of the created runners.
92+
The ids are used to terminate the EC2 instances when the runners are not needed anymore.
8493
runs:
8594
using: node16
8695
main: ./dist/index.js

src/aws.js

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ function buildUserDataScript(githubRegistrationToken, label) {
3838
}
3939
}
4040

41-
async function startEc2Instance(label, githubRegistrationToken) {
41+
async function startEc2Instances(label, count, githubRegistrationToken) {
4242
const ec2 = new AWS.EC2();
4343

4444
// User data scripts are run as the root user.
@@ -48,8 +48,8 @@ async function startEc2Instance(label, githubRegistrationToken) {
4848
const params = {
4949
ImageId: config.input.ec2ImageId,
5050
InstanceType: config.input.ec2InstanceType,
51-
MinCount: 1,
52-
MaxCount: 1,
51+
MinCount: count,
52+
MaxCount: count,
5353
UserData: Buffer.from(userData.join('\n')).toString('base64'),
5454
SubnetId: config.input.subnetId,
5555
SecurityGroupIds: [config.input.securityGroupId],
@@ -60,51 +60,51 @@ async function startEc2Instance(label, githubRegistrationToken) {
6060

6161
try {
6262
const result = await ec2.runInstances(params).promise();
63-
const ec2InstanceId = result.Instances[0].InstanceId;
64-
core.info(`AWS EC2 instance ${ec2InstanceId} is started`);
65-
return ec2InstanceId;
63+
const ec2InstanceIds = result.Instances.map(i => i.InstanceId);
64+
core.info(`AWS EC2 instances ${JSON.stringify(ec2InstanceIds)} are started`);
65+
return ec2InstanceIds;
6666
} catch (error) {
6767
core.error('AWS EC2 instance starting error');
6868
throw error;
6969
}
7070
}
7171

72-
async function terminateEc2Instance() {
72+
async function terminateEc2Instances() {
7373
const ec2 = new AWS.EC2();
7474

7575
const params = {
76-
InstanceIds: [config.input.ec2InstanceId],
76+
InstanceIds: config.input.ec2InstanceIds,
7777
};
7878

7979
try {
8080
await ec2.terminateInstances(params).promise();
81-
core.info(`AWS EC2 instance ${config.input.ec2InstanceId} is terminated`);
81+
core.info(`AWS EC2 instances ${JSON.stringify(config.input.ec2InstanceIds)} are terminated`);
8282
return;
8383
} catch (error) {
84-
core.error(`AWS EC2 instance ${config.input.ec2InstanceId} termination error`);
84+
core.error(`AWS EC2 instances ${JSON.stringify(config.input.ec2InstanceIds)} termination error`);
8585
throw error;
8686
}
8787
}
8888

89-
async function waitForInstanceRunning(ec2InstanceId) {
89+
async function waitForInstancesRunning(ec2InstanceIds) {
9090
const ec2 = new AWS.EC2();
9191

9292
const params = {
93-
InstanceIds: [ec2InstanceId],
93+
InstanceIds: ec2InstanceIds,
9494
};
9595

9696
try {
9797
await ec2.waitFor('instanceRunning', params).promise();
98-
core.info(`AWS EC2 instance ${ec2InstanceId} is up and running`);
98+
core.info(`AWS EC2 instances ${JSON.stringify(ec2InstanceIds)} are up and running`);
9999
return;
100100
} catch (error) {
101-
core.error(`AWS EC2 instance ${ec2InstanceId} initialization error`);
101+
core.error(`AWS EC2 instances ${JSON.stringify(ec2InstanceIds)} initialization error`);
102102
throw error;
103103
}
104104
}
105105

106106
module.exports = {
107-
startEc2Instance,
108-
terminateEc2Instance,
109-
waitForInstanceRunning,
107+
startEc2Instances,
108+
terminateEc2Instances,
109+
waitForInstancesRunning,
110110
};

src/config.js

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ class Config {
88
githubToken: core.getInput('github-token'),
99
ec2ImageId: core.getInput('ec2-image-id'),
1010
ec2InstanceType: core.getInput('ec2-instance-type'),
11+
ec2InstanceCount: core.getInput('ec2-instance-count'),
1112
subnetId: core.getInput('subnet-id'),
1213
securityGroupId: core.getInput('security-group-id'),
1314
label: core.getInput('label'),
14-
ec2InstanceId: core.getInput('ec2-instance-id'),
15+
ec2InstanceIds: core.getInput('ec2-instance-id'),
1516
iamRoleName: core.getInput('iam-role-name'),
1617
keyPairName: core.getInput('key-pair-name'),
1718
runnerHomeDir: core.getInput('runner-home-dir')
@@ -47,10 +48,29 @@ class Config {
4748
if (!this.input.ec2ImageId || !this.input.ec2InstanceType || !this.input.subnetId || !this.input.securityGroupId || !this.input.keyPairName) {
4849
throw new Error(`Not all the required inputs are provided for the 'start' mode`);
4950
}
51+
52+
if (this.input.ec2InstanceCount === undefined) {
53+
this.input.ec2InstanceCount = 1;
54+
}
55+
const parsedEc2InstanceCount = parseInt(this.input.ec2InstanceCount);
56+
if (isNaN(parsedEc2InstanceCount)) {
57+
throw new Error(`The 'ec2-instance-count' input has illegal value '${this.input.ec2InstanceCount}'`);
58+
} else if (parsedEc2InstanceCount < 1) {
59+
throw new Error(`The 'ec2-instance-count' input must be greater than zero`);
60+
}
61+
this.input.ec2InstanceCount = parsedEc2InstanceCount;
5062
} else if (this.input.mode === 'stop') {
51-
if (!this.input.label || !this.input.ec2InstanceId) {
63+
if (!this.input.label || !this.input.ec2InstanceIds) {
5264
throw new Error(`Not all the required inputs are provided for the 'stop' mode`);
5365
}
66+
67+
try {
68+
const parsedEc2InstanceIds = JSON.parse(this.input.ec2InstanceIds);
69+
this.input.ec2InstanceIds = parsedEc2InstanceIds;
70+
} catch (error) {
71+
core.info(`Got error ${error} when parsing '${this.input.ec2InstanceIds}' as JSON, assuming that it is a raw string containing a single EC2 instance ID`);
72+
this.input.ec2InstanceIds = [this.input.ec2InstanceIds];
73+
}
5474
} else {
5575
throw new Error('Wrong mode. Allowed values: start, stop.');
5676
}

0 commit comments

Comments
 (0)