netbench-cdk/lib/netbench.ts (172 lines of code) (raw):

#!/usr/bin/env node import { Construct } from 'constructs'; import { BucketDeployment } from 'aws-cdk-lib/aws-s3-deployment'; import * as cdk from 'aws-cdk-lib' import { S3Origin } from 'aws-cdk-lib/aws-cloudfront-origins'; import * as logs from 'aws-cdk-lib/aws-logs' import { Config, NetbenchStackProps } from './config'; import path from 'path'; import { IBucket } from 'aws-cdk-lib/aws-s3'; import { readFileSync } from 'fs'; import { Schedule } from 'aws-cdk-lib/aws-events'; export class NetbenchInfra extends cdk.Stack { private config: Config = new Config; constructor(scope: Construct, id: string, props?: NetbenchStackProps) { super(scope, id, props); this.createVPC(); this.createCloudwatchGroup(); this.createRole(); this.createMonitorLambda(); const GHAUser = this.createGHAIamUser(); // We're over-riding CF's naming scheme so this name // must be globally unique. By default, AWSStage will be username. if (props?.reportStack) { let bucketName: string = ""; if (props && props.bucketSuffix) { bucketName = `netbenchrunnerlogs-public-${props.bucketSuffix}`; } else { throw new Error('Unable to determine reporting bucket suffix'); } // Create the public logs bucket const distBucket = this.createS3Bucket(bucketName, true); new cdk.CfnOutput(this, "output:NetbenchRunnerPublicLogsBucket", { value: distBucket.bucketName }) this.createCloudFront('CFdistribution', distBucket); // Create the private source code bucket, without any distribution. const srcCodeBucket = this.createS3Bucket(`netbenchrunner-private-source-${props.bucketSuffix}`, false); new cdk.CfnOutput(this, "output:NetbenchRunnerPrivateSrcBucket", { value: srcCodeBucket.bucketName }) // Stitch together the buckets, a policy, and the GHA user distBucket.grantReadWrite(GHAUser); srcCodeBucket.grantReadWrite(GHAUser); } } private createCloudwatchGroup() { //SSM logs //TODO: add a retention policy const logGroup = new logs.LogGroup(this, 'NetbenchRunnerLogGroup'); new cdk.CfnOutput(this, "output:NetbenchRunnerLogGroup", { value: logGroup.logGroupName }) } private createVPC() { // Creating VPC for clients and servers const vpc = new cdk.aws_ec2.Vpc(this, 'vpc', { ipAddresses: cdk.aws_ec2.IpAddresses.cidr(this.config.VpcCidr), maxAzs: this.config.VpcMaxAzs, subnetConfiguration: [ { cidrMask: this.config.VpcCidrMask, name: 'NetbenchRunnerSubnet', subnetType: cdk.aws_ec2.SubnetType.PUBLIC, } ], }); //Tag all available subnets the same. This behavior might need to change when MultiRegion is added. const subnetTagKey = "aws-cdk:netbench-subnet-name"; const subnetTagValue = "public-subnet-for-netbench-runners"; vpc.publicSubnets.forEach(element => { cdk.Tags.of(element).add(subnetTagKey, subnetTagValue); }); new cdk.CfnOutput(this, "output:NetbenchSubnetTagKey", { value: subnetTagKey }); new cdk.CfnOutput(this, "output:NetbenchSubnetTagValue", { value: subnetTagValue }); new cdk.CfnOutput(this, "output:" + this.stackName + "Region", { value: this.region }); }; private createCloudFront(id: string, bucket: IBucket) { const cfDistribution = new cdk.aws_cloudfront.Distribution(this, id, { defaultBehavior: { origin: new S3Origin(bucket), viewerProtocolPolicy: cdk.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, }, defaultRootObject: "index.html" }); new cdk.CfnOutput(this, 'output:NetbenchCloudfrontDistribution', { value: "https://" + cfDistribution.distributionDomainName }); }; private createRole() { // Create IAM role for the EC2 instances const instanceRole = new cdk.aws_iam.Role(this, 'NetbenchRunnerInstanceRole', { assumedBy: new cdk.aws_iam.ServicePrincipal('ec2.amazonaws.com'), }); // Create an instance profile to allow ec2 to use the role. // https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2_instance-profiles.html const instanceProfile = new cdk.aws_iam.InstanceProfile(this, 'instanceProfile', { role: instanceRole }) new cdk.CfnOutput(this, "output:NetbenchRunnerInstanceProfile", { value: instanceProfile.instanceProfileName }) // Attach managed policies to the IAM role instanceRole.addManagedPolicy(cdk.aws_iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMFullAccess')); // TODO: This is too permissive- scope this down to just the netbench bucket. instanceRole.addManagedPolicy(cdk.aws_iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonS3FullAccess')); }; private createGHAIamUser(): cdk.aws_iam.User { return new cdk.aws_iam.User(this, "s2n-netbench-githubactions", { userName: "s2n-netbench-githubactions" }); } /* For now, let CDK create this policy with bucket.GrantReadWrite() private createGHAIamPolicy(s3Bucket: cdk.aws_s3.Bucket): cdk.aws_iam.Policy { return new cdk.aws_iam.Policy(this, "s2n-netbench-githubactions-policy", { statements: [new iam.PolicyStatement({ effect: Effect.ALLOW, actions: ["s3:PutObject", "s3:GetObject", "s3:AbortMultipartUpload", "s3:ListBucket", "s3:GetObjectVersion"], resources: [s3Bucket.bucketArn, s3Bucket.bucketArn + "/*"], })] }); } */ private createS3Bucket(id: string, reportBucket: boolean): cdk.aws_s3.Bucket { // NOTE: putting the bucketName in the bucketProperties // over-rides CloudFormation's unique naming scheme let bucketProperties = { bucketName: id, blockPublicAccess: cdk.aws_s3.BlockPublicAccess.BLOCK_ALL, encryption: cdk.aws_s3.BucketEncryption.S3_MANAGED, enforceSSL: true, // On stack destroy, keep the bucket and it's contents, leaving an orphan. // This will require manual cleanup if you'd like to recreate the stack. removalPolicy: cdk.RemovalPolicy.RETAIN, } const netbenchBucket = new cdk.aws_s3.Bucket(this, id, bucketProperties) if (reportBucket) { // If this is a reporting bucket, populate it with the contents of ./staticfiles/. const deployment = new BucketDeployment(this, 'NetbenchReportBucketContents', { sources: [cdk.aws_s3_deployment.Source.asset(path.join(__dirname, "../staticfiles"))], destinationBucket: netbenchBucket, prune: false, // Do NOT delete objects in s3 that don't exist locally. }); } const bucketActions = ['s3:AbortMultipartUpload', 's3:GetBucketLocation', 's3:GetObject', 's3:ListBucket', 's3:ListBucketMultipartUploads', 's3:ListMultipartUploadParts', 's3:PutObject'] netbenchBucket.addToResourcePolicy(new cdk.aws_iam.PolicyStatement({ sid: 'netbenchec2', // Special CDK construct that implicitly adds a condition to the policy. principals: [new cdk.aws_iam.AnyPrincipal().inOrganization(`arn:aws:sts::${this.account}:assumed-role`)], effect: cdk.aws_iam.Effect.ALLOW, actions: bucketActions, resources: [`${netbenchBucket.bucketArn}/*`, netbenchBucket.bucketArn] })) return netbenchBucket; }; private createMonitorLambda(): any { const monitorLambda = new cdk.aws_lambda.Function(this, "netbenchMonitor", { runtime: cdk.aws_lambda.Runtime.PYTHON_3_12, handler: "index.lambda_handler", code: cdk.aws_lambda.Code.fromInline(readFileSync('./netbench-monitor/handler.py', 'utf-8')), timeout: cdk.Duration.seconds(15) }) const describePolicy = cdk.aws_iam.PolicyStatement.fromJson({ "Effect": "Allow", "Action": "ec2:DescribeInstances", "Resource": "*" }); monitorLambda.addToRolePolicy(describePolicy); new cdk.aws_events.Rule(this, 'ScheduledRun', { description: "Netbench EC2 long running instance monitor; managed by cdk", schedule: Schedule.cron({ year: "*", month: "*", day: "*", hour: "5,17", minute: "0" }), targets: [new cdk.aws_events_targets.LambdaFunction(monitorLambda)], }); return monitorLambda; } }