Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 41 additions & 7 deletions .github/workflows/build-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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!"
Expand Down
5 changes: 0 additions & 5 deletions typescript/ec2-instance-connect-endpoint/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,13 @@
"@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",
"ts-jest": "^29.2.5",
"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"
Expand Down
6 changes: 0 additions & 6 deletions typescript/ec2-instance/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,3 @@ cdk.out
# Parcel default cache directory
.parcel-cache

# Build output
lib
coverage
.nyc_output
dist
.DS_Store
188 changes: 188 additions & 0 deletions typescript/ec2-instance/lib/constructs/server.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
41 changes: 41 additions & 0 deletions typescript/ec2-instance/lib/constructs/vpc.ts
Original file line number Diff line number Diff line change
@@ -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));
}
}
41 changes: 41 additions & 0 deletions typescript/ec2-instance/lib/ec2-stack.ts
Original file line number Diff line number Diff line change
@@ -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}`,
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Sample file that can be downloaded during deploy.
Original file line number Diff line number Diff line change
@@ -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
}
]
}
}
}
}
2 changes: 2 additions & 0 deletions typescript/ec2-instance/lib/resources/server/config/config.sh
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading