@@ -56,6 +56,7 @@ public static class IdeProps {
5656 "ms-azuretools.vscode-docker"
5757 );
5858 private String gitBranch = "main" ;
59+ private String templateType = "base" ;
5960
6061 public static IdeProps .Builder builder () { return new Builder (); }
6162
@@ -71,6 +72,7 @@ public static class Builder {
7172 public Builder bootstrapTimeoutMinutes (int bootstrapTimeoutMinutes ) { props .bootstrapTimeoutMinutes = bootstrapTimeoutMinutes ; return this ; }
7273 public Builder vscodeExtensions (List <String > vscodeExtensions ) { props .vscodeExtensions = vscodeExtensions ; return this ; }
7374 public Builder gitBranch (String gitBranch ) { props .gitBranch = gitBranch ; return this ; }
75+ public Builder templateType (String templateType ) { props .templateType = templateType ; return this ; }
7476
7577 public IdeProps build () { return props ; }
7678 }
@@ -85,6 +87,7 @@ public static class Builder {
8587 public int getBootstrapTimeoutMinutes () { return bootstrapTimeoutMinutes ; }
8688 public List <String > getVscodeExtensions () { return vscodeExtensions ; }
8789 public String getGitBranch () { return gitBranch ; }
90+ public String getTemplateType () { return templateType ; }
8891 }
8992
9093 public Ide (final Construct scope , final String id , final IVpc vpc ) {
@@ -241,14 +244,120 @@ public Ide(final Construct scope, final String id, final IdeProps props) {
241244 // Create User Data for bootstrap with CloudWatch logging
242245 var userData = UserData .forLinux ();
243246 String extensionsString = String .join ("," , props .getVscodeExtensions ());
244- String bootstrapScript = loadFile ("/ec2-userdata.sh" )
245- .replace ("${vscodeExtensions}" , extensionsString )
246- .replace ("${templateType}" , "base" )
247- .replace ("${gitBranch}" , props .getGitBranch ())
248- .replace ("${stackName}" , Aws .STACK_NAME )
249- .replace ("${awsRegion}" , Aws .REGION )
250- .replace ("${idePassword}" , ideSecretsManagerPassword .secretValueFromJson ("password" ).unsafeUnwrap ());
251- userData .addCommands (bootstrapScript .split ("\n " ));
247+ String gitBranch = props .getGitBranch ();
248+ String templateType = props .getTemplateType ();
249+
250+ // Build UserData content with proper substitutions
251+ String userDataContent = String .format ("""
252+ #!/bin/bash
253+ set -e
254+
255+ # Minimal EC2 UserData script - downloads and runs full bootstrap
256+ # This keeps UserData under size limits while allowing unlimited bootstrap size
257+
258+ # Configuration from CDK
259+ GIT_BRANCH="%s"
260+ IDE_PASSWORD="%s"
261+ STACK_NAME="%s"
262+ AWS_REGION="%s"
263+ TEMPLATE_TYPE="%s"
264+ VSCODE_EXTENSIONS="%s"
265+
266+ # Setup logging
267+ LOG_GROUP_NAME="ide-bootstrap-$(date +%%Y%%m%%d-%%H%%M%%S)"
268+ echo "Bootstrap logs will be written to CloudWatch log group: $LOG_GROUP_NAME"
269+
270+ # Install CloudWatch agent for logging
271+ dnf install -y amazon-cloudwatch-agent
272+
273+ # Create CloudWatch agent configuration
274+ cat > /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json << EOF
275+ {
276+ "logs": {
277+ "logs_collected": {
278+ "files": {
279+ "collect_list": [
280+ {
281+ "file_path": "/var/log/bootstrap.log",
282+ "log_group_name": "$LOG_GROUP_NAME",
283+ "log_stream_name": "{instance_id}",
284+ "retention_in_days": 7
285+ }
286+ ]
287+ }
288+ }
289+ }
290+ }
291+ EOF
292+
293+ # Start CloudWatch agent
294+ /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \\
295+ -a fetch-config -m ec2 -c file:/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json -s
296+
297+ # Redirect all output to log file and console
298+ exec > >(tee -a /var/log/bootstrap.log)
299+ exec 2>&1
300+
301+ echo "UserData started at $(date) - Logging to $LOG_GROUP_NAME"
302+
303+ # Download and run full bootstrap script with retry logic
304+ download_bootstrap() {
305+ local urls=(
306+ "https://raw.githubusercontent.com/aws-samples/java-on-aws/${GIT_BRANCH}/infra/scripts/ide/bootstrap.sh"
307+ "https://github.com/aws-samples/java-on-aws/raw/${GIT_BRANCH}/infra/scripts/ide/bootstrap.sh"
308+ )
309+ local max_attempts=5
310+ local delay=5
311+
312+ for attempt in $(seq 1 $max_attempts); do
313+ echo "Download attempt $attempt of $max_attempts"
314+
315+ for url in "${urls[@]}"; do
316+ echo "Trying to download bootstrap from: $url"
317+ if curl -fsSL --connect-timeout 30 --max-time 60 "$url" -o /tmp/bootstrap.sh; then
318+ echo "Successfully downloaded bootstrap script on attempt $attempt"
319+ return 0
320+ fi
321+ echo "Failed to download from: $url"
322+ done
323+
324+ if [ $attempt -lt $max_attempts ]; then
325+ echo "All URLs failed on attempt $attempt, waiting ${delay}s before retry..."
326+ sleep $delay
327+ fi
328+ done
329+
330+ echo "All download attempts failed after $max_attempts tries"
331+ return 1
332+ }
333+
334+ if download_bootstrap; then
335+ chmod +x /tmp/bootstrap.sh
336+ echo "Executing full bootstrap script..."
337+ export VSCODE_EXTENSIONS="$VSCODE_EXTENSIONS"
338+ if /tmp/bootstrap.sh "$IDE_PASSWORD" "$GIT_BRANCH" "$STACK_NAME" "$AWS_REGION" "$TEMPLATE_TYPE"; then
339+ echo "Bootstrap completed successfully"
340+ /opt/aws/bin/cfn-signal -e 0 --stack "$STACK_NAME" --resource IdeBootstrapWaitCondition --region "$AWS_REGION"
341+ else
342+ echo "FATAL: Bootstrap script failed"
343+ /opt/aws/bin/cfn-signal -e 1 --stack "$STACK_NAME" --resource IdeBootstrapWaitCondition --region "$AWS_REGION"
344+ exit 1
345+ fi
346+ else
347+ echo "FATAL: Could not download bootstrap script from any source"
348+ /opt/aws/bin/cfn-signal -e 1 --stack "$STACK_NAME" --resource IdeBootstrapWaitCondition --region "$AWS_REGION"
349+ exit 1
350+ fi
351+ """ ,
352+ gitBranch ,
353+ ideSecretsManagerPassword .secretValueFromJson ("password" ).unsafeUnwrap (),
354+ Aws .STACK_NAME ,
355+ Aws .REGION ,
356+ templateType ,
357+ extensionsString
358+ );
359+
360+ userData .addCommands (userDataContent );
252361
253362 // Create instance launcher Lambda with multi-AZ and multi-instance-type failover
254363 var instanceLauncher = new Lambda (this , "InstanceLauncher" ,
0 commit comments