--- AWSTemplateFormatVersion: '2010-09-09' Description: AWS CloudFormation template to create a Cloud9 environment and prepare the HTC-Grid setup Metadata: Author: Description: Pierre-Louis Gounod License: Description: 'Copyright 2024 Amazon.com, Inc. and its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License 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.nse.' Parameters: C9InstanceType: Description: Cloud9 Instance Type Type: String Default: m5.large AllowedValues: - t3.small - t3.medium - m4.large - m5.large ConstraintDescription: Must be a valid Cloud9 instance type. C9KubectlVersion: Description: kubectl version to install on Cloud9 Type: String Default: 1.25.12 ConstraintDescription: Must be a valid kubectl version. C9TerraformVersion: Description: Terraform version to install on Cloud9 Type: String Default: 1.5.4 ConstraintDescription: Must be a valid Terraform version. C9EKSctlVersion: Description: eksctl version to install on Cloud9 Type: String Default: 0.151.0 ConstraintDescription: Must be a valid eksctl version. C9HelmVersion: Description: Helm version to install on Cloud9 Type: String Default: 3.12.2 ConstraintDescription: Must be a valid Helm version HTCGridVersion: Description: HTC Grid version to install on Cloud9 Type: String Default: latest ConstraintDescription: Must be a valid HTC Grid version. HTCGridRepo: Description: HTC Grid repo to pull on Cloud9 Type: String Default: https://github.com/awslabs/aws-htc-grid ConstraintDescription: Must be a valid HTC Grid version. ParticipantRoleARN: Description: "ARN of the Participant Role" Default: NONE Type: String ConstraintDescription: This is ONLY used via AWS WorkshopStudio. DON'T change this if you are self-deploying the stack. Conditions: NotWSParticipant: !Equals [!Ref ParticipantRoleARN, NONE] Resources: ################## PERMISSIONS AND ROLES ################# C9Role: Type: AWS::IAM::Role Properties: Tags: - Key: Environment Value: !Ref 'AWS::StackName' AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - !Sub 'ec2.${AWS::URLSuffix}' - !Sub 'ssm.${AWS::URLSuffix}' Action: - sts:AssumeRole ManagedPolicyArns: - !Sub 'arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess' Path: "/" C9LambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - !Sub 'lambda.${AWS::URLSuffix}' Action: - sts:AssumeRole Path: "/" Policies: - PolicyName: !Sub 'C9LambdaPolicy-${AWS::Region}' PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: !Sub 'arn:${AWS::Partition}:logs:*:*:*' - Effect: Allow Action: - cloudformation:DescribeStacks - cloudformation:DescribeStackEvents - cloudformation:DescribeStackResource - cloudformation:DescribeStackResources - ec2:DescribeInstances - ec2:DescribeVolumes - ec2:AssociateIamInstanceProfile - ec2:DescribeIamInstanceProfileAssociations - ec2:ModifyInstanceAttribute - ec2:ModifyVolume - ec2:ReplaceIamInstanceProfileAssociation - ec2:RebootInstances - iam:ListInstanceProfiles - iam:PassRole Resource: "*" ################## LAMBDA BOOTSTRAP FUNCTION ################ C9BootstrapInstanceLambdaExecution: # Description: Bootstrap Cloud9 Instance Type: Custom::C9BootstrapInstanceLambdaExecution Properties: Tags: - Key: Environment Value: !Ref 'AWS::StackName' ServiceToken: !GetAtt C9BootstrapInstanceLambdaFunction.Arn REGION: !Ref 'AWS::Region' StackName: !Ref 'AWS::StackName' EnvironmentId: !Ref C9Instance LabIdeInstanceProfileArn: !GetAtt C9InstanceProfile.Arn C9BootstrapInstanceLambdaFunction: Type: AWS::Lambda::Function #checkov:skip=CKV_AWS_116:C9BootstrapInstanceLambdaFunction is used for bootstrapping and doesnt require DLQ #checkov:skip=CKV_AWS_117:C9BootstrapInstanceLambdaFunction runs inside the default VPC #checkov:skip=CKV_AWS_115:C9BootstrapInstanceLambdaFunction is triggered by CFN and doesnt require concurrency Properties: Tags: - Key: Environment Value: !Ref 'AWS::StackName' Handler: index.lambda_handler Role: !GetAtt C9LambdaExecutionRole.Arn Runtime: python3.11 MemorySize: 256 Timeout: '600' Code: ZipFile: | from __future__ import print_function import boto3 import json import os import time import traceback import cfnresponse def lambda_handler(event, context): print(f'event: {event}') print(f'context: {context}') responseData = {} if event['RequestType'] == 'Delete': try: responseData = {'Success': 'Finished cleanup'} cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, 'CustomResourcePhysicalID') except Exception as e: responseData = {'Error': traceback.format_exc(e)} cfnresponse.send(event, context, cfnresponse.FAILED, responseData, 'CustomResourcePhysicalID') if event['RequestType'] == 'Create': try: # Open AWS clients ec2 = boto3.client('ec2') # Get the InstanceId from Cloud9 IDE print('tag:aws:cloud9:environment : {}'.format(event['ResourceProperties']['EnvironmentId'])) instance = ec2.describe_instances(Filters=[{'Name': 'tag:aws:cloud9:environment','Values': [event['ResourceProperties']['EnvironmentId']]}])['Reservations'][0]['Instances'][0] print(f'instance: {instance}') volume_id = instance['BlockDeviceMappings'][0]['Ebs']['VolumeId'] print(f'Volume Id {volume_id}') ec2.modify_volume(VolumeId=volume_id, Size=100) print('Changed Volume to 100GB') # Create the IamInstanceProfile request object iam_instance_profile = { 'Arn': event['ResourceProperties']['LabIdeInstanceProfileArn'] } print(f'iam_instance_profile: {iam_instance_profile}') print(f'Will wait for Instance to become ready before adding Role') # Wait for Instance to become ready before adding Role instance_state = instance['State']['Name'] while instance_state != 'running': time.sleep(5) instance_state = ec2.describe_instances(InstanceIds=[instance['InstanceId']]) print(f'Waiting for the instance state to be "running", current instance_state: {instance_state}') print(f'Instance is ready, attaching IAM instance profile: {iam_instance_profile}') # attach instance profile print(f'Instance is running , about to associate iam_instance_profile: {iam_instance_profile}') print(f'Check if there is already an Associated instance profile') try: associationID = ec2.describe_iam_instance_profile_associations( Filters=[ { 'Name': 'instance-id', 'Values': [ instance['InstanceId'], ] }, ], )['IamInstanceProfileAssociations'][0]['AssociationId'] except Exception as e: print(e) associationID= None if associationID: try: print(f'Association found, AssociationID is: {associationID}') print(f'Replacing association') response = ec2.replace_iam_instance_profile_association( IamInstanceProfile=iam_instance_profile, AssociationId=associationID ) print(f'response - associate_iam_instance_profile: {response}') except Exception as e: print(e) else: try: print(f'No existing association. Associating C9 instance profile...') response = ec2.associate_iam_instance_profile(IamInstanceProfile=iam_instance_profile, InstanceId=instance['InstanceId']) print(f'response - associate_iam_instance_profile: {response}') except Exception as e: print(e) print(f'Reboot instance to enforce registration with SSM') try: response = ec2.reboot_instances( InstanceIds=[ instance['InstanceId'], ], DryRun=False ) print(f'response - reboot_instances: {response}') except Exception as e: print(e) responseData = {'Success': 'Started bootstrapping for instance: '+instance['InstanceId']} cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, 'CustomResourcePhysicalID') except Exception as e: print(type(e)) print(e) responseData = {'Error': traceback.format_exc(e)} cfnresponse.send(event, context, cfnresponse.FAILED, responseData, 'CustomResourcePhysicalID') ################## SSM BOOTSRAP HANDLER ############### C9OutputBucket: Type: AWS::S3::Bucket #checkov:skip=AVD-AWS-0088:C9OutputBucket is a logging bucket and doesnt require encryption #checkov:skip=AVD-AWS-0132:C9OutputBucket is a logging bucket and doesnt require encryption #checkov:skip=CKV_AWS_21:C9OutputBucket is a logging bucket and doesnt require versioning DeletionPolicy: Delete Properties: PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true C9OutputBucketPolicy: Type: 'AWS::S3::BucketPolicy' Properties: Bucket: !Ref C9OutputBucket PolicyDocument: Statement: - Action: - 's3:GetObject' - 's3:PutObject' Condition: Bool: 'aws:SecureTransport': false Effect: Deny Principal: '*' Resource: !Sub 'arn:${AWS::Partition}:s3:::${C9OutputBucket}/*' Sid: DenyUnencryptedConnections - Action: - 's3:PutBucketAcl' - 's3:PutObject' - 's3:PutObjectAcl' Condition: StringEquals: 's3:x-amz-acl': - authenticated-read - public-read - public-read-write Effect: Deny Principal: '*' Resource: - !Sub 'arn:${AWS::Partition}:s3:::${C9OutputBucket}' - !Sub 'arn:${AWS::Partition}:s3:::${C9OutputBucket}/*' Sid: DenyPublicReadAcl - Action: - 's3:PutBucketAcl' - 's3:PutObject' - 's3:PutObjectAcl' Condition: StringLike: 's3:x-amz-grant-read': - !Sub '*http://acs.${AWS::URLSuffix}/groups/global/AllUsers*' - !Sub '*http://acs.${AWS::URLSuffix}/groups/global/AuthenticatedUsers*' Effect: Deny Principal: '*' Resource: - !Sub 'arn:${AWS::Partition}:s3:::${C9OutputBucket}' - !Sub 'arn:${AWS::Partition}:s3:::${C9OutputBucket}/*' Sid: DenyGrantingPublicRead C9SSMDocument: Type: AWS::SSM::Document Properties: Tags: - Key: Environment Value: !Ref 'AWS::StackName' DocumentFormat: YAML DocumentType: Command Content: schemaVersion: '2.2' description: Bootstrap Cloud9 Instance mainSteps: - action: aws:runShellScript name: C9bootstrap inputs: runCommand: - "#!/bin/bash" - !Sub 'export KUBECTL_VERSION="${C9KubectlVersion}"' - !Sub 'export EKSCTL_VERSION="${C9EKSctlVersion}"' - !Sub 'export TERRAFORM_VERSION="${C9TerraformVersion}"' - !Sub 'export HELM_VERSION="${C9HelmVersion}"' - !Sub 'export HTCGRID_VERSION="${HTCGridVersion}"' - !Sub 'export HTCGRID_REPO="${HTCGridRepo}"' - | # set -x echo -e "========================================================" echo -e " Starting the Cloud9 instance boostrap at: $(date)" echo -e "========================================================" echo -e "\n" echo -e "========================================================" echo -e " Checking user identity" echo -e "========================================================" echo -e "[INFO] Running as OS user: $(whoami)" echo -e "[INFO] Running with the following IAM Role:" sudo -i -u ec2-user aws sts get-caller-identity echo -e "========================================================" echo -e "\n\n" echo -e "========================================================" echo -e " Increasing volume capacity" echo -e "========================================================" if sudo growpart /dev/nvme0n1 1; then sudo xfs_growfs -d /; fi echo -e "========================================================" echo -e "\n\n" echo -e "========================================================" echo -e " Setting up temporary directory" echo -e "========================================================" export TMP_DIR="$(mktemp -d)" if [[ "${TMP_DIR}" != "/tmp/"* ]]; then export TMP_DIR="/tmp"; fi echo -e "[INFO] All the packages will be temporarily downloaded to ${TMP_DIR}" echo -e "========================================================" echo -e "\n\n" echo -e "========================================================" echo -e " Installing virtualenv" echo -e "========================================================" sudo pip3 install --upgrade --force-reinstall virtualenv echo -e "========================================================" echo -e "\n\n" echo -e "========================================================" echo -e " Installing kubectl" echo -e "========================================================" sudo curl --silent --location \ -o "${TMP_DIR}/kubectl" \ "https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VERSION}/bin/linux/amd64/kubectl" sudo chmod +x "${TMP_DIR}/kubectl" sudo mv "${TMP_DIR}/kubectl" "/usr/local/bin/kubectl" if kubectl > /dev/null 2>&1; then echo -e "[INFO] Successfully installed [$(which kubectl)] with version:\n$(kubectl version --client=true)"; else echo -e "[ERROR] Failed to install kubectl.."; fi echo -e "========================================================" echo -e "\n\n" echo -e "========================================================" echo -e " Installing pre-requisite tools" echo -e "========================================================" sudo yum -y install git jq gettext unzip python python3.11 echo -e "========================================================" echo -e "\n\n" echo -e "========================================================" echo -e " Updating AWS CLI to the latest version" echo -e "========================================================" sudo -i -u ec2-user aws --version curl --silent --location \ -o "${TMP_DIR}/awscliv2.zip" \ "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" unzip -q "${TMP_DIR}/awscliv2.zip" -d "${TMP_DIR}/awscli" sudo "${TMP_DIR}/awscli/aws/install" --update source /home/ec2-user/.bash_profile if aws --version > /dev/null 2>&1; then echo -e "[INFO] Successfully installed [$(which aws)] with version:\n$(aws --version)"; else echo -e "[ERROR] Failed to install awscli.."; fi echo -e "========================================================" echo -e "\n\n" echo -e "========================================================" echo -e " Setting up AWS Configs" echo -e "========================================================" rm -rvf /home/ec2-user/.aws/credentials export ACCOUNT_ID="$(aws sts get-caller-identity --output text --query Account)" export AWS_REGION="$(curl -s 169.254.169.254/latest/dynamic/instance-identity/document | jq -r '.region')" if ! grep -q 'ACCOUNT_ID' /home/ec2-user/.bash_profile; then echo -e "export ACCOUNT_ID=\"${ACCOUNT_ID}\"" >> /home/ec2-user/.bash_profile; else echo -e "ACCOUNT_ID already exists in the file; fi if ! grep -q 'AWS_REGION' /home/ec2-user/.bash_profile; then echo -e "export AWS_REGION=\"${AWS_REGION}\"" >> /home/ec2-user/.bash_profile; else echo -e "AWS_REGION already exists in the file; fi sudo -i -u ec2-user aws configure set default.region "${AWS_REGION}" > /dev/null 2>&1 sudo -i -u ec2-user aws configure get default.region echo -e "[INFO] Running with the following IAM Role:" sudo -i -u ec2-user aws sts get-caller-identity echo -e "========================================================" echo -e "\n\n" echo -e "========================================================" echo -e " Installing eksctl" echo -e "========================================================" curl --silent --location \ -o "${TMP_DIR}/eksctl_Linux_amd64.tar.gz" \ "https://github.com/weaveworks/eksctl/releases/download/v${EKSCTL_VERSION}/eksctl_Linux_amd64.tar.gz" tar -zxf "${TMP_DIR}/eksctl_Linux_amd64.tar.gz" -C "${TMP_DIR}/" chmod +x "${TMP_DIR}/eksctl" sudo mv "${TMP_DIR}/eksctl" /usr/local/bin/eksctl if eksctl > /dev/null 2>&1; then echo -e "[INFO] Successfully installed $(which eksctl) with version:\n$(eksctl version)"; else echo -e "[ERROR] Failed to install eksctl.."; fi echo -e "========================================================" echo -e "\n\n" echo -e "========================================================" echo -e " Installing Terraform" echo -e "========================================================" curl --silent --location \ -o "${TMP_DIR}/terraform.zip" \ "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip" unzip -q "${TMP_DIR}/terraform.zip" -d "${TMP_DIR}/terraform" sudo chmod +x "${TMP_DIR}/terraform/terraform" sudo mv "${TMP_DIR}/terraform/terraform" "/usr/local/bin/terraform" if terraform version > /dev/null 2>&1; then echo -e "[INFO] Successfully installed [$(which terraform)] with version:\n$(terraform version | head -1)"; else echo -e "[ERROR] Failed to install terraform.."; fi echo -e "========================================================" echo -e "\n\n" echo -e "========================================================" echo -e " Installing Helm" echo -e "========================================================" curl --silent --location \ -o "${TMP_DIR}/helm.tar.gz" \ "https://get.helm.sh/helm-v${HELM_VERSION}-linux-amd64.tar.gz" mkdir -p "${TMP_DIR}/helm" tar -zxf "${TMP_DIR}/helm.tar.gz" -C "${TMP_DIR}/helm/" sudo chmod +x "${TMP_DIR}/helm/linux-amd64/helm" sudo mv "${TMP_DIR}/helm/linux-amd64/helm" "/usr/local/bin/" if helm > /dev/null 2>&1; then echo -e "[INFO] Successfully installed [$(which helm)] with version:\n$(helm version)"; else echo -e "[ERROR] Failed to install helm.."; fi echo -e "========================================================" echo -e "\n\n" echo -e "========================================================" echo -e " Downloading HTC-Grid" echo -e "========================================================" export DEFAULT_HTCGRID_REPO="https://github.com/awslabs/aws-htc-grid" export HTCGRID_HOME="/home/ec2-user/environment" cd "${HTCGRID_HOME}" if [ -e "aws-htc-grid" ]; then export OLD_HTCGRID_DIR="aws-htc-grid.$(date "+%F-%T" | sed 's|:|_|g')" mv aws-htc-grid "${OLD_HTCGRID_DIR}" if [ $? -eq 0 ]; then echo -e "[WARN] Found existing aws-htc-grid directory. Renaming to:\n${OLD_HTCGRID_DIR}" else echo -e "[ERROR] Found existing aws-htc-grid directory, however failed to rename it as:\n${OLD_HTCGRID_DIR}" fi fi if [ "${HTCGRID_REPO}" == "${DEFAULT_HTCGRID_REPO}" ]; then echo -e "[INFO] Downloading HTC Grid release ${HTCGRID_VERSION} from default repo:\n${HTCGRID_REPO}" if [ "${HTCGRID_VERSION}" == "latest" ]; then export HTCGRID_VERSION="$(curl -Ls -o /dev/null -w %{url_effective} ${HTCGRID_REPO}/releases/latest | awk -F '/v' '{ print $NF }')"; echo -e "[INFO] The value for HTCGRID_VERSION is 'latest'. Discovered the latest release as: ${HTCGRID_VERSION}" fi if [ "${HTCGRID_VERSION}" == "main" ]; then export HTCGRID_URI_REF="heads/main"; else export HTCGRID_URI_REF="tags/v${HTCGRID_VERSION}"; fi curl --silent --location \ -o "${HTCGRID_HOME}/aws-htc-grid.tar.gz" \ "${HTCGRID_REPO}/archive/refs/${HTCGRID_URI_REF}.tar.gz" cd "${HTCGRID_HOME}"; tar -zxf aws-htc-grid.tar.gz; rm -fr aws-htc-grid.tar.gz mv "aws-htc-grid-${HTCGRID_VERSION}" "aws-htc-grid" if [ $? -eq 0 ]; then echo -e "[INFO] Successfully downloaded HTC-Grid to [${HTCGRID_HOME}/aws-htc-grid] with version:\n${HTCGRID_VERSION}" else echo -e "[ERROR] Failed to rename aws-htc-grid-${HTCGRID_VERSION} to aws-htc-grid .." fi else echo -e "[INFO] Cloning HTC-Grid from:\n${HTCGRID_REPO}/tree/${HTCGRID_VERSION}" mkdir -p "${HTCGRID_HOME}/aws-htc-grid"; cd "${HTCGRID_HOME}/aws-htc-grid" git init && git branch -m main || true && \ git remote add origin "${HTCGRID_REPO}.git" -f && git reset --hard origin/main && \ git checkout "${HTCGRID_VERSION}" if [ $? -eq 0 ]; then echo -e "[INFO] Successfully downloaded HTC-Grid to [${HTCGRID_HOME}/aws-htc-grid] with version:\n${HTCGRID_VERSION}" else echo -e "[ERROR] Failed to clone ${HTCGRID_REPO}.git with version ${HTCGRID_VERSION}. Exiting ..."; exit 254 fi fi sudo chown -R ec2-user:ec2-user "${HTCGRID_HOME}" echo -e "========================================================" echo -e "\n\n" echo -e "========================================================" echo -e " Completed instance boostrap process at: $(date)" echo -e "========================================================" C9BootstrapAssociation: Type: AWS::SSM::Association Properties: Name: !Ref C9SSMDocument OutputLocation: S3Location: OutputS3BucketName: !Ref C9OutputBucket OutputS3KeyPrefix: bootstrapoutput Targets: - Key: tag:SSMBootstrap Values: - Active - Key: tag:Environment Values: - !Ref 'AWS::StackName' ################## INSTANCE ##################### C9InstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Path: "/" Roles: - !Ref C9Role C9Instance: Type: AWS::Cloud9::EnvironmentEC2 Properties: Tags: - Key: SSMBootstrap Value: Active - Key: Environment Value: !Ref 'AWS::StackName' Name: !Ref 'AWS::StackName' Description: AWS Cloud9 Instance used to deploy HTC Grid AutomaticStopTimeMinutes: 3600 InstanceType: !Ref C9InstanceType ImageId: amazonlinux-2023-x86_64 OwnerArn: !If [NotWSParticipant , !Ref 'AWS::NoValue' , !Ref ParticipantRoleARN] Outputs: Cloud9IDE: Value: !Sub 'https://${AWS::Region}.console.aws.amazon.com/cloud9/ide/${C9Instance}?region=${AWS::Region}'