Skip to content

Commit 86eb364

Browse files
yogeekclaude
andauthored
Add option to enable nested virtualization on EC2 agents (#2012)
Adds a `nestedVirtualizationEnabled` option to the EC2 agent template. When enabled, instances are launched with `CpuOptions.NestedVirtualization=ENABLED`, allowing the agent to run a hypervisor (KVM or Hyper-V) and start nested VMs. This requires AWS SDK >= 2.42.33 (CpuOptionsRequest#nestedVirtualization), so the aws-java-sdk2 plugin is pinned to a release providing it. That release still targets Jenkins core 2.479.1, so the plugin baseline does not change. Closes #1996 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0517679 commit 86eb364

6 files changed

Lines changed: 199 additions & 0 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ def slaveTemplateUsEast1Parameters = [
352352
metadataTokensRequired: true, // `true` enforces IMDSv2 only (over IMDSv1), an important AWS security best practice
353353
metadataHopsLimit: 1,
354354
enclaveEnabled: true, // launches the instance with Nitro Enclave enabled
355+
nestedVirtualizationEnabled: false, // launches the instance with nested virtualization enabled (set via setter, see below)
355356
minimumNumberOfInstances: 0,
356357
minimumNumberOfSpareInstances: 0,
357358
maxTotalUses: -1,
@@ -464,6 +465,9 @@ SlaveTemplate slaveTemplateUsEast1 = new SlaveTemplate(
464465
slaveTemplateUsEast1Parameters.enclaveEnabled,
465466
)
466467
468+
// Optional settings exposed as setters rather than constructor arguments
469+
slaveTemplateUsEast1.setNestedVirtualizationEnabled(slaveTemplateUsEast1Parameters.nestedVirtualizationEnabled)
470+
467471
// https://javadoc.jenkins.io/plugin/ec2/hudson/plugins/ec2/EC2Cloud.html
468472
EC2Cloud ec2Cloud = new EC2Cloud(
469473
EC2CloudParameters.name,

pom.xml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,24 @@ THE SOFTWARE.
9494
<type>pom</type>
9595
<scope>import</scope>
9696
</dependency>
97+
<!--
98+
Override the aws-java-sdk2 plugin (which bundles the AWS SDK v2) to a release providing
99+
AWS SDK >= 2.42.33. That version adds CpuOptionsRequest#nestedVirtualization, required to
100+
launch instances with nested virtualization enabled. This release still targets Jenkins
101+
core 2.479.1, so the plugin baseline does not need to change. These direct entries take
102+
precedence over the version managed by the imported bom and can be dropped once the Jenkins
103+
BOM baseline ships this aws-java-sdk2 version.
104+
-->
105+
<dependency>
106+
<groupId>io.jenkins.plugins.aws-java-sdk2</groupId>
107+
<artifactId>aws-java-sdk2-core</artifactId>
108+
<version>2.42.33-70.vd69c0763fa_60</version>
109+
</dependency>
110+
<dependency>
111+
<groupId>io.jenkins.plugins.aws-java-sdk2</groupId>
112+
<artifactId>aws-java-sdk2-ec2</artifactId>
113+
<version>2.42.33-70.vd69c0763fa_60</version>
114+
</dependency>
97115
</dependencies>
98116
</dependencyManagement>
99117

src/main/java/hudson/plugins/ec2/SlaveTemplate.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
import software.amazon.awssdk.services.ec2.Ec2Client;
9090
import software.amazon.awssdk.services.ec2.model.BlockDeviceMapping;
9191
import software.amazon.awssdk.services.ec2.model.CancelSpotInstanceRequestsRequest;
92+
import software.amazon.awssdk.services.ec2.model.CpuOptionsRequest;
9293
import software.amazon.awssdk.services.ec2.model.CreateTagsRequest;
9394
import software.amazon.awssdk.services.ec2.model.CreditSpecificationRequest;
9495
import software.amazon.awssdk.services.ec2.model.DescribeImagesRequest;
@@ -119,6 +120,7 @@
119120
import software.amazon.awssdk.services.ec2.model.InstanceTypeHypervisor;
120121
import software.amazon.awssdk.services.ec2.model.InstanceTypeInfo;
121122
import software.amazon.awssdk.services.ec2.model.MarketType;
123+
import software.amazon.awssdk.services.ec2.model.NestedVirtualizationSpecification;
122124
import software.amazon.awssdk.services.ec2.model.NitroEnclavesSupport;
123125
import software.amazon.awssdk.services.ec2.model.Placement;
124126
import software.amazon.awssdk.services.ec2.model.RequestSpotInstancesRequest;
@@ -254,6 +256,8 @@ public class SlaveTemplate implements Describable<SlaveTemplate> {
254256

255257
private Boolean enclaveEnabled;
256258

259+
private Boolean nestedVirtualizationEnabled;
260+
257261
private transient /* almost final */ Set<LabelAtom> labelSet;
258262

259263
private transient /* almost final */ Set<String> securityGroupSet;
@@ -2074,6 +2078,15 @@ public Boolean getEnclaveEnabled() {
20742078
return enclaveEnabled;
20752079
}
20762080

2081+
public boolean getNestedVirtualizationEnabled() {
2082+
return nestedVirtualizationEnabled != null && nestedVirtualizationEnabled;
2083+
}
2084+
2085+
@DataBoundSetter
2086+
public void setNestedVirtualizationEnabled(boolean nestedVirtualizationEnabled) {
2087+
this.nestedVirtualizationEnabled = nestedVirtualizationEnabled;
2088+
}
2089+
20772090
public DescribableList<NodeProperty<?>, NodePropertyDescriptor> getNodeProperties() {
20782091
return Objects.requireNonNull(nodeProperties);
20792092
}
@@ -2323,6 +2336,13 @@ HashMap<RunInstancesRequest, List<Filter>> makeRunInstancesRequestAndFilters(
23232336
riRequestBuilder.enclaveOptions(enclaveOptionsRequestBuilder.build());
23242337
}
23252338

2339+
if (getNestedVirtualizationEnabled()) {
2340+
riRequestBuilder.cpuOptions(CpuOptionsRequest.builder()
2341+
.nestedVirtualization(NestedVirtualizationSpecification.ENABLED)
2342+
.build());
2343+
logProvisionInfo("Setting CPU Options: NestedVirtualization=enabled");
2344+
}
2345+
23262346
HashMap<RunInstancesRequest, List<Filter>> ret = new HashMap<>();
23272347
ret.put(riRequestBuilder.build(), diFilters);
23282348
return ret;
@@ -3744,5 +3764,15 @@ public FormValidation doCheckEnclaveEnabled(
37443764
}
37453765
return FormValidation.ok();
37463766
}
3767+
3768+
public FormValidation doCheckNestedVirtualizationEnabled(@QueryParameter boolean nestedVirtualizationEnabled) {
3769+
if (nestedVirtualizationEnabled) {
3770+
return FormValidation.warning(
3771+
"Nested virtualization is only supported on specific instance families (for example C8i, M8i, R8i)"
3772+
+ " and requires an AMI with a compatible L1 hypervisor (KVM or Hyper-V)."
3773+
+ " Enabling it automatically disables Virtual Secure Mode (VSM).");
3774+
}
3775+
return FormValidation.ok();
3776+
}
37473777
}
37483778
}

src/main/resources/hudson/plugins/ec2/SlaveTemplate/config.jelly

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,10 @@ THE SOFTWARE.
251251
<f:checkbox default="false"/>
252252
</f:entry>
253253

254+
<f:entry title="${%Nested Virtualization Enabled}" field="nestedVirtualizationEnabled">
255+
<f:checkbox default="false"/>
256+
</f:entry>
257+
254258
</f:advanced>
255259

256260
<f:entry title="">
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<div>
2+
<p>
3+
If checked, the instance is launched with nested virtualization enabled, allowing it to run a
4+
hypervisor (KVM or Hyper-V) and start nested virtual machines.
5+
</p>
6+
<p>
7+
Nested virtualization is only supported on specific instance families (for example C8i, M8i and
8+
R8i) and requires an AMI with a compatible L1 hypervisor. Leave it unchecked for instance types
9+
or AMIs that do not support it. Enabling nested virtualization automatically disables Virtual
10+
Secure Mode (VSM).
11+
</p>
12+
<p>
13+
See
14+
<a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/amazon-ec2-nested-virtualization.html" rel="noopener noreferrer" target="_blank">
15+
Use nested virtualization to run hypervisors in Amazon EC2 instances</a>
16+
for details.
17+
</p>
18+
</div>

src/test/java/hudson/plugins/ec2/SlaveTemplateTest.java

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
import software.amazon.awssdk.services.ec2.model.InstanceStateName;
7575
import software.amazon.awssdk.services.ec2.model.InstanceType;
7676
import software.amazon.awssdk.services.ec2.model.KeyPairInfo;
77+
import software.amazon.awssdk.services.ec2.model.NestedVirtualizationSpecification;
7778
import software.amazon.awssdk.services.ec2.model.RequestSpotInstancesRequest;
7879
import software.amazon.awssdk.services.ec2.model.Reservation;
7980
import software.amazon.awssdk.services.ec2.model.RunInstancesRequest;
@@ -1610,6 +1611,130 @@ void provisionOnDemandWithEnclaveEnabled() throws Exception {
16101611
assertEquals(Boolean.TRUE, enclaveOptionsRequest.enabled());
16111612
}
16121613

1614+
@Test
1615+
void provisionOnDemandWithNestedVirtualizationEnabled() throws Exception {
1616+
SlaveTemplate template = new SlaveTemplate(
1617+
TEST_AMI,
1618+
TEST_ZONE,
1619+
TEST_SPOT_CFG,
1620+
TEST_SEC_GROUPS,
1621+
TEST_REMOTE_FS,
1622+
TEST_INSTANCE_TYPE.toString(),
1623+
TEST_EBSO,
1624+
TEST_LABEL,
1625+
Node.Mode.NORMAL,
1626+
"",
1627+
"bar",
1628+
"bbb",
1629+
"aaa",
1630+
"10",
1631+
"fff",
1632+
null,
1633+
"java",
1634+
"-Xmx1g",
1635+
false,
1636+
"subnet 456",
1637+
null,
1638+
null,
1639+
0,
1640+
0,
1641+
null,
1642+
"",
1643+
true,
1644+
false,
1645+
"",
1646+
AssociateIPStrategy.SUBNET,
1647+
"",
1648+
true,
1649+
false,
1650+
false,
1651+
ConnectionStrategy.PUBLIC_IP,
1652+
-1,
1653+
Collections.emptyList(),
1654+
null,
1655+
Tenancy.Default,
1656+
EbsEncryptRootVolume.DEFAULT,
1657+
null,
1658+
true,
1659+
null,
1660+
true,
1661+
false);
1662+
template.setNestedVirtualizationEnabled(true);
1663+
1664+
Ec2Client mockedEC2 = setupTestForProvisioning(template);
1665+
1666+
ArgumentCaptor<RunInstancesRequest> riRequestCaptor = ArgumentCaptor.forClass(RunInstancesRequest.class);
1667+
1668+
template.provision(2, EnumSet.noneOf(ProvisionOptions.class));
1669+
verify(mockedEC2).runInstances(riRequestCaptor.capture());
1670+
1671+
RunInstancesRequest actualRequest = riRequestCaptor.getValue();
1672+
assertNotNull(actualRequest.cpuOptions());
1673+
assertEquals(
1674+
NestedVirtualizationSpecification.ENABLED,
1675+
actualRequest.cpuOptions().nestedVirtualization());
1676+
}
1677+
1678+
@Test
1679+
void provisionOnDemandWithoutNestedVirtualizationEnabled() throws Exception {
1680+
SlaveTemplate template = new SlaveTemplate(
1681+
TEST_AMI,
1682+
TEST_ZONE,
1683+
TEST_SPOT_CFG,
1684+
TEST_SEC_GROUPS,
1685+
TEST_REMOTE_FS,
1686+
TEST_INSTANCE_TYPE.toString(),
1687+
TEST_EBSO,
1688+
TEST_LABEL,
1689+
Node.Mode.NORMAL,
1690+
"",
1691+
"bar",
1692+
"bbb",
1693+
"aaa",
1694+
"10",
1695+
"fff",
1696+
null,
1697+
"java",
1698+
"-Xmx1g",
1699+
false,
1700+
"subnet 456",
1701+
null,
1702+
null,
1703+
0,
1704+
0,
1705+
null,
1706+
"",
1707+
true,
1708+
false,
1709+
"",
1710+
AssociateIPStrategy.SUBNET,
1711+
"",
1712+
true,
1713+
false,
1714+
false,
1715+
ConnectionStrategy.PUBLIC_IP,
1716+
-1,
1717+
Collections.emptyList(),
1718+
null,
1719+
Tenancy.Default,
1720+
EbsEncryptRootVolume.DEFAULT,
1721+
null,
1722+
true,
1723+
null,
1724+
true,
1725+
false);
1726+
1727+
Ec2Client mockedEC2 = setupTestForProvisioning(template);
1728+
1729+
ArgumentCaptor<RunInstancesRequest> riRequestCaptor = ArgumentCaptor.forClass(RunInstancesRequest.class);
1730+
1731+
template.provision(2, EnumSet.noneOf(ProvisionOptions.class));
1732+
verify(mockedEC2).runInstances(riRequestCaptor.capture());
1733+
1734+
RunInstancesRequest actualRequest = riRequestCaptor.getValue();
1735+
assertNull(actualRequest.cpuOptions());
1736+
}
1737+
16131738
@Test
16141739
void testWindowsSSHConfigRoundTrip() throws Exception {
16151740
String description = "foo ami";

0 commit comments

Comments
 (0)