in cdk/ide/lib/ide-stack.ts [95:430]
constructor(scope: Construct, id: string, props: FoundationStackProps) {
super(scope, id, props);
// These parameters appear to be supplied by Event Engine. We'll
// take advantage of them to locate the Zip file containing this
// source code.
const assetBucketName = new CfnParameter(this, "EEAssetsBucket", {
default: "BucketNameNotSet",
type: "String",
});
const assetPrefix = new CfnParameter(this, "EEAssetsKeyPrefix", {
default: "KeyPrefixNotSet",
//default: "modules/local_testing/v1/",
type: "String",
});
const teamRoleArn = new CfnParameter(this, "EETeamRoleArn", {
default: "RoleNotSet", // set this for testing in non-EE environment
type: "String",
});
// We supply the value of this parameter ourselves via the ZIPFILE
// environment variable. It will be automatically rendered into the
// generated YAML template.
const sourceZipFile = new CfnParameter(this, "SourceZipFile", {
default: props.sourceZipFile,
type: "String",
});
const sourceZipFileChecksum = new CfnParameter(
this,
"SourceZipFileChecksum",
{
default: props.sourceZipFileChecksum,
type: "String",
}
);
const assetBucket = s3.Bucket.fromBucketName(
this,
"SourceBucket",
assetBucketName.valueAsString
);
// We need to create the Cloud9 environment here, instead of in the cluster stack
// created in CodeBuild, so that the stack creator can access the environment.
// (CodeBuild builds perform in a different role context, which makes the
// environment inaccessible.)
//
// ------VPC------
const vpc = new ec2.Vpc(this, "VPC", {
maxAzs: 2,
cidr: "10.0.0.0/16",
natGateways: 1,
subnetConfiguration: [
{
subnetType: ec2.SubnetType.PUBLIC,
name: "Public",
cidrMask: 18,
},
{
subnetType: ec2.SubnetType.PRIVATE_WITH_NAT,
name: "Private",
cidrMask: 18,
},
],
});
// ------Cloud9------
//const workspace = new cloud9.Ec2Environment(this, "Workspace", {
const vpcSubnets = { subnetType: ec2.SubnetType.PUBLIC };
const workspace = new c9.CfnEnvironmentEC2(this, "Workspace", {
name: "aws-workshop",
description: "AWS Event Workshop",
// Can't use t3a instances for Cloud9 due to silly server-side regex filter. Oh, well.
instanceType: ec2.InstanceType.of(
ec2.InstanceClass.T3,
ec2.InstanceSize.MEDIUM
).toString(),
subnetId: vpc.selectSubnets(vpcSubnets).subnetIds[0],
ownerArn: teamRoleArn.valueAsString,
});
// Output the Cloud9 IDE URL
// new CfnOutput(this, "URL", { value: workspace.ideUrl });
// CLOUD9 - INSTANCE PROFILE
// --------------------------------------------------------------------------------
// Create an EC2 instance role for the Cloud9 environment. This instance
// role is powerful, allowing the participant to have unfettered access to
// the provisioned account. This might be too broad. It's possible to
// tighten this down, but there may be unintended consequences.
const instanceRole = new iam.Role(this, "WorkspaceInstanceRole", {
assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName("AdministratorAccess"),
],
description: "Cloud9 Workspace EC2 instance role",
});
// During internal testing we found that Isengard account baselining
// was attaching IAM roles to instances in the background. This prevents
// the stack from being cleanly destroyed, so we will record the instance
// role name and use it later to delete any attached policies before
// cleanup.
new CfnOutput(this, "WorkspaceInstanceRoleName", {
value: instanceRole.roleName,
});
// Create an instance profile for the Cloud9 environment.
const instanceProfile = new iam.CfnInstanceProfile(
this,
"WorkspaceInstanceProfile",
{
roles: [instanceRole.roleName],
}
);
// ------------LAMBDA EXECUTION ROLE----------------
// Build IAM role for custom resource lambda functions to initialize Cloud9
const lambdaRole = new iam.Role(this, "Cloud9InitializtionLambdaRole", {
assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
description: "Execution role for Cloud9 bootstrapping functions",
});
lambdaRole.addToPolicy(
new iam.PolicyStatement({
actions: [
"cloudformation:DescribeStackResources",
"ec2:AssociateIamInstanceProfile",
"ec2:AuthorizeSecurityGroupIngress",
"ec2:DescribeInstances",
"ec2:DescribeInstanceStatus",
"ec2:DescribeInstanceAttribute",
"ec2:DescribeIamInstanceProfileAssociations",
"ec2:DescribeVolumes",
"ec2:DesctibeVolumeAttribute",
"ec2:DescribeVolumesModifications",
"ec2:DescribeVolumeStatus",
"ec2:StartInstances",
"ec2:StopInstances",
"ssm:DescribeInstanceInformation",
"ec2:ModifyVolume",
"ec2:ReplaceIamInstanceProfileAssociation",
"ec2:ReportInstanceStatus",
"ssm:SendCommand",
"ssm:GetCommandInvocation",
"s3:GetObject",
],
resources: ["*"], //TODO: Refactor and scope this down
})
);
lambdaRole.addToPolicy(
new iam.PolicyStatement({
actions: ["iam:PassRole"],
resources: [instanceRole.roleArn],
})
);
lambdaRole.addToPolicy(
new iam.PolicyStatement({
actions: ["lambda:AddPermission", "lambda:RemovePermission"],
resources: ["*"],
})
);
lambdaRole.addToPolicy(
new iam.PolicyStatement({
actions: [
"events:PutRule",
"events:DeleteRule",
"events:PutTargets",
"events:RemoveTargets",
],
resources: ["*"],
})
);
lambdaRole.addManagedPolicy({
managedPolicyArn:
"arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
});
// Create a membership to grant access to the Cloud9 instance
const createMembership = new cr.AwsCustomResource(
this,
"Cloud9CreateMembership",
{
policy: cr.AwsCustomResourcePolicy.fromSdkCalls({
resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE,
}),
onCreate: {
service: "Cloud9",
action: "createEnvironmentMembership",
physicalResourceId: cr.PhysicalResourceId.of(workspace.ref),
parameters: {
environmentId: workspace.ref,
permissions: "read-write",
userArn: teamRoleArn.valueAsString,
},
},
}
);
// Attach instance profile to the Cloud9 environment via Lambda backed custom resource
// Thanks to maishsk@ for this code: https://gitlab.aws.dev/maishsk/cloud9-event-engine-cfn-template
const updateInstanceProfileFunction = new pylambda.PythonFunction(
this,
"UpdateInstanceProfileFunction",
{
entry: path.join(__dirname, "../lambda_functions/c9InstanceProfile"),
index: "lambda_function.py",
handler: "handler",
role: lambdaRole,
runtime: lambda.Runtime.PYTHON_3_9,
timeout: Duration.minutes(1),
}
);
const updateInstanceProfileProvider = new cr.Provider(
this,
"UpdateInstanceProfileProvider",
{
onEventHandler: updateInstanceProfileFunction,
}
);
new CustomResource(this, "UpdateInstanceProfile", {
serviceToken: updateInstanceProfileProvider.serviceToken,
properties: {
InstanceProfile: instanceProfile.attrArn,
Cloud9Environment: workspace.ref,
},
});
// Disable Cloud9 managed AWS credentials
const disableCredentialManagement = new cr.AwsCustomResource(
this,
"Cloud9DisableCredentialManagement",
{
policy: cr.AwsCustomResourcePolicy.fromSdkCalls({
resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE,
}),
onCreate: {
service: "Cloud9",
action: "updateEnvironment",
physicalResourceId: cr.PhysicalResourceId.of(workspace.ref),
parameters: {
environmentId: workspace.ref,
managedCredentialsAction: "DISABLE",
},
},
}
);
// CLOUD9 - DISK RESIZE
// --------------------------------------------------------------------------------
// Resize the EBS volume attached to the Cloud9 EC2 instance
// Thanks to maishsk@ for this code: https://gitlab.aws.dev/maishsk/cloud9-event-engine-cfn-template
const diskResizeFunction = new pylambda.PythonFunction(
this,
"Cloud9DiskResizeFunction",
{
entry: path.join(__dirname, "../lambda_functions/c9DiskResize"),
index: "lambda_function.py",
handler: "handler",
role: lambdaRole,
runtime: lambda.Runtime.PYTHON_3_9,
timeout: Duration.minutes(1),
}
);
const diskResizeProvider = new cr.Provider(
this,
"Cloud9DiskResizeProvider",
{
onEventHandler: diskResizeFunction,
}
);
new CustomResource(this, "Cloud9ResizeDisk", {
serviceToken: diskResizeProvider.serviceToken,
properties: {
EBSVolumeSize: 32, //TODO: Parameterize this?
},
});
// CLOUD9 - BOOTSTRAP
// --------------------------------------------------------------------------------
// Finish configuring the Cloud9 environment, e.g. installing packages, running scripts
// Thanks to maishsk@ for this code: https://gitlab.aws.dev/maishsk/cloud9-event-engine-cfn-template
const bootstrapFunction = new pylambda.PythonFunction(
this,
"Cloud9BootstrapFunction",
{
entry: path.join(__dirname, "../lambda_functions/c9bootstrap"),
index: "lambda_function.py",
handler: "handler",
role: lambdaRole,
runtime: lambda.Runtime.PYTHON_3_9,
timeout: Duration.minutes(1),
}
);
const bootstrapProvider = new cr.Provider(this, "Cloud9BootstrapProvider", {
onEventHandler: bootstrapFunction,
});
new CustomResource(this, "Cloud9Bootstrap", {
serviceToken: bootstrapProvider.serviceToken,
properties: {
InstanceId: instanceProfile.attrArn,
BootstrapPath:
"s3://" +
assetBucketName.valueAsString +
"/" +
assetPrefix.valueAsString +
"/bootstrap.sh",
BootstrapArguments: "",
},
});
new CfnOutput(this, "AssetBucketName", {
value: assetBucketName.valueAsString,
});
new CfnOutput(this, "AssetBucketPrefix", {
value: assetPrefix.valueAsString,
});
}