Description: AWS Cloud9 environment for AWS CLI Workshop Parameters: EnvironmentName: Description: An environment name that is prefixed to resource names Type: String Default: awscliworkshop C9InstanceType: Description: Cloud9 instance type Type: String Default: t3.medium AllowedValues: - t2.micro - t3.micro - t3.small - t3.medium ConstraintDescription: "[Default: t3.medium] Must be t2.micro, t3.micro, t3.small, or t3.medium." C9EnvType: Description: Cloud9 Environment type. Default: self # Default: event-engine Type: String AllowedValues: - self - event-engine ConstraintDescription: must specify self or event-engine. C9InstanceVolumeSize: Type: Number Description: The Size in GB of the Cloud9 Instance Volume. Default: 30 VpcCIDR: Description: Please enter the IP range (CIDR notation) for this VPC Type: String Default: 10.10.0.0/16 PublicSubnet1CIDR: Description: Please enter the IP range (CIDR notation) for the public subnet in the first Availability Zone Type: String Default: 10.10.10.0/24 PublicSubnet2CIDR: Description: Please enter the IP range (CIDR notation) for the public subnet in the second Availability Zone Type: String Default: 10.10.20.0/24 PrivateSubnet1CIDR: Description: Please enter the IP range (CIDR notation) for the private subnet in the first Availability Zone Type: String Default: 10.10.30.0/24 PrivateSubnet2CIDR: Description: Please enter the IP range (CIDR notation) for the private subnet in the second Availability Zone Type: String Default: 10.10.40.0/24 Conditions: isEventEngine: !Equals [ !Ref C9EnvType, event-engine ] Resources: ################## Cloud 9 Instance ##################### C9Instance: DependsOn: C9BootstrapAssociation Type: AWS::Cloud9::EnvironmentEC2 Properties: Name: !Sub ${EnvironmentName} Description: !Sub AWS Cloud9 instance for ${EnvironmentName} OwnerArn: !If [isEventEngine, !Sub "arn:aws:iam::${AWS::AccountId}:assumed-role/TeamRole/MasterKey", !Ref "AWS::NoValue"] SubnetId: !Ref PublicSubnet1 AutomaticStopTimeMinutes: 3600 ImageId: amazonlinux-2-x86_64 InstanceType: !Ref C9InstanceType Tags: - Key: SSMBootstrap Value: Active - Key: Environment Value: !Sub ${EnvironmentName} C9InstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Path: "/" Roles: - Ref: C9Role C9BootstrapAssociation: Type: AWS::SSM::Association Properties: Name: !Ref C9SSMDocument OutputLocation: S3Location: OutputS3BucketName: !Ref C9OutputBucket OutputS3KeyPrefix: bootstrapoutput Targets: - Key: tag:SSMBootstrap Values: - Active ################## SSM Parameters ##################### C9EnvironmentId: Type: AWS::SSM::Parameter Properties: Description: DO NOT UPDATE IN CONSOLE. This parameter is managed by a CFN stack. Name: /workshop/pre-provisioned/c9-environment-id Type: String Value: !Ref C9Instance C9StackName: Type: AWS::SSM::Parameter Properties: Description: DO NOT UPDATE IN CONSOLE. This parameter is managed by a CFN stack. Name: /workshop/pre-provisioned/stack-name Type: String Value: !Ref AWS::StackName ################## Roles And Permissions ################# C9Role: Type: AWS::IAM::Role Metadata: cfn_nag: rules_to_suppress: - id: W43 reason: "C9 Instance will be used workshop attendees" - id: W28 reason: "Needed in order to enable consistent environment for attendees" Properties: RoleName: !Sub ${EnvironmentName}-admin 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: - arn:aws:iam::aws:policy/AdministratorAccess - arn:aws:iam::aws:policy/AWSCloud9Administrator Path: "/" C9LambdaExecutionRole: 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: - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" Policies: - PolicyName: !Sub C9LambdaPolicy-${AWS::Region} PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - cloudformation:DescribeStacks - cloudformation:DescribeStackEvents - cloudformation:DescribeStackResource - cloudformation:DescribeStackResources Resource: !Sub "arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/*" - Effect: Allow Action: - ec2:AssociateIamInstanceProfile - ec2:ModifyInstanceAttribute - ec2:ReplaceIamInstanceProfileAssociation Resource: !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:instance/*" - Effect: Allow Action: - ec2:DescribeInstances - ec2:DescribeIamInstanceProfileAssociations Resource: "*" - Effect: Allow Action: - iam:ListInstanceProfiles Resource: !Sub arn:aws:iam::${AWS::AccountId}:instance-profile/* - Effect: Allow Action: - iam:PassRole Resource: !GetAtt C9Role.Arn ################## Cloud9 Bootstrap Lambda Function ################ C9BootstrapInstanceLambda: Type: Custom::C9BootstrapInstanceLambda DependsOn: - C9LambdaExecutionRole Properties: Tags: - Key: Environment Value: !Sub ${EnvironmentName} ServiceToken: !GetAtt C9BootstrapInstanceLambdaFunction.Arn REGION: !Ref AWS::Region StackName: !Ref AWS::StackName EnvironmentId: !Ref C9Instance LabIdeInstanceProfileName: !Ref C9InstanceProfile LabIdeInstanceProfileArn: !GetAtt C9InstanceProfile.Arn C9BootstrapInstanceLambdaFunction: Type: AWS::Lambda::Function Properties: Tags: - Key: Environment Value: AWS Example Handler: index.lambda_handler Role: !GetAtt C9LambdaExecutionRole.Arn Runtime: python3.6 MemorySize: 256 Timeout: 600 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__) logger.setLevel(logging.INFO) def lambda_handler(event, context): logger.info('event: {}'.format(event)) logger.info('context: {}'.format(context)) responseData = {} if event['RequestType'] == 'Create': try: # Open AWS clients ec2 = boto3.client('ec2') # Get the InstanceId of the Cloud9 IDE instance = ec2.describe_instances(Filters=[{'Name': 'tag:Name','Values': ['aws-cloud9-awscliworkshop'+'-'+event['ResourceProperties']['EnvironmentId']]}])['Reservations'][0]['Instances'][0] logger.info('instance: {}'.format(instance)) # Create the IamInstanceProfile request object iam_instance_profile = { 'Arn': event['ResourceProperties']['LabIdeInstanceProfileArn'], 'Name': event['ResourceProperties']['LabIdeInstanceProfileName'] } logger.info('iam_instance_profile: {}'.format(iam_instance_profile)) # Wait for Instance to become ready before adding Role instance_state = instance['State']['Name'] logger.info('instance_state: {}'.format(instance_state)) while instance_state != 'running': time.sleep(5) instance_state = ec2.describe_instances(InstanceIds=[instance['InstanceId']]) logger.info('instance_state: {}'.format(instance_state)) # attach instance profile response = ec2.associate_iam_instance_profile(IamInstanceProfile=iam_instance_profile, InstanceId=instance['InstanceId']) logger.info('response - associate_iam_instance_profile: {}'.format(response)) #r_ec2 = boto3.resource('ec2') responseData = {'Success': 'Started bootstrapping for instance: '+instance['InstanceId']} cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, 'CustomResourcePhysicalID') except Exception as e: logger.error(e, exc_info=True) # responseData = {'Error': traceback.format_exc(e)} responseData = {'Error':'There was a problem associating IAM profile to the Cloud9 Instance'} cfnresponse.send(event, context, cfnresponse.FAILED, responseData, 'CustomResourcePhysicalID') else: responseData = {'Success': 'Update or delete event'} cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, 'CustomResourcePhysicalID') ################## Cloud9 Bootstrap SSM Automation Document ############### C9OutputBucket: Type: AWS::S3::Bucket Metadata: cfn_nag: rules_to_suppress: - id: W35 reason: "Access Logs aren't needed for this bucket" DeletionPolicy: Retain Properties: AccessControl: Private BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true C9OutputBucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref C9OutputBucket PolicyDocument: Version: 2012-10-17 Statement: - Action: - 's3:GetObject' - 's3:PutObject' - 's3:PutObjectAcl' Effect: Allow Resource: !Sub arn:aws:s3:::${C9OutputBucket}/* Principal: AWS: !GetAtt C9LambdaExecutionRole.Arn C9SSMDocument: 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: C9bootstrap inputs: runCommand: - "#!/bin/bash" - date - echo LANG=en_US.utf-8 >> /etc/environment - echo LC_ALL=en_US.UTF-8 >> /etc/environment - . /home/ec2-user/.bashrc - echo '=== Resizing the Instance volume' - !Sub SIZE=${C9InstanceVolumeSize} - !Sub REGION=${AWS::Region} - | INSTANCEID=$(curl http://169.254.169.254/latest/meta-data/instance-id) VOLUMEID=$(aws ec2 describe-instances \ --instance-id $INSTANCEID \ --query "Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId" \ --output text --region $REGION) aws ec2 modify-volume --volume-id $VOLUMEID --size $SIZE --region $REGION while [ \ "$(aws ec2 describe-volumes-modifications \ --volume-id $VOLUMEID \ --filters Name=modification-state,Values="optimizing","completed" \ --query "length(VolumesModifications)"\ --output text --region $REGION)" != "1" ]; do sleep 1 done if [ $(readlink -f /dev/xvda) = "/dev/xvda" ] then sudo growpart /dev/xvda 1 STR=$(cat /etc/os-release) SUB="VERSION_ID=\"2\"" if [[ "$STR" == *"$SUB"* ]] then sudo xfs_growfs -d / else sudo resize2fs /dev/xvda1 fi else sudo growpart /dev/nvme0n1 1 STR=$(cat /etc/os-release) SUB="VERSION_ID=\"2\"" if [[ "$STR" == *"$SUB"* ]] then sudo xfs_growfs -d / else sudo resize2fs /dev/nvme0n1p1 fi fi - echo '=== CONFIGURE default python version ===' - | sudo amazon-linux-extras enable python3.8 yum -y install python38 python38-devel sudo ln -sf /usr/bin/python3.8 /usr/bin/python3 - echo '=== Installing AWS CLI v2 ===' - | yum -y remove aws-cli sudo -H -u ec2-user bash -c 'python3.8 -m pip install --user pipx' sudo -H -u ec2-user bash -c 'python3.8 -m pipx install https://ee-assets-prod-us-east-1.s3.us-east-1.amazonaws.com/modules/32b2684a3a1148d0af1dc3ed1db92191/v3/awscli-2.3.2-py3-none-any.whl' sudo -H -u ec2-user bash -c 'python3.8 -m pipx ensurepath' echo "complete -C aws_completer aws" >> /home/ec2-user/.bashrc echo 'export PATH="/home/ec2-user/.local/bin:$PATH"' >> /home/ec2-user/.bashrc echo "export AWS_DEFAULT_REGION=\"$REGION\"" >> /home/ec2-user/.bashrc sudo -H -u ec2-user bash -c 'mkdir -p /home/ec2-user/.aws/cli' sudo -H -u ec2-user bash -c 'wget https://ee-assets-prod-us-east-1.s3.us-east-1.amazonaws.com/modules/32b2684a3a1148d0af1dc3ed1db92191/v3/alias -O /home/ec2-user/.aws/cli/alias' - echo '=== Installing jp ===' - | sudo wget https://github.com/jmespath/jp/releases/latest/download/jp-linux-amd64 -O /usr/local/bin/jp && sudo chmod +x /usr/local/bin/jp - echo '=== Installing SSM session manager plugin ===' - | yum install -y "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/linux_64bit/session-manager-plugin.rpm" - echo '=== Installing c9 utility ===' - | sudo npm install -g c9 - echo '=== Installing git-remote-codecommit helper ===' - | sudo -H -u ec2-user bash -c 'python3.8 -m pipx install git-remote-codecommit' - | sudo -H -u ec2-user bash -c 'git config --global user.name "Workshop User"' sudo -H -u ec2-user bash -c 'git config --global user.email "<>"' - echo "Bootstrap completed with return code $?" ################## Networking ############### VPC: Type: AWS::EC2::VPC Metadata: cfn_nag: rules_to_suppress: - id: W60 reason: "No need for Flow logs in this workshop" Properties: CidrBlock: !Ref VpcCIDR EnableDnsSupport: true EnableDnsHostnames: true Tags: - Key: Name Value: !Ref EnvironmentName InternetGateway: Type: AWS::EC2::InternetGateway Properties: Tags: - Key: Name Value: !Ref EnvironmentName InternetGatewayAttachment: Type: AWS::EC2::VPCGatewayAttachment Properties: InternetGatewayId: !Ref InternetGateway VpcId: !Ref VPC PublicSubnet1: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC AvailabilityZone: !Select [ 0, !GetAZs '' ] CidrBlock: !Ref PublicSubnet1CIDR MapPublicIpOnLaunch: false Tags: - Key: Name Value: !Sub ${EnvironmentName} Public Subnet (AZ1) PublicSubnet2: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC AvailabilityZone: !Select [ 1, !GetAZs '' ] CidrBlock: !Ref PublicSubnet2CIDR MapPublicIpOnLaunch: false Tags: - Key: Name Value: !Sub ${EnvironmentName} Public Subnet (AZ2) PrivateSubnet1: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC AvailabilityZone: !Select [ 0, !GetAZs '' ] CidrBlock: !Ref PrivateSubnet1CIDR MapPublicIpOnLaunch: false Tags: - Key: Name Value: !Sub ${EnvironmentName} Private Subnet (AZ1) PrivateSubnet2: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC AvailabilityZone: !Select [ 1, !GetAZs '' ] CidrBlock: !Ref PrivateSubnet2CIDR MapPublicIpOnLaunch: false Tags: - Key: Name Value: !Sub ${EnvironmentName} Private Subnet (AZ2) NatGateway1EIP: Type: AWS::EC2::EIP DependsOn: InternetGatewayAttachment Properties: Domain: vpc NatGateway2EIP: Type: AWS::EC2::EIP DependsOn: InternetGatewayAttachment Properties: Domain: vpc NatGateway1: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt NatGateway1EIP.AllocationId SubnetId: !Ref PublicSubnet1 NatGateway2: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt NatGateway2EIP.AllocationId SubnetId: !Ref PublicSubnet2 PublicRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub ${EnvironmentName} Public Routes DefaultPublicRoute: Type: AWS::EC2::Route DependsOn: InternetGatewayAttachment Properties: RouteTableId: !Ref PublicRouteTable DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref InternetGateway PublicSubnet1RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PublicRouteTable SubnetId: !Ref PublicSubnet1 PublicSubnet2RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PublicRouteTable SubnetId: !Ref PublicSubnet2 PrivateRouteTable1: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub ${EnvironmentName} Private Routes (AZ1) DefaultPrivateRoute1: Type: AWS::EC2::Route Properties: RouteTableId: !Ref PrivateRouteTable1 DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref NatGateway1 PrivateSubnet1RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PrivateRouteTable1 SubnetId: !Ref PrivateSubnet1 PrivateRouteTable2: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub ${EnvironmentName} Private Routes (AZ2) DefaultPrivateRoute2: Type: AWS::EC2::Route Properties: RouteTableId: !Ref PrivateRouteTable2 DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref NatGateway2 PrivateSubnet2RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PrivateRouteTable2 SubnetId: !Ref PrivateSubnet2 NoIngressSecurityGroup: Type: AWS::EC2::SecurityGroup Metadata: cfn_nag: rules_to_suppress: - id: W5 reason: "Outbound access to the world is needed to download dependencies in C9" Properties: GroupDescription: "Security group with no ingress rule" VpcId: !Ref VPC SecurityGroupEgress: - IpProtocol: tcp FromPort: 80 ToPort: 80 CidrIp: 0.0.0.0/0 Description: "HTTP Outbound traffic" - IpProtocol: tcp FromPort: 443 ToPort: 443 CidrIp: 0.0.0.0/0 Description: "HTTPS Outbound traffic" Outputs: Cloud9IDE: Value: !Sub https://${AWS::Region}.console.aws.amazon.com/cloud9/ide/${C9Instance}?region=${AWS::Region}