diff --git a/deployment/app.ts b/deployment/app.ts index 98f6e2a98..2f654f84f 100644 --- a/deployment/app.ts +++ b/deployment/app.ts @@ -1,57 +1,62 @@ #!/usr/bin/env node import "source-map-support/register"; import * as cdk from "aws-cdk-lib"; +import { WithoutImportsParentStack } from "./stacks/without_imports/parent"; +import { WithImportsParentStack} from "./stacks/with_imports/parent"; import { ParentStack } from "./stacks/parent"; import { determineDeploymentConfig } from "./deployment-config"; import { getSecret } from "./utils/secrets-manager"; import { getDeploymentConfigParameters } from "./utils/systems-manager"; async function main() { - try { - const app = new cdk.App({ - defaultStackSynthesizer: new cdk.DefaultStackSynthesizer( - JSON.parse((await getSecret("cdkSynthesizerConfig"))!) - ), - }); + const app = new cdk.App({ + defaultStackSynthesizer: new cdk.DefaultStackSynthesizer( + JSON.parse((await getSecret("cdkSynthesizerConfig"))!) + ), + }); - const stage = app.node.getContext("stage"); - const config = await determineDeploymentConfig(stage); + const stage = app.node.getContext("stage"); + const config = await determineDeploymentConfig(stage); - const parametersToFetch = { - cloudfrontCertificateArn: { - name: "cloudfront/certificateArn", - useDefault: true, - }, - cloudfrontDomainName: { - name: "cloudfront/domainName", - useDefault: false, - }, - vpnIpSetArn: { name: "vpnIpSetArn", useDefault: true }, - vpnIpv6SetArn: { name: "vpnIpv6SetArn", useDefault: true }, - hostedZoneId: { name: "route53/hostedZoneId", useDefault: true }, - domainName: { name: "route53/domainName", useDefault: true }, - }; + const parametersToFetch = { + cloudfrontCertificateArn: { + name: "cloudfront/certificateArn", + useDefault: true, + }, + cloudfrontDomainName: { + name: "cloudfront/domainName", + useDefault: false, + }, + vpnIpSetArn: { name: "vpnIpSetArn", useDefault: true }, + vpnIpv6SetArn: { name: "vpnIpv6SetArn", useDefault: true }, + hostedZoneId: { name: "route53/hostedZoneId", useDefault: true }, + domainName: { name: "route53/domainName", useDefault: true }, + }; - const deploymentConfigParameters = await getDeploymentConfigParameters( - parametersToFetch, - stage - ); + const deploymentConfigParameters = await getDeploymentConfigParameters( + parametersToFetch, + stage + ); - cdk.Tags.of(app).add("STAGE", stage); - cdk.Tags.of(app).add("PROJECT", config.project); + cdk.Tags.of(app).add("STAGE", stage); + cdk.Tags.of(app).add("PROJECT", config.project); - new ParentStack(app, `${config.project}-${stage}`, { - ...config, - env: { - account: process.env.CDK_DEFAULT_ACCOUNT, - region: process.env.CDK_DEFAULT_REGION, - }, - deploymentConfigParameters, - }); - } catch (error) { - console.error("Error:", error); - process.exit(1); + let correctParentStack; + if (process.env.WITHOUT_IMPORTS) { + correctParentStack = WithoutImportsParentStack + } else if (process.env.WITH_IMPORTS) { + correctParentStack = WithImportsParentStack + } else { + correctParentStack = ParentStack } + new correctParentStack(app, `${config.project}-${stage}`, { + ...config, + env: { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION, + }, + deploymentConfigParameters, + }); } main(); diff --git a/deployment/import_instructions.md b/deployment/import_instructions.md new file mode 100644 index 000000000..3407ef286 --- /dev/null +++ b/deployment/import_instructions.md @@ -0,0 +1,111 @@ +# Import Instructions + +## From `pete-sls` branch: + +1. Deploy sls to get it ready for deletion with retained resources configured for import + +``` +./run deploy --stage +``` + +2. Collect information about the resources we're going to be importing into the new cdk stack. +``` +cloudfront.Distribution - +cognito.UserPool - +``` + +3. Destroy sls + +``` +./run destroy --stage +``` + +## From `jon-cdk` branch: + + +1. Create just the new cdk stack without anything inside of it. + +```bash +WITHOUT_IMPORTS=true ./run deploy --stage +``` + +2. Now import all the serverless ejected resources. + +```bash +WITH_IMPORTS=true PROJECT=seds cdk import --context stage= --force +``` +As this import occurs you'll have to provide the information you gathered just before destroying the serverless stacks. + +3. Run a deploy on that same imported resource set. + +```bash +WITH_IMPORTS=true ./run deploy --stage +``` + +4. Run a full deploy by kicking off the full cdk deploy via Github Action. Permissions for individual developers are limited so you must use Github Action to do this part. + +5. Find the Cloudfront Url in the Github Action's logs (or in the outputs section of your Cloudformation Stack). Visit the site and confirm that you can login and use the application. :tada: Congrats, you did it! + + +## What if it all goes pear shaped? + +### If during the middle of the migration, things begin to break and we need to reinstate the serverless stack, we need a way to bring the Cloudfront Distribution back into the newly rebuilt serverless stack. Fortunately this is possible if you follow these steps. + +:grey_exclamation: These instructions are specific to reimporting a Cloudfront Distribution but the same pattern should also apply to any other imported resources that need un-importing should the need arise. + +1) Get the Cloudfront Distribution unaffiliated with any Cloudformation stack. If it's already been successfully imported into the new cdk stack then you'll need to destroy the cdk stack to eject it from that stack. +``` +# this assumes you're on `jon-cdk` branch +./run destroy --stage +``` + +2) Now you need switch to `pete-sls` branch and comment out any CloudfrontDistribution and dependent configuration. +These are the necessary changes: https://github.com/Enterprise-CMCS/macpro-mdct-seds/commit/8eb551f980a37355729dc1795f5d229987699c84 + +3) Deploy serverless stack (without CloudfrontDistribution) via Github Action (necessary because of permissions limitations) by pushing up changes made in the last step. + +4) Now you need to import the CloudfrontDistribution to the existing ui-XXXXX stack created by the last step. First you'll need to get the existing stack's template and save it to a local file. +``` +aws cloudformation get-template --stack-name ui-cmdct-4188-sls | jq '.TemplateBody' > deployment/cfn_template.json +``` + +5) Now open up the file you just created (deployment/cfn_template.json) and add the following Cloudfront Distribution config to it's resources section: +```json + "CloudFrontDistribution": { + "Type": "AWS::CloudFront::Distribution", + "DeletionPolicy": "Retain", + "Properties": { + "DistributionConfig": { + "CustomOrigin": { + "DNSName": "www.example.com", + "OriginProtocolPolicy": "http-only", + "OriginSSLProtocols": [ + "TLSv1" + ] + }, + "Enabled": true, + "DefaultCacheBehavior": { + "CachePolicyId": "Managed-CachingDisabled", + "TargetOriginId": "some_target_origin_id", + "ViewerProtocolPolicy": "allow-all" + } + } + } + }, +``` + +6) Now open up the AWS console and navigate to the Cloudformation console's show page for the particular ui-XXXXX stack. + +7) Import the stack by doing the following: + - Under `Stack Actions` select `Import resources into stack` + - In the `Specify template` section choose upload a template file and upload the one we just created: `deployment/cfn_template.json` + - In the `Identify resources` section you'll have to provide the ID of the incoming Cloudfront Distribution + - Next and confirm until it begins the import + +8) Once the import is complete, take a breath. + +9) Revert the changes where you commented out the Serverless definition of Cloudfront Distribution (undo step 2). + +10) Verify that the Serverless definition now contains the Cloudfront Distribution then deploy via Github Action. + +11) Verify that Cloudfront Distribution is back into the stack and appropriately pointing at the application. diff --git a/deployment/stacks/api.ts b/deployment/stacks/api.ts index 7d7a8ab0d..3aef6c6ce 100644 --- a/deployment/stacks/api.ts +++ b/deployment/stacks/api.ts @@ -140,7 +140,7 @@ export function createApiComponents(props: CreateApiComponentsProps) { BOOTSTRAP_BROKER_STRING_TLS: brokerString, stage, ...Object.fromEntries( - tables.map((table) => [`${table.id}Name`, table.name]) + tables.map((table) => [`${table.id}TableName`, table.name]) ), }; diff --git a/deployment/stacks/data.ts b/deployment/stacks/data.ts index 8573c62a3..684f6164d 100644 --- a/deployment/stacks/data.ts +++ b/deployment/stacks/data.ts @@ -79,26 +79,29 @@ export function createDataComponents(props: CreateDataComponentsProps) { "service-role/AWSLambdaVPCAccessExecutionRole" ), ], + inlinePolicies: { + DynamoPolicy: new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + "dynamodb:DescribeTable", + "dynamodb:Query", + "dynamodb:Scan", + "dynamodb:GetItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + ], + resources: ["*"], + }) + ] + }) + }, permissionsBoundary: props.iamPermissionsBoundary, path: props.iamPath, }); - lambdaApiRole.addToPolicy( - new iam.PolicyStatement({ - effect: iam.Effect.ALLOW, - actions: [ - "dynamodb:DescribeTable", - "dynamodb:Query", - "dynamodb:Scan", - "dynamodb:GetItem", - "dynamodb:PutItem", - "dynamodb:UpdateItem", - "dynamodb:DeleteItem", - ], - resources: ["*"], - }) - ); - // TODO: test deploy and watch performance with this using lambda.Function vs lambda_nodejs.NodejsFunction const seedDataFunction = new lambda_nodejs.NodejsFunction(scope, "seedData", { entry: "services/database/handlers/seed/seed.js", diff --git a/deployment/stacks/ui-auth.ts b/deployment/stacks/ui-auth.ts index 5a929473e..538514f00 100644 --- a/deployment/stacks/ui-auth.ts +++ b/deployment/stacks/ui-auth.ts @@ -6,7 +6,6 @@ import { aws_lambda_nodejs as lambda_nodejs, aws_wafv2 as wafv2, aws_ssm as ssm, - RemovalPolicy, Aws, Duration, custom_resources as cr, @@ -40,7 +39,6 @@ export function createUiAuthComponents(props: CreateUiAuthComponentsProps) { const userPool = new cognito.UserPool(scope, "UserPool", { userPoolName: `${stage}-user-pool`, - removalPolicy: RemovalPolicy.DESTROY, signInAliases: { email: true, }, @@ -208,36 +206,33 @@ export function createUiAuthComponents(props: CreateUiAuthComponentsProps) { "service-role/AWSLambdaVPCAccessExecutionRole" ), ], + inlinePolicies: { + LambdaApiRolePolicy: new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + actions: [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + resources: ["arn:aws:logs:*:*:*"], + effect: iam.Effect.ALLOW, + }), + new iam.PolicyStatement({ + actions: ["*"], + resources: [userPool.userPoolArn], + effect: iam.Effect.ALLOW, + }), + new iam.PolicyStatement({ + actions: ["ssm:GetParameter"], + resources: [bootstrapUsersPasswordArn], + effect: iam.Effect.ALLOW, + }), + ], + }), + }, }); - lambdaApiRole.addToPolicy( - new iam.PolicyStatement({ - effect: iam.Effect.ALLOW, - actions: [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents", - ], - resources: ["arn:aws:logs:*:*:*"], - }) - ); - - lambdaApiRole.addToPolicy( - new iam.PolicyStatement({ - effect: iam.Effect.ALLOW, - actions: ["*"], - resources: [userPool.userPoolArn], - }) - ); - - lambdaApiRole.addToPolicy( - new iam.PolicyStatement({ - effect: iam.Effect.ALLOW, - actions: ["ssm:GetParameter"], - resources: [bootstrapUsersPasswordArn], - }) - ); - // TODO: test deploy and watch performance with scope using lambda.Function vs lambda_nodejs.NodejsFunction bootstrapUsersFunction = new lambda_nodejs.NodejsFunction( scope, diff --git a/deployment/stacks/ui.ts b/deployment/stacks/ui.ts index eddfdf6d7..ca253b3b2 100644 --- a/deployment/stacks/ui.ts +++ b/deployment/stacks/ui.ts @@ -4,8 +4,6 @@ import { aws_cloudfront_origins as cloudfrontOrigins, aws_iam as iam, aws_kinesisfirehose as firehose, - aws_route53 as route53, - aws_route53_targets as route53Targets, aws_s3 as s3, aws_wafv2 as wafv2, Aws, @@ -136,7 +134,6 @@ export function createUiComponents(props: CreateUiComponentsProps) { const applicationEndpointUrl = `https://${distribution.distributionDomainName}/`; setupWaf(scope, stage, project, deploymentConfigParameters); - setupRoute53(scope, distribution, deploymentConfigParameters); createFirehoseLogging( scope, @@ -229,29 +226,6 @@ function setupWaf( }); } -function setupRoute53( - scope: Construct, - distribution: cloudfront.Distribution, - deploymentConfigParameters: { [name: string]: string | undefined } -) { - if ( - deploymentConfigParameters.hostedZoneId && - deploymentConfigParameters.domainName - ) { - const zone = route53.HostedZone.fromHostedZoneAttributes(scope, "Zone", { - hostedZoneId: deploymentConfigParameters.hostedZoneId, - zoneName: deploymentConfigParameters.domainName, - }); - - new route53.ARecord(scope, "AliasRecord", { - zone, - target: route53.RecordTarget.fromAlias( - new route53Targets.CloudFrontTarget(distribution) - ), - }); - } -} - function createFirehoseLogging( scope: Construct, stage: string, diff --git a/deployment/stacks/with_imports/parent.ts b/deployment/stacks/with_imports/parent.ts new file mode 100644 index 000000000..5e341fd8f --- /dev/null +++ b/deployment/stacks/with_imports/parent.ts @@ -0,0 +1,28 @@ +import { Construct } from "constructs"; +import { + Stack, + StackProps, +} from "aws-cdk-lib"; +import { DeploymentConfigProperties } from "../../deployment-config"; +import { createUiComponents } from "./ui"; +import { createUiAuthComponents } from "./ui-auth"; + +export class WithImportsParentStack extends Stack { + constructor( + scope: Construct, + id: string, + props: StackProps & DeploymentConfigProperties + ) { + super(scope, id, props); + + const { + stage, + } = props; + + createUiComponents({scope: this}); + createUiAuthComponents({ + scope: this, + stage, + }); + } +} diff --git a/deployment/stacks/with_imports/ui-auth.ts b/deployment/stacks/with_imports/ui-auth.ts new file mode 100644 index 000000000..ba4731963 --- /dev/null +++ b/deployment/stacks/with_imports/ui-auth.ts @@ -0,0 +1,45 @@ +import { Construct } from "constructs"; +import { + aws_cognito as cognito, +} from "aws-cdk-lib"; + +interface CreateUiAuthComponentsProps { + scope: Construct; + stage: string; +} + +export function createUiAuthComponents(props: CreateUiAuthComponentsProps) { + const { + scope, + stage, + } = props; + + new cognito.UserPool(scope, "UserPool", { + userPoolName: `${stage}-user-pool`, + signInAliases: { + email: true, + }, + autoVerify: { + email: true, + }, + selfSignUpEnabled: false, + standardAttributes: { + givenName: { + required: false, + mutable: true, + }, + familyName: { + required: false, + mutable: true, + }, + phoneNumber: { + required: false, + mutable: true, + }, + }, + customAttributes: { + ismemberof: new cognito.StringAttribute({ mutable: true }), + }, + advancedSecurityMode: cognito.AdvancedSecurityMode.ENFORCED, + }); +} diff --git a/deployment/stacks/with_imports/ui.ts b/deployment/stacks/with_imports/ui.ts new file mode 100644 index 000000000..4945c3940 --- /dev/null +++ b/deployment/stacks/with_imports/ui.ts @@ -0,0 +1,29 @@ +import { Construct } from "constructs"; +import { + aws_cloudfront as cloudfront, + aws_cloudfront_origins as cloudfrontOrigins, +} from "aws-cdk-lib"; + +interface CreateUiComponentsProps { + scope: Construct; +} + +export function createUiComponents(props: CreateUiComponentsProps) { + const { + scope, + } = props; + + new cloudfront.Distribution( + scope, + 'CloudFrontDistribution', + { + defaultBehavior: { + origin: new cloudfrontOrigins.HttpOrigin( + 'www.example.com', + { originId: 'Default'} + ), + cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, + }, + } + ); +} diff --git a/deployment/stacks/without_imports/parent.ts b/deployment/stacks/without_imports/parent.ts new file mode 100644 index 000000000..f4b3c6c24 --- /dev/null +++ b/deployment/stacks/without_imports/parent.ts @@ -0,0 +1,16 @@ +import { Construct } from "constructs"; +import { + Stack, + StackProps, +} from "aws-cdk-lib"; +import { DeploymentConfigProperties } from "../../deployment-config"; + +export class WithoutImportsParentStack extends Stack { + constructor( + scope: Construct, + id: string, + props: StackProps & DeploymentConfigProperties + ) { + super(scope, id, props); + } +} diff --git a/services/ui-auth/serverless.yml b/services/ui-auth/serverless.yml index ba3a4d1cf..4faa8b166 100644 --- a/services/ui-auth/serverless.yml +++ b/services/ui-auth/serverless.yml @@ -14,9 +14,9 @@ provider: name: aws runtime: nodejs20.x region: us-east-1 - stackTags: + stackTags: PROJECT: ${self:custom.project} - SERVICE: ${self:service} + SERVICE: ${self:service} iam: role: # Even though we are creating our own IAM role that is used in each lambda function below @@ -155,7 +155,7 @@ resources: Properties: ResourceArn: !GetAtt CognitoUserPool.Arn WebACLArn: !GetAtt WafPluginAcl.Arn - + CognitoUserPoolClient: Type: AWS::Cognito::UserPoolClient Properties: diff --git a/services/ui/serverless.yml b/services/ui/serverless.yml index 9aa4aac9f..13789d277 100644 --- a/services/ui/serverless.yml +++ b/services/ui/serverless.yml @@ -11,9 +11,9 @@ provider: name: aws runtime: nodejs20.x region: us-east-1 - stackTags: + stackTags: PROJECT: ${self:custom.project} - SERVICE: ${self:service} + SERVICE: ${self:service} custom: project: "seds" diff --git a/src/run.ts b/src/run.ts index 18f1ca7bc..e7def9080 100644 --- a/src/run.ts +++ b/src/run.ts @@ -47,7 +47,7 @@ async function confirmDestroyCommand(stack: string) { const confirmation = await question(` ${orange}********************************* STOP ******************************* -You've requested a destroy for: +You've requested a destroy for: ${stack} @@ -209,7 +209,7 @@ async function deploy(options: { stage: string }) { const stage = options.stage; const runner = new LabeledProcessRunner(); await prepare_services(runner); - const deployCmd = ["cdk", "deploy", "--context", `stage=${stage}`, "--all"]; + const deployCmd = ["cdk", "deploy", "--context", `stage=${stage}`, "--method=direct", "--all"]; await runner.run_command_and_output("CDK deploy", deployCmd, "."); }