infrastructure/parallelcluster-ui.yaml (1,057 lines of code) (raw):

Parameters: AdminUserEmail: Description: Email address of administrative user to setup by default (only with new Cognito instances). Type: String Default: '' PublicEcrImageUri: Description: When specified, the URI of the Docker image for the Lambda of the ParallelCluster UI container Type: String Default: public.ecr.aws/pcm/parallelcluster-ui:2025.04.0 VpcEndpointId: Description: Enter a VPC endpoint with type interface for the execute-api service to enable private PCUI implementation. When enabled, the API will only accept requests from within the given VPC. Type: String Default: '' LambdaSubnetIds: Description: Comma separated list of subnet IDs to be associated with the PCUI Lambda function. These subnets should be private and associated with your VPC endpoint. Type: CommaDelimitedList Default: '' LambdaSecurityGroupIds: Description: Comma separated list of security groups to be associated with the PCUI Lambda function. Type: CommaDelimitedList Default: '' UserPoolId: Description: UserPoolId of a previously deployed PCUI Cognito User Pool. Leave blank to create a new one. Type: String Default: '' UserPoolAuthDomain: Description: UserPoolAuthDomain of a previously deployed PCUI Cognito User Pool. Leave blank to create a new one. Type: String Default: '' SNSRole: Description: SNSRole ARN of a previously deployed PCUI Cognito Stack. Leave blank to create a new one. Type: String Default: '' Version: Description: Version of AWS ParallelCluster to deploy. Type: String AllowedPattern: "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$" ConstraintDescription: Please specify a valid ParallelCluster version. ImageBuilderVpcId: Description: (Optional) Select the VPC to use for building the container images. If not selected, default VPC will be used. Type: String Default: '' ImageBuilderSubnetId: Description: (Optional) Select the subnet to use for building the container images. The subnet must be public and auto-assign public IPs. If not selected, the default Subnet will be used. Type: String Default: '' InfrastructureBucket: Description: (Optional) S3 bucket where CloudFormation files are stored. Change this parameter only when testing changes made to the infrastructure itself. Type: String Default: '' PermissionsBoundaryPolicy: Type: String Description: 'ARN of the IAM policy to use as permissions boundary for every IAM role created by ParallelCluster UI infrastructure.' Default: '' AllowedPattern: "^(arn:.*:iam::.*:policy\\/([a-zA-Z0-9_-]+))|()$" PermissionsBoundaryPolicyPCAPI: Type: String Description: 'ARN of the IAM policy to use as permissions boundary for every IAM role created by ParallelCluster API infrastructure. [ParallelCluster >= 3.8.0]' Default: '' AllowedPattern: "^(arn:.*:iam::.*:policy\\/([a-zA-Z0-9_-]+))|()$" AdditionalPoliciesPCAPI: Type: String Description: | (OPTIONAL) ARN of the additional IAM policy to be attached to the default execution role for the ParallelCluster Lambda function. Only one policy can be specified. Default: '' AllowedPattern: "^(arn:.*:iam::.*:policy\\/([a-zA-Z0-9_-]+))|()$" IAMRoleAndPolicyPrefix: Type: String Description: 'Prefix applied to the name of every IAM role and policy (max length: 10). [ParallelCluster >= 3.8.0]' Default: '' MaxLength: 10 CustomDomain: Type: String Description: (Optional) Custom domain name. If omitted, the default domain name will be used. Default: '' CustomDomainCertificateArn: Type: String Description: '(Optional) ARN of the ACM Certificate issued for the custom domain. This is required only if `CustomDomain` is specified.' Default: '' CognitoCustomDomain: Type: String Description: '(Optional) Custom domain name for Cognito. If omitted, the default Cognito domain name will be used.' Default: '' CognitoCustomDomainCertificateArn: Type: String Description: '(Optional) ARN of the ACM Certificate issued for the Cognito custom domain. This is required only if `CognitoCustomDomain` is specified.' Default: '' Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: Admin User (only with new Cognito instances) Parameters: - AdminUserEmail - Label: default: ParallelCluster UI Parameters: - PublicEcrImageUri - Label: default: (Optional) Private PCUI Parameters: - VpcEndpointId - LambdaSubnetIds - LambdaSecurityGroupIds - Label: default: (Optional) External PCUI Cognito Parameters: - UserPoolId - UserPoolAuthDomain - SNSRole - Label: default: ParallelCluster API Parameters: - Version - Label: default: (Optional) ImageBuilder Custom VPC Parameters: - ImageBuilderVpcId - ImageBuilderSubnetId - Label: default: (Optional) Permissions Parameters: - AdditionalPoliciesPCAPI - IAMRoleAndPolicyPrefix - PermissionsBoundaryPolicy - PermissionsBoundaryPolicyPCAPI - Label: default: (Optional) Custom Domain Parameters: - CustomDomain - CustomDomainCertificateArn - CognitoCustomDomain - CognitoCustomDomainCertificateArn - Label: default: (Debugging only) Infrastructure S3 Bucket Parameters: - InfrastructureBucket ParameterLabels: AdminUserEmail: default: Admin's Email UserPoolId: default: UserPoolId from a previously deployed PCUI UserPoolAuthDomain: default: UserPoolAuthDomain from a previously deployed PCUI SNSRole: default: SNSRole ARN from a previously deployed PCUI Conditions: NonDefaultVpc: Fn::And: - !Not [!Equals [!Ref ImageBuilderVpcId, ""]] - !Not [!Equals [!Ref ImageBuilderSubnetId, ""]] IsPrivate: !Not [!Equals [!Ref VpcEndpointId, ""]] HasDefaultInfrastructure: !Equals [!Ref InfrastructureBucket, ''] UseExistingCognito: !And - !Not [!Equals [!Ref UserPoolId, ""]] - !Not [!Equals [!Ref UserPoolAuthDomain, ""]] - !Not [!Equals [!Ref SNSRole, ""]] UseNewCognito: !Not [ Condition: UseExistingCognito] UseNonDockerizedPCAPI: !Not [ Condition: UseDockerizedPCAPI] UseDockerizedPCAPI: !And - !Equals ['3', !Select [ 0, !Split ['.', !Ref Version] ] ] # Check PC version major is 3 and PC version minor is 0-5 - !Or - !Equals ['0', !Select [ 1, !Split ['.', !Ref Version] ] ] - !Equals ['1', !Select [ 1, !Split ['.', !Ref Version] ] ] - !Equals ['2', !Select [ 1, !Split ['.', !Ref Version] ] ] - !Equals ['3', !Select [ 1, !Split ['.', !Ref Version] ] ] - !Equals ['4', !Select [ 1, !Split ['.', !Ref Version] ] ] - !Equals ['5', !Select [ 1, !Split ['.', !Ref Version] ] ] InGovCloud: !Equals ['us-gov-west-1', !Ref "AWS::Region"] UsePermissionBoundary: !Not [!Equals [!Ref PermissionsBoundaryPolicy, '']] UsePermissionBoundaryPCAPI: !Not [!Equals [!Ref PermissionsBoundaryPolicyPCAPI, '']] UseIAMRoleAndPolicyPrefix: !Not [!Equals [!Ref IAMRoleAndPolicyPrefix, '']] UseCustomDomain: !Not [!Equals [!Ref CustomDomain, '']] UseCognitoCustomDomain: !Not [!Equals [!Ref CognitoCustomDomain, '']] UseAdditionalPoliciesPCAPI: !Not [!Equals [!Ref AdditionalPoliciesPCAPI, '']] Mappings: ParallelClusterUI: Constants: Version: 2025.04.0 # format YYYY.MM.REVISION CustomDomainBasePath: pcui Resources: Cognito: Condition: UseNewCognito Type: AWS::CloudFormation::Stack DeletionPolicy: Retain Properties: Parameters: AdminUserEmail: !Ref AdminUserEmail PermissionsBoundaryPolicy: !Ref PermissionsBoundaryPolicy IAMRoleAndPolicyPrefix: !Ref IAMRoleAndPolicyPrefix CustomDomain: !Ref CognitoCustomDomain CustomDomainCertificateArn: !Ref CognitoCustomDomainCertificateArn TemplateURL: !Sub - '${Bucket}/parallelcluster-ui-cognito.yaml' - Bucket: !If - HasDefaultInfrastructure - PLACEHOLDER - !Sub ${InfrastructureBucket} TimeoutInMinutes: 10 ParallelClusterApi: Type: AWS::CloudFormation::Stack Properties: Parameters: PermissionsBoundaryPolicy: !If [ UsePermissionBoundaryPCAPI, !Ref PermissionsBoundaryPolicyPCAPI, !Ref AWS::NoValue ] IAMRoleAndPolicyPrefix: !If [ UseIAMRoleAndPolicyPrefix, !Ref IAMRoleAndPolicyPrefix, !Ref AWS::NoValue ] ParallelClusterFunctionAdditionalPolicies: !If [ UseAdditionalPoliciesPCAPI, !Ref AdditionalPoliciesPCAPI, !Ref AWS::NoValue ] ApiDefinitionS3Uri: !Sub s3://${AWS::Region}-aws-parallelcluster/parallelcluster/${Version}/api/ParallelCluster.openapi.yaml CreateApiUserRole: False EnableIamAdminAccess: True VpcEndpointId: !If [ IsPrivate, !Ref VpcEndpointId, !Ref AWS::NoValue ] ImageBuilderSubnetId: !If - UseNonDockerizedPCAPI - !Ref AWS::NoValue - Fn::If: - NonDefaultVpc - !Ref ImageBuilderSubnetId - !Ref AWS::NoValue ImageBuilderVpcId: !If - UseNonDockerizedPCAPI - !Ref AWS::NoValue - Fn::If: - NonDefaultVpc - !Ref ImageBuilderVpcId - !Ref AWS::NoValue TemplateURL: !Sub https://${AWS::Region}-aws-parallelcluster.s3.${AWS::Region}.amazonaws.com/parallelcluster/${Version}/api/parallelcluster-api.yaml TimeoutInMinutes: 30 ParallelClusterUIFun: Type: AWS::Lambda::Function Properties: Role: !GetAtt ParallelClusterUIUserRole.Arn PackageType: Image MemorySize: 512 Timeout: 30 Tags: - Key: 'parallelcluster:ui:version' Value: !FindInMap [ ParallelClusterUI, Constants, Version ] TracingConfig: Mode: Active VpcConfig: Fn::If: - IsPrivate - SubnetIds: !Ref LambdaSubnetIds SecurityGroupIds: !Ref LambdaSecurityGroupIds - !Ref AWS::NoValue Environment: Variables: API_BASE_URL: !GetAtt [ ParallelClusterApi, Outputs.ParallelClusterApiInvokeUrl ] API_VERSION: !Ref Version SITE_URL: !If - UseCustomDomain - !Sub - https://${CustomDomain}/${CustomDomainBasePath} - { CustomDomainBasePath: !FindInMap [ ParallelClusterUI, Constants, CustomDomainBasePath ] } - !Sub - https://${Api}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage} - Api: !If - IsPrivate - !Sub - '${ApiId}-${VpceId}' - { ApiId: !Ref ApiGatewayRestApi, VpceId: !Ref VpcEndpointId } - !Ref ApiGatewayRestApi Stage: !Ref ApiGatewayRestStage AUTH_PATH: !If [ UseExistingCognito, !Ref UserPoolAuthDomain, !GetAtt [ Cognito, Outputs.UserPoolAuthDomain ]] SECRET_ID: !GetAtt UserPoolClientSecret.SecretName AUDIENCE: !Ref CognitoAppClient OIDC_PROVIDER: 'Cognito' API_GATEWAY_BASE_PATH: !If - UseCustomDomain - !FindInMap [ ParallelClusterUI, Constants, CustomDomainBasePath ] - !Ref AWS::NoValue SSM_LOG_GROUP_NAME: !Ref SsmLogGroup FunctionName: !Sub - ParallelClusterUIFun-${StackIdSuffix} - { StackIdSuffix: !Select [2, !Split ['/', !Ref 'AWS::StackId']] } Code: ImageUri: !Sub - ${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/${Repository}:${Version} - Repository: !Ref PrivateEcrRepository Version: !Join - '-' - [!Select [2, !Split ['/', !Ref EcrImage]], !Select [3, !Split ['/', !Ref EcrImage]]] ApiGatewayRestApi: Type: AWS::ApiGateway::RestApi Properties: Name: ParallelClusterUI Description: ParallelClusterUI Lambda Proxy Policy: Fn::If: - IsPrivate - Version: "2012-10-17" Statement: - Effect: "Deny" Principal: "*" Action: "execute-api:Invoke" Resource: "execute-api:/*" Condition: StringNotEquals: aws:sourceVpce: !Ref VpcEndpointId - Effect: "Allow" Principal: "*" Action: "execute-api:Invoke" Resource: "execute-api:/*" - Version: "2012-10-17" Statement: - Effect: "Allow" Principal: "*" Action: "execute-api:Invoke" Resource: "execute-api:/*" EndpointConfiguration: Types: - !If [ IsPrivate, PRIVATE, REGIONAL ] VpcEndpointIds: - !If [ IsPrivate, !Ref VpcEndpointId, !Ref AWS::NoValue ] Tags: - Key: 'parallelcluster:ui:version' Value: !FindInMap [ParallelClusterUI, Constants, Version] ApiGatewayProxyResource: Type: AWS::ApiGateway::Resource Properties: RestApiId: !Ref ApiGatewayRestApi ParentId: !GetAtt ApiGatewayRestApi.RootResourceId PathPart: '{proxy+}' ApiGatewayMethod: Type: AWS::ApiGateway::Method Properties: RestApiId: !Ref ApiGatewayRestApi ResourceId: !Ref ApiGatewayProxyResource HttpMethod: ANY AuthorizationType: NONE Integration: Type: AWS_PROXY IntegrationHttpMethod: POST Uri: !Sub - arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:ParallelClusterUIFun-${StackIdSuffix}/invocations - { StackIdSuffix: !Select [2, !Split ['/', !Ref 'AWS::StackId']] } ApiGatewayRootMethod: Type: AWS::ApiGateway::Method Properties: RestApiId: !Ref ApiGatewayRestApi ResourceId: !GetAtt ApiGatewayRestApi.RootResourceId HttpMethod: ANY AuthorizationType: NONE Integration: Type: AWS_PROXY IntegrationHttpMethod: POST Uri: !Sub - arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:ParallelClusterUIFun-${StackIdSuffix}/invocations - { StackIdSuffix: !Select [2, !Split ['/', !Ref 'AWS::StackId']] } ApiGatewayLogRole: Type: AWS::IAM::Role Properties: RoleName: !Sub - ${IAMRoleAndPolicyPrefix}ApiGatewayLogRole-${StackIdSuffix} - { StackIdSuffix: !Select [ 0, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] } AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: apigateway.amazonaws.com Action: - 'sts:AssumeRole' ManagedPolicyArns: - 'arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs' PermissionsBoundary: !If [UsePermissionBoundary, !Ref PermissionsBoundaryPolicy, !Ref 'AWS::NoValue'] ApiGatewayAccessLog: Type: AWS::Logs::LogGroup Properties: RetentionInDays: 90 ApiGatewayAccount: Type: AWS::ApiGateway::Account Properties: CloudWatchRoleArn: !GetAtt ApiGatewayLogRole.Arn ApiGatewayDeployment: Type: AWS::ApiGateway::Deployment DependsOn: ApiGatewayMethod Properties: RestApiId: !Ref ApiGatewayRestApi ApiGatewayRestStage: Type: AWS::ApiGateway::Stage DependsOn: ApiGatewayAccount Properties: AccessLogSetting: DestinationArn: !GetAtt ApiGatewayAccessLog.Arn Format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","path":"$context.path", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }' RestApiId: !Ref ApiGatewayRestApi DeploymentId: !Ref ApiGatewayDeployment StageName: !If [ UseCustomDomain, prod, pcui ] MethodSettings: - ResourcePath: '/*' HttpMethod: '*' ThrottlingBurstLimit: 50 ThrottlingRateLimit: 100 CognitoAppClient: Type: AWS::Cognito::UserPoolClient Properties: GenerateSecret: true AllowedOAuthFlows: - code AllowedOAuthFlowsUserPoolClient: true AllowedOAuthScopes: - email - openid ExplicitAuthFlows: - ALLOW_REFRESH_TOKEN_AUTH CallbackURLs: - !If - UseCustomDomain - !Sub - https://${CustomDomain}/${CustomDomainBasePath}/login - { CustomDomainBasePath: !FindInMap [ ParallelClusterUI, Constants, CustomDomainBasePath ] } - !Sub - https://${Api}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/login - Api: !If - IsPrivate - !Sub - '${ApiId}-${VpceId}' - { ApiId: !Ref ApiGatewayRestApi, VpceId: !Ref VpcEndpointId } - !Ref ApiGatewayRestApi Stage: !Ref ApiGatewayRestStage SupportedIdentityProviders: - COGNITO UserPoolId: !If [ UseExistingCognito, !Ref UserPoolId, !GetAtt [ Cognito, Outputs.UserPoolId ]] PreventUserExistenceErrors: ENABLED RefreshTokenValidity: 7 AccessTokenValidity: 5 IdTokenValidity: 5 TokenValidityUnits: AccessToken: "minutes" IdToken: "minutes" UserPoolClientSecret: Type: Custom::UserPoolClientSecret Properties: ServiceToken: !GetAtt UserPoolClientSecretFunction.Arn UserPoolId: !If [ UseExistingCognito, !Ref UserPoolId, !GetAtt [ Cognito, Outputs.UserPoolId ]] AppClientId: !Ref CognitoAppClient UserPoolClientSecretFunction: Type: AWS::Lambda::Function Properties: Handler: index.handler Runtime: python3.12 MemorySize: 128 Timeout: 20 TracingConfig: Mode: Active Role: !GetAtt UserPoolClientSecretRole.Arn Code: ZipFile: | import cfnresponse import boto3 import random import string import json cognito = boto3.client("cognito-idp") secretsmanager = boto3.client("secretsmanager") def generate_secret(stack_name, resource_id): alnum = string.ascii_uppercase + string.digits return f"{stack_name}-{resource_id}-" + "".join(random.choice(alnum) for _ in range(12)) def handler(event, context): print(event) print("boto version {}".format(boto3.__version__)) stack_name = event["StackId"].split("/")[1] user_pool_id = event["ResourceProperties"]["UserPoolId"] app_client_id = event["ResourceProperties"]["AppClientId"] logical_resource_id = event["LogicalResourceId"] response_data = {} reason = None response_status = cfnresponse.SUCCESS try: if event["RequestType"] == "Create": response_data["Message"] = "Resource creation successful!" user_pool_client = cognito.describe_user_pool_client(UserPoolId=user_pool_id, ClientId=app_client_id) client_secret = user_pool_client["UserPoolClient"]["ClientSecret"] secret_name = generate_secret(stack_name, logical_resource_id) secret = json.dumps({"userPoolId": user_pool_id, "clientId": app_client_id, "clientSecret": client_secret}) resp = secretsmanager.create_secret( Name=secret_name, Description=f"Client Secret for {app_client_id} / user pool {user_pool_id}", SecretString=secret, Tags=[ {"Key": "custom:cloudformation:stack-name", "Value": stack_name}, {"Key": "custom:cloudformation:logical-id", "Value": logical_resource_id}, ], ) response_data = {"SecretArn": resp["ARN"], "SecretName": resp["Name"], "SecretVersionId": resp["VersionId"]} elif event["RequestType"] == "Update": user_pool_client = cognito.describe_user_pool_client(UserPoolId=user_pool_id, ClientId=app_client_id) client_secret = user_pool_client["UserPoolClient"]["ClientSecret"] secret_name = event["PhysicalResourceId"] secret = json.dumps({"userPoolId": user_pool_id, "clientId": app_client_id, "clientSecret": client_secret}) resp = secretsmanager.update_secret( SecretId=secret_name, Description=f"Client Secret for {app_client_id} / user pool {user_pool_id}", SecretString=secret, ) response_data = {"SecretArn": resp["ARN"], "SecretName": resp["Name"], "SecretVersionId": resp["VersionId"]} else: secret_name = event["PhysicalResourceId"] resp = secretsmanager.delete_secret(SecretId=secret_name, ForceDeleteWithoutRecovery=True) response_data = {"SecretArn": resp["ARN"], "SecretName": resp["Name"]} except Exception as exception: response_status = cfnresponse.FAILED reason = "Failed {}: {}".format(event["RequestType"], exception) cfnresponse.send(event, context, response_status, response_data, secret_name, reason) UserPoolClientSecretRole: Type: AWS::IAM::Role Properties: RoleName: !Sub - ${IAMRoleAndPolicyPrefix}UserPoolClientSecretRole-${StackIdSuffix} - { StackIdSuffix: !Select [ 0, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] } AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: - 'sts:AssumeRole' Policies: - PolicyName: !Sub ${IAMRoleAndPolicyPrefix}UserPoolPermissions PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - cognito-idp:DescribeUserPoolClient Resource: - !Sub - arn:${AWS::Partition}:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${UserPoolId} - { UserPoolId: !If [UseExistingCognito, !Ref UserPoolId, !GetAtt [ Cognito, Outputs.UserPoolId ]]} - PolicyName: !Sub ${IAMRoleAndPolicyPrefix}SecretsManagerPermissions PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - secretsmanager:CreateSecret - secretsmanager:TagResource - secretsmanager:UpdateSecret - secretsmanager:DeleteSecret Resource: - !Sub arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${AWS::StackName}* PermissionsBoundary: !If [UsePermissionBoundary, !Ref PermissionsBoundaryPolicy, !Ref 'AWS::NoValue'] PrivateEcrRepository: DependsOn: ParallelClusterApi Type: AWS::ECR::Repository Properties: RepositoryName: !Sub - 'parallelcluster-ui-${StackIdSuffix}' - { StackIdSuffix: !Select [2, !Split ['/', !Ref 'AWS::StackId']] } RepositoryPolicyText: Version: 2012-10-17 Statement: - Sid: ReadEcrImages Effect: Allow Principal: Service: !Sub lambda.${AWS::URLSuffix} Action: - ecr:BatchGetImage - ecr:GetDownloadUrlForLayer Condition: StringLike: aws:SourceArn: !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:* ImageBuilderInstanceRole: Type: AWS::IAM::Role Properties: RoleName: !Sub - ${IAMRoleAndPolicyPrefix}ImageBuilderInstanceRole-${StackIdSuffix} - { StackIdSuffix: !Select [ 0, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] } ManagedPolicyArns: - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore - !Sub arn:${AWS::Partition}:iam::aws:policy/EC2InstanceProfileForImageBuilderECRContainerBuilds AssumeRolePolicyDocument: Statement: - Action: - sts:AssumeRole Effect: Allow Principal: Service: - !Sub ec2.${AWS::URLSuffix} Version: '2012-10-17' Path: /executionServiceEC2Role/ PermissionsBoundary: !If [UsePermissionBoundary, !Ref PermissionsBoundaryPolicy, !Ref 'AWS::NoValue'] ImageBuilderInstanceProfile: Type: AWS::IAM::InstanceProfile Properties: InstanceProfileName: !Sub - ${IAMRoleAndPolicyPrefix}ImageBuilderInstanceProfile-${StackIdSuffix} - { StackIdSuffix: !Select [ 0, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] } Path: /executionServiceEC2Role/ Roles: - !Ref ImageBuilderInstanceRole InfrastructureConfigurationSecurityGroup: Condition: NonDefaultVpc Type: AWS::EC2::SecurityGroup Properties: VpcId: !Ref ImageBuilderVpcId GroupDescription: Parallel cluster image builder security group InfrastructureConfiguration: Type: AWS::ImageBuilder::InfrastructureConfiguration Properties: Name: !Sub - ParallelClusterUIImageBuilderInfrastructureConfiguration-${Version}-${StackIdSuffix} - { Version: !Join ['_', !Split ['.', !FindInMap [ParallelClusterUI, Constants, Version]]], StackIdSuffix: !Select [2, !Split ['/', !Ref 'AWS::StackId']] } InstanceProfileName: !Ref ImageBuilderInstanceProfile TerminateInstanceOnFailure: true SubnetId: Fn::If: - NonDefaultVpc - !Ref ImageBuilderSubnetId - !Ref AWS::NoValue SecurityGroupIds: Fn::If: - NonDefaultVpc - [!Ref InfrastructureConfigurationSecurityGroup] - !Ref AWS::NoValue InstanceMetadataOptions: HttpTokens: required EcrImageRecipe: Type: AWS::ImageBuilder::ContainerRecipe Properties: Components: - ComponentArn: !Sub arn:${AWS::Partition}:imagebuilder:${AWS::Region}:aws:component/update-linux/x.x.x ContainerType: DOCKER Name: !Sub - 'parallelcluster-ui-${Version}-${StackIdSuffix}' - { Version: !Join ['_', !Split ['.', !FindInMap [ParallelClusterUI, Constants, Version]]], StackIdSuffix: !Select [2, !Split ['/', !Ref 'AWS::StackId']] } Version: !FindInMap [ParallelClusterUI, Constants, Version] ParentImage: !Ref PublicEcrImageUri PlatformOverride: Linux TargetRepository: Service: ECR RepositoryName: !Ref PrivateEcrRepository DockerfileTemplateData: 'FROM {{{ imagebuilder:parentImage }}}' WorkingDirectory: '/tmp' EcrImage: Type: AWS::ImageBuilder::Image Properties: ContainerRecipeArn: !Ref EcrImageRecipe EnhancedImageMetadataEnabled: true InfrastructureConfigurationArn: !Ref InfrastructureConfiguration ImageTestsConfiguration: ImageTestsEnabled: false EcrImagePipeline: Type: AWS::ImageBuilder::ImagePipeline Properties: Name: !Sub - 'EcrImagePipeline-${Version}-${StackIdSuffix}' - { Version: !Join ['_', !Split ['.', !FindInMap [ParallelClusterUI, Constants, Version]]], StackIdSuffix: !Select [2, !Split ['/', !Ref 'AWS::StackId']] } Status: ENABLED ContainerRecipeArn: !Ref EcrImageRecipe InfrastructureConfigurationArn: !Ref InfrastructureConfiguration ImageTestsConfiguration: ImageTestsEnabled: false EcrImageDeletionLambda: Type: AWS::Lambda::Function Properties: MemorySize: 128 Code: ZipFile: | import cfnresponse import boto3 import random import string ecr = boto3.client('ecr') imagebuilder = boto3.client('imagebuilder') def get_image_ids(repository_name, version): image_digests = set() paginator = ecr.get_paginator('list_images') response_iterator = paginator.paginate(repositoryName=repository_name, filter={'tagStatus': 'TAGGED'}) for response in response_iterator: image_digests.update([image_id['imageDigest'] for image_id in response['imageIds']]) return list({'imageDigest': image_digest} for image_digest in image_digests) def get_imagebuilder_images(ecr_image_pipeline_arn): response = imagebuilder.list_image_pipeline_images(imagePipelineArn=ecr_image_pipeline_arn) images = [image['arn'] for image in response['imageSummaryList']] while 'nextToken' in response: response = imagebuilder.list_image_pipeline_images(imagePipelineArn=ecr_image_pipeline_arn, nextToken=response['nextToken']) images.extend([image['arn'] for image in response['imageSummaryList']]) return images def create_physical_resource_id(): alnum = string.ascii_uppercase + string.ascii_lowercase + string.digits return ''.join(random.choice(alnum) for _ in range(16)) def handler(event, context): print(event) print('boto version {}'.format(boto3.__version__)) response_data = {} reason = None response_status = cfnresponse.SUCCESS if event['RequestType'] == 'Create': response_data['Message'] = 'Resource creation successful!' physical_resource_id = create_physical_resource_id() else: physical_resource_id = event['PhysicalResourceId'] if event['RequestType'] == 'Update' or event['RequestType'] == 'Delete': try: resource_key = 'OldResourceProperties' if 'OldResourceProperties' in event else 'ResourceProperties' ecr_repository_name = event[resource_key]['EcrRepositoryName'] ecr_image_pipeline_arn = event[resource_key]['EcrImagePipelineArn'] version = event[resource_key]['Version'] image_ids = get_image_ids(ecr_repository_name, version) if image_ids: ecr.batch_delete_image(repositoryName=ecr_repository_name, imageIds=image_ids) reason = 'Image deletion successful!' else: reason = 'No image found, considering image deletion successful' for imagebuilder_image in get_imagebuilder_images(ecr_image_pipeline_arn): imagebuilder.delete_image(imageBuildVersionArn=imagebuilder_image) except ecr.exceptions.RepositoryNotFoundException: reason = 'Repository was not found, considering image deletion successfull' except Exception as exception: response_status = cfnresponse.FAILED reason = 'Failed image deletion with error: {}'.format(exception) cfnresponse.send(event, context, response_status, response_data, physical_resource_id, reason) Handler: index.handler Runtime: python3.12 Role: !GetAtt EcrImageDeletionLambdaRole.Arn EcrImageDeletionLambdaLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/lambda/${EcrImageDeletionLambda} EcrImageDeletionLambdaRole: Type: AWS::IAM::Role Properties: RoleName: !Sub - ${IAMRoleAndPolicyPrefix}EcrImageDeletionLambdaRole-${StackIdSuffix} - { StackIdSuffix: !Select [ 0, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] } AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: - 'sts:AssumeRole' Policies: - PolicyName: !Sub ${IAMRoleAndPolicyPrefix}LogsPermissions PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - logs:CreateLogStream - logs:PutLogEvents Resource: !Sub arn:${AWS::Partition}:logs:*:*:* - PolicyName: !Sub ${IAMRoleAndPolicyPrefix}EcrPermissions PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - ecr:BatchDeleteImage - ecr:ListImages Resource: !GetAtt PrivateEcrRepository.Arn - Effect: Allow Action: - imagebuilder:ListImagePipelineImages Resource: !Sub - arn:${AWS::Partition}:imagebuilder:${AWS::Region}:${AWS::AccountId}:image-pipeline/ecrimagepipeline-*${StackIdSuffix}* - { StackIdSuffix: !Select [2, !Split ['/', !Ref 'AWS::StackId']] } - Effect: Allow Action: - imagebuilder:DeleteImage Resource: !Sub - arn:${AWS::Partition}:imagebuilder:${AWS::Region}:${AWS::AccountId}:image/*${StackIdSuffix}* - { StackIdSuffix: !Select [2, !Split ['/', !Ref 'AWS::StackId']] } PermissionsBoundary: !If [UsePermissionBoundary, !Ref PermissionsBoundaryPolicy, !Ref 'AWS::NoValue'] EcrImagesRemover: Type: Custom::EcrImagesRemover Properties: ServiceToken: !GetAtt EcrImageDeletionLambda.Arn EcrRepositoryName: !Ref PrivateEcrRepository Version: !FindInMap [ParallelClusterUI, Constants, Version] EcrImagePipelineArn: !GetAtt EcrImagePipeline.Arn ParallelClusterUILambdaLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/lambda/${ParallelClusterUIFun} RetentionInDays: 90 SsmLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/ssm/ParallelClusterUI-${AWS::StackName} RetentionInDays: 1 LogGroupClass: STANDARD ParallelClusterUIUserRole: Type: AWS::IAM::Role Properties: RoleName: !Sub - ${IAMRoleAndPolicyPrefix}ParallelClusterUIUserRole-${StackIdSuffix} - { StackIdSuffix: !Select [ 0, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] } AssumeRolePolicyDocument: Statement: - Effect: Allow Action: sts:AssumeRole Principal: Service: lambda.amazonaws.com ManagedPolicyArns: # Required for Lambda logging and XRay - !Sub arn:${AWS::Partition}:iam::aws:policy/AWSXRayDaemonWriteAccess - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole # Access to the ParallelCluster API - !Ref ParallelClusterApiGatewayInvoke # Required to run ParallelClusterUI functionalities - !Ref CognitoPolicy - !Ref EC2Policy - !Ref StoragePolicy - !Ref LogsPolicy - !Ref CostMonitoringAndPricingPolicy - !Ref SsmPolicy PermissionsBoundary: !If [UsePermissionBoundary, !Ref PermissionsBoundaryPolicy, !Ref 'AWS::NoValue'] ParallelClusterUIApiGatewayInvoke: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt ParallelClusterUIFun.Arn Principal: apigateway.amazonaws.com SourceArn: !Sub - arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGateway}/* - { ApiGateway: !Ref ApiGatewayRestApi } ParallelClusterApiGatewayInvoke: Type: AWS::IAM::ManagedPolicy Properties: ManagedPolicyName: !Sub - ${IAMRoleAndPolicyPrefix}ParallelClusterApiGatewayInvoke-${StackIdSuffix} - { StackIdSuffix: !Select [ 0, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] } PolicyDocument: Version: '2012-10-17' Statement: - Action: - execute-api:Invoke Effect: Allow Resource: !Sub - arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${PCApiGateway}/*/* - { PCApiGateway: !Select [2, !Split ['/', !Select [0, !Split ['.', !GetAtt [ ParallelClusterApi, Outputs.ParallelClusterApiInvokeUrl ]]]]] } CognitoPolicy: Type: AWS::IAM::ManagedPolicy Properties: ManagedPolicyName: !Sub - ${IAMRoleAndPolicyPrefix}CognitoPolicy-${StackIdSuffix} - { StackIdSuffix: !Select [ 0, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId']]]] } PolicyDocument: Version: '2012-10-17' Statement: - Action: - cognito-idp:AdminRemoveUserFromGroup - cognito-idp:AdminAddUserToGroup - cognito-idp:AdminListGroupsForUser - cognito-idp:ListUsers - cognito-idp:AdminCreateUser - cognito-idp:AdminDeleteUser Resource: !Sub - arn:${AWS::Partition}:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${UserPoolId} - { UserPoolId: !If [UseExistingCognito, !Ref UserPoolId, !GetAtt [ Cognito, Outputs.UserPoolId ]]} Effect: Allow Sid: CognitoPolicy - Action: - secretsmanager:GetSecretValue Resource: - !GetAtt UserPoolClientSecret.SecretArn Effect: Allow Sid: SecretsRole EC2Policy: Type: AWS::IAM::ManagedPolicy Properties: ManagedPolicyName: !Sub - ${IAMRoleAndPolicyPrefix}EC2Policy-${StackIdSuffix} - { StackIdSuffix: !Select [ 0, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] } PolicyDocument: Version: '2012-10-17' Statement: - Action: - ec2:DescribeSecurityGroups - ec2:DescribeVpcs - ec2:DescribeInstanceTypes - ec2:DescribeSubnets - ec2:DescribeKeyPairs Resource: - '*' Effect: Allow Sid: EC2Policy - Action: - ec2:StartInstances - ec2:StopInstances Resource: - !Sub arn:${AWS::Partition}:ec2:*:${AWS::AccountId}:instance/* Condition: StringLike: ec2:ResourceTag/parallelcluster:version: "*" Effect: Allow Sid: EC2ManagePolicy - Fn::If: - IsPrivate - Action: - ec2:CreateNetworkInterface - ec2:DeleteNetworkInterface - ec2:AttachNetworkInterface Resource: - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:* Effect: Allow Sid: PrivateDeploymentWritePolicy - !Ref AWS::NoValue - Fn::If: - IsPrivate - Action: - ec2:DescribeNetworkInterfaces - ec2:DescribeInstances Resource: - '*' Effect: Allow Sid: PrivateDeploymentReadPolicy - !Ref AWS::NoValue StoragePolicy: Type: AWS::IAM::ManagedPolicy Properties: ManagedPolicyName: !Sub - ${IAMRoleAndPolicyPrefix}StoragePolicy-${StackIdSuffix} - { StackIdSuffix: !Select [ 0, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] } PolicyDocument: Version: '2012-10-17' Statement: - Action: - fsx:DescribeFileSystems - fsx:DescribeVolumes - fsx:DescribeFileCaches Resource: - !Sub arn:${AWS::Partition}:fsx:*:${AWS::AccountId}:volume/* - !Sub arn:${AWS::Partition}:fsx:*:${AWS::AccountId}:file-system/* - !Sub arn:${AWS::Partition}:fsx:*:${AWS::AccountId}:file-cache/* Effect: Allow Sid: FsxRead - Action: - elasticfilesystem:DescribeFileSystems Resource: - !Sub arn:${AWS::Partition}:elasticfilesystem:*:${AWS::AccountId}:file-system/* Effect: Allow Sid: EfsRead CostMonitoringAndPricingPolicy: Type: AWS::IAM::ManagedPolicy Properties: ManagedPolicyName: !Sub - ${IAMRoleAndPolicyPrefix}CostMonitoringAndPricingPolicy-${StackIdSuffix} - { StackIdSuffix: !Select [ 0, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] } PolicyDocument: Version: '2012-10-17' Statement: - !If - InGovCloud - !Ref AWS::NoValue - Action: - ce:ListCostAllocationTags - ce:UpdateCostAllocationTagsStatus - ce:GetCostAndUsage Resource: - !Sub 'arn:aws:ce:us-east-1:${AWS::AccountId}:/*' # CE only available in us-east-1, aws partition, see https://docs.aws.amazon.com/general/latest/gr/billing.html Effect: Allow Sid: CostMonitoringPolicy - Action: - pricing:GetProducts Resource: - '*' Effect: Allow Sid: PricingPolicy SsmPolicy: Type: AWS::IAM::ManagedPolicy Properties: ManagedPolicyName: !Sub - ${IAMRoleAndPolicyPrefix}SsmPolicy-${StackIdSuffix} - { StackIdSuffix: !Select [ 0, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] } PolicyDocument: Version: '2012-10-17' Statement: - Action: - ssm:SendCommand Resource: - !Sub arn:${AWS::Partition}:ec2:*:${AWS::AccountId}:instance/* Effect: Allow Sid: SsmSendPolicyInstance Condition: StringLike: ssm:resourceTag/parallelcluster:version: "*" - Action: - ssm:SendCommand Resource: - !Sub arn:${AWS::Partition}:ssm:*::document/AWS-RunShellScript Effect: Allow Sid: SsmSendPolicyCommand - Action: - ssm:GetCommandInvocation Resource: - '*' Effect: Allow Sid: SsmGetCommandInvocationPolicy LogsPolicy: Type: AWS::IAM::ManagedPolicy Properties: ManagedPolicyName: !Sub - ${IAMRoleAndPolicyPrefix}LogsPolicy-${StackIdSuffix} - { StackIdSuffix: !Select [ 0, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] } PolicyDocument: Version: '2012-10-17' Statement: - Action: - logs:GetLogEvents Resource: - !Sub "arn:${AWS::Partition}:logs:*:${AWS::AccountId}:log-group:${SsmLogGroup}:*" - !Sub "arn:${AWS::Partition}:logs:*:${AWS::AccountId}:log-group:${SsmLogGroup}:log-stream:*" Effect: Allow Sid: CloudWatchLogsRead - Action: - logs:DeleteLogStream Resource: - !Sub "arn:${AWS::Partition}:logs:*:${AWS::AccountId}:log-group:${SsmLogGroup}:log-stream:*/*/aws-runShellScript/stdout" Effect: Allow Sid: CloudWatchLogsDelete ApiGatewayCustomDomain: Condition: UseCustomDomain Type: AWS::ApiGateway::DomainName Properties: # CertificateArn: !Ref CustomDomainCertificateArn DomainName: !Ref CustomDomain EndpointConfiguration: Types: - REGIONAL RegionalCertificateArn: !Ref CustomDomainCertificateArn SecurityPolicy: TLS_1_2 ApiGatewayCustomDomainMapping: Condition: UseCustomDomain Type: AWS::ApiGateway::BasePathMapping Properties: BasePath: !FindInMap [ ParallelClusterUI, Constants, CustomDomainBasePath ] DomainName: !Ref ApiGatewayCustomDomain RestApiId: !Ref ApiGatewayRestApi Stage: !Ref ApiGatewayRestStage Outputs: ParallelClusterUILambdaArn: Description: 'ARN of the ParallelCluster UI Lambda function' Value: !GetAtt ParallelClusterUIFun.Arn ParallelClusterUIUrl: Description: 'Url to reach the ParallelCluster UI Site.' Export: Name: !Sub ${AWS::StackName}-ParallelClusterUISite Value: !If - UseCustomDomain - !Sub - https://${CustomDomain}/${CustomDomainBasePath} - { CustomDomainBasePath: !FindInMap [ ParallelClusterUI, Constants, CustomDomainBasePath ] } - !Sub - https://${Api}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage} - Api: !If - IsPrivate - !Sub - '${ApiId}-${VpceId}' - { ApiId: !Ref ApiGatewayRestApi, VpceId: !Ref VpcEndpointId } - !Ref ApiGatewayRestApi Stage: !Ref ApiGatewayRestStage CustomDomainEndpoint: Condition: UseCustomDomain Description: | The endpoint associated with the custom domain name. Add an A record in your DNS for the PCUI custom domain name pointing to this endpoint. Value: !GetAtt ApiGatewayCustomDomain.RegionalDomainName AppClientId: Description: The id of the Cognito app client Value: !Ref CognitoAppClient UserPoolClientSecretArn: Description: The app client secret ARN for ParallelCluster UI. Value: !GetAtt UserPoolClientSecret.SecretArn UserPoolClientSecretName: Description: The app client secret name for ParallelCluster UI. Value: !GetAtt UserPoolClientSecret.SecretName CognitoCustomDomainEndpoint: Condition: UseCognitoCustomDomain Description: | The endpoint associated with the Cognito custom domain name. Add an A record in your DNS for the Cognito custom domain name pointing to this endpoint. Value: !GetAtt Cognito.Outputs.CustomDomainEndpoint