Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -174,17 +174,15 @@ data "aws_iam_policy_document" "cloudformation_permission_assume_role" {
}

resource "aws_iam_policy" "cloudformation_permission" {
name = "cloudformation-control"
path = "/${var.deployment_name}/"
name = "${var.deployment_name}_cf-control"
description = "Managed by Terraform Next.js"
policy = data.aws_iam_policy_document.cloudformation_permission.json

tags = var.tags
}

resource "aws_iam_role" "cloudformation_permission" {
name = "cloudformation-control"
path = "/${var.deployment_name}/"
name = "${var.deployment_name}_cf-control"
assume_role_policy = data.aws_iam_policy_document.cloudformation_permission_assume_role.json
managed_policy_arns = [
aws_iam_policy.cloudformation_permission.arn
Expand Down Expand Up @@ -227,11 +225,16 @@ module "statics_deploy" {
deploy_status_sns_topic_arn = module.deploy_controller.sns_topic_arn

dynamodb_region = data.aws_region.current.name
dynamodb_table_aliases_arn = aws_dynamodb_table.aliases.arn
dynamodb_table_aliases_name = aws_dynamodb_table.aliases.id
dynamodb_table_deployments_arn = aws_dynamodb_table.deployments.arn
dynamodb_table_deployments_name = aws_dynamodb_table.deployments.id

cloudformation_role_arn = aws_iam_role.cloudformation_permission.arn

enable_multiple_deployments = var.enable_multiple_deployments
multiple_deployments_base_domain = var.multiple_deployments_base_domain

lambda_role_permissions_boundary = var.lambda_role_permissions_boundary

deployment_name = var.deployment_name
Expand Down
3 changes: 1 addition & 2 deletions modules/api/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,7 @@ data "aws_iam_policy_document" "access_api" {
}

resource "aws_iam_policy" "access_api" {
name = "api-access"
path = "/${var.deployment_name}/"
name = "${var.deployment_name}_api-access"

description = "Managed by Terraform Next.js"

Expand Down
8 changes: 7 additions & 1 deletion modules/statics-deploy/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,10 @@ data "aws_iam_policy_document" "access_dynamodb_table_deployments" {
"dynamodb:PutItem",
"dynamodb:UpdateItem"
]
resources = [var.dynamodb_table_deployments_arn]
resources = [
var.dynamodb_table_aliases_arn,
var.dynamodb_table_deployments_arn
]
}
}

Expand Down Expand Up @@ -234,8 +237,11 @@ module "deploy_trigger" {
SQS_QUEUE_URL = aws_sqs_queue.this.id
DEPLOY_STATUS_SNS_ARN = var.deploy_status_sns_topic_arn
TABLE_REGION = var.dynamodb_region
TABLE_NAME_ALIASES = var.dynamodb_table_aliases_name
TABLE_NAME_DEPLOYMENTS = var.dynamodb_table_deployments_name
CLOUDFORMATION_ROLE_ARN = var.cloudformation_role_arn
# Remove the * from the base domain (e.g. *.example.com -> .example.com)
MULTI_DEPLOYMENTS_BASE_DOMAIN = var.enable_multiple_deployments ? replace(var.multiple_deployments_base_domain, "/^\\*/", "") : null
}

event_source_mapping = {
Expand Down
21 changes: 21 additions & 0 deletions modules/statics-deploy/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ variable "cloudformation_role_arn" {
type = string
}

######################
# Multiple deployments
######################

variable "enable_multiple_deployments" {
type = bool
}

variable "multiple_deployments_base_domain" {
type = string
default = null
}

#####################
# Deployment database
#####################
Expand All @@ -35,6 +48,14 @@ variable "dynamodb_region" {
type = string
}

variable "dynamodb_table_aliases_arn" {
type = string
}

variable "dynamodb_table_aliases_name" {
type = string
}

variable "dynamodb_table_deployments_arn" {
type = string
}
Expand Down
2 changes: 2 additions & 0 deletions packages/deploy-trigger/src/declarations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ declare namespace NodeJS {
DEPLOY_STATUS_SNS_ARN: string;
TABLE_REGION: string;
TABLE_NAME_DEPLOYMENTS: string;
TABLE_NAME_ALIASES: string;
CLOUDFORMATION_ROLE_ARN: string;
MULTI_DEPLOYMENTS_BASE_DOMAIN?: string;
}
}
103 changes: 71 additions & 32 deletions packages/deploy-trigger/src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import {
updateDeploymentStatusCreateInProgress,
getDeploymentById,
updateDeploymentStatusCreateFailed,
updateDeploymentStatusFinished,
reverseHostname,
createAlias,
} from '@millihq/tfn-dynamodb-actions';
import { S3Event, S3EventRecord, SQSEvent, SQSRecord } from 'aws-lambda';
import CloudFront from 'aws-sdk/clients/cloudfront';
Expand Down Expand Up @@ -136,6 +139,7 @@ export const handler = async function (event: S3Event | SQSEvent) {
async function s3Handler(Record: S3EventRecord) {
const dynamoDBRegion = ensureEnv('TABLE_REGION');
const dynamoDBTableNameDeployments = ensureEnv('TABLE_NAME_DEPLOYMENTS');
const dynamoDBTableNameAliases = ensureEnv('TABLE_NAME_ALIASES');

const dynamoDBClient = new DynamoDB({
region: dynamoDBRegion,
Expand Down Expand Up @@ -186,46 +190,81 @@ async function s3Handler(Record: S3EventRecord) {
// TODO: Cleanup extracted files from S3
}

// Create the stack
const atomicDeployment =
deployment.DeploymentTemplate === 'API_GATEWAY'
? new AtomicDeploymentAPIGateway({
deploymentId,
deploymentBucketId: deployBucket,
lambdas: lambdas,
})
: new AtomicDeploymentFunctionUrls({
deploymentId,
deploymentBucketId: deployBucket,
lambdas: lambdas,
});

try {
const stackName = `tfn-${deploymentId}`;
const { stackARN } = await createCloudFormationStack({
notificationARNs: [process.env.DEPLOY_STATUS_SNS_ARN],
stack: atomicDeployment,
// Stackname has to match [a-zA-Z][-a-zA-Z0-9]*
stackName,
cloudFormationRoleArn: process.env.CLOUDFORMATION_ROLE_ARN,
});
// Static deployment, doesn't need a CloudFormation template
if (lambdas.length === 0) {
const lambdaRoutes = '{}';
const routes = JSON.stringify(deploymentConfig.routes);
const prerenders = JSON.stringify(deploymentConfig.prerenders);

// TODO: Move this to the deployment controller
await updateDeploymentStatusCreateInProgress({
// TODO: Handle case when multi deployments is not enabled
const deploymentAliasBasePath = '/';
const deploymentAliasHostname =
deploymentId + process.env.MULTI_DEPLOYMENTS_BASE_DOMAIN;
const deploymentAliasHostnameRev = reverseHostname(deploymentAliasHostname);
await createAlias({
dynamoDBClient,
deploymentTableName: dynamoDBTableNameDeployments,
hostnameRev: deploymentAliasHostnameRev,
isDeploymentAlias: true,
aliasTableName: dynamoDBTableNameAliases,
createDate: new Date(),
deploymentId,
routes: JSON.stringify(deploymentConfig.routes),
prerenders: JSON.stringify(deploymentConfig.prerenders),
cloudFormationStack: stackARN,
lambdaRoutes,
routes,
prerenders,
basePath: deploymentAliasBasePath,
});
} catch (error) {
console.error(error);
await updateDeploymentStatusCreateFailed({

await updateDeploymentStatusFinished({
dynamoDBClient,
deploymentTableName: dynamoDBTableNameDeployments,
deploymentId,
routes,
prerenders,
lambdaRoutes,
deploymentAlias: deploymentAliasHostname + deploymentAliasBasePath,
});
} else {
// Create the CloudFormation stack for the lambdas
const atomicDeployment =
deployment.DeploymentTemplate === 'API_GATEWAY'
? new AtomicDeploymentAPIGateway({
deploymentId,
deploymentBucketId: deployBucket,
lambdas: lambdas,
})
: new AtomicDeploymentFunctionUrls({
deploymentId,
deploymentBucketId: deployBucket,
lambdas: lambdas,
});

try {
const stackName = `tfn-${deploymentId}`;
const { stackARN } = await createCloudFormationStack({
notificationARNs: [process.env.DEPLOY_STATUS_SNS_ARN],
stack: atomicDeployment,
// Stackname has to match [a-zA-Z][-a-zA-Z0-9]*
stackName,
cloudFormationRoleArn: process.env.CLOUDFORMATION_ROLE_ARN,
});

// TODO: Move this to the deployment controller
await updateDeploymentStatusCreateInProgress({
dynamoDBClient,
deploymentTableName: dynamoDBTableNameDeployments,
deploymentId,
routes: JSON.stringify(deploymentConfig.routes),
prerenders: JSON.stringify(deploymentConfig.prerenders),
cloudFormationStack: stackARN,
});
} catch (error) {
console.error(error);
await updateDeploymentStatusCreateFailed({
dynamoDBClient,
deploymentTableName: dynamoDBTableNameDeployments,
deploymentId,
});
}
}

// Update the manifest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,23 @@ type UpdateDeploymentStatusFinishedOptions = {
* The alias that is assigned with this deployment.
*/
deploymentAlias?: string;
/**
* Only used for static deployments.
* Stringified JSON object that contains routes that are served from
* prerendered generated HTML files.
*
*/
prerenders?: string;
/**
* Only used for static deployments.
* Stringified JSON object that contains the route config.
*/
routes?: string;
/**
* Only used for static deployments.
* Stringified routing table for Lambdas
*/
lambdaRoutes?: string;
};

/**
Expand All @@ -32,6 +49,9 @@ function updateDeploymentStatusFinished({
deploymentTableName,
deploymentId,
deploymentAlias,
prerenders,
routes,
lambdaRoutes,
}: UpdateDeploymentStatusFinishedOptions) {
return updateDeployment({
dynamoDBClient,
Expand All @@ -40,6 +60,9 @@ function updateDeploymentStatusFinished({
updateAttributes: {
DeploymentAlias: deploymentAlias,
Status: 'FINISHED',
LambdaRoutes: lambdaRoutes,
Prerenders: prerenders,
Routes: routes,
},
});
}
Expand Down
5 changes: 3 additions & 2 deletions packages/proxy-config/src/actions/deployment-file-exists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,21 @@ async function deploymentFileExists({
}

const decodedKey = decodeURIComponent(key);
const absoluteKey = deploymentId + '/static/' + decodedKey;

try {
const result = await s3Client
.headObject({
Bucket: s3BucketId,
Key: deploymentId + '/static/' + decodedKey,
Key: absoluteKey,
})
.promise();

return {
status: '200',
body: JSON.stringify({
status: 200,
key: decodedKey,
key: absoluteKey,
cacheControl: result.CacheControl,
contentType: result.ContentType,
}),
Expand Down
4 changes: 1 addition & 3 deletions packages/proxy/src/actions/fetch-file-system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ function fetchFileSystem(
deploymentId: string,
filePath: string
): Promise<FileSystemEntry | null> {
const url = `${endpointUrl}/filesystem/${deploymentId}/${encodeURIComponent(
filePath
)}`;
const url = `${endpointUrl}/filesystem/${deploymentId}/${filePath}`;
const cacheKey = deploymentId + filePath;
return fetchCached(fetch, cache, url, cacheKey);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/proxy/src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ async function handler(
const notFound =
proxyResult.phase === 'error' && proxyResult.status === 404;
const uri = !notFound && proxyResult.found ? proxyResult.dest : undefined;
return serveRequestFromS3Origin(request, proxyConfig.deploymentId, uri);
return serveRequestFromS3Origin(request, uri);
} catch (error: any) {
if (!error.isHandled) {
// Log full error message to CloudWatch Logs
Expand Down
2 changes: 1 addition & 1 deletion packages/proxy/src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export class Proxy {
);

if (file) {
return requestedFilePathWithoutTrailingSlash;
return '/' + file.key;
}
} catch (error) {
console.error(
Expand Down
1 change: 1 addition & 0 deletions packages/proxy/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,5 @@ export interface RouteResult {

export type FileSystemEntry = {
etag: string;
key: string;
};
4 changes: 1 addition & 3 deletions packages/proxy/src/util/custom-origin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,10 @@ function serveRequestFromCustomOrigin(
*
* @param request Incoming request from the handler. Gets modified by the
* function.
* @param deploymentId DeploymentId that should be used to serve the content.
* @returns Modified request that is served from S3.
*/
function serveRequestFromS3Origin(
request: CloudFrontRequest,
deploymentId: string,
uri?: string
): CloudFrontRequest {
// Modify `Host` header to match the S3 host. If the `Host` header is
Expand All @@ -141,7 +139,7 @@ function serveRequestFromS3Origin(
});

if (typeof uri === 'string') {
request.uri = `/${deploymentId}/static${uri}`;
request.uri = uri;
}

// Querystring is not supported by S3 origin
Expand Down
Loading