diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/base-api-stack.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/base-api-stack.ts index f495cbff5c3..6f886524e54 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/base-api-stack.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/base-api-stack.ts @@ -2,6 +2,7 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecr from '@aws-cdk/aws-ecr'; import * as ecs from '@aws-cdk/aws-ecs'; import * as iam from '@aws-cdk/aws-iam'; +import { CfnFunction } from '@aws-cdk/aws-lambda'; import * as logs from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; import * as ssm from '@aws-cdk/aws-secretsmanager'; @@ -31,10 +32,8 @@ export enum DEPLOYMENT_MECHANISM { export type ContainersStackProps = Readonly<{ skipWait?: boolean; - envName: string; categoryName: string; apiName: string; - deploymentBucketName: string; dependsOn: ReadonlyArray<{ category: string; resourceName: string; @@ -70,6 +69,9 @@ export abstract class ContainersStack extends cdk.Stack { protected readonly userPoolId: string | undefined; protected readonly ecsServiceSecurityGroup: ec2.CfnSecurityGroup; protected readonly parameters: ReadonlyMap; + protected readonly envName: string; + protected readonly deploymentBucketName: string; + protected readonly awaiterS3Key: string; constructor(scope: cdk.Construct, id: string, private readonly props: ContainersStackProps) { super(scope, id); @@ -86,6 +88,9 @@ export abstract class ContainersStack extends cdk.Stack { isAuthCondition, appClientId, userPoolId, + envName, + deploymentBucketName, + awaiterS3Key, } = this.init(); this.parameters = parameters; @@ -100,7 +105,9 @@ export abstract class ContainersStack extends cdk.Stack { this.isAuthCondition = isAuthCondition; this.appClientId = appClientId; this.userPoolId = userPoolId; - + this.envName = envName; + this.deploymentBucketName = deploymentBucketName; + this.awaiterS3Key = awaiterS3Key; const { service, serviceSecurityGroup, containersInfo, cloudMapService } = this.ecs(); this.cloudMapService = cloudMapService; @@ -192,6 +199,17 @@ export abstract class ContainersStack extends cdk.Stack { ), }); + const stackNameParameter = new cdk.CfnParameter(this, 'rootStackName', { + type: 'String', + }); + + const deploymentBucketName = new cdk.CfnParameter(this, 'deploymentBucketName', { + type: 'String', + }); + const awaiterS3Key = new cdk.CfnParameter(this, 'awaiterS3Key', { + type: 'String', + default: PIPELINE_AWAITER_ZIP, + }); return { parameters, vpcId: paramVpcId.valueAsString, @@ -204,12 +222,14 @@ export abstract class ContainersStack extends cdk.Stack { isAuthCondition, userPoolId: paramUserPoolId && paramUserPoolId.valueAsString, appClientId: paramAppClientIdWeb && paramAppClientIdWeb.valueAsString, + envName: stackNameParameter.valueAsString, + deploymentBucketName: deploymentBucketName.valueAsString, + awaiterS3Key: awaiterS3Key.valueAsString, }; } private ecs() { const { - envName, categoryName, apiName, policies, @@ -245,7 +265,7 @@ export abstract class ContainersStack extends cdk.Stack { compatibility: ecs.Compatibility.FARGATE, memoryMiB: '1024', cpu: '512', - family: `${envName}-${apiName}`, + family: `${this.envName}-${apiName}`, }); (task.node.defaultChild as ecs.CfnTaskDefinition).overrideLogicalId('TaskDefinition'); policies.forEach(policy => { @@ -274,7 +294,7 @@ export abstract class ContainersStack extends cdk.Stack { secrets: containerSecrets, }) => { const logGroup = new logs.LogGroup(this, `${name}ContainerLogGroup`, { - logGroupName: `/ecs/${envName}-${apiName}-${name}`, + logGroupName: `/ecs/${this.envName}-${apiName}-${name}`, retention: logs.RetentionDays.ONE_MONTH, removalPolicy: cdk.RemovalPolicy.DESTROY, }); @@ -293,13 +313,13 @@ export abstract class ContainersStack extends cdk.Stack { if (build) { const logicalId = `${name}Repository`; - const repositoryName = `${envName}-${categoryName}-${apiName}-${name}`; + const repositoryName = `${this.envName}-${categoryName}-${apiName}-${name}`; if (this.props.existingEcrRepositories.has(repositoryName)) { repository = ecr.Repository.fromRepositoryName(this, logicalId, repositoryName); } else { repository = new ecr.Repository(this, logicalId, { - repositoryName: `${envName}-${categoryName}-${apiName}-${name}`, + repositoryName: `${this.envName}-${categoryName}-${apiName}-${name}`, removalPolicy: cdk.RemovalPolicy.RETAIN, lifecycleRules: [ { @@ -446,15 +466,15 @@ export abstract class ContainersStack extends cdk.Stack { }[]; gitHubSourceActionInfo?: GitHubSourceActionInfo; }) { - const { envName, deploymentBucketName, deploymentMechanism, desiredCount } = this.props; + const { deploymentMechanism, desiredCount } = this.props; const s3SourceActionKey = this.zipPath; - const bucket = s3.Bucket.fromBucketName(this, 'Bucket', deploymentBucketName); + const bucket = s3.Bucket.fromBucketName(this, 'Bucket', this.deploymentBucketName); const pipelineWithAwaiter = new PipelineWithAwaiter(this, 'ApiPipeline', { skipWait, - envName, + envName: this.envName, containersInfo, service, bucket, @@ -477,26 +497,29 @@ export abstract class ContainersStack extends cdk.Stack { const pipelineName = this.getPipelineName(); return `https://${region}.console.aws.amazon.com/codesuite/codepipeline/pipelines/${pipelineName}/view`; } - toCloudFormation() { + this.node + .findAll() + .filter(construct => construct instanceof CfnFunction) + .map(construct => construct as CfnFunction) + .forEach(lambdaFunction => { + if (lambdaFunction.logicalId.includes('AwaiterMyProvider')) { + lambdaFunction.code = { + s3Bucket: this.deploymentBucketName, + s3Key: this.awaiterS3Key, + }; + } + }); + prepareApp(this); const cfn = this._toCloudFormation(); Object.keys(cfn.Parameters).forEach(k => { if (k.startsWith('AssetParameters')) { - let value = ''; - - if (k.includes('Bucket')) { - value = this.props.deploymentBucketName; - } else if (k.includes('VersionKey')) { - value = `${PIPELINE_AWAITER_ZIP}||`; - } - - cfn.Parameters[k].Default = value; + delete cfn.Parameters[k]; } }); - return cfn; } } diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/ecs-alb-stack.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/ecs-alb-stack.ts index acf1b119fe5..c66f3719603 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/ecs-alb-stack.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/ecs-alb-stack.ts @@ -7,14 +7,15 @@ import * as elb2 from '@aws-cdk/aws-elasticloadbalancingv2'; import * as route53 from '@aws-cdk/aws-route53'; import * as route53targets from '@aws-cdk/aws-route53-targets'; import * as cdk from '@aws-cdk/core'; -import { ContainersStack, ContainersStackProps } from "./base-api-stack"; -import { v4 as uuid } from "uuid"; +import { ContainersStack, ContainersStackProps } from './base-api-stack'; +import { v4 as uuid } from 'uuid'; -type EcsStackProps = ContainersStackProps & Readonly<{ - domainName: string; - hostedZoneId?: string; - authName: string; -}>; +type EcsStackProps = ContainersStackProps & + Readonly<{ + domainName: string; + hostedZoneId?: string; + authName: string; + }>; export class EcsAlbStack extends ContainersStack { private readonly userPoolDomain: string; @@ -40,13 +41,9 @@ export class EcsAlbStack extends ContainersStack { private alb() { const { - envName, domainName, hostedZoneId, - exposedContainer: { - name: containerName, - port - }, + exposedContainer: { name: containerName, port }, restrictAccess, } = this.ecsProps; @@ -70,39 +67,37 @@ export class EcsAlbStack extends ContainersStack { ]); const [distributionDomainName, , domainNameSuffix] = domainName.match(/([^\.]+)\.(.*)/); - const lbPrefix = `lb-${envName}`; + const lbPrefix = `lb-${this.envName}`; const albDomainName = `${lbPrefix}.${domainNameSuffix}`; const wildcardDomainName = `*.${domainNameSuffix}`; const wildcardCertificate = new acm.CfnCertificate(this, 'Certificate', { domainName: wildcardDomainName, validationMethod: hostedZoneId ? acm.ValidationMethod.DNS : acm.ValidationMethod.EMAIL, - domainValidationOptions: [{ - domainName: wildcardDomainName, - validationDomain: hostedZoneId === undefined ? domainNameSuffix : undefined, - hostedZoneId, - }] + domainValidationOptions: [ + { + domainName: wildcardDomainName, + validationDomain: hostedZoneId === undefined ? domainNameSuffix : undefined, + hostedZoneId, + }, + ], }); - const userPoolClient = restrictAccess ? new cognito.CfnUserPoolClient(this, 'UserPoolClient', { - userPoolId: this.userPoolId, - allowedOAuthFlows: [ - // 'implicit', - 'code', - ], - allowedOAuthFlowsUserPoolClient: true, - allowedOAuthScopes: [ - "profile", - "phone", - "email", - "openid", - "aws.cognito.signin.user.admin" - ], - generateSecret: true, - supportedIdentityProviders: ['COGNITO'], - callbackUrLs: [`https://${distributionDomainName}/oauth2/idpresponse`], - logoutUrLs: [`https://${distributionDomainName}/oauth2/idpresponse`], - }) : undefined; + const userPoolClient = restrictAccess + ? new cognito.CfnUserPoolClient(this, 'UserPoolClient', { + userPoolId: this.userPoolId, + allowedOAuthFlows: [ + // 'implicit', + 'code', + ], + allowedOAuthFlowsUserPoolClient: true, + allowedOAuthScopes: ['profile', 'phone', 'email', 'openid', 'aws.cognito.signin.user.admin'], + generateSecret: true, + supportedIdentityProviders: ['COGNITO'], + callbackUrLs: [`https://${distributionDomainName}/oauth2/idpresponse`], + logoutUrLs: [`https://${distributionDomainName}/oauth2/idpresponse`], + }) + : undefined; const targetGroup = new elb2.CfnTargetGroup(this, 'TargetGroup', { healthCheckIntervalSeconds: cdk.Duration.seconds(90).toSeconds(), @@ -119,38 +114,44 @@ export class EcsAlbStack extends ContainersStack { const albSecurityGroup = new ec2.CfnSecurityGroup(this, 'AlbSecurityGroup', { vpcId, groupDescription: 'ALB Security Group', - securityGroupEgress: [{ - description: 'Allow all outbound traffic by default', - ipProtocol: '-1', - cidrIp: '0.0.0.0/0', - }], - securityGroupIngress: [{ - description: 'Allow from anyone on port 443', - ipProtocol: ec2.Protocol.TCP, - cidrIp: '0.0.0.0/0', - fromPort: 443, - toPort: 443, - }] + securityGroupEgress: [ + { + description: 'Allow all outbound traffic by default', + ipProtocol: '-1', + cidrIp: '0.0.0.0/0', + }, + ], + securityGroupIngress: [ + { + description: 'Allow from anyone on port 443', + ipProtocol: ec2.Protocol.TCP, + cidrIp: '0.0.0.0/0', + fromPort: 443, + toPort: 443, + }, + ], }); const loadBalancer = new elb2.CfnLoadBalancer(this, 'LoadBalancer', { type: 'application', - securityGroups: [ - albSecurityGroup.attrGroupId, + securityGroups: [albSecurityGroup.attrGroupId], + loadBalancerAttributes: [ + { + key: 'deletion_protection.enabled', + value: 'false', + }, ], - loadBalancerAttributes: [{ - key: 'deletion_protection.enabled', - value: 'false', - }], scheme: 'internet-facing', subnets, }); - (this.ecsService.loadBalancers) = [{ - containerName, - containerPort: port, - targetGroupArn: targetGroup.ref, - }]; + (this.ecsService.loadBalancers) = [ + { + containerName, + containerPort: port, + targetGroupArn: targetGroup.ref, + }, + ]; (this.ecsServiceSecurityGroup.securityGroupIngress).push({ ipProtocol: ec2.Protocol.TCP, fromPort: port, @@ -159,16 +160,18 @@ export class EcsAlbStack extends ContainersStack { }); const listener = new elb2.CfnListener(this, 'AlbListener', { - defaultActions: [{ - fixedResponseConfig: { - statusCode: '403', + defaultActions: [ + { + fixedResponseConfig: { + statusCode: '403', + }, + type: 'fixed-response', }, - type: 'fixed-response', - }], + ], loadBalancerArn: loadBalancer.ref, port: 443, protocol: elb2.Protocol.HTTPS, - certificates: [{ certificateArn: wildcardCertificate.ref }] + certificates: [{ certificateArn: wildcardCertificate.ref }], }); this.ecsService.addDependsOn(listener); @@ -178,20 +181,22 @@ export class EcsAlbStack extends ContainersStack { priority: 1, listenerArn: listener.ref, actions: [].concat( - restrictAccess ? { - order: actionsOrderCounter++, - type: 'authenticate-cognito', - authenticateCognitoConfig: { - userPoolArn, - userPoolClientId: userPoolClient.ref, - userPoolDomain, - } - } : undefined, + restrictAccess + ? { + order: actionsOrderCounter++, + type: 'authenticate-cognito', + authenticateCognitoConfig: { + userPoolArn, + userPoolClientId: userPoolClient.ref, + userPoolDomain, + }, + } + : undefined, { order: actionsOrderCounter++, type: 'forward', targetGroupArn: targetGroup.ref, - } + }, ), conditions: [ { @@ -205,8 +210,8 @@ export class EcsAlbStack extends ContainersStack { httpHeaderConfig: { httpHeaderName: sharedSecretHeaderName, values: [sharedSecretHeader], - } - } + }, + }, ], }); @@ -227,25 +232,29 @@ export class EcsAlbStack extends ContainersStack { queryString: true, }, targetOriginId: originId, - viewerProtocolPolicy: 'redirect-to-https' + viewerProtocolPolicy: 'redirect-to-https', }, - origins: [{ - customOriginConfig: { - originProtocolPolicy: 'https-only', + origins: [ + { + customOriginConfig: { + originProtocolPolicy: 'https-only', + }, + domainName: albDomainName, + id: originId, + originCustomHeaders: [ + { + headerName: sharedSecretHeaderName, + headerValue: sharedSecretHeader, + }, + ], }, - domainName: albDomainName, - id: originId, - originCustomHeaders: [{ - headerName: sharedSecretHeaderName, - headerValue: sharedSecretHeader, - }] - }], + ], viewerCertificate: { acmCertificateArn: wildcardCertificate.ref, minimumProtocolVersion: 'TLSv1.2_2019', sslSupportMethod: 'sni-only', }, - } + }, }); if (hostedZoneId) { @@ -257,7 +266,7 @@ export class EcsAlbStack extends ContainersStack { type: route53.RecordType.A, aliasTarget: { hostedZoneId: loadBalancer.attrCanonicalHostedZoneId, - dnsName: loadBalancer.attrDnsName + dnsName: loadBalancer.attrDnsName, }, }, { @@ -265,10 +274,10 @@ export class EcsAlbStack extends ContainersStack { type: route53.RecordType.A, aliasTarget: { hostedZoneId: route53targets.CloudFrontTarget.CLOUDFRONT_ZONE_ID, - dnsName: distribution.attrDomainName + dnsName: distribution.attrDomainName, }, - } - ] + }, + ], }); } @@ -278,8 +287,8 @@ export class EcsAlbStack extends ContainersStack { cdk.Aws.REGION, '.console.aws.amazon.com/codesuite/codepipeline/pipelines/', this.getPipelineName(), - '/view' - ]) + '/view', + ]), }); new cdk.CfnOutput(this, 'LoadBalancerAliasDomainName', { value: loadBalancer.attrDnsName }); diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/ecs-apigw-stack.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/ecs-apigw-stack.ts index 3925b5835bb..4d740dd29d3 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/ecs-apigw-stack.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/ecs-apigw-stack.ts @@ -1,13 +1,14 @@ import * as apigw2 from '@aws-cdk/aws-apigatewayv2'; import * as cdk from '@aws-cdk/core'; -import { ContainersStack, ContainersStackProps } from "./base-api-stack"; +import { ContainersStack, ContainersStackProps } from './base-api-stack'; import { API_TYPE } from './service-walkthroughs/containers-walkthrough'; -type EcsStackProps = Readonly; +type EcsStackProps = Readonly< + ContainersStackProps & { + apiType: API_TYPE; + } +>; export class EcsStack extends ContainersStack { - constructor(scope: cdk.Construct, id: string, private readonly ecsProps: EcsStackProps) { super(scope, id, { ...ecsProps, @@ -33,10 +34,10 @@ export class EcsStack extends ContainersStack { } private apiGateway() { - const { envName, apiName } = this.ecsProps + const { apiName } = this.ecsProps; const api = new apigw2.CfnApi(this, 'Api', { - name: `${envName}-${apiName}`, + name: `${this.envName}-${apiName}`, protocolType: 'HTTP', corsConfiguration: { allowHeaders: ['*'], @@ -67,12 +68,7 @@ export class EcsStack extends ContainersStack { authorizerType: 'JWT', jwtConfiguration: { audience: [this.appClientId], - issuer: cdk.Fn.join('', [ - 'https://cognito-idp.', - cdk.Aws.REGION, - '.amazonaws.com/', - this.userPoolId, - ]), + issuer: cdk.Fn.join('', ['https://cognito-idp.', cdk.Aws.REGION, '.amazonaws.com/', this.userPoolId]), }, identitySource: ['$request.header.Authorization'], }); diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/containers-artifacts.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/containers-artifacts.ts index c1745a4fb9b..7ed760fa8f5 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/containers-artifacts.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/containers-artifacts.ts @@ -54,7 +54,7 @@ export async function generateContainersArtifacts( providers: { [cloudformationProviderName]: provider }, } = context.amplify.getProjectMeta(); - const { StackName: envName, DeploymentBucketName: deploymentBucketName } = provider; + const { StackName: envName } = provider; const { category: categoryName, @@ -78,7 +78,6 @@ export async function generateContainersArtifacts( srcPath, askForExposedContainer, ); - const repositories = await context.amplify.executeProviderUtils(context, 'awscloudformation', 'describeEcrRepositories'); const existingEcrRepositories: Set = new Set( @@ -88,7 +87,6 @@ export async function generateContainersArtifacts( ); const stack = new EcsStack(undefined, 'ContainersStack', { - envName, categoryName, apiName: resourceName, taskPorts: containersPorts, @@ -97,7 +95,6 @@ export async function generateContainersArtifacts( taskEnvironmentVariables: environmentMap, gitHubSourceActionInfo: gitHubInfo, deploymentMechanism, - deploymentBucketName, containers, isInitialDeploy, desiredCount, diff --git a/packages/amplify-container-hosting/lib/ElasticContainer/index.js b/packages/amplify-container-hosting/lib/ElasticContainer/index.js index f1b91154153..a19956e57d8 100644 --- a/packages/amplify-container-hosting/lib/ElasticContainer/index.js +++ b/packages/amplify-container-hosting/lib/ElasticContainer/index.js @@ -268,13 +268,11 @@ export async function generateHostingResources( ); const stack = new EcsAlbStack(undefined, 'ContainersHosting', { - envName, categoryName, apiName: resourceName, authName, dependsOn, policies: [], // TODO - deploymentBucketName, restrictAccess, createCloudMapService: false, secretsArns, diff --git a/packages/amplify-provider-awscloudformation/src/push-resources.ts b/packages/amplify-provider-awscloudformation/src/push-resources.ts index 331d0e35346..a84782bcff0 100644 --- a/packages/amplify-provider-awscloudformation/src/push-resources.ts +++ b/packages/amplify-provider-awscloudformation/src/push-resources.ts @@ -73,14 +73,8 @@ export async function run(context: $TSContext, resourceDefinition: $TSObject) { let layerResources = []; try { - const { - resourcesToBeCreated, - resourcesToBeUpdated, - resourcesToBeSynced, - resourcesToBeDeleted, - tagsUpdated, - allResources, - } = resourceDefinition; + const { resourcesToBeCreated, resourcesToBeUpdated, resourcesToBeSynced, resourcesToBeDeleted, tagsUpdated, allResources } = + resourceDefinition; const cloudformationMeta = context.amplify.getProjectMeta().providers.awscloudformation; const { parameters: { options }, @@ -1018,6 +1012,11 @@ async function formNestedStack( } } + if ((category === 'api' || category === 'hosting') && resourceDetails.service === ApiServiceNameElasticContainer) { + parameters['deploymentBucketName'] = Fn.Ref('DeploymentBucketName'); + parameters['rootStackName'] = Fn.Ref('AWS::StackName'); + } + const currentEnv = context.amplify.getEnvInfo().envName; if (!skipEnv && resourceName) { @@ -1031,14 +1030,8 @@ async function formNestedStack( // If auth is imported check the parameters section of the nested template // and if it has auth or unauth role arn or name or userpool id, then inject it from the // imported auth resource's properties - const { - imported, - userPoolId, - authRoleArn, - authRoleName, - unauthRoleArn, - unauthRoleName, - } = context.amplify.getImportedAuthProperties(context); + const { imported, userPoolId, authRoleArn, authRoleName, unauthRoleArn, unauthRoleName } = + context.amplify.getImportedAuthProperties(context); if (category !== 'auth' && resourceDetails.service !== 'Cognito' && imported) { if (parameters.AuthCognitoUserPoolId) {