Skip to content
This repository was archived by the owner on Jun 28, 2022. It is now read-only.

Commit

Permalink
feat: improve acm and ttls
Browse files Browse the repository at this point in the history
  • Loading branch information
arantespp committed Nov 15, 2020
1 parent f7e099d commit 19671da
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 54 deletions.
21 changes: 6 additions & 15 deletions packages/cli/src/deploy/staticApp/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,8 @@ export const deployStaticAppCommand: CommandModule = {
yargs
.options({
'acm-arn': {
conflicts: 'acmArnExportedName',
describe:
'The ARN of the certificate that will be associated to CloudFront.',
type: 'string',
},
'acm-arn-exported-name': {
conflicts: 'acmArn',
describe:
'The exported name of the ARN value of the ACM if it was created via CloudFormation.',
'The ARN of the certificate or the name of the exported variable whose value is the ARN of the certificate that will be associated to CloudFront.',
type: 'string',
},
aliases: {
Expand Down Expand Up @@ -59,16 +52,14 @@ export const deployStaticAppCommand: CommandModule = {
},
})
.middleware((argv) => {
const { acmArn, acmArnExportedName, aliases, spa } = argv;
if (acmArn || acmArnExportedName || aliases || spa) {
const { acmArn, aliases, spa } = argv;
if (acmArn || aliases || spa) {
argv.cloudfront = true;
}
})
.check(({ aliases, acmArnExportedName, acmArn }) => {
if (aliases && !(acmArn || acmArnExportedName)) {
throw new Error(
'"alias" is defined but "acm-arn" or "acm-arn-exported-name" is not.',
);
.check(({ aliases, acmArn }) => {
if (aliases && !acmArn) {
throw new Error('"acm-arn" must be defined when "alias" is defined.');
} else {
return true;
}
Expand Down
82 changes: 50 additions & 32 deletions packages/cli/src/deploy/staticApp/staticApp.template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,28 +87,22 @@ const defaultScp = [
*/
export const getLambdaEdgeOriginResponseZipFile = ({
scp = defaultScp,
spa,
}: {
scp?: string[];
spa: boolean;
}) => `
exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
const response = event.Records[0].cf.response;
const headers = response.headers;
const isSpa = ${spa};
const cacheRegex = new RegExp('${originCacheExpression}');
const cacheControlValue = isSpa || cacheRegex.test(request.uri)
? 'public, max-age=31536000, immutable'
: 'public, max-age=0, must-revalidate';
const maxAge = cacheRegex.test(request.uri) ? 60 * 60 * 24 * 365 : 60;
headers['cache-control'] = [
{
key: 'Cache-Control',
value: cacheControlValue
value: \`public, max-age=\${maxAge}, immutable\`
}
];
headers['strict-transport-security'] = [
Expand Down Expand Up @@ -152,7 +146,13 @@ exports.handler = (event, context, callback) => {
};
`;

const getBaseTemplate = (): CloudFormationTemplate => {
const getBaseTemplate = (
{
cloudfront,
}: {
cloudfront?: boolean;
} = { cloudfront: false },
): CloudFormationTemplate => {
return {
AWSTemplateFormatVersion: '2010-09-09',
Resources: {
Expand Down Expand Up @@ -195,7 +195,23 @@ const getBaseTemplate = (): CloudFormationTemplate => {
],
],
},
Principal: '*',
Principal: cloudfront
? /**
* https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html
*/
{
AWS: {
'Fn::Sub': [
'arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${OAI}',
{
OAI: {
Ref: CLOUDFRONT_DISTRIBUTION_ORIGIN_ACCESS_IDENTITY_LOGICAL_ID,
},
},
],
},
}
: '*',
},
],
},
Expand Down Expand Up @@ -307,7 +323,7 @@ const getCloudFrontEdgeLambdas = ({
[LAMBDA_EDGE_ORIGIN_RESPONSE_LOGICAL_ID]: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: { ZipFile: getLambdaEdgeOriginResponseZipFile({ scp, spa }) },
Code: { ZipFile: getLambdaEdgeOriginResponseZipFile({ scp }) },
Description: 'Lambda@Edge function serving as origin response.',
Handler: 'index.handler',
MemorySize: 128,
Expand Down Expand Up @@ -369,20 +385,18 @@ const getCloudFrontEdgeLambdas = ({

const getCloudFrontTemplate = ({
acmArn,
acmArnExportedName,
aliases,
scp,
spa = false,
hostedZoneName,
}: {
acmArn?: string;
acmArnExportedName?: string;
aliases?: string[];
scp?: string[];
spa?: boolean;
hostedZoneName?: string;
}): CloudFormationTemplate => {
const template = { ...getBaseTemplate() };
const template = { ...getBaseTemplate({ cloudfront: true }) };

const cloudFrontResources: { [key: string]: Resource } = {
...getCloudFrontEdgeLambdas({ scp, spa }),
Expand Down Expand Up @@ -430,9 +444,18 @@ const getCloudFrontTemplate = ({
AllowedMethods: ['GET', 'HEAD', 'OPTIONS'],
Compress: true,
/**
* https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html#DownloadDistValuesDefaultTTL
* How MinTTL, MaxTTL and DefaultTTL work together with Cache Control header.
* https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Expiration.html#ExpirationDownloadDist
*
* Returns MinTTL, MaxTTL and DefaultTTL.
*/
DefaultTTL: 60 * 60 * 24 * 365, // one year
...(() => {
const ttl = 60 * 60 * 24 * 365; // One year
return {
MinTTL: ttl,
DefaultTTL: ttl,
};
})(),
ForwardedValues: {
QueryString: true,
},
Expand Down Expand Up @@ -490,7 +513,7 @@ const getCloudFrontTemplate = ({
},
};

if (acmArn || acmArnExportedName) {
if (acmArn) {
/**
* Add ACM to CloudFront template.
*/
Expand All @@ -499,9 +522,13 @@ const getCloudFrontTemplate = ({
.DistributionConfig,
Aliases: aliases || { Ref: 'AWS::NoValue' },
ViewerCertificate: {
AcmCertificateArn: acmArn || {
'Fn::ImportValue': acmArnExportedName,
},
AcmCertificateArn: /^arn:aws:acm:[-a-z0-9]+:\d{12}:certificate\/[-a-z0-9]+$/.test(
acmArn,
)
? acmArn
: {
'Fn::ImportValue': acmArn,
},
SslSupportMethod: 'sni-only',
},
};
Expand All @@ -518,7 +545,7 @@ const getCloudFrontTemplate = ({
'Fn::GetAtt': `${CLOUDFRONT_DISTRIBUTION_LOGICAL_ID}.DomainName`,
},
],
TTL: `${60 * 60 * 12}`, // 12 hours.
TTL: `${60 * 60 * 24}`, // 24 hours.
Type: 'CNAME',
}));

Expand Down Expand Up @@ -588,30 +615,21 @@ const getCloudFrontTemplate = ({

export const getStaticAppTemplate = ({
acmArn,
acmArnExportedName,
aliases,
cloudfront,
scp,
spa,
hostedZoneName,
}: {
acmArn?: string;
acmArnExportedName?: string;
aliases?: string[];
cloudfront: boolean;
scp?: string[];
spa: boolean;
hostedZoneName?: string;
}): CloudFormationTemplate => {
if (cloudfront) {
return getCloudFrontTemplate({
acmArn,
acmArnExportedName,
aliases,
scp,
spa,
hostedZoneName,
});
return getCloudFrontTemplate({ acmArn, aliases, scp, spa, hostedZoneName });
}
return getBaseTemplate();
};
9 changes: 2 additions & 7 deletions packages/cli/src/deploy/staticApp/staticApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,11 @@ export const invalidateCloudFront = async ({
* for instance, only S3, SPA...
* 3. Create AWS resources using the templated created.
* 4. Upload static files to the host bucket S3.
* 5. If is a SPA and has CloudFront, an CloudFront invalidation will be
* 5. If is a CloudFront deployment, an CloudFront invalidation will be
* created.
*/
export const deployStaticApp = async ({
acmArn,
acmArnExportedName,
aliases,
buildFolder,
cloudfront,
Expand All @@ -111,7 +110,6 @@ export const deployStaticApp = async ({
hostedZoneName,
}: {
acmArn?: string;
acmArnExportedName?: string;
aliases?: string[];
buildFolder: string;
cloudfront: boolean;
Expand All @@ -129,7 +127,6 @@ export const deployStaticApp = async ({

const template = getStaticAppTemplate({
acmArn,
acmArnExportedName,
aliases,
cloudfront,
scp,
Expand All @@ -147,9 +144,7 @@ export const deployStaticApp = async ({

await uploadBuiltAppToS3({ buildFolder, bucket });

if (spa) {
await invalidateCloudFront({ outputs: Outputs });
}
await invalidateCloudFront({ outputs: Outputs });
} catch (err) {
log.error(logPrefix, 'An error occurred. Cannot deploy static app');
log.error(logPrefix, 'Error message: %j', err.message);
Expand Down

0 comments on commit 19671da

Please sign in to comment.