--- AWSTemplateFormatVersion: '2010-09-09' Description: AWS CloudFormation template to create a Cloud9 environment setup with kubectl and Terraform. Metadata: Author: Description: Christian Melendez License: Description: 'Copyright 2023 Amazon.com, Inc. and its affiliates. All Rights Reserved. Licensed under the Amazon Software License (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at http://aws.amazon.com/asl/ or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.' Parameters: C9InstanceType: Description: Example Cloud9 instance type Type: String Default: t3.small AllowedValues: - t3.small - m5.large ConstraintDescription: Must be a valid Cloud9 instance type C9KubectlVersion: Description: Cloud9 instance kubectl version Type: String Default: v1.29.1 ConstraintDescription: Must be a valid kubectl version C9TerraformVersion: Type: String Default: 1.7.1 ConstraintDescription: Must be a valid terraform version C9NodeViewerVersion: Type: String Default: v0.5.0 ConstraintDescription: Must be a valid eks node viewer version C9PublicSubnet: Description: Public subnet to use for the Cloud9 instance Type: AWS::EC2::Subnet::Id C9ImageId: Description: ImageId for the Cloud9 instance Type: String Default: amazonlinux-2023-x86_64 Resources: ################## PERMISSIONS AND ROLES ################# C9Role: Type: AWS::IAM::Role Properties: Tags: - Key: Environment Value: AWS Example 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 Path: "/" C9LambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: "/" Policies: - PolicyName: Fn::Join: - '' - - C9LambdaPolicy- - Ref: AWS::Region PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: arn:aws:logs:*:*:* - Effect: Allow Action: - cloudformation:DescribeStacks - cloudformation:DescribeStackEvents - cloudformation:DescribeStackResource - cloudformation:DescribeStackResources - ec2:DescribeInstances - ec2:AssociateIamInstanceProfile - ec2:ModifyInstanceAttribute - ec2:ReplaceIamInstanceProfileAssociation - ec2:DescribeIamInstanceProfileAssociations - ec2:RebootInstances - iam:ListInstanceProfiles - iam:PassRole Resource: "*" - Effect: Allow Action: - s3:ListBucket - s3:DeleteObject Resource: - !Sub arn:${AWS::Partition}:s3:::${C9OutputBucket} - !Sub arn:${AWS::Partition}:s3:::${C9OutputBucket}/* ################## LAMBDA BOOTSTRAP FUNCTION ################ C9BootstrapInstanceLambda: Type: Custom::Cloud9BootstrapInstanceLambda DependsOn: - C9LambdaExecutionRole Properties: Tags: - Key: Environment Value: AWS Example ServiceToken: Fn::GetAtt: - C9BootstrapInstanceLambdaFunction - Arn Region: Ref: AWS::Region StackName: Ref: AWS::StackName EnvironmentId: Ref: C9Instance LabIdeInstanceProfileName: Ref: C9InstanceProfile LabIdeInstanceProfileArn: Fn::GetAtt: - C9InstanceProfile - Arn LogBucket: Ref: C9OutputBucket C9BootstrapInstanceLambdaFunction: 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: AWS Example Handler: index.lambda_handler Role: Fn::GetAtt: - C9LambdaExecutionRole - Arn Runtime: python3.9 MemorySize: 1024 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__) logger.setLevel(logging.INFO) def restart_instance(instance_id): logger.info('Restart EC2 instance to restart SSM Agent') ec2 = boto3.client('ec2') try: response = ec2.reboot_instances( InstanceIds=[ instance_id ] ) except Exception as error: raise error logger.info('response: %s', response) 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:aws:cloud9:environment", "Values": [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)) response = ec2.describe_iam_instance_profile_associations( Filters=[ { "Name": "instance-id", "Values": [instance["InstanceId"]], } ] ) if len(response['IamInstanceProfileAssociations']) > 0: for association in response['IamInstanceProfileAssociations']: if association['State'] == 'associated': print("{} is active with state {}".format(association['AssociationId'], association['State'])) logger.info( "{} is active with state {}".format(association['AssociationId'], association['State']) ) time.sleep(120) ec2.replace_iam_instance_profile_association( AssociationId=association['AssociationId'], IamInstanceProfile=iam_instance_profile ) else: time.sleep(120) # 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') restart_instance(instance['InstanceId']) 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", ) elif event["RequestType"] == "Update": responseData["Message"] = {"Success": "Update event"} cfnresponse.send( event, context, cfnresponse.SUCCESS, responseData, "CustomResourcePhysicalID", ) elif event["RequestType"] == "Delete": try: # Need to empty the S3 bucket before it is deleted AssetsBucketName = (event["ResourceProperties"]["LogBucket"]) s3 = boto3.resource("s3") bucket = s3.Bucket(AssetsBucketName) bucket.objects.all().delete() responseData = { "Success": "S3 Log bucket emptied for S3 Log Bucket: " + AssetsBucketName } 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 emptying the S3 bucket"} cfnresponse.send( event, context, cfnresponse.FAILED, responseData, "CustomResourcePhysicalID", ) else: responseData = {"Success": "Other event"} cfnresponse.send( event, context, cfnresponse.SUCCESS, responseData, "CustomResourcePhysicalID", ) ################## SSM BOOTSRAP HANDLER ############### C9OutputBucket: Type: AWS::S3::Bucket DeletionPolicy: Delete C9SSMDocument: Type: AWS::SSM::Document Properties: Tags: - Key: Environment Value: AWS Example Content: Yaml DocumentType: Command Content: schemaVersion: '2.2' description: Bootstrap Cloud9 Instance mainSteps: - action: aws:runShellScript name: C9bootstrap inputs: runCommand: - "#!/bin/bash" - date - . /home/ec2-user/.bashrc - whoami - !Sub 'echo "export KUBECTL_VERSION=${C9KubectlVersion}"' - sudo -H -u ec2-user aws sts get-caller-identity - echo '=== INSTALL kubectl ===' - !Sub 'export KUBECTL_VERSION=${C9KubectlVersion}' - sudo curl --silent --location -o /usr/local/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl - sudo chmod +x /usr/local/bin/kubectl - echo '=== Install JQ and envsubst ===' - sudo yum -y install jq gettext - echo '=== Update to the latest AWS CLI ===' - sudo -H -u ec2-user aws --version - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" - unzip awscliv2.zip - sudo ./aws/install --update - rm awscliv2.zip - sudo -H -u ec2-user aws --version - echo '=== setup AWS configs ===' - rm -vf /home/ec2-user/.aws/credentials - !Sub 'echo "export AWS_REGION=${AWS::Region}" >> /home/ec2-user/.bash_profile' - sudo -H -u ec2-user aws configure set default.region ${AWS_REGION} - sudo -H -u ec2-user aws sts get-caller-identity - echo '=== Install Terraform ===' - sudo yum install -y yum-utils - sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo - !Sub 'export TERRAFORM_VERSION=${C9TerraformVersion}' - sudo yum -y install terraform-${TERRAFORM_VERSION} - terraform -v - echo '=== Install Helm ===' - curl -sSL https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash - helm repo add stable https://charts.helm.sh/stable/ - helm repo update - echo '=== Install eks-node-viewer ===' - !Sub 'export NODEVIEWER_VERSION=${C9NodeViewerVersion}' - sudo -H -u ec2-user go install github.com/awslabs/eks-node-viewer/cmd/eks-node-viewer@${NODEVIEWER_VERSION} - sudo -H -u ec2-user echo "export PATH=/home/ec2-user/go/bin:$PATH" >> /home/ec2-user/.bashrc - eks-node-viewer -v - echo '=== Finishing ===' - !Sub 'export CFN_RESPONSE_URL="${WaitForStackCreationHandle}"' - | curl -X PUT -H 'Content-Type:' --data-binary '{"Status" : "SUCCESS","Reason" : "Creation Complete", "UniqueId" : "1","Data" : "Creation complete"}' $CFN_RESPONSE_URL - sudo shutdown --reboot 1 C9BootstrapAssociation: Type: AWS::SSM::Association DependsOn: - C9OutputBucket Properties: Name: !Ref C9SSMDocument OutputLocation: S3Location: OutputS3BucketName: !Ref C9OutputBucket OutputS3KeyPrefix: bootstrapoutput Targets: - Key: tag:SSMBootstrap Values: - Active WaitForStackCreationHandle: Type: AWS::CloudFormation::WaitConditionHandle WaitCondition: Type: AWS::CloudFormation::WaitCondition DependsOn: [C9BootstrapInstanceLambda] Properties: Handle: !Ref WaitForStackCreationHandle Timeout: 7200 Count: 1 ################## INSTANCE ##################### C9InstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Path: "/" Roles: - Ref: C9Role C9Instance: Description: "-" DependsOn: C9BootstrapAssociation Type: AWS::Cloud9::EnvironmentEC2 Properties: Description: AWS Cloud9 instance for Examples AutomaticStopTimeMinutes: 3600 ImageId: Ref: C9ImageId SubnetId: Ref: C9PublicSubnet InstanceType: Ref: C9InstanceType Name: Ref: AWS::StackName OwnerArn: !Ref AWS::NoValue Tags: - Key: SSMBootstrap Value: Active - Key: Environment Value: Ref: AWS::StackName Outputs: Cloud9IDE: Value: Fn::Join: - '' - - https:// - Ref: AWS::Region - ".console.aws.amazon.com/cloud9/ide/" - Ref: C9Instance - "?region=" - Ref: AWS::Region