diff --git a/.github/workflows/build-pull-request.yml b/.github/workflows/build-pull-request.yml index 9df882583e..0f451b17b2 100644 --- a/.github/workflows/build-pull-request.yml +++ b/.github/workflows/build-pull-request.yml @@ -128,18 +128,52 @@ jobs: # Change to language directory cd ./${{ matrix.language }} - # Run the build_file function in parallel for each project to be built - # Halt the execution if any of the build_file invocations fail + # Run the build_file function for each project to be built echo "::group::Build Output" echo "Starting builds for all projects..." - parallel --keep-order --halt-on-error 2 build_file ::: "${apps_to_build[@]}" + + # Track failed builds + failed_builds=() + + # Process each project one by one with clear grouping + for app in "${apps_to_build[@]}"; do + echo "::group::Building $app" + + # Run the build directly + set +e # Don't exit on error + ../scripts/build-${language}.sh "$app" + build_exit=$? + set -e # Re-enable exit on error + + if [ $build_exit -ne 0 ]; then + echo "❌ BUILD FAILED: $app (exit code $build_exit)" + failed_builds+=("$app") + else + echo "✅ BUILD SUCCEEDED: $app" + fi + echo "::endgroup::" + done echo "::endgroup::" - - # Check the exit status of parallel - parallel_exit=$? + + # Print summary outside of any group + echo "" + echo "====== BUILD SUMMARY ======" + if [ ${#failed_builds[@]} -gt 0 ]; then + echo "❌ FAILED BUILDS:" + for failed in "${failed_builds[@]}"; do + echo " - $failed" + done + parallel_exit=1 + else + echo "✅ ALL BUILDS SUCCEEDED" + parallel_exit=0 + fi + echo "==========================" + echo "" + # If parallel failed, make sure the workflow fails too if [ $parallel_exit -ne 0 ]; then - echo "::error::One or more builds failed. See build output above for details." + echo "::error::One or more builds failed. See build summary above for details." exit $parallel_exit else echo "✅ All builds completed successfully!" diff --git a/typescript/ec2-instance-connect-endpoint/package.json b/typescript/ec2-instance-connect-endpoint/package.json index e790bb54c7..a71f151d6b 100644 --- a/typescript/ec2-instance-connect-endpoint/package.json +++ b/typescript/ec2-instance-connect-endpoint/package.json @@ -20,7 +20,6 @@ "@typescript-eslint/eslint-plugin": "^5", "@typescript-eslint/parser": "^5", "aws-cdk": "2.1010.0", - "aws-cdk-lib": "2.85.0", "constructs": "10.0.5", "eslint": "^8", "jest": "^29.7.0", @@ -28,10 +27,6 @@ "ts-node": "^10.9.2", "typescript": "~5.6.3" }, - "peerDependencies": { - "aws-cdk-lib": "^2.85.0", - "constructs": "^10.0.5" - }, "dependencies": { "aws-cdk-lib": "2.190.0", "constructs": "^10.0.0" diff --git a/typescript/ec2-instance/.gitignore b/typescript/ec2-instance/.gitignore index c0ed8424a2..c6615ed68f 100644 --- a/typescript/ec2-instance/.gitignore +++ b/typescript/ec2-instance/.gitignore @@ -11,9 +11,3 @@ cdk.out # Parcel default cache directory .parcel-cache -# Build output -lib -coverage -.nyc_output -dist -.DS_Store diff --git a/typescript/ec2-instance/lib/constructs/server.ts b/typescript/ec2-instance/lib/constructs/server.ts new file mode 100644 index 0000000000..2bf4f9de5d --- /dev/null +++ b/typescript/ec2-instance/lib/constructs/server.ts @@ -0,0 +1,188 @@ +import { RemovalPolicy, Duration, Stack } from 'aws-cdk-lib'; +import { + Vpc, + SecurityGroup, + Instance, + InstanceType, + InstanceClass, + InstanceSize, + CloudFormationInit, + InitConfig, + InitFile, + InitCommand, + UserData, + MachineImage, + AmazonLinuxCpuType, +} from 'aws-cdk-lib/aws-ec2'; +import { + Role, + ServicePrincipal, + ManagedPolicy, + PolicyDocument, + PolicyStatement, +} from 'aws-cdk-lib/aws-iam'; +import { Bucket, ObjectOwnership } from 'aws-cdk-lib/aws-s3'; +import { Source, BucketDeployment } from 'aws-cdk-lib/aws-s3-deployment'; +import { Construct } from 'constructs'; + +interface ServerProps { + vpc: Vpc; + sshSecurityGroup: SecurityGroup; + logLevel: string; + sshPubKey: string; + cpuType: string; + instanceSize: string; +} + +let cpuType: AmazonLinuxCpuType; +let instanceClass: InstanceClass; +let instanceSize: InstanceSize; + +export class ServerResources extends Construct { + public instance: Instance; + + constructor(scope: Construct, id: string, props: ServerProps) { + super(scope, id); + + // Create an Asset Bucket for the Instance. Assets in this bucket will be downloaded to the EC2 during deployment + const assetBucket = new Bucket(this, 'assetBucket', { + publicReadAccess: false, + removalPolicy: RemovalPolicy.DESTROY, + objectOwnership: ObjectOwnership.BUCKET_OWNER_PREFERRED, + autoDeleteObjects: true, + }); + + // Deploy the local assets to the Asset Bucket during the CDK deployment + new BucketDeployment(this, 'assetBucketDeployment', { + sources: [Source.asset('lib/resources/server/assets')], + destinationBucket: assetBucket, + retainOnDelete: false, + exclude: ['**/node_modules/**', '**/dist/**'], + memoryLimit: 512, + }); + + // Create a role for the EC2 instance to assume. This role will allow the instance to put log events to CloudWatch Logs + const serverRole = new Role(this, 'serverEc2Role', { + assumedBy: new ServicePrincipal('ec2.amazonaws.com'), + inlinePolicies: { + ['RetentionPolicy']: new PolicyDocument({ + statements: [ + new PolicyStatement({ + resources: ['*'], + actions: ['logs:PutRetentionPolicy'], + }), + ], + }), + }, + managedPolicies: [ + ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'), + ManagedPolicy.fromAwsManagedPolicyName('CloudWatchAgentServerPolicy'), + ], + }); + + // Grant the EC2 role access to the bucket + assetBucket.grantReadWrite(serverRole); + + const userData = UserData.forLinux(); + + // Add user data that is used to configure the EC2 instance + userData.addCommands( + 'yum update -y', + 'curl -sL https://dl.yarnpkg.com/rpm/yarn.repo | sudo tee /etc/yum.repos.d/yarn.repo', + 'curl -sL https://rpm.nodesource.com/setup_18.x | sudo -E bash - ', + 'yum install -y amazon-cloudwatch-agent nodejs python3-pip zip unzip docker yarn', + 'sudo systemctl enable docker', + 'sudo systemctl start docker', + 'mkdir -p /home/ec2-user/sample', + 'aws s3 cp s3://' + + assetBucket.bucketName + + '/sample /home/ec2-user/sample --recursive', + ); + + // Create a Security Group for the EC2 instance. This group will allow SSH access to the EC2 instance + const ec2InstanceSecurityGroup = new SecurityGroup( + this, + 'ec2InstanceSecurityGroup', + { vpc: props.vpc, allowAllOutbound: true }, + ); + + // Determine the correct CPUType and Instance Class based on the props passed in + if (props.cpuType == 'ARM64') { + cpuType = AmazonLinuxCpuType.ARM_64; + instanceClass = InstanceClass.M7G; + } else { + cpuType = AmazonLinuxCpuType.X86_64; + instanceClass = InstanceClass.M5; + } + + // Determine the correct InstanceSize based on the props passed in + switch (props.instanceSize) { + case 'large': + instanceSize = InstanceSize.LARGE; + break; + case 'xlarge': + instanceSize = InstanceSize.XLARGE; + break; + case 'xlarge2': + instanceSize = InstanceSize.XLARGE2; + break; + case 'xlarge4': + instanceSize = InstanceSize.XLARGE4; + break; + default: + instanceSize = InstanceSize.LARGE; + } + + // Create the EC2 instance + this.instance = new Instance(this, 'Instance', { + vpc: props.vpc, + instanceType: InstanceType.of(instanceClass, instanceSize), + machineImage: MachineImage.latestAmazonLinux2023({ + cachedInContext: false, + cpuType: cpuType, + }), + userData: userData, + securityGroup: ec2InstanceSecurityGroup, + init: CloudFormationInit.fromConfigSets({ + configSets: { + default: ['config'], + }, + configs: { + config: new InitConfig([ + InitFile.fromObject('/etc/config.json', { + // Use CloudformationInit to create an object on the EC2 instance + STACK_ID: Stack.of(this).artifactId, + }), + InitFile.fromFileInline( + // Use CloudformationInit to copy a file to the EC2 instance + '/tmp/amazon-cloudwatch-agent.json', + './lib/resources/server/config/amazon-cloudwatch-agent.json', + ), + InitFile.fromFileInline( + '/etc/config.sh', + 'lib/resources/server/config/config.sh', + ), + InitFile.fromString( + // Use CloudformationInit to write a string to the EC2 instance + '/home/ec2-user/.ssh/authorized_keys', + props.sshPubKey + '\n', + ), + InitCommand.shellCommand('chmod +x /etc/config.sh'), // Use CloudformationInit to run a shell command on the EC2 instance + InitCommand.shellCommand('/etc/config.sh'), + ]), + }, + }), + + initOptions: { + timeout: Duration.minutes(10), + includeUrl: true, + includeRole: true, + printLog: true, + }, + role: serverRole, + }); + + // Add the SSH Security Group to the EC2 instance + this.instance.addSecurityGroup(props.sshSecurityGroup); + } +} diff --git a/typescript/ec2-instance/lib/constructs/vpc.ts b/typescript/ec2-instance/lib/constructs/vpc.ts new file mode 100644 index 0000000000..ce5948c9ca --- /dev/null +++ b/typescript/ec2-instance/lib/constructs/vpc.ts @@ -0,0 +1,41 @@ +import { + SecurityGroup, + Peer, + Port, + SubnetType, + Vpc, +} from 'aws-cdk-lib/aws-ec2'; +import { Construct } from 'constructs'; + +export class VPCResources extends Construct { + public sshSecurityGroup: SecurityGroup; + public vpc: Vpc; + + constructor(scope: Construct, id: string) { + super(scope, id); + + // Create a VPC with public subnets in 2 AZs + this.vpc = new Vpc(this, 'VPC', { + natGateways: 0, + subnetConfiguration: [ + { + cidrMask: 24, + name: 'ServerPublic', + subnetType: SubnetType.PUBLIC, + mapPublicIpOnLaunch: true, + }, + ], + maxAzs: 2, + }); + + // Create a security group for SSH + this.sshSecurityGroup = new SecurityGroup(this, 'SSHSecurityGroup', { + vpc: this.vpc, + description: 'Security Group for SSH', + allowAllOutbound: true, + }); + + // Allow SSH inbound traffic on TCP port 22 + this.sshSecurityGroup.addIngressRule(Peer.anyIpv4(), Port.tcp(22)); + } +} diff --git a/typescript/ec2-instance/lib/ec2-stack.ts b/typescript/ec2-instance/lib/ec2-stack.ts new file mode 100644 index 0000000000..9dcb7f8ac9 --- /dev/null +++ b/typescript/ec2-instance/lib/ec2-stack.ts @@ -0,0 +1,41 @@ +import { Stack, StackProps, CfnOutput } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { VPCResources } from './constructs/vpc'; +import { ServerResources } from './constructs/server'; +import { EC2ExampleProps, envValidator } from './utils/env-validator'; + +export interface EC2StackProps extends StackProps, EC2ExampleProps {} + +export class EC2Stack extends Stack { + constructor(scope: Construct, id: string, props: EC2StackProps) { + super(scope, id, props); + + const { logLevel, sshPubKey, cpuType, instanceSize } = props; + + // Validate environment variables + envValidator(props); + + // Create VPC and Security Group + const vpcResources = new VPCResources(this, 'VPC'); + + // Create EC2 Instance + const serverResources = new ServerResources(this, 'EC2', { + vpc: vpcResources.vpc, + sshSecurityGroup: vpcResources.sshSecurityGroup, + logLevel: logLevel, + sshPubKey: sshPubKey, + cpuType: cpuType, + instanceSize: instanceSize.toLowerCase(), + }); + + // SSM Command to start a session + new CfnOutput(this, 'ssmCommand', { + value: `aws ssm start-session --target ${serverResources.instance.instanceId}`, + }); + + // SSH Command to connect to the EC2 Instance + new CfnOutput(this, 'sshCommand', { + value: `ssh ec2-user@${serverResources.instance.instancePublicDnsName}`, + }); + } +} diff --git a/typescript/ec2-instance/lib/resources/server/assets/sample/sample.txt b/typescript/ec2-instance/lib/resources/server/assets/sample/sample.txt new file mode 100644 index 0000000000..8a6a68e83d --- /dev/null +++ b/typescript/ec2-instance/lib/resources/server/assets/sample/sample.txt @@ -0,0 +1 @@ +Sample file that can be downloaded during deploy. diff --git a/typescript/ec2-instance/lib/resources/server/config/amazon-cloudwatch-agent.json b/typescript/ec2-instance/lib/resources/server/config/amazon-cloudwatch-agent.json new file mode 100644 index 0000000000..eff36a2c2f --- /dev/null +++ b/typescript/ec2-instance/lib/resources/server/config/amazon-cloudwatch-agent.json @@ -0,0 +1,25 @@ +{ + "agent": { + "run_as_user": "root" + }, + "logs": { + "logs_collected": { + "files": { + "collect_list": [ + { + "file_path": "/var/log/cloud-init-output.log", + "log_group_name": "/ec2/log/ec2-example/", + "log_stream_name": "{instance_id}-cloud-init-output", + "retention_in_days": 7 + }, + { + "file_path": "/var/log/cloud-init.log", + "log_group_name": "/ec2/log/ec2-example/", + "log_stream_name": "{instance_id}-cloud-init", + "retention_in_days": 7 + } + ] + } + } + } +} \ No newline at end of file diff --git a/typescript/ec2-instance/lib/resources/server/config/config.sh b/typescript/ec2-instance/lib/resources/server/config/config.sh new file mode 100644 index 0000000000..d88b37ed38 --- /dev/null +++ b/typescript/ec2-instance/lib/resources/server/config/config.sh @@ -0,0 +1,2 @@ +#!/bin/bash -xe +/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -s -c file:/tmp/amazon-cloudwatch-agent.json diff --git a/typescript/ec2-instance/lib/utils/env-validator.ts b/typescript/ec2-instance/lib/utils/env-validator.ts new file mode 100644 index 0000000000..f5bfc35502 --- /dev/null +++ b/typescript/ec2-instance/lib/utils/env-validator.ts @@ -0,0 +1,40 @@ +export interface EC2ExampleProps { + logLevel: string; + sshPubKey: string; + cpuType: string; + instanceSize: string; +} + +export enum InstanceSize { + 'LARGE' = 'large', + 'XLARGE' = 'xlarge', + 'XLARGE2' = 'xlarge2', + 'XLARGE4' = 'xlarge4', +} + +export enum CPUTypes { + 'X86' = 'x86', + 'ARM64' = 'arm64', +} + +export function envValidator(props: EC2ExampleProps) { + const validCpuTypes = Object.keys(CPUTypes).join(', '); + if (props.cpuType) { + if (props.cpuType !== 'X86' && props.cpuType !== 'ARM64') { + throw new Error( + `Invalid CPU type. Valid CPU Types are ${validCpuTypes}`, + ); + } + } + + if (props.instanceSize) { + const validSizes = Object.keys(InstanceSize).join(', '); + if ( + !Object.values(InstanceSize).includes( + props.instanceSize.toLowerCase() as InstanceSize, + ) + ) { + throw new Error(`Invalid instance size. Valid sizes are: ${validSizes}`); + } + } +} diff --git a/typescript/ec2-instance/package.json b/typescript/ec2-instance/package.json index 188d49bf15..8b5a7fe15b 100644 --- a/typescript/ec2-instance/package.json +++ b/typescript/ec2-instance/package.json @@ -11,18 +11,18 @@ "cdk": "cdk" }, "devDependencies": { - "@types/jest": "^29.5.3", - "@types/node": "^16", + "@types/jest": "^29.5.14", + "@types/node": "22.7.9", "aws-cdk": "2.1010.0", - "jest": "^29.6.2", - "ts-jest": "^29.1.1", - "ts-node": "^10.9.1", - "typescript": "^5.1.6" + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "typescript": "~5.6.3" }, "dependencies": { - "aws-cdk-lib": "2.189.0", - "constructs": "^10.0.5", + "aws-cdk-lib": "2.190.0", + "constructs": "^10.0.0", "dotenv": "^16.3.1" }, - "license": "MIT-0" + "license": "MIT" } diff --git a/typescript/ec2-instance/tsconfig.json b/typescript/ec2-instance/tsconfig.json index ebe7df3d7e..66345eb644 100644 --- a/typescript/ec2-instance/tsconfig.json +++ b/typescript/ec2-instance/tsconfig.json @@ -11,8 +11,8 @@ "strictNullChecks": true, "noImplicitThis": true, "alwaysStrict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, + "noUnusedLocals": false, + "noUnusedParameters": false, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "inlineSourceMap": true, @@ -23,7 +23,11 @@ "./node_modules/@types" ], "outDir": "lib", - "rootDir": "../.." + "rootDir": "../..", + "baseUrl": ".", + "paths": { + "../../test-utils/*": ["../../test-utils/*"] + } }, "exclude": [ "node_modules", diff --git a/typescript/opensearch/cwlogs_ingestion/package.json b/typescript/opensearch/cwlogs_ingestion/package.json index 6f997993fd..0bce4d724f 100644 --- a/typescript/opensearch/cwlogs_ingestion/package.json +++ b/typescript/opensearch/cwlogs_ingestion/package.json @@ -14,7 +14,6 @@ "@types/jest": "^29.5.14", "@types/node": "22.7.9", "aws-cdk": "2.1010.0", - "aws-cdk-lib": "^2.165.0", "constructs": "^10.2.43", "globals": "^15.6.0", "jest": "^29.7.0", @@ -24,7 +23,6 @@ }, "dependencies": { "@aws-cdk/aws-lambda-python-alpha": "2.165.0-alpha.0", - "aws-cdk": "^2.165.0", "aws-cdk-lib": "2.190.0", "constructs": "^10.0.0", "source-map-support": "^0.5.21"