Skip to content

Commit

Permalink
feat: let's try static site!
Browse files Browse the repository at this point in the history
  • Loading branch information
ap0nia committed Jul 16, 2023
1 parent 93625d0 commit 8beb5a4
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 54 deletions.
153 changes: 143 additions & 10 deletions ant.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,21 @@
* import * as cdk from 'aws-cdk-lib'
* ```
*/

import path from "node:path";
import { consola } from "consola";
import * as core from "@actions/core";
import * as github from "@actions/github";
import { App, Stack } from "aws-cdk-lib/core";
import * as aws_core from "aws-cdk-lib/core";
import * as aws_certificatemanager from "aws-cdk-lib/aws-certificatemanager";
import * as aws_cloudfront from "aws-cdk-lib/aws-cloudfront";
import * as aws_cloudfront_origins from "aws-cdk-lib/aws-cloudfront-origins";
import * as aws_route53 from "aws-cdk-lib/aws-route53";
import * as aws_route53_targets from "aws-cdk-lib/aws-route53-targets";
import { Api } from "ant-stack/constructs/api";
import { GitHub } from "ant-stack/constructs/github";
import { StaticSite } from "ant-stack/constructs/staticSite";
import { getWorkspaceRoot } from "ant-stack/utils";

/**
* @see https://github.com/evanw/esbuild/issues/1921#issuecomment-1491470829
Expand All @@ -26,10 +35,10 @@ const __filename = topLevelUrl.fileURLToPath(import.meta.url);
const __dirname = topLevelPath.dirname(__filename);
`;

export class MyStack extends Stack {
export class ApiStack extends aws_core.Stack {
api: Api;

constructor(scope: App, id: string) {
constructor(scope: aws_core.App, id: string) {
super(scope, id);

this.api = new Api(this, "Api", {
Expand All @@ -53,18 +62,122 @@ export class MyStack extends Stack {
}
}

export class DocsStack extends aws_core.Stack {
staticSite: StaticSite;

cloudFrontTarget: aws_route53_targets.CloudFrontTarget;

aRecord: aws_route53.ARecord;

zone: aws_route53.IHostedZone;

constructor(scope: aws_core.App, id: string) {
if (!process.env.CERTIFICATE_ARN) throw new Error("Certificate ARN not provided. Stop.");
if (!process.env.DATABASE_URL) throw new Error("Database URL not provided. Stop.");
if (!process.env.HOSTED_ZONE_ID) throw new Error("Hosted Zone ID not provided. Stop.");

let stage = "prod";

switch (process.env.NODE_ENV) {
case "production":
stage = "prod";
break;

case "staging":
if (!process.env.PR_NUM)
throw new Error("Running in staging environment but no PR number specified. Stop.");
stage = `staging-${process.env.PR_NUM}`;
break;

case "development":
throw new Error("Cannot deploy stack in development environment. Stop.");

default:
throw new Error("Invalid environment specified. Stop.");
}

super(scope, id, {
env: {
region: "us-east-1",
},
terminationProtection: /*stage === "prod"*/ false,
});

const certificateArn = process.env.CERTIFICATE_ARN;
const hostedZoneId = process.env.HOSTED_ZONE_ID ?? "hi";
const recordName = `${stage === "prod" ? "" : `${stage}-`}docs.api-next`;
const zoneName = "peterportal.org";

const workspaceRoot = getWorkspaceRoot(process.cwd());

this.staticSite = new StaticSite(this, "Docs", {
assets: path.join(workspaceRoot, "apps", "docs", "build"),
constructs: {
bucketProps() {
return {
bucketName: `${recordName}.${zoneName}`,
removalPolicy: aws_core.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
};
},
distributionProps(scope) {
return {
certificate: aws_certificatemanager.Certificate.fromCertificateArn(
scope,
"peterportal-cert",
certificateArn
),
defaultRootObject: "index.html",
domainNames: [`${recordName}.${zoneName}`],
defaultBehavior: {
origin: new aws_cloudfront_origins.S3Origin(scope.bucket, {
originAccessIdentity: scope.originAccessIdentity,
}),
allowedMethods: aws_cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
viewerProtocolPolicy: aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
},
errorResponses: [
{
httpStatus: 403,
responseHttpStatus: 200,
responsePagePath: "/index.html",
},
],
};
},
},
});

this.cloudFrontTarget = new aws_route53_targets.CloudFrontTarget(this.staticSite.distribution);

this.zone = aws_route53.HostedZone.fromHostedZoneAttributes(this, "peterportal-hosted-zone", {
zoneName,
hostedZoneId,
});

this.aRecord = new aws_route53.ARecord(this, `peterportal-api-next-docs-a-record-${stage}`, {
zone: this.zone,
recordName,
target: aws_route53.RecordTarget.fromAlias(this.cloudFrontTarget),
});
}
}

export default function main() {
const app = new App();
const app = new aws_core.App();

const myStack = new MyStack(app, "TestingPpaReleaseCandidateStack");
const myStack = new ApiStack(app, "TestingPpaReleaseCandidateStack");

const stack = new Stack(app, "GitHubStuff");
const stack = new DocsStack(app, "GitHubStuff");

new GitHub(stack, "GitHub", {
outputs: {
invokeUrl: {
apiUrl: {
value: myStack.api.api.urlForPath(),
},
docsUrl: {
value: stack.aRecord.domainName,
},
},
callbacks: {
async onPostDeploy(outputs) {
Expand All @@ -89,8 +202,16 @@ export default function main() {
environment: "staging - api",
});

if (apiDeployment.status !== 201) {
throw new Error("❌ Deployment failed!");
const docsDeployment = await octokit.rest.repos.createDeployment({
owner,
repo,
ref,
required_contexts: [],
environment: "staging - docs",
});

if (apiDeployment.status !== 201 || docsDeployment.status !== 201) {
throw new Error("❌ Creating deployments failed!");
}

const apiDeploymentStatus = await octokit.rest.repos.createDeploymentStatus({
Expand All @@ -99,7 +220,17 @@ export default function main() {
deployment_id: apiDeployment.data.id,
state: "success",
description: "Deployment succeeded",
environment_url: outputs.invokeUrl,
environment_url: outputs.apiUrl,
auto_inactive: false,
});

const docsDeploymentStatus = await octokit.rest.repos.createDeploymentStatus({
repo,
owner,
deployment_id: docsDeployment.data.id,
state: "success",
description: "Deployment succeeded",
environment_url: outputs.docsUrl,
auto_inactive: false,
});

Expand All @@ -113,6 +244,8 @@ export default function main() {
🚀 Staging instances deployed!
API - ${apiDeploymentStatus.data.environment_url}
Docs - ${docsDeploymentStatus.data.environment_url}
`,
});
},
Expand Down
12 changes: 7 additions & 5 deletions packages/ant-stack/src/cdk/constructs/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export interface DefaultApiRouteConfig extends Pick<ApiRouteConfig, "runtime" |
* Additional construct prop overrides are accessible only at the root.
*/
export interface RootApiConstructConfig {
restApiProps?: (scope: Construct, id: string) => RestApiProps;
restApiProps?: (scope: Api, id: string) => RestApiProps;
}

/**
Expand Down Expand Up @@ -134,13 +134,15 @@ export class Api extends Construct {
export async function getApi(initializedApp?: App): Promise<Api> {
const app = initializedApp ?? (await synthesizeConfig());

const stacks = app.node.children.find(Stack.isStack);
const stacks = app.node.children.filter(Stack.isStack);

if (!stacks) {
throw new Error(`No stacks found.`);
}

const api = stacks?.node.children.find(Api.isApi);
const stackWithApi = stacks.find((stack) => stack.node.children.some(Api.isApi));

const api = stackWithApi?.node.children.find(Api.isApi);

if (!api) {
throw new Error(`No ${Api.type} construct found.`);
Expand All @@ -152,8 +154,8 @@ export async function getApi(initializedApp?: App): Promise<Api> {
/**
* Get the API config with the current route at the highest priority (if it exists).
*/
export async function getApiRoute(directory: string = process.cwd()) {
const api = await getApi();
export async function getApiRoute(directory: string = process.cwd(), app?: App) {
const api = await getApi(app);

if (!api.routes[directory]) {
throw new Error(`No ${ApiRoute.type} found for directory: ${directory}`);
Expand Down
32 changes: 16 additions & 16 deletions packages/ant-stack/src/cdk/constructs/api/apiRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,15 @@ export interface ApiRouteRuntimeConfig {
*/
export interface ApiRouteConstructProps {
lambdaIntegrationOptions?: (
scope: Construct,
scope: ApiRoute,
id: string,
methodAndRoute: string
) => aws_apigateway.LambdaIntegrationOptions;

functionProps?: (scope: Construct, id: string) => aws_lambda.FunctionProps;
functionProps?: (scope: ApiRoute, id: string) => aws_lambda.FunctionProps;

methodOptions?: (
scope: Construct,
scope: ApiRoute,
id: string,
methodAndRoute: string
) => aws_apigateway.MethodOptions;
Expand Down Expand Up @@ -264,19 +264,19 @@ export class ApiRoute extends Construct {
.forEach((httpMethod) => {
const functionName = `${this.id}-${httpMethod}`.replace(/\//g, "-");

const functionProps: aws_lambda.FunctionProps = defu(
this.config.constructs.functionProps?.(this, this.id),
{
functionName,
runtime: aws_lambda.Runtime.NODEJS_18_X,
code: aws_lambda.Code.fromAsset(this.config.directory, { exclude: ["node_modules"] }),
handler: path.join(outDirectory, this.outFiles.node.replace(/.js$/, `.${httpMethod}`)),
architecture: aws_lambda.Architecture.ARM_64,
environment: { ...this.config.runtime.environment },
timeout: aws_core.Duration.seconds(15),
memorySize: 512,
}
);
const functionProps: aws_lambda.FunctionProps = this.config.constructs.functionProps?.(
this,
this.id
) ?? {
functionName,
runtime: aws_lambda.Runtime.NODEJS_18_X,
code: aws_lambda.Code.fromAsset(this.config.directory, { exclude: ["node_modules"] }),
handler: path.join(outDirectory, this.outFiles.node.replace(/.js$/, `.${httpMethod}`)),
architecture: aws_lambda.Architecture.ARM_64,
environment: { ...this.config.runtime.environment },
timeout: aws_core.Duration.seconds(15),
memorySize: 512,
};

const handler = new aws_lambda.Function(
this,
Expand Down
34 changes: 16 additions & 18 deletions packages/ant-stack/src/cdk/constructs/staticSite/staticSite.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import aws_cloudfront from "aws-cdk-lib/aws-cloudfront";
import aws_cloudfront_origins from "aws-cdk-lib/aws-cloudfront-origins";
import aws_iam from "aws-cdk-lib/aws-iam";
import aws_s3 from "aws-cdk-lib/aws-s3";
import aws_s3_deployment from "aws-cdk-lib/aws-s3-deployment";
import * as aws_cloudfront from "aws-cdk-lib/aws-cloudfront";
import * as aws_cloudfront_origins from "aws-cdk-lib/aws-cloudfront-origins";
import * as aws_iam from "aws-cdk-lib/aws-iam";
import * as aws_s3 from "aws-cdk-lib/aws-s3";
import * as aws_s3_deployment from "aws-cdk-lib/aws-s3-deployment";
import { Construct } from "constructs";
import { defu } from "defu";

export interface StaticSiteProps {
/**
Expand All @@ -15,21 +14,21 @@ export interface StaticSiteProps {
/**
* Override props passed to constructs.
*/
constructs?: StaticSiteConstructProps;
constructs?: Partial<StaticSiteConstructProps>;
}

export interface StaticSiteConstructProps {
bucketProps?: (scope: Construct, id: string) => aws_s3.BucketProps;
bucketProps: (scope: StaticSite, id: string) => aws_s3.BucketProps;

oaiProps?: (scope: Construct, id: string) => aws_cloudfront.OriginAccessIdentityProps;
oaiProps: (scope: StaticSite, id: string) => aws_cloudfront.OriginAccessIdentityProps;

policyStatementProps?: (scope: Construct, id: string) => aws_iam.PolicyStatementProps;
policyStatementProps: (scope: StaticSite, id: string) => aws_iam.PolicyStatementProps;

originProps?: (scope: Construct, id: string) => aws_cloudfront_origins.S3OriginProps;
originProps: (scope: StaticSite, id: string) => aws_cloudfront_origins.S3OriginProps;

distributionProps?: (scope: Construct, id: string) => aws_cloudfront.DistributionProps;
distributionProps: (scope: StaticSite, id: string) => aws_cloudfront.DistributionProps;

bucketDeploymentProps?: (scope: Construct, id: string) => aws_s3_deployment.BucketDeploymentProps;
bucketDeploymentProps: (scope: StaticSite, id: string) => aws_s3_deployment.BucketDeploymentProps;
}

export class StaticSite extends Construct {
Expand Down Expand Up @@ -75,7 +74,7 @@ export class StaticSite extends Construct {
};

this.policyStatement = new aws_iam.PolicyStatement(
defu(policyStatementProps, defaultPolicyStatementProps)
policyStatementProps ?? defaultPolicyStatementProps
);

this.bucket.addToResourcePolicy(this.policyStatement);
Expand All @@ -88,13 +87,12 @@ export class StaticSite extends Construct {

this.origin = new aws_cloudfront_origins.S3Origin(
this.bucket,
defu(originProps, defaultOriginProps)
originProps ?? defaultOriginProps
);

const distributionProps = props.constructs?.distributionProps?.(this, id);

const defaultDistributionProps: aws_cloudfront.DistributionProps = {
// certificate: this.certificate --> user should figure this out,
defaultRootObject: "index.html",
defaultBehavior: {
origin: this.origin,
Expand All @@ -106,7 +104,7 @@ export class StaticSite extends Construct {
this.distribution = new aws_cloudfront.Distribution(
this,
`${id}-distribution`,
defu(distributionProps, defaultDistributionProps)
distributionProps ?? defaultDistributionProps
);

const bucketDeploymentProps = props.constructs?.bucketDeploymentProps?.(this, id);
Expand All @@ -121,7 +119,7 @@ export class StaticSite extends Construct {
this.bucketDeployment = new aws_s3_deployment.BucketDeployment(
this,
`${id}-bucket-deployment`,
defu(bucketDeploymentProps, defaultBucketDeploymentProps)
bucketDeploymentProps ?? defaultBucketDeploymentProps
);
}
}
Loading

0 comments on commit 8beb5a4

Please sign in to comment.