diff --git a/content/design-patterns/ex1capacity/Step2.en.md b/content/design-patterns/ex1capacity/Step2.en.md index 307821b6..aaf27757 100644 --- a/content/design-patterns/ex1capacity/Step2.en.md +++ b/content/design-patterns/ex1capacity/Step2.en.md @@ -7,7 +7,7 @@ weight = 3 Now that you have created the table, you can load some sample data into the table by running the following Python script. ```bash -cd /home/ec2-user/workshop +cd /home/ubuntu/workshop python load_logfile.py logfile ./data/logfile_small1.csv ``` The parameters in the preceding command: 1) Table name = `logfile` 2) File name = `logfile_small1.csv` diff --git a/content/design-patterns/setup/Step1.en.md b/content/design-patterns/setup/Step1.en.md index b847a78f..c3853a01 100644 --- a/content/design-patterns/setup/Step1.en.md +++ b/content/design-patterns/setup/Step1.en.md @@ -9,28 +9,28 @@ weight = 10 1. In the services search bar, search for **Systems Manager** and click on it to open the AWS Systems Manager section of the AWS Management Console. 1. In the AWS Systems Manager console, locate the menu in the left, identify the section **Node Management** and select **Session Manager** from the list. 1. Choose **Start session** to launch a shell session. -1. Click the radio button to select the EC2 instance for the lab. If you see no instance, wait a few minutes and then click refresh. Wait until an ec2 instance with name of `mod-XXXXXXXXXXXXXXXX` OR `ddb` is available before continuing. You will see only one of the two names. -1. Click in the **Start Session** button (This action will open a new tab in your browser with a new black shell). -1. In the new black shell, switch to the ec2-user account by running `sudo su - ec2-user` +1. Click the radio button to select the EC2 instance for the lab. If you see no instance, wait a few minutes and then click refresh. Wait until an ec2 instance with name of `DynamoDBC9` is available before continuing. Select the instance. +1. Click the **Start Session** button (This action will open a new tab in your browser with a new black shell). +1. In the new black shell, switch to the ubuntu account by running `sudo su - ubuntu` ```bash - sudo su - ec2-user + sudo su - ubuntu ``` -1. run `shopt login_shell` and be sure it says `login_shell on`. Then, change into the workshop directory to begin: `cd ~/workshop` - +1. run `shopt login_shell` and be sure it says `login_shell on` and then change into the workshop directory. ```bash #Verify login_shell is 'on' shopt login_shell #Change into the workshop directory - cd ~/workshop + cd ~/workshop/ ``` - The output of your commands in the Session Manager session should look like the following: +The output of your commands in the Session Manager session should look like the following: ```bash - sh-4.2$ sudo su - ec2-user - [ec2-user@ip-172-31-24-0 ~]$ #Verify login_shell is 'on' - [ec2-user@ip-172-31-24-0 ~]$ shopt login_shell + $ sudo su - ubuntu + :~ $ #Verify login_shell is 'on' + shopt login_shell + #Change into the workshop directory + cd ~/workshop/ login_shell on - [ec2-user@ip-172-31-24-0 ~]$ #Change into the workshop directory - [ec2-user@ip-172-31-24-0 ~]$ cd ~/workshop + :~/workshop $ ``` diff --git a/content/design-patterns/setup/Step2.en.md b/content/design-patterns/setup/Step2.en.md index 6e0d0d78..f2f9e783 100644 --- a/content/design-patterns/setup/Step2.en.md +++ b/content/design-patterns/setup/Step2.en.md @@ -14,8 +14,9 @@ python --version Output: ```plain -Python 3.6.12 +Python 3.10.12 ``` +**Note: The major and minor version of Python may vary from what you see above** Run the following command to check the AWS CLI on your EC2 instance: @@ -28,9 +29,9 @@ Sample output: ```bash #Note that your linux kernel version may differ from the example. -aws-cli/1.18.139 Python/3.6.12 Linux/4.14.193-113.317.amzn1.x86_64 botocore/1.17.62 +aws-cli/2.13.26 Python/3.11.6 Linux/6.2.0-1013-aws exe/x86_64.ubuntu.22 prompt/off ``` {{% notice note %}} -_Make sure you have AWS CLI version 1.18.139 and python 3.6.12 before proceeding. If you do not have these versions, review [Step 1]({{< ref "design-patterns/setup/Step1" >}}) to ensure you have completed each command correctly._ +_Make sure you have AWS CLI version 2.x or higher and python 3.10 or higher before proceeding. If you do not have these versions, you may have difficultly successfully completing the lab._ {{% /notice %}} diff --git a/content/design-patterns/setup/Step3.en.md b/content/design-patterns/setup/Step3.en.md index 3f51fe9f..7c80d04d 100644 --- a/content/design-patterns/setup/Step3.en.md +++ b/content/design-patterns/setup/Step3.en.md @@ -1,5 +1,5 @@ +++ -title = "Step 3 - Check boto3 installation" +title = "Step 3 - Boto3 installation" date = 2019-12-02T10:07:52-08:00 weight = 30 +++ diff --git a/content/design-patterns/setup/Step4.en.md b/content/design-patterns/setup/Step4.en.md index 5ea41b84..207ee455 100644 --- a/content/design-patterns/setup/Step4.en.md +++ b/content/design-patterns/setup/Step4.en.md @@ -7,7 +7,7 @@ weight = 40 On the EC2 instance, go to the workshop folder and run the ls command: ```bash -cd /home/ec2-user/workshop +cd /home/ubuntu/workshop ls -l . ``` diff --git a/design-patterns/cloudformation/C9.yml b/design-patterns/cloudformation/C9.yml new file mode 100644 index 00000000..f6bebe18 --- /dev/null +++ b/design-patterns/cloudformation/C9.yml @@ -0,0 +1,598 @@ +#Source: https://tiny.amazon.com/1dbfklsd7 +Description: Provides a Cloud9 instance, resizes the instance volume size, and installs required components. + +Parameters: + EnvironmentName: + Description: An environment name that is tagged to the resources. + Type: String + Default: DynamoDBC9 + InstanceName: + Description: Cloud9 instance name. + Type: String + Default: Workshop + InstanceType: + Description: The memory and CPU of the EC2 instance that will be created for Cloud9 to run on. + Type: String + Default: t3.medium + AllowedValues: + - t2.micro + - t3.micro + - t3.small + - t3.medium + - t2.medium + - m5.large + ConstraintDescription: Must be a valid Cloud9 instance type + InstanceVolumeSize: + Description: The size in GB of the Cloud9 instance volume + Type: Number + Default: 16 + ### TODO remove Default + InstanceOwner: + Type: String + Description: Assumed role username of Cloud9 owner, in the format 'Role/username'. Leave blank to assign leave the instance assigned to the role running the CloudFormation template. + Default: "Admin/seanshi-Isengard" + AutomaticStopTimeMinutes: + Description: How long Cloud9 can be inactive (no user input) before auto-hibernating. This helps prevent unnecessary charges. + Type: Number + Default: 0 + WorkshopZIP: + Type: String + Description: Location of LEDA code ZIP + Default: https://amazon-dynamodb-labs.com/assets/workshop.zip + +Metadata: + AWS::CloudFormation::Interface: + ParameterGroups: + - Label: + default: General configuration + Parameters: + - EnvironmentName + - Label: + default: Cloud9 configuration + Parameters: + - InstanceName + - InstanceType + - InstanceVolumeSize + - InstanceOwner + - AutomaticStopTimeMinutes + ParameterLabels: + EnvironmentName: + default: Environment name + InstanceName: + default: Name + InstanceType: + default: Instance type + InstanceVolumeSize: + default: Attached volume size + InstanceOwner: + default: Role and username + AutomaticStopTimeMinutes: + default: Timeout + +Conditions: + AssignCloud9Owner: !Not [!Equals [!Ref InstanceOwner, ""]] + +Resources: +#LADV Role + DDBReplicationRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + Path: / + Policies: + - PolicyName: root + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - dynamodb:DescribeStream + - dynamodb:GetRecords + - dynamodb:GetShardIterator + - dynamodb:ListStreams + Resource: + - '*' + - Effect: Allow + Action: + - dynamodb:DeleteItem + - dynamodb:PutItem + Resource: + - '*' + - Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: + - '*' + ################## PERMISSIONS AND ROLES ################# + Cloud9Role: + Type: AWS::IAM::Role + Properties: + Tags: + - Key: Environment + Value: !Sub ${EnvironmentName} + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - ec2.amazonaws.com + - ssm.amazonaws.com + Action: + - sts:AssumeRole + ManagedPolicyArns: + - !Sub arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess + Path: '/' + Policies: + - PolicyName: !Sub Cloud9InstanceDenyPolicy-${AWS::Region} + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Deny + Action: + - cloud9:UpdateEnvironment + Resource: '*' + + Cloud9LambdaExecutionRole: + Type: AWS::IAM::Role + Metadata: + cfn_nag: + rules_to_suppress: + - id: W11 + reason: Describe Action doesn't support any resource condition + Properties: + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + Path: '/' + ManagedPolicyArns: + - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + Policies: + - PolicyName: !Sub Cloud9LambdaPolicy-${AWS::Region} + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - cloudformation:DescribeStacks + - cloudformation:DescribeStackEvents + - cloudformation:DescribeStackResource + - cloudformation:DescribeStackResources + Resource: + - !Sub arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/* + - Effect: Allow + Action: + - ec2:AssociateIamInstanceProfile + - ec2:ModifyInstanceAttribute + - ec2:ReplaceIamInstanceProfileAssociation + - ec2:RebootInstances + Resource: + - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:instance/* + - Effect: Allow + Action: + - ec2:DescribeInstances + - ec2:DescribeVolumesModifications + - ec2:DescribeVolumes + - ec2:DescribeIamInstanceProfileAssociations + - ec2:ModifyVolume + - ssm:DescribeInstanceInformation + - ssm:SendCommand + - ssm:GetCommandInvocation + Resource: '*' + - Effect: Allow + Action: + - iam:ListInstanceProfiles + Resource: + - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:instance-profile/* + - Effect: Allow + Action: + - s3:ListBucket + - s3:DeleteObject + Resource: + - !Sub arn:${AWS::Partition}:s3:::${Cloud9LogBucket} + - !Sub arn:${AWS::Partition}:s3:::${Cloud9LogBucket}/* + - Effect: Allow + Action: + - iam:PassRole + Resource: + Fn::GetAtt: + - Cloud9Role + - Arn + + ################## LAMBDA BOOTSTRAP FUNCTION ################ + Cloud9BootstrapInstanceLambda: + Type: Custom::Cloud9BootstrapInstanceLambda + DependsOn: + - Cloud9LambdaExecutionRole + Properties: + Tags: + - Key: Environment + Value: !Sub ${EnvironmentName} + ServiceToken: + Fn::GetAtt: + - Cloud9BootstrapInstanceLambdaFunction + - Arn + Region: + Ref: AWS::Region + StackName: + Ref: AWS::StackName + Cloud9Name: !GetAtt Cloud9Instance.Name + EnvironmentId: + Ref: Cloud9Instance + SsmDocument: + Ref: Cloud9BootStrapSSMDocument + LabIdeInstanceProfileName: + Ref: Cloud9InstanceProfile + LabIdeInstanceProfileArn: + Fn::GetAtt: + - Cloud9InstanceProfile + - Arn + LogBucket: + Ref: Cloud9LogBucket + Cloud9BootstrapInstanceLambdaFunction: + Type: AWS::Lambda::Function + Metadata: + cfn_nag: + rules_to_suppress: + - id: W58 + reason: Cloud9LambdaExecutionRole has the AWSLambdaBasicExecutionRole managed policy attached, allowing writing to CloudWatch logs + - id: W89 + reason: Bootstrap function does not need the scaffolding of a VPC or provisioned concurrency + - id: W92 + reason: Bootstrap function does not need provisioned concurrency + Properties: + Tags: + - Key: Environment + Value: !Sub ${EnvironmentName} + Handler: index.lambda_handler + Role: + Fn::GetAtt: + - Cloud9LambdaExecutionRole + - Arn + Runtime: python3.9 + MemorySize: 1024 + Environment: + Variables: + DiskSize: + Ref: InstanceVolumeSize + Timeout: 400 + Code: + ZipFile: | + from __future__ import print_function + import boto3 + import json + import os + import time + import traceback + import cfnresponse + import logging + logger = logging.getLogger(__name__) + + def lambda_handler(event, context): + print(event.values()) + print('context: {}'.format(context)) + responseData = {} + + status = cfnresponse.SUCCESS + + if event['RequestType'] == 'Delete': + responseData = {'Success': 'Custom Resource removed'} + cfnresponse.send(event, context, status, responseData, 'CustomResourcePhysicalID') + else: + try: + # Open AWS clients + ec2 = boto3.client('ec2') + ssm = boto3.client('ssm') + + # Get the InstanceId of the Cloud9 IDE + instance = ec2.describe_instances(Filters=[{'Name': 'tag:Name','Values': ['aws-cloud9-'+event['ResourceProperties']['Cloud9Name']+'-'+event['ResourceProperties']['EnvironmentId']]}])['Reservations'][0]['Instances'][0] + print('instance: {}'.format(instance)) + instance_id = instance['InstanceId'] + + # Create the IamInstanceProfile request object + iam_instance_profile = { + 'Arn': event['ResourceProperties']['LabIdeInstanceProfileArn'], + 'Name': event['ResourceProperties']['LabIdeInstanceProfileName'] + } + print('Found IAM instance profile: {}'.format(iam_instance_profile)) + + time.sleep(10) + + print('Waiting for the instance to be ready...') + + # Wait for Instance to become ready before adding Role + instance_state = instance['State']['Name'] + print('instance_state: {}'.format(instance_state)) + while instance_state != 'running': + time.sleep(5) + instance_state = ec2.describe_instances(InstanceIds=[instance_id]) + print('instance_state: {}'.format(instance_state)) + + print('Instance is ready') + + associations = ec2.describe_iam_instance_profile_associations( + Filters=[ + { + 'Name': 'instance-id', + 'Values': [instance_id], + }, + ], + ) + + if len(associations['IamInstanceProfileAssociations']) > 0: + print('Replacing existing IAM profile...') + for association in associations['IamInstanceProfileAssociations']: + if association['State'] == 'associated': + print("{} is active with state {}".format(association['AssociationId'], association['State'])) + ec2.replace_iam_instance_profile_association(AssociationId=association['AssociationId'], IamInstanceProfile=iam_instance_profile) + else: + print('Associating IAM profile...') + ec2.associate_iam_instance_profile(IamInstanceProfile=iam_instance_profile, InstanceId=instance_id) + + block_volume_id = instance['BlockDeviceMappings'][0]['Ebs']['VolumeId'] + + block_device = ec2.describe_volumes(VolumeIds=[block_volume_id])['Volumes'][0] + + DiskSize = int(os.environ['DiskSize']) + if block_device['Size'] < DiskSize: + ec2.modify_volume(VolumeId=block_volume_id,Size=DiskSize) + print('Modifying block volume: {}'.format(block_volume_id)) + time.sleep(10) + + for i in range(1, 30): + response = ec2.describe_volumes_modifications( + VolumeIds=[ + block_volume_id + ] + ) + modify_state = response['VolumesModifications'][0]['ModificationState'] + if modify_state != 'modifying': + print('Volume has been resized') + break + time.sleep(10) + else: + print('Volume is already sized') + + # Reboot is required to avoid weird race condition with IAM role and SSM agent + # It also causes the file system to expand in the OS + print('Rebooting instance') + + ec2.reboot_instances( + InstanceIds=[ + instance_id, + ], + ) + + time.sleep(60) + + print('Waiting for instance to come online in SSM...') + + for i in range(1, 60): + response = ssm.describe_instance_information(Filters=[{'Key': 'InstanceIds', 'Values': [instance_id]}]) + if len(response["InstanceInformationList"]) == 0: + print('No instances in SSM') + elif len(response["InstanceInformationList"]) > 0 and \ + response["InstanceInformationList"][0]["PingStatus"] == "Online" and \ + response["InstanceInformationList"][0]["InstanceId"] == instance_id: + print('Instance is online in SSM') + break + time.sleep(10) + + ssm_document = event['ResourceProperties']['SsmDocument'] + + print('Sending SSM command...') + + response = ssm.send_command( + InstanceIds=[instance_id], + DocumentName=ssm_document) + + command_id = response['Command']['CommandId'] + + waiter = ssm.get_waiter('command_executed') + + waiter.wait( + CommandId=command_id, + InstanceId=instance_id, + WaiterConfig={ + 'Delay': 10, + 'MaxAttempts': 30 + } + ) + + responseData = {'Success': 'Started bootstrapping for instance: '+instance_id} + cfnresponse.send(event, context, status, responseData, 'CustomResourcePhysicalID') + + except Exception as e: + status = cfnresponse.FAILED + print(traceback.format_exc()) + responseData = {'Error': traceback.format_exc(e)} + finally: + cfnresponse.send(event, context, status, responseData, 'CustomResourcePhysicalID') + LambdaLogGroup: + Type: AWS::Logs::LogGroup + DeletionPolicy: Delete + UpdateReplacePolicy: Delete + Properties: + LogGroupName: !Sub /aws/lambda/${Cloud9BootstrapInstanceLambdaFunction} + RetentionInDays: 7 + + ################## SSM BOOTSTRAP HANDLER ############### + Cloud9LogBucket: + Type: AWS::S3::Bucket + Metadata: + cfn_nag: + rules_to_suppress: + - id: W35 + reason: Access logs aren't needed for this bucket + DeletionPolicy: Delete + Properties: + AccessControl: Private + BucketEncryption: + ServerSideEncryptionConfiguration: + - ServerSideEncryptionByDefault: + SSEAlgorithm: AES256 + PublicAccessBlockConfiguration: + BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls: true + RestrictPublicBuckets: true + Cloud9LogBucketPolicy: + Type: AWS::S3::BucketPolicy + Properties: + Bucket: !Ref Cloud9LogBucket + PolicyDocument: + Version: 2012-10-17 + Statement: + - Action: + - s3:GetObject + - s3:PutObject + - s3:PutObjectAcl + Effect: Allow + Resource: + - !Sub arn:${AWS::Partition}:s3:::${Cloud9LogBucket} + - !Sub arn:${AWS::Partition}:s3:::${Cloud9LogBucket}/* + Principal: + AWS: + Fn::GetAtt: + - Cloud9LambdaExecutionRole + - Arn + + Cloud9BootStrapSSMDocument: + Type: AWS::SSM::Document + Properties: + Tags: + - Key: Environment + Value: !Sub ${EnvironmentName} + DocumentType: Command + Content: + schemaVersion: '2.2' + description: Bootstrap Cloud9 Instance + mainSteps: + - action: aws:runShellScript + name: Cloud9bootstrap + inputs: + runCommand: + - '#!/bin/bash' + - | + function log + { + NANOSEC=`date +%N` + echo -e [`date -u +"%Y-%m-%dT%H:%M:%S"`.${NANOSEC:0:3}Z] "$@" + } + - echo LANG=en_US.utf-8 >> /etc/environment + - echo LC_ALL=en_US.UTF-8 >> /etc/environment + - log INSTALL and CONFIGURE default software components + - . /home/ubuntu/.zshrc + - sudo apt-get update + - sudo apt-get -y install sqlite + - sudo apt-get -y install telnet + - sudo apt-get -y install jq + - sudo apt-get -y install strace + - sudo apt-get -y install tree + - sudo apt-get -y install gcc + - sudo apt-get -y install python3 + - sudo apt-get -y install python3-pip + - sudo apt-get -y install gettext + - sudo apt-get -y install bash-completion + - sudo apt-get -y install awscli + ### TODO REMOVED INST RESIZE + ### CONFIGURE awscli and setting ENVIRONMENT VARS ### + - log CONFIGURE awscli and setting ENVIRONMENT VARS + - echo "complete -C '/usr/local/bin/aws_completer' aws" >> /home/ubuntu/.bashrc + - mkdir -p /home/ubuntu/.aws + - echo "[default]" > /home/ubuntu/.aws/config + - echo "output = json" >> /home/ubuntu/.aws/config + - !Sub echo "region = ${AWS::Region}" >> /home/ubuntu/.aws/config + - chmod 600 /home/ubuntu/.aws/config + - echo 'PATH=$PATH:/usr/local/bin' >> /home/ubuntu/.bashrc + - echo 'export PATH' >> /home/ubuntu/.bashrc + - !Sub echo "export AWS_ACCOUNT_ID=${AWS::AccountId}" | tee -a /home/ubuntu/.bash_profile + - !Sub echo "export AWS_REGION=${AWS::Region}" | tee -a /home/ubuntu/.bash_profile + ### LEDA setup + - log setting up LEDA files + - mkdir -p /home/ubuntu/workshop + - chown -R ubuntu:ubuntu /home/ubuntu/workshop + - !Sub curl -o /home/ubuntu/workshop/workshop.zip -O "${WorkshopZIP}" + - unzip -o /home/ubuntu/workshop/workshop.zip -d /home/ubuntu/workshop + - !Sub echo ${DDBReplicationRole.Arn} > /home/ubuntu/workshop/ddb-replication-role-arn.txt + - rm /home/ubuntu/workshop/workshop.zip + - chown -R ubuntu:ubuntu /home/ubuntu/workshop/* + - sudo pip3 install boto3 > /dev/null 2>&1 + ### Disable Temporary credentials on login ### + - echo 'aws cloud9 update-environment --environment-id $C9_PID --managed-credentials-action DISABLE --region $AWS_REGION &> /dev/null' | tee -a /home/ubuntu/.bash_profile + - echo 'rm -vf ${HOME}/.aws/credentials &> /dev/null' | tee -a /home/ubuntu/.bash_profile + ### General Cleanup and reboot ### + - log CLEANING /home/ubuntu + - rm -vf /home/ubuntu/.aws/credentials + - for f in cloud9; do rm -rf /home/ubuntu/$f; done + - chown -R ubuntu:ubuntu /home/ubuntu/ + - log PREPARE REBOOT in 1 minute + - sudo shutdown --reboot + - log Bootstrap completed with return code "${?}" + Cloud9BootstrapAssociation: + Type: AWS::SSM::Association + Properties: + Name: !Ref Cloud9BootStrapSSMDocument + OutputLocation: + S3Location: + OutputS3BucketName: !Ref Cloud9LogBucket + OutputS3KeyPrefix: bootstrap + Targets: + - Key: tag:SSMBootstrap + Values: + - Active + + ################## INSTANCE ##################### + Cloud9InstanceProfile: + Type: AWS::IAM::InstanceProfile + Properties: + Path: '/' + Roles: + - Ref: Cloud9Role + + Cloud9Instance: + DependsOn: Cloud9BootstrapAssociation + Type: AWS::Cloud9::EnvironmentEC2 + Properties: + Description: !Sub AWS Cloud9 instance for ${EnvironmentName} + AutomaticStopTimeMinutes: !Ref AutomaticStopTimeMinutes + InstanceType: !Ref InstanceType + ImageId: ubuntu-22.04-x86_64 + ### TODO remove subnet + SubnetId: subnet-03165a2b + Name: !Ref InstanceName + OwnerArn: + Fn::If: + - AssignCloud9Owner + - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:assumed-role/${InstanceOwner} + - Ref: AWS::NoValue + Tags: + - Key: SSMBootstrap + Value: Active + - Key: SSMInstallFiles + Value: Active + - Key: Environment + Value: !Ref EnvironmentName + +################## OUTPUTS ##################### +Outputs: + Cloud9IdeUrl: + Description: URL to launch the Cloud9 IDE + Value: !Sub https://${AWS::Region}.console.aws.amazon.com/cloud9/ide/${Cloud9Instance}?region=${AWS::Region} +