Skip to content

Commit 835fa99

Browse files
authored
Add support for resizing root volume (#17)
1 parent b81e663 commit 835fa99

File tree

8 files changed

+148
-5
lines changed

8 files changed

+148
-5
lines changed

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ INPUT_GITHUB_ACTION_RUNNER_VERSION=
1313
INPUT_GITHUB_ACTION_RUNNER_LABEL=
1414
INPUT_EC2_INSTANCE_TYPE=
1515
INPUT_EC2_AMI_ID=
16+
INPUT_EC2_ROOT_DISK_SIZE_GB=
17+
INPUT_EC2_ROOT_DISK_EBS_CLASS=
1618
INPUT_EC2_INSTANCE_IAM_ROLE=
1719
INPUT_EC2_INSTANCE_TAGS=
1820
INPUT_EC2_INSTANCE_TTL=

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ Sources:
4040
- [GH Action Runner Pricing](https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#per-minute-rates)
4141

4242
### Customizable Machine Image
43-
Users can provide their own custom AMI image pre-loaded with all the necessary tooling of their choice saving time and cost.
43+
- Set custom EC2 root volume size
44+
- Set custom storage class for root volume
45+
- Users can provide their own custom AMI image pre-loaded with all the necessary tooling of their choice saving time and cost.
4446

4547
### Enhance Security
4648
- EC2 instances run within your infrastructure
@@ -153,9 +155,11 @@ jobs:
153155
github_action_runner_version: v2.300.2 # Optional (default is latest release)
154156
ec2_instance_type: c5.4xlarge
155157
ec2_ami_id: ami-008fe2fc65df48dac
158+
ec2_root_disk_size_gb: "100" # Optional - (defaults to AMI settings)
159+
ec2_root_disk_ebs_class: "gp2" # Optional - Only used with custom volume root size (defaults to gp2)
156160
ec2_subnet_id: "SUBNET_ID_REDACTED"
157161
ec2_security_group_id: "SECURITY_GROUP_ID_REDACTED"
158-
ec2_instance_ttl: 40 # Optional (default is 60 minutes)
162+
ec2_instance_ttl: 40 # Optional - (default is 60 minutes)
159163
ec2_spot_instance_strategy: MaxPerformance # Other options are: None, BestEffort, MaxPerformance
160164
ec2_instance_tags: > # Required for IAM role resource permission scoping
161165
[

action.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ inputs:
3636
ec2_ami_id:
3737
description: 'Ec2 ami ID'
3838
required: true
39+
ec2_root_disk_size_gb:
40+
description: 'Ec2 root disk size (defaults to AMI setting)'
41+
required: false
42+
default: "0"
43+
ec2_root_disk_ebs_class:
44+
description: 'Ec2 root disk storage class (defaults to gp2)'
45+
required: false
46+
default: "gp2"
3947
ec2_instance_iam_role:
4048
description: 'IAM role for to associate with ec2 instance'
4149
required: false

dist/index.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ class ActionConfig {
5252
// Ec2 params
5353
this.ec2InstanceType = core.getInput("ec2_instance_type");
5454
this.ec2AmiId = core.getInput("ec2_ami_id");
55+
this.ec2InstanceRootDiskSizeGB = core.getInput("ec2_root_disk_size_gb");
56+
this.ec2InstanceRootDiskEbsClass = core.getInput("ec2_root_disk_ebs_class");
5557
this.ec2InstanceIamRole = core.getInput("ec2_instance_iam_role");
5658
this.ec2InstanceTags = core.getInput("ec2_instance_tags");
5759
this.ec2InstanceTtl = core.getInput("ec2_instance_ttl");
@@ -353,7 +355,26 @@ class Ec2Instance {
353355
},
354356
],
355357
UserData: yield userData.getUserData(),
358+
BlockDeviceMappings: undefined
356359
};
360+
// Add EBS volume if one was requested
361+
const sizeGB = parseInt(this.config.ec2InstanceRootDiskSizeGB.trim(), 10) || 0;
362+
if (sizeGB > 0) {
363+
const deviceInfo = yield this.getRootDeviceInfo(this.config.ec2AmiId);
364+
if (!deviceInfo || !(deviceInfo === null || deviceInfo === void 0 ? void 0 : deviceInfo.isEbs)) {
365+
throw Error(`${this.config.ec2AmiId} must support EBS as volume type`);
366+
}
367+
params.BlockDeviceMappings = [
368+
{
369+
DeviceName: deviceInfo.deviceName,
370+
Ebs: {
371+
VolumeSize: Number(this.config.ec2InstanceRootDiskSizeGB),
372+
VolumeType: this.config.ec2InstanceRootDiskEbsClass,
373+
DeleteOnTermination: true // Ensure volume is deleted on termination
374+
}
375+
}
376+
];
377+
}
357378
switch (ec2SpotInstanceStrategy.toLowerCase()) {
358379
case "spotonly": {
359380
params.InstanceMarketOptions = {
@@ -473,6 +494,29 @@ class Ec2Instance {
473494
}
474495
});
475496
}
497+
getRootDeviceInfo(amiId) {
498+
return __awaiter(this, void 0, void 0, function* () {
499+
const client = yield this.getEc2Client();
500+
try {
501+
const command = new client_ec2_1.DescribeImagesCommand({ ImageIds: [amiId.trim()] });
502+
const response = yield client.send(command);
503+
if (response.Images && response.Images.length > 0) {
504+
const image = response.Images[0];
505+
if (image.RootDeviceName && image.RootDeviceType) {
506+
return {
507+
deviceName: image.RootDeviceName,
508+
isEbs: image.RootDeviceType.includes("ebs")
509+
};
510+
}
511+
return { deviceName: "", isEbs: false };
512+
}
513+
}
514+
catch (error) {
515+
core.error("Error querying AMI information:", error);
516+
throw error;
517+
}
518+
});
519+
}
476520
}
477521
exports.Ec2Instance = Ec2Instance;
478522

docs/CrossAccountIAM.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"ec2:DescribeSubnets",
2121
"ec2:describeSpotPriceHistory",
2222
"pricing:GetProducts",
23-
"pricing:GetAttributeValues"
23+
"pricing:GetAttributeValues",
24+
"ec2:DescribeImages"
2425
],
2526
"Resource": "*",
2627
"Effect": "Allow"

src/config/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export interface ConfigInterface {
1818

1919
ec2InstanceType: string;
2020
ec2AmiId: string;
21+
ec2InstanceRootDiskSizeGB: string;
22+
ec2InstanceRootDiskEbsClass: string;
2123
ec2InstanceIamRole: string;
2224
ec2InstanceTags: string;
2325
ec2InstanceTtl: string;
@@ -43,6 +45,8 @@ export class ActionConfig implements ConfigInterface {
4345

4446
ec2InstanceType: string;
4547
ec2AmiId: string;
48+
ec2InstanceRootDiskSizeGB: string;
49+
ec2InstanceRootDiskEbsClass: string;
4650
ec2InstanceIamRole: string;
4751
ec2InstanceTags: string;
4852
ec2InstanceTtl: string;
@@ -72,6 +76,8 @@ export class ActionConfig implements ConfigInterface {
7276
// Ec2 params
7377
this.ec2InstanceType = core.getInput("ec2_instance_type");
7478
this.ec2AmiId = core.getInput("ec2_ami_id");
79+
this.ec2InstanceRootDiskSizeGB = core.getInput("ec2_root_disk_size_gb");
80+
this.ec2InstanceRootDiskEbsClass = core.getInput("ec2_root_disk_ebs_class");
7581
this.ec2InstanceIamRole = core.getInput("ec2_instance_iam_role");
7682
this.ec2InstanceTags = core.getInput("ec2_instance_tags");
7783
this.ec2InstanceTtl = core.getInput("ec2_instance_ttl");

src/ec2/ec2.ts

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import { ConfigInterface } from "../config/config";
22
import * as _ from "lodash";
33
import { AwsCredentialIdentity } from "@smithy/types";
4-
import { EC2, waitUntilInstanceRunning, DescribeSpotPriceHistoryCommandInput, _InstanceType, RunInstancesCommandInput } from "@aws-sdk/client-ec2";
4+
import {
5+
EC2,
6+
waitUntilInstanceRunning,
7+
DescribeSpotPriceHistoryCommandInput,
8+
_InstanceType,
9+
RunInstancesCommandInput,
10+
DescribeImagesCommand
11+
} from "@aws-sdk/client-ec2";
512
import { STS } from "@aws-sdk/client-sts";
613
import * as core from "@actions/core";
714
import { UserData } from "./userdata";
815
import { Ec2Pricing } from "./pricing";
16+
import {VolumeType} from "@aws-sdk/client-ec2/dist-types/models/models_1";
917

1018
interface Tag {
1119
Key: string;
@@ -289,8 +297,30 @@ export class Ec2Instance {
289297
},
290298
],
291299
UserData: await userData.getUserData(),
300+
BlockDeviceMappings: undefined
292301
};
293302

303+
// Add EBS volume if one was requested
304+
const sizeGB = parseInt(this.config.ec2InstanceRootDiskSizeGB.trim(), 10) || 0;
305+
if (sizeGB > 0) {
306+
const deviceInfo = await this.getRootDeviceInfo(this.config.ec2AmiId);
307+
308+
if (!deviceInfo || !deviceInfo?.isEbs) {
309+
throw Error(`${this.config.ec2AmiId} must support EBS as volume type`);
310+
}
311+
312+
params.BlockDeviceMappings = [
313+
{
314+
DeviceName: deviceInfo.deviceName,
315+
Ebs: {
316+
VolumeSize: Number(this.config.ec2InstanceRootDiskSizeGB),
317+
VolumeType: this.config.ec2InstanceRootDiskEbsClass as VolumeType,
318+
DeleteOnTermination: true // Ensure volume is deleted on termination
319+
}
320+
}
321+
]
322+
}
323+
294324
switch (ec2SpotInstanceStrategy.toLowerCase()) {
295325
case "spotonly": {
296326
params.InstanceMarketOptions = {
@@ -406,12 +436,35 @@ export class Ec2Instance {
406436
async terminateInstances(instanceId: string) {
407437
const client = await this.getEc2Client();
408438
try {
409-
await client.terminateInstances({ InstanceIds: [instanceId] });
439+
await client.terminateInstances({InstanceIds: [instanceId]});
410440
core.info(`AWS EC2 instance ${instanceId} is terminated`);
411441
return;
412442
} catch (error) {
413443
core.error(`Failed terminate instance ${instanceId}`);
414444
throw error;
415445
}
416446
}
447+
448+
async getRootDeviceInfo(amiId: string): Promise<{ deviceName: string, isEbs: boolean } | undefined> {
449+
const client = await this.getEc2Client();
450+
451+
try {
452+
const command = new DescribeImagesCommand({ImageIds: [amiId.trim()]});
453+
const response = await client.send(command);
454+
455+
if (response.Images && response.Images.length > 0) {
456+
const image = response.Images[0];
457+
if (image.RootDeviceName && image.RootDeviceType) {
458+
return {
459+
deviceName: image.RootDeviceName,
460+
isEbs: image.RootDeviceType.includes("ebs")
461+
}
462+
}
463+
return {deviceName: "", isEbs: false}
464+
}
465+
} catch (error) {
466+
core.error("Error querying AMI information:", error);
467+
throw error;
468+
}
469+
}
417470
}

tests/ec2/ec2.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,30 @@ describe('EC2 lib tests', () => {
3232
expect(nextInstanceType).to.include("c5")
3333
});
3434

35+
it('get root volume device name for AMI', async () => {
36+
37+
for (const amiID of
38+
["ami-0e30b3388d74cee6d", // Ubuntu
39+
"ami-0c2644caf041bb6de", // Debian
40+
"ami-089d88d106dd8e9b5"]// Amazon Linux
41+
) {
42+
// Get device info
43+
let deviceInfo = await ec2.getRootDeviceInfo(amiID);
44+
expect(deviceInfo).is.not.undefined
45+
expect(deviceInfo!.isEbs).is.not.false
46+
expect(deviceInfo!.deviceName).is.not.undefined
47+
expect(deviceInfo!.deviceName).is.string
48+
expect(deviceInfo!.deviceName).to.include("/dev/xvda")
49+
}
50+
51+
52+
// Windows
53+
let deviceInfo = await ec2.getRootDeviceInfo("ami-0a335fb413d7589ee ");
54+
expect(deviceInfo).is.not.undefined
55+
expect(deviceInfo!.isEbs).is.not.false
56+
expect(deviceInfo!.deviceName).is.not.undefined
57+
expect(deviceInfo!.deviceName).is.string
58+
expect(deviceInfo!.deviceName).to.include("/dev/sda1")
59+
});
3560

3661
});

0 commit comments

Comments
 (0)