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