1717import software .amazon .awscdk .services .cloudfront .ViewerProtocolPolicy ;
1818import software .amazon .awscdk .services .cloudfront .origins .HttpOrigin ;
1919import software .amazon .awscdk .services .cloudfront .origins .HttpOriginProps ;
20- import software .amazon .awscdk .services .ec2 .BlockDevice ;
21- import software .amazon .awscdk .services .ec2 .BlockDeviceVolume ;
2220import software .amazon .awscdk .services .ec2 .CfnEIP ;
2321import software .amazon .awscdk .services .ec2 .CfnEIPAssociation ;
24- import software .amazon .awscdk .services .ec2 .EbsDeviceOptions ;
25- import software .amazon .awscdk .services .ec2 .EbsDeviceVolumeType ;
2622import software .amazon .awscdk .services .ec2 .IMachineImage ;
2723import software .amazon .awscdk .services .ec2 .ISecurityGroup ;
2824import software .amazon .awscdk .services .ec2 .IVpc ;
29- import software .amazon .awscdk .services .ec2 .Instance ;
3025import software .amazon .awscdk .services .ec2 .InstanceClass ;
3126import software .amazon .awscdk .services .ec2 .InstanceSize ;
3227import software .amazon .awscdk .services .ec2 .InstanceType ;
@@ -80,7 +75,8 @@ public static class VSCodeIdeProps {
8075 private IVpc vpc ;
8176 private String availabilityZone ;
8277 private IMachineImage machineImage = MachineImage .latestAmazonLinux2023 ();
83- private InstanceType instanceType = InstanceType .of (InstanceClass .T3 , InstanceSize .MEDIUM );
78+ // private InstanceType instanceType = InstanceType.of(InstanceClass.T3, InstanceSize.MEDIUM);
79+ private List <String > instanceTypes = Arrays .asList ("m5.xlarge" , "m6i.xlarge" , "t3.xlarge" );
8480 private String codeServerVersion = "4.104.3" ;
8581 private List <IManagedPolicy > additionalIamPolicies = new ArrayList <>();
8682 private List <ISecurityGroup > additionalSecurityGroups = new ArrayList <>();
@@ -113,8 +109,11 @@ public static class VSCodeIdeProps {
113109 public IMachineImage getMachineImage () { return machineImage ; }
114110 public void setMachineImage (IMachineImage machineImage ) { this .machineImage = machineImage ; }
115111
116- public InstanceType getInstanceType () { return instanceType ; }
117- public void setInstanceType (InstanceType instanceType ) { this .instanceType = instanceType ; }
112+ // public InstanceType getInstanceType() { return instanceType; }
113+ // public void setInstanceType(InstanceType instanceType) { this.instanceType = instanceType; }
114+
115+ public List <String > getInstanceTypes () { return instanceTypes ; }
116+ public void setInstanceTypes (List <String > instanceTypes ) { this .instanceTypes = instanceTypes ; }
118117
119118 public String getCodeServerVersion () { return codeServerVersion ; }
120119 public void setCodeServerVersion (String codeServerVersion ) { this .codeServerVersion = codeServerVersion ; }
@@ -164,9 +163,11 @@ public VSCodeIde(final Construct scope, final String id, final VSCodeIdeProps pr
164163 throw new IllegalArgumentException ("VPC must be provided in the properties and cannot be null" );
165164 }
166165
167- if (props .getAvailabilityZone () == null ) {
168- props .setAvailabilityZone (props .getVpc ().getAvailabilityZones ().get (0 ));
169- }
166+ // Note: Commented out to allow AWS to choose an AZ with available capacity
167+ // instead of hardcoding to the first AZ which may not have capacity
168+ // if (props.getAvailabilityZone() == null) {
169+ // props.setAvailabilityZone(props.getVpc().getAvailabilityZones().get(0));
170+ // }
170171
171172 // Check IAM role
172173 if (props .getRole () == null ) {
@@ -275,52 +276,79 @@ public VSCodeIde(final Construct scope, final String id, final VSCodeIdeProps pr
275276 .domain ("vpc" )
276277 .build ();
277278
278- // Create EC2 instance
279- var ec2Instance = Instance .Builder .create (this , "IdeEC2Instance" )
280- .instanceName (props .getInstanceName ())
281- .vpc (props .getVpc ())
282- .machineImage (props .getMachineImage ())
283- .instanceType (props .getInstanceType ())
284- // .role(props.getRole())
285- .instanceProfile (instanceProfile )
286- .securityGroup (ideSecurityGroup )
287- .vpcSubnets (SubnetSelection .builder ()
288- .subnetType (SubnetType .PUBLIC )
289- .build ())
290- .blockDevices (List .of (BlockDevice .builder ()
291- .deviceName ("/dev/xvda" )
292- .volume (BlockDeviceVolume .ebs (props .getDiskSize (), EbsDeviceOptions .builder ()
293- .volumeType (EbsDeviceVolumeType .GP3 )
294- .deleteOnTermination (true )
295- .encrypted (true )
296- .build ()))
297- .build ()))
279+ // Create instance launcher Lambda with multi-AZ and multi-instance-type support
280+ Function instanceLauncherFunction = Function .Builder .create (this , "IdeInstanceLauncherFunction" )
281+ .code (Code .fromInline (loadFile ("/instance-launcher.py" )))
282+ .handler ("index.lambda_handler" )
283+ .runtime (Runtime .PYTHON_3_13 )
284+ .timeout (Duration .minutes (5 ))
285+ .functionName (props .getInstanceName () + "-instance-launcher" )
298286 .build ();
299287
300- // Associate Elastic IP with the instance
301- var ipAssociation = CfnEIPAssociation .Builder .create (this , "IdeEipAssociation" )
302- .allocationId (elasticIP .getAttrAllocationId ())
303- .instanceId (ec2Instance .getInstanceId ())
304- .build ();
288+ instanceLauncherFunction .addToRolePolicy (PolicyStatement .Builder .create ()
289+ .resources (List .of ("*" ))
290+ .actions (List .of (
291+ "ec2:RunInstances" ,
292+ "ec2:TerminateInstances" ,
293+ "ec2:CreateTags" ,
294+ "ec2:DescribeInstances" ,
295+ "ec2:DescribeSubnets"
296+ ))
297+ .build ());
298+
299+ instanceLauncherFunction .addToRolePolicy (PolicyStatement .Builder .create ()
300+ .resources (List .of (props .getRole ().getRoleArn (), instanceProfile .getInstanceProfileArn ()))
301+ .actions (List .of ("iam:PassRole" ))
302+ .build ());
305303
306- // Internal security group, allow traffic only between members
304+ // Get public subnets
305+ var publicSubnets = props .getVpc ().selectSubnets (SubnetSelection .builder ()
306+ .subnetType (SubnetType .PUBLIC )
307+ .build ());
308+
309+ // Internal security group (created before instance)
307310 ideInternalSecurityGroup = SecurityGroup .Builder .create (this , "IdeInternalSecurityGroup" )
308311 .vpc (props .getVpc ())
309312 .allowAllOutbound (false )
310313 .securityGroupName (props .getInstanceName () + "-internal-sg" )
311314 .description ("IDE internal security group" )
312315 .build ();
313- // Add ingress rule to allow all traffic from within the same security group
314316 ideInternalSecurityGroup .getConnections ().allowInternally (
315317 Port .allTraffic (),
316318 "Allow all internal traffic"
317319 );
318- ec2Instance .addSecurityGroup (ideInternalSecurityGroup );
320+
321+ // Build security group IDs list
322+ List <String > securityGroupIds = new ArrayList <>();
323+ securityGroupIds .add (ideSecurityGroup .getSecurityGroupId ());
324+ securityGroupIds .add (ideInternalSecurityGroup .getSecurityGroupId ());
319325 if (props .getAppPort () > 0 ) {
320- ec2Instance . addSecurityGroup (appSecurityGroup );
326+ securityGroupIds . add (appSecurityGroup . getSecurityGroupId () );
321327 }
322- // Add additional security groups if any
323- props .getAdditionalSecurityGroups ().forEach (sg -> ec2Instance .addSecurityGroup (sg ));
328+ props .getAdditionalSecurityGroups ().forEach (sg -> securityGroupIds .add (sg .getSecurityGroupId ()));
329+
330+ // Create EC2 instance via Custom Resource with failover support
331+ CustomResource ec2InstanceResource = CustomResource .Builder .create (this , "IdeEC2InstanceResource" )
332+ .serviceToken (instanceLauncherFunction .getFunctionArn ())
333+ .properties (Map .of (
334+ "SubnetIds" , String .join ("," , publicSubnets .getSubnetIds ()),
335+ "InstanceTypes" , String .join ("," , props .getInstanceTypes ()),
336+ "ImageId" , props .getMachineImage ().getImage (this ).getImageId (),
337+ "SecurityGroupIds" , String .join ("," , securityGroupIds ),
338+ "IamInstanceProfileArn" , instanceProfile .getInstanceProfileArn (),
339+ "VolumeSize" , String .valueOf (props .getDiskSize ()),
340+ "InstanceName" , props .getInstanceName (),
341+ "UserData" , Fn .base64 ("#!/bin/bash" )
342+ ))
343+ .build ();
344+
345+ String instanceId = ec2InstanceResource .getAttString ("InstanceId" );
346+
347+ // Associate Elastic IP with the instance
348+ var ipAssociation = CfnEIPAssociation .Builder .create (this , "IdeEipAssociation" )
349+ .allocationId (elasticIP .getAttrAllocationId ())
350+ .instanceId (instanceId )
351+ .build ();
324352
325353 // Set up wait condition
326354 var waitHandle = CfnWaitConditionHandle .Builder .create (this , "IdeBootstrapWaitConditionHandle" )
@@ -331,12 +359,26 @@ public VSCodeIde(final Construct scope, final String id, final VSCodeIdeProps pr
331359 .handle (waitHandle .getRef ())
332360 .timeout (String .valueOf (props .getBootstrapTimeoutMinutes () * 60 ))
333361 .build ();
334- waitCondition .getNode ().addDependency (ec2Instance );
362+ waitCondition .getNode ().addDependency (ec2InstanceResource );
363+
364+ // Get public DNS name from EIP (after association) using Fn::GetAtt
365+ // This ensures we get the correct DNS after EIP is associated
366+ String publicDnsName = Fn .join ("" , List .of (
367+ "ec2-" ,
368+ Fn .select (0 , Fn .split ("." , Fn .select (0 , Fn .split ("." , elasticIP .getAttrPublicIp ())))),
369+ "-" ,
370+ Fn .select (1 , Fn .split ("." , elasticIP .getAttrPublicIp ())),
371+ "-" ,
372+ Fn .select (2 , Fn .split ("." , elasticIP .getAttrPublicIp ())),
373+ "-" ,
374+ Fn .select (3 , Fn .split ("." , elasticIP .getAttrPublicIp ())),
375+ ".compute-1.amazonaws.com"
376+ ));
335377
336378 // Create CloudFront distribution
337379 var distribution = Distribution .Builder .create (this , "IdeDistribution" )
338380 .defaultBehavior (BehaviorOptions .builder ()
339- .origin (new HttpOrigin (ec2Instance . getInstancePublicDnsName () ,
381+ .origin (new HttpOrigin (publicDnsName ,
340382 HttpOriginProps .builder ()
341383 .protocolPolicy (OriginProtocolPolicy .HTTP_ONLY )
342384 .httpPort (80 )
@@ -348,22 +390,6 @@ public VSCodeIde(final Construct scope, final String id, final VSCodeIdeProps pr
348390 .build ())
349391 .httpVersion (HttpVersion .HTTP2 )
350392 .build ();
351- // if (props.getAppPort() > 0) {
352- // distribution.addBehavior(
353- // "/app/*",
354- // new HttpOrigin(ec2Instance.getInstancePublicDnsName(),
355- // HttpOriginProps.builder()
356- // .protocolPolicy(OriginProtocolPolicy.HTTP_ONLY)
357- // .httpPort(props.getAppPort())
358- // .build()),
359- // AddBehaviorOptions.builder()
360- // .allowedMethods(AllowedMethods.ALLOW_ALL)
361- // .cachePolicy(CachePolicy.CACHING_DISABLED)
362- // .originRequestPolicy(OriginRequestPolicy.ALL_VIEWER)
363- // .viewerProtocolPolicy(ViewerProtocolPolicy.ALLOW_ALL)
364- // .build()
365- // );
366- // }
367393 distribution .applyRemovalPolicy (RemovalPolicy .DESTROY );
368394 distribution .getNode ().addDependency (ipAssociation );
369395
@@ -386,7 +412,7 @@ public VSCodeIde(final Construct scope, final String id, final VSCodeIdeProps pr
386412 .build ())
387413 .secretName (props .getInstanceName () + "-password-lambda" )
388414 .build ();
389- ec2Instance .getNode ().addDependency (ideSecretsManagerPassword );
415+ ec2InstanceResource .getNode ().addDependency (ideSecretsManagerPassword );
390416
391417 ideSecretsManagerPassword .grantRead (props .getRole ());
392418 var outputIdePassword = CfnOutput .Builder .create (this , "IdePassword" )
@@ -474,7 +500,7 @@ public VSCodeIde(final Construct scope, final String id, final VSCodeIdeProps pr
474500 CustomResource .Builder .create (this , "IdeBootstrapResource" )
475501 .serviceToken (bootstrapFunction .getFunctionArn ())
476502 .properties (Map .of (
477- "InstanceId" , ec2Instance . getInstanceId () ,
503+ "InstanceId" , instanceId ,
478504 "SsmDocument" , ssmDocument .getRef (),
479505 "LogGroupName" , logGroup .getLogGroupName ()
480506 ))
0 commit comments