diff --git a/packages/@aws-cdk/aws-cloud9/README.md b/packages/@aws-cdk/aws-cloud9/README.md index 691fd78d9ee64..a7b371bcd836d 100644 --- a/packages/@aws-cdk/aws-cloud9/README.md +++ b/packages/@aws-cdk/aws-cloud9/README.md @@ -18,3 +18,38 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. + +AWS Cloud9 is a cloud-based integrated development environment (IDE) that lets you write, run, and debug your code with just a browser. It includes a code editor, debugger, and terminal. Cloud9 comes prepackaged with essential tools for popular programming languages, including JavaScript, Python, PHP, and more, so you don’t need to install files or configure your development machine to start new projects. Since your Cloud9 IDE is cloud-based, you can work on your projects from your office, home, or anywhere using an internet-connected machine. Cloud9 also provides a seamless experience for developing serverless applications enabling you to easily define resources, debug, and switch between local and remote execution of serverless applications. With Cloud9, you can quickly share your development environment with your team, enabling you to pair program and track each other's inputs in real time. + + +### Creating EC2 Environment + +EC2 Environments are defined with `Ec2Environment`. To create an EC2 environment in the private subnet, specify `subnetSelection` with private `subnetType`. + + +```ts +import * as cloud9 from '@aws-cdk/aws-cloud9'; + +// create a cloud9 ec2 environment in a new VPC +const vpc = new ec2.Vpc(this, 'VPC', { maxAzs: 3}); +new cloud9.Ec2Environment(this, 'Cloud9Env', { vpc }); + +// or create the cloud9 environment in the default VPC with specific instanceType +const defaultVpc = ec2.Vpc.fromLookup(this, 'DefaultVPC', { isDefault: true }); +new cloud9.Ec2Environment(this, 'Cloud9Env2', { + vpc: defaultVpc, + instanceType: new ec2.InstanceType('t3.large') +}); + +// or specify in a different subnetSelection +const c9env = new cloud9.Ec2Environment(this, 'Cloud9Env3', { + vpc, + subnetSelection: { + subnetType: ec2.SubnetType.PRIVATE + } +}); + +// print the Cloud9 IDE URL in the output +new cdk.CfnOutput(this, 'URL', { value: c9env.ideUrl }); +``` + diff --git a/packages/@aws-cdk/aws-cloud9/lib/environment.ts b/packages/@aws-cdk/aws-cloud9/lib/environment.ts new file mode 100644 index 0000000000000..d99fd459e1b1c --- /dev/null +++ b/packages/@aws-cdk/aws-cloud9/lib/environment.ts @@ -0,0 +1,135 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import { CfnEnvironmentEC2 } from '../lib/cloud9.generated'; + +/** + * A Cloud9 Environment + * + */ +export interface IEc2Environment extends cdk.IResource { + /** + * The name of the EnvironmentEc2 + * + * @attribute environmentEc2Name + */ + readonly ec2EnvironmentName: string; + + /** + * The arn of the EnvironmentEc2 + * + * @attribute environmentE2Arn + */ + readonly ec2EnvironmentArn: string; + +} + +/** + * Properties for Ec2Environment + */ +export interface Ec2EnvironmentProps { + /** + * The type of instance to connect to the environment. + * + * @default - t2.micro + */ + readonly instanceType?: ec2.InstanceType; + + /** + * The subnetSelection of the VPC that AWS Cloud9 will use to communicate with + * the Amazon EC2 instance. + * + * @default - all public subnets of the VPC are selected. + */ + readonly subnetSelection?: ec2.SubnetSelection; + + /** + * The VPC that AWS Cloud9 will use to communicate with the Amazon Elastic Compute Cloud (Amazon EC2) instance. + * + */ + readonly vpc: ec2.IVpc; + + /** + * Name of the environment + * + * @default - automatically generated name + */ + readonly ec2EnvironmentName?: string; + + /** + * Description of the environment + * + * @default - no description + */ + readonly description?: string; +} + +/** + * A Cloud9 Environment with Amazon EC2 + * @resource AWS::Cloud9::EnvironmentEC2 + */ +export class Ec2Environment extends cdk.Resource implements IEc2Environment { + /** + * import from EnvironmentEc2Name + */ + public static fromEc2EnvironmentName(scope: cdk.Construct, id: string, ec2EnvironmentName: string): IEc2Environment { + class Import extends cdk.Resource { + public ec2EnvironmentName = ec2EnvironmentName; + public ec2EnvironmentArn = cdk.Stack.of(this).formatArn({ + service: 'cloud9', + resource: 'environment', + resourceName: this.ec2EnvironmentName, + }); + } + return new Import(scope, id); + } + + /** + * The environment name of this Cloud9 environment + * + * @attribute + */ + public readonly ec2EnvironmentName: string; + + /** + * The environment ARN of this Cloud9 environment + * + * @attribute + */ + public readonly ec2EnvironmentArn: string; + + /** + * The environment ID of this Cloud9 environment + */ + public readonly environmentId: string; + + /** + * The complete IDE URL of this Cloud9 environment + */ + public readonly ideUrl: string; + + /** + * VPC ID + */ + public readonly vpc: ec2.IVpc; + + constructor(scope: cdk.Construct, id: string, props: Ec2EnvironmentProps) { + super(scope, id); + + this.vpc = props.vpc; + if (!props.subnetSelection && this.vpc.publicSubnets.length === 0) { + throw new Error('no subnetSelection specified and no public subnet found in the vpc, please specify subnetSelection'); + } + + const vpcSubnets = props.subnetSelection ?? { subnetType: ec2.SubnetType.PUBLIC }; + const c9env = new CfnEnvironmentEC2(this, 'Resource', { + name: props.ec2EnvironmentName, + description: props.description, + instanceType: props.instanceType?.toString() ?? ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.MICRO).toString(), + subnetId: this.vpc.selectSubnets(vpcSubnets).subnetIds[0] , + }); + this.environmentId = c9env.ref; + this.ec2EnvironmentArn = c9env.getAtt('Arn').toString(); + this.ec2EnvironmentName = c9env.getAtt('Name').toString(); + this.ideUrl = `https://${this.stack.region}.console.aws.amazon.com/cloud9/ide/${this.environmentId}`; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloud9/lib/index.ts b/packages/@aws-cdk/aws-cloud9/lib/index.ts index 96f6a78740758..b3fde34a8442b 100644 --- a/packages/@aws-cdk/aws-cloud9/lib/index.ts +++ b/packages/@aws-cdk/aws-cloud9/lib/index.ts @@ -1,2 +1,3 @@ // AWS::Cloud9 CloudFormation Resources: export * from './cloud9.generated'; +export * from './environment'; diff --git a/packages/@aws-cdk/aws-cloud9/package.json b/packages/@aws-cdk/aws-cloud9/package.json index 9ffbfe86e9403..ab793ab547a44 100644 --- a/packages/@aws-cdk/aws-cloud9/package.json +++ b/packages/@aws-cdk/aws-cloud9/package.json @@ -81,21 +81,31 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0" }, "dependencies": { "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", "constructs": "^1.1.2" }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", "constructs": "^1.1.2" }, "engines": { "node": ">= 10.3.0" }, + "awslint": { + "exclude": [ + "resource-attribute:@aws-cdk/aws-cloud9.Ec2Environment.environmentEc2Arn", + "resource-attribute:@aws-cdk/aws-cloud9.Ec2Environment.environmentEc2Name", + "props-physical-name:@aws-cdk/aws-cloud9.Ec2EnvironmentProps" + ] + }, "stability": "experimental", "awscdkio": { "announce": false diff --git a/packages/@aws-cdk/aws-cloud9/test/cloud9.environment.test.ts b/packages/@aws-cdk/aws-cloud9/test/cloud9.environment.test.ts new file mode 100644 index 0000000000000..09ed7301d5004 --- /dev/null +++ b/packages/@aws-cdk/aws-cloud9/test/cloud9.environment.test.ts @@ -0,0 +1,69 @@ +import { expect as expectCDK, haveResource } from '@aws-cdk/assert'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import * as cloud9 from '../lib'; + +let stack: cdk.Stack; +let vpc: ec2.IVpc; + +beforeEach(() => { + stack = new cdk.Stack(); + vpc = new ec2.Vpc(stack, 'VPC'); +}); + +test('create resource correctly with only vpc provide', () => { + // WHEN + new cloud9.Ec2Environment(stack, 'C9Env', { vpc }); + // THEN + expectCDK(stack).to(haveResource('AWS::Cloud9::EnvironmentEC2')); +}); + +test('create resource correctly with both vpc and subnetSelectio', () => { + // WHEN + new cloud9.Ec2Environment(stack, 'C9Env', { + vpc, + subnetSelection: { + subnetType: ec2.SubnetType.PRIVATE + } + }); + // THEN + expectCDK(stack).to(haveResource('AWS::Cloud9::EnvironmentEC2')); +}); + +test('import correctly from existing environment', () => { + // WHEN + const c9env = cloud9.Ec2Environment.fromEc2EnvironmentName(stack, 'ImportedEnv', 'existingEnvName'); + // THEN + expect(c9env).toHaveProperty('ec2EnvironmentName'); +}); + +test('create correctly with instanceType specified', () => { + // WHEN + new cloud9.Ec2Environment(stack, 'C9Env', { + vpc, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.C5, ec2.InstanceSize.LARGE) + }); + // THEN + expectCDK(stack).to(haveResource('AWS::Cloud9::EnvironmentEC2')); +}); + +test('throw error when subnetSelection not specified and the provided VPC has no public subnets', () => { + // WHEN + const privateOnlyVpc = new ec2.Vpc(stack, 'PrivateOnlyVpc', { + maxAzs: 2, + subnetConfiguration: [ + { + subnetType: ec2.SubnetType.ISOLATED, + name: 'IsolatedSubnet', + cidrMask: 24 + } + ] + }); + // THEN + expect(() => { + new cloud9.Ec2Environment(stack, 'C9Env', { + vpc: privateOnlyVpc, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.C5, ec2.InstanceSize.LARGE) + }); + }).toThrow(/no subnetSelection specified and no public subnet found in the vpc, please specify subnetSelection/); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloud9/test/integ.cloud9.expected.json b/packages/@aws-cdk/aws-cloud9/test/integ.cloud9.expected.json new file mode 100644 index 0000000000000..acb59e3a7705f --- /dev/null +++ b/packages/@aws-cdk/aws-cloud9/test/integ.cloud9.expected.json @@ -0,0 +1,363 @@ +{ + "Resources": { + "VPCB9E5F0B4": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "C9Stack/VPC" + } + ] + } + }, + "VPCPublicSubnet1SubnetB4246D30": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "C9Stack/VPC/PublicSubnet1" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "C9Stack/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableAssociation0B0896DC": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + } + } + }, + "VPCPublicSubnet1DefaultRoute91CEF279": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet1EIP6AD938E8": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "C9Stack/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1NATGatewayE0556630": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + "Tags": [ + { + "Key": "Name", + "Value": "C9Stack/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet2Subnet74179F39": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "C9Stack/VPC/PublicSubnet2" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "VPCPublicSubnet2RouteTable6F1A15F1": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "C9Stack/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTableAssociation5A808732": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + } + }, + "VPCPublicSubnet2DefaultRouteB7481BBA": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPrivateSubnet1Subnet8BCA10E0": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "C9Stack/VPC/PrivateSubnet1" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "C9Stack/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableAssociation347902D1": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + } + } + }, + "VPCPrivateSubnet1DefaultRouteAE1D6490": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet1NATGatewayE0556630" + } + } + }, + "VPCPrivateSubnet2SubnetCFCDAA7A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.192.0/18", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "C9Stack/VPC/PrivateSubnet2" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + } + ] + } + }, + "VPCPrivateSubnet2RouteTable0A19E10E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "C9Stack/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTableAssociation0C73D413": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + } + }, + "VPCPrivateSubnet2DefaultRouteF4F5CFD2": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet1NATGatewayE0556630" + } + } + }, + "VPCIGWB7E252D3": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "C9Stack/VPC" + } + ] + } + }, + "VPCVPCGW99B986DC": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "InternetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "C9EnvF05FC3BE": { + "Type": "AWS::Cloud9::EnvironmentEC2", + "Properties": { + "InstanceType": "t2.micro", + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + } + } + } + }, + "Outputs": { + "URL": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "AWS::Region" + }, + ".console.aws.amazon.com/cloud9/ide/", + { + "Ref": "C9EnvF05FC3BE" + } + ] + ] + } + }, + "ARN": { + "Value": { + "Fn::GetAtt": [ + "C9EnvF05FC3BE", + "Arn" + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloud9/test/integ.cloud9.ts b/packages/@aws-cdk/aws-cloud9/test/integ.cloud9.ts new file mode 100644 index 0000000000000..eed96d93edc40 --- /dev/null +++ b/packages/@aws-cdk/aws-cloud9/test/integ.cloud9.ts @@ -0,0 +1,23 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import * as cloud9 from '../lib'; + +export class Cloud9Env extends cdk.Stack { + constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const vpc = new ec2.Vpc(this, 'VPC', { + maxAzs: 2, + natGateways: 1 + }); + + // create a cloud9 ec2 environment in a new VPC + const c9env = new cloud9.Ec2Environment(this, 'C9Env', { vpc }); + new cdk.CfnOutput(this, 'URL', { value: c9env.ideUrl }); + new cdk.CfnOutput(this, 'ARN', { value: c9env.ec2EnvironmentArn }); + } +} + +const app = new cdk.App(); + +new Cloud9Env(app, 'C9Stack'); \ No newline at end of file