Skip to content

Commit

Permalink
CMDCT-4226 - sets up deploy-prerequisites to create resources needed …
Browse files Browse the repository at this point in the history
…in AWS account (#15025)

Co-authored-by: Jon Holman <[email protected]>
  • Loading branch information
peoplespete and JonHolman authored Jan 8, 2025
1 parent 20821ba commit f634b78
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 40 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,14 @@ This repository uses 3 webhooks to publish to 3 different channels all in CMS Sl
- Secrets are added to GitHub secrets by GitHub Admins
- Development secrets are maintained in a 1Password vault

## Deployment

While application deployment is generally handled by Github Actions, when you initially set up a new AWS account to host this application, you'll need to deploy a prerequisite stack like so:
```bash
./run deploy-prerequisites
```
That will create a stack called `seds-prerequisites` which will contain resources needed by any application stacks.

## License

[![License](https://img.shields.io/badge/License-CC0--1.0--Universal-blue.svg)](https://creativecommons.org/publicdomain/zero/1.0/legalcode)
Expand Down
2 changes: 1 addition & 1 deletion deployment/deployment-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const determineDeploymentConfig = async (stage: string) => {
return config;
};

const loadDefaultSecret = async (project: string) => {
export const loadDefaultSecret = async (project: string) => {
return JSON.parse((await getSecret(`${project}-default`))!);
};

Expand Down
88 changes: 88 additions & 0 deletions deployment/prerequisites.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/usr/bin/env node
import "source-map-support/register";
import {
aws_apigateway as apigateway,
aws_iam as iam,
App,
DefaultStackSynthesizer,
Stack,
StackProps,
Tags,
} from "aws-cdk-lib";
import { CloudWatchLogsResourcePolicy } from "./constructs/cloudwatch-logs-resource-policy";
import { loadDefaultSecret } from "./deployment-config";
import { getSecret } from "./utils/secrets-manager";
import { Construct } from "constructs";

interface PrerequisiteConfigProps {
project: string;
iamPath: string;
iamPermissionsBoundaryArn: string;
}

export class PrerequisiteStack extends Stack {
constructor(
scope: Construct,
id: string,
props: StackProps & PrerequisiteConfigProps
) {
super(scope, id, props);

const {
project,
iamPermissionsBoundaryArn,
iamPath,
} = props;

new CloudWatchLogsResourcePolicy(this, "logPolicy", { project });

const cloudWatchRole = new iam.Role(
this,
"ApiGatewayRestApiCloudWatchRole",
{
assumedBy: new iam.ServicePrincipal("apigateway.amazonaws.com"),
permissionsBoundary: iam.ManagedPolicy.fromManagedPolicyArn(
this,
"iamPermissionsBoundary",
iamPermissionsBoundaryArn
),
path: iamPath,
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName(
"service-role/AmazonAPIGatewayPushToCloudWatchLogs"
),
],
}
);

new apigateway.CfnAccount(
this,
"ApiGatewayRestApiAccount",
{
cloudWatchRoleArn: cloudWatchRole.roleArn,
}
);
}
}

async function main() {
const app = new App({
defaultStackSynthesizer: new DefaultStackSynthesizer(
JSON.parse((await getSecret("cdkSynthesizerConfig"))!)
),
});

Tags.of(app).add("PROJECT", "SEDS");

const project = process.env.PROJECT!;
new PrerequisiteStack(app, "seds-prerequisites", {
project,
...(await loadDefaultSecret(project)),
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
},
});
}

main();
28 changes: 1 addition & 27 deletions deployment/stacks/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,31 +110,6 @@ export function createApiComponents(props: CreateApiComponentsProps) {
},
});

const cloudWatchRole = new iam.Role(
scope,
"ApiGatewayRestApiCloudWatchRole",
{
assumedBy: new iam.ServicePrincipal("apigateway.amazonaws.com"),
permissionsBoundary: props.iamPermissionsBoundary,
path: props.iamPath,
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName(
"service-role/AmazonAPIGatewayPushToCloudWatchLogs"
),
],
}
);
cloudWatchRole.applyRemovalPolicy(RemovalPolicy.RETAIN);

const apiGatewayRestApiAccount = new apigateway.CfnAccount(
scope,
"ApiGatewayRestApiAccount",
{
cloudWatchRoleArn: cloudWatchRole.roleArn,
}
);
apiGatewayRestApiAccount.applyRemovalPolicy(RemovalPolicy.RETAIN);

const environment = {
BOOTSTRAP_BROKER_STRING_TLS: brokerString,
stage,
Expand Down Expand Up @@ -480,8 +455,7 @@ export function createApiComponents(props: CreateApiComponentsProps) {
webAclArn: waf.webAcl.attrArn,
});


if (!isDev) { // resources that must be in AWS account
if (!isDev) {
const logBucket = new s3.Bucket(scope, "WafLogBucket", {
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
Expand Down
5 changes: 0 additions & 5 deletions deployment/stacks/parent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
Stack,
StackProps,
} from "aws-cdk-lib";
import { CloudWatchLogsResourcePolicy } from "../constructs/cloudwatch-logs-resource-policy";
import { DeploymentConfigProperties } from "../deployment-config";
import { createDataComponents } from "./data";
import { createUiAuthComponents } from "./ui-auth";
Expand Down Expand Up @@ -54,10 +53,6 @@ export class ParentStack extends Stack {
const vpc = ec2.Vpc.fromLookup(this, "Vpc", { vpcName });
const privateSubnets = sortSubnets(vpc.privateSubnets).slice(0, 3);

if (!isDev) { // resources that must be in AWS account
new CloudWatchLogsResourcePolicy(this, "logPolicy", { project });
}

const { customResourceRole } = createCustomResourceRole({ ...commonProps });

const { tables } = createDataComponents({
Expand Down
6 changes: 6 additions & 0 deletions deployment/stacks/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ export function createUiComponents(props: CreateUiComponentsProps) {
},
}
);
securityHeadersPolicy.applyRemovalPolicy(
isDev ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN
)

const distribution = new cloudfront.Distribution(
scope,
Expand Down Expand Up @@ -129,6 +132,9 @@ export function createUiComponents(props: CreateUiComponentsProps) {
],
}
);
distribution.applyRemovalPolicy(
isDev ? RemovalPolicy.DESTROY : RemovalPolicy.RETAIN
)

const applicationEndpointUrl = `https://${distribution.distributionDomainName}/`;

Expand Down
36 changes: 29 additions & 7 deletions src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import readline from "node:readline";
import {
CloudFormationClient,
DeleteStackCommand,
DescribeStacksCommand,
waitUntilStackDeleteComplete,
} from "@aws-sdk/client-cloudformation";
import { writeUiEnvFile } from "./write-ui-env-file.js";
Expand Down Expand Up @@ -126,12 +127,35 @@ async function prepare_services(runner: LabeledProcessRunner) {
}
}

async function deploy_prerequisites() {
const runner = new LabeledProcessRunner();
await prepare_services(runner);
const deployPrequisitesCmd = ["cdk", "deploy", "--app", "\"npx tsx deployment/prerequisites.ts\""];
await runner.run_command_and_output("CDK prerequisite deploy", deployPrequisitesCmd, ".");
}

const stackExists = async (stackName: string): Promise<boolean> => {
const client = new CloudFormationClient({ region });
try {
await client.send(
new DescribeStacksCommand({ StackName: stackName })
);
return true;
} catch (error: any) {
return false;
}
};

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"];
await runner.run_command_and_output("CDK deploy", deployCmd, ".");
if (await stackExists("seds-prerequisites")) {
const deployCmd = ["cdk", "deploy", "--context", `stage=${stage}`, "--all"];
await runner.run_command_and_output("CDK deploy", deployCmd, ".");
} else {
console.error("MISSING PREREQUISITE STACK! Must deploy it before attempting to deploy the application.")
}
}

const waitForStackDeleteComplete = async (
Expand Down Expand Up @@ -201,12 +225,10 @@ yargs(process.argv.slice(2))
run_local
)
.command(
"test",
"run all tests",
"deploy-prerequisites",
"deploy the app's AWS account prerequisites with cdk to the cloud",
() => {},
() => {
console.log("Testing 1. 2. 3.");
}
deploy_prerequisites
)
.command(
"deploy",
Expand Down

0 comments on commit f634b78

Please sign in to comment.