diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..2a3043b --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "bracketSpacing": false, + "jsxBracketSameLine": true +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c104a19..90a3dbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,41 @@ # Changelog -## [0.9.5] - 2019-02-21 +## [0.0.6] - 2022-05-04 + +### Fixed + +- serverless version of peerDependencies + +### Updated + +- Throw exception when the lambda parameter is not set. + +## [0.0.5] - 2022-05-01 + +### Fixed + +- add peerDependencies + +### Updated + +- README + +## [0.0.2] - 2022-04-21 + +### Fixed + +- OriginPath should be '' + +### Fixed + +- Support for serverless v3 + +## [0.0.1] - 2022-04-21 + ### Added -- .gitignore file [#23](https://github.com/Droplr/serverless-api-cloudfront/pull/23) -- MinimumProtocolVersion [#25](https://github.com/Droplr/serverless-api-cloudfront/pull/25) + +- for lambda url + ### Fixed -- Missing bound in dependencies [#24](https://github.com/Droplr/serverless-api-cloudfront/pull/24) -- PriceClass documentation [#14](https://github.com/Droplr/serverless-api-cloudfront/pull/14) -- Incorrect node version [#26](https://github.com/Droplr/serverless-api-cloudfront/pull/26) -- Headers documentation [#27](https://github.com/Droplr/serverless-api-cloudfront/pull/27) + +- Support for serverless v3 diff --git a/README.md b/README.md index 9734c03..01689ff 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,18 @@ -# serverless-api-cloudfront +# serverless-lambda-cloudfront [![serverless](http://public.serverless.com/badges/v3.svg)](http://www.serverless.com) -[![npm version](https://badge.fury.io/js/serverless-api-cloudfront.svg)](https://badge.fury.io/js/serverless-api-cloudfront) -[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/Droplr/serverless-api-cloudfront/master/LICENSE) -[![npm downloads](https://img.shields.io/npm/dt/serverless-api-cloudfront.svg?style=flat)](https://www.npmjs.com/package/serverless-api-cloudfront) +[![npm version](https://badge.fury.io/js/serverless-lambda-cloudfront.svg)](https://badge.fury.io/js/serverless-lambda-cloudfront) +[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/serverless-lambda-cloudfront/master/LICENSE) +[![npm downloads](https://img.shields.io/npm/dt/serverless-lambda-cloudfront.svg?style=flat)](https://www.npmjs.com/package/serverless-lambda-cloudfront) Automatically creates properly configured AWS CloudFront distribution that routes traffic -to API Gateway. - -Due to limitations of API Gateway Custom Domains, we realized that setting self-managed CloudFront distribution is much more powerful. +to Lambda Url. **:zap: Pros** -- Allows you to set-up custom domain for your API Gateway -- Enables CDN caching of resources - so you don't waste Lambda invocations or API Gateway traffic +- Allows you to set-up custom domain for your Lambda Url + - [Lambda Url](https://www.serverless.com/blog/aws-lambda-function-urls-with-serverless-framework) +- Enables CDN caching of resources - so you don't waste Lambda invocations for serving static files (just set proper Cache-Control in API responses) - Much more CloudWatch statistics of API usage (like bandwidth metrics) - Real world [access log](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/AccessLogs.html) - out of the box, API Gateway currently does not provide any kind of real "apache-like" access logs for your invocations @@ -22,27 +21,28 @@ Due to limitations of API Gateway Custom Domains, we realized that setting self- ## Installation ``` -$ npm install --save-dev serverless-api-cloudfront +$ npm install --save-dev serverless-lambda-cloudfront ``` ## Configuration -* All apiCloudFront configuration parameters are optional - e.g. don't provide ACM Certificate ARN +- All lambdaCloudFront configuration parameters are optional - e.g. don't provide ACM Certificate ARN to use default CloudFront certificate (which works only for default cloudfront.net domain). -* This plugin **does not** set-up automatically Route53 for newly created CloudFront distribution. +- This plugin **does not** set-up automatically Route53 for newly created CloudFront distribution. After creating CloudFront distribution, manually add Route53 ALIAS record pointing to your CloudFront domain name. -* First deployment may be quite long (e.g. 10 min) as Serverless is waiting for +- First deployment may be quite long (e.g. 10 min) as Serverless is waiting for CloudFormation to deploy CloudFront distribution. ``` # add in your serverless.yml plugins: - - serverless-api-cloudfront + - serverless-lambda-cloudfront custom: - apiCloudFront: + lambdaCloudFront: + lambda: myFunction domain: my-custom-domain.com certificate: arn:aws:acm:us-east-1:000000000000:certificate/00000000-1111-2222-3333-444444444444 waf: 00000000-0000-0000-0000-000000000000 @@ -58,11 +58,19 @@ custom: - per_page priceClass: PriceClass_100 minimumProtocolVersion: TLSv1 + +... + +functions: + myFunction: + url: true + ... + ``` ### Notes -* `domain` can be list, so if you want to add more domains, instead string you list multiple ones: +- `domain` can be list, so if you want to add more domains, instead string you list multiple ones: ``` domain: @@ -70,14 +78,15 @@ domain: - secondary-custom-domain.com ``` -* `cookies` can be *all* (default), *none* or a list that lists the cookies to whitelist +- `cookies` can be _all_ (default), _none_ or a list that lists the cookies to whitelist + ``` cookies: - FirstCookieName - SecondCookieName ``` -* [`headers`][headers-default-cache] can be *all*, *none* (default) or a list of headers ([see CloudFront custom behaviour][headers-list]): +- [`headers`][headers-default-cache] can be _all_, _none_ (default) or a list of headers ([see CloudFront custom behaviour][headers-list]): ``` headers: all @@ -86,14 +95,13 @@ headers: all [headers-default-cache]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-defaultcachebehavior.html#cfn-cloudfront-distribution-defaultcachebehavior-forwardedvalues [headers-list]: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/RequestAndResponseBehaviorCustomOrigin.html#request-custom-headers-behavior -* `querystring` can be *all* (default), *none* or a list, in which case all querystring parameters are forwarded, but cache is based on the list: +- `querystring` can be _all_ (default), _none_ or a list, in which case all querystring parameters are forwarded, but cache is based on the list: ``` querystring: all ``` -* [`priceClass`][price-class] can be `PriceClass_All` (default), `PriceClass_100` or `PriceClass_200`: - +- [`priceClass`][price-class] can be `PriceClass_All` (default), `PriceClass_100` or `PriceClass_200`: ``` priceClass: PriceClass_All @@ -101,7 +109,7 @@ priceClass: PriceClass_All [price-class]: https://docs.aws.amazon.com/cloudfront/latest/APIReference/API_GetDistributionConfig.html#cloudfront-GetDistributionConfig-response-PriceClass -* [`minimumProtocolVersion`][minimum-protocol-version] can be `TLSv1` (default), `TLSv1_2016`, `TLSv1.1_2016`, `TLSv1.2_2018` or `SSLv3`: +- [`minimumProtocolVersion`][minimum-protocol-version] can be `TLSv1` (default), `TLSv1_2016`, `TLSv1.1_2016`, `TLSv1.2_2018` or `SSLv3`: ``` minimumProtocolVersion: TLSv1 @@ -109,21 +117,16 @@ minimumProtocolVersion: TLSv1 [minimum-protocol-version]: https://docs.aws.amazon.com/cloudfront/latest/APIReference/API_ViewerCertificate.html#cloudfront-Type-ViewerCertificate-MinimumProtocolVersion - ### IAM Policy In order to make this plugin work as expected a few additional IAM Policies might be needed on your AWS profile. More specifically this plugin needs the following policies attached: -* `cloudfront:CreateDistribution` -* `cloudfront:GetDistribution` -* `cloudfront:UpdateDistribution` -* `cloudfront:DeleteDistribution` -* `cloudfront:TagResource` +- `cloudfront:CreateDistribution` +- `cloudfront:GetDistribution` +- `cloudfront:UpdateDistribution` +- `cloudfront:DeleteDistribution` +- `cloudfront:TagResource` You can read more about IAM profiles and policies in the [Serverless documentation](https://serverless.com/framework/docs/providers/aws/guide/credentials#creating-aws-access-keys). - -## Error troubleshooting - -* Make sure you have at least one http event otherwise you'll get ```The CloudFormation template is invalid: Template format error: Unresolved resource dependencies [ApiGatewayRestApi] in the Resources block of the template``` diff --git a/index.js b/index.js index f010acf..02b5be6 100644 --- a/index.js +++ b/index.js @@ -4,24 +4,28 @@ const chalk = require('chalk'); const yaml = require('js-yaml'); const fs = require('fs'); -class ServerlessApiCloudFrontPlugin { +const TAG = '[serverless-lambda-cloudfront]'; + +class ServerlessLambdaCloudFrontPlugin { constructor(serverless, options) { this.serverless = serverless; this.options = options; this.hooks = { - 'before:deploy:createDeploymentArtifacts': this.createDeploymentArtifacts.bind(this), + 'before:package:createDeploymentArtifacts': + this.createDeploymentArtifacts.bind(this), 'aws:info:displayStackOutputs': this.printSummary.bind(this), }; } createDeploymentArtifacts() { - const baseResources = this.serverless.service.provider.compiledCloudFormationTemplate; + const baseResources = + this.serverless.service.provider.compiledCloudFormationTemplate; const filename = path.resolve(__dirname, 'resources.yml'); const content = fs.readFileSync(filename, 'utf-8'); - const resources = yaml.safeLoad(content, { - filename: filename + const resources = yaml.load(content, { + filename: filename, }); this.prepareResources(resources); @@ -31,9 +35,12 @@ class ServerlessApiCloudFrontPlugin { printSummary() { const cloudTemplate = this.serverless; - const awsInfo = _.find(this.serverless.pluginManager.getPlugins(), (plugin) => { - return plugin.constructor.name === 'AwsInfo'; - }); + const awsInfo = _.find( + this.serverless.pluginManager.getPlugins(), + (plugin) => { + return plugin.constructor.name === 'AwsInfo'; + }, + ); if (!awsInfo || !awsInfo.gatheredData) { return; @@ -45,17 +52,20 @@ class ServerlessApiCloudFrontPlugin { }); if (!apiDistributionDomain || !apiDistributionDomain.OutputValue) { - return ; + return; } const cnameDomain = this.getConfig('domain', '-'); this.serverless.cli.consoleLog(chalk.yellow('CloudFront domain name')); - this.serverless.cli.consoleLog(` ${apiDistributionDomain.OutputValue} (CNAME: ${cnameDomain})`); + this.serverless.cli.consoleLog( + ` ${apiDistributionDomain.OutputValue} (CNAME: ${cnameDomain})`, + ); } prepareResources(resources) { - const distributionConfig = resources.Resources.ApiDistribution.Properties.DistributionConfig; + const distributionConfig = + resources.Resources.ApiDistribution.Properties.DistributionConfig; this.prepareLogging(distributionConfig); this.prepareDomain(distributionConfig); @@ -77,7 +87,6 @@ class ServerlessApiCloudFrontPlugin { if (loggingBucket !== null) { distributionConfig.Logging.Bucket = loggingBucket; distributionConfig.Logging.Prefix = this.getConfig('logging.prefix', ''); - } else { delete distributionConfig.Logging; } @@ -87,7 +96,7 @@ class ServerlessApiCloudFrontPlugin { const domain = this.getConfig('domain', null); if (domain !== null) { - distributionConfig.Aliases = Array.isArray(domain) ? domain : [ domain ]; + distributionConfig.Aliases = Array.isArray(domain) ? domain : [domain]; } else { delete distributionConfig.Aliases; } @@ -99,37 +108,50 @@ class ServerlessApiCloudFrontPlugin { } prepareOrigins(distributionConfig) { - distributionConfig.Origins[0].OriginPath = `/${this.options.stage}`; + let lambda = this.getConfig('lambda', ''); + if (!lambda) { + throw `${TAG} Error: lambda must be set`; + } + lambda = lambda.charAt(0).toUpperCase() + lambda.slice(1); + distributionConfig.Origins[0].DomainName['Fn::Select'][1]['Fn::Split'][1][ + 'Fn::GetAtt' + ][0] = `${lambda}LambdaFunctionUrl`; } prepareCookies(distributionConfig) { - const forwardCookies = this.getConfig('cookies', 'all'); - distributionConfig.DefaultCacheBehavior.ForwardedValues.Cookies.Forward = Array.isArray(forwardCookies) ? 'whitelist' : forwardCookies; - if (Array.isArray(forwardCookies)) { - distributionConfig.DefaultCacheBehavior.ForwardedValues.Cookies.WhitelistedNames = forwardCookies; - } + const forwardCookies = this.getConfig('cookies', 'all'); + distributionConfig.DefaultCacheBehavior.ForwardedValues.Cookies.Forward = + Array.isArray(forwardCookies) ? 'whitelist' : forwardCookies; + if (Array.isArray(forwardCookies)) { + distributionConfig.DefaultCacheBehavior.ForwardedValues.Cookies.WhitelistedNames = + forwardCookies; + } } - + prepareHeaders(distributionConfig) { - const forwardHeaders = this.getConfig('headers', 'none'); - - if (Array.isArray(forwardHeaders)) { - distributionConfig.DefaultCacheBehavior.ForwardedValues.Headers = forwardHeaders; - } else { - distributionConfig.DefaultCacheBehavior.ForwardedValues.Headers = forwardHeaders === 'none' ? [] : ['*']; - } + const forwardHeaders = this.getConfig('headers', 'none'); + + if (Array.isArray(forwardHeaders)) { + distributionConfig.DefaultCacheBehavior.ForwardedValues.Headers = + forwardHeaders; + } else { + distributionConfig.DefaultCacheBehavior.ForwardedValues.Headers = + forwardHeaders === 'none' ? [] : ['*']; } + } prepareQueryString(distributionConfig) { - const forwardQueryString = this.getConfig('querystring', 'all'); - - if (Array.isArray(forwardQueryString)) { - distributionConfig.DefaultCacheBehavior.ForwardedValues.QueryString = true; - distributionConfig.DefaultCacheBehavior.ForwardedValues.QueryStringCacheKeys = forwardQueryString; - } else { - distributionConfig.DefaultCacheBehavior.ForwardedValues.QueryString = forwardQueryString === 'all' ? true : false; - } - } + const forwardQueryString = this.getConfig('querystring', 'all'); + + if (Array.isArray(forwardQueryString)) { + distributionConfig.DefaultCacheBehavior.ForwardedValues.QueryString = true; + distributionConfig.DefaultCacheBehavior.ForwardedValues.QueryStringCacheKeys = + forwardQueryString; + } else { + distributionConfig.DefaultCacheBehavior.ForwardedValues.QueryString = + forwardQueryString === 'all' ? true : false; + } + } prepareComment(distributionConfig) { const name = this.serverless.getProvider('aws').naming.getApiGatewayName(); @@ -155,22 +177,31 @@ class ServerlessApiCloudFrontPlugin { delete distributionConfig.WebACLId; } } - + prepareCompress(distributionConfig) { - distributionConfig.DefaultCacheBehavior.Compress = (this.getConfig('compress', false) === true) ? true : false; + distributionConfig.DefaultCacheBehavior.Compress = + this.getConfig('compress', false) === true ? true : false; } prepareMinimumProtocolVersion(distributionConfig) { - const minimumProtocolVersion = this.getConfig('minimumProtocolVersion', undefined); + const minimumProtocolVersion = this.getConfig( + 'minimumProtocolVersion', + undefined, + ); if (minimumProtocolVersion) { - distributionConfig.ViewerCertificate.MinimumProtocolVersion = minimumProtocolVersion; + distributionConfig.ViewerCertificate.MinimumProtocolVersion = + minimumProtocolVersion; } } getConfig(field, defaultValue) { - return _.get(this.serverless, `service.custom.apiCloudFront.${field}`, defaultValue) + return _.get( + this.serverless, + `service.custom.lambdaCloudFront.${field}`, + defaultValue, + ); } } -module.exports = ServerlessApiCloudFrontPlugin; +module.exports = ServerlessLambdaCloudFrontPlugin; diff --git a/package.json b/package.json index d38d3a2..c121b01 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,21 @@ { - "name": "serverless-api-cloudfront", - "version": "0.9.5", + "name": "serverless-lambda-cloudfront", + "version": "0.0.6", "engines": { - "node": ">=6.4" + "node": ">=12" }, - "description": "CloudFront distribution in front of your API Gateway", - "author": "Droplr, Inc.", + "description": "CloudFront distribution in front of your Lambda Url", + "author": "t2tx", "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/Droplr/serverless-api-cloudfront" + "url": "https://github.com/t2tx/serverless-lambda-cloudfront" }, "keywords": [ "serverless", "serverless plugin", - "api gateway", "cloudfront", - "api gateway", - "lambda", + "lambda url", "aws", "aws lambda", "amazon web services", @@ -28,5 +26,8 @@ "chalk": "2.4.2", "js-yaml": "3.13.1", "lodash": "4.17.21" + }, + "peerDependencies": { + "serverless": ">=3.12.0" } -} +} \ No newline at end of file diff --git a/resources.yml b/resources.yml index fab031c..7b10458 100644 --- a/resources.yml +++ b/resources.yml @@ -5,20 +5,26 @@ Resources: Properties: DistributionConfig: Origins: - - Id: ApiGateway - DomainName: - Fn::Join: - - "" - - - Ref: ApiGatewayRestApi - - ".execute-api." - - Ref: AWS::Region - - ".amazonaws.com" - CustomOriginConfig: - HTTPPort: '80' - HTTPSPort: '443' - OriginProtocolPolicy: https-only - OriginSSLProtocols: [ "TLSv1", "TLSv1.1", "TLSv1.2" ] - OriginPath: "/dev" + - Id: LambdaUrl + DomainName: + Fn::Select: + [ + 2, + Fn::Split: + ['/', Fn::GetAtt: [IndexLambdaFunctionUrl, FunctionUrl]], + ] + # Fn::Join: + # - "" + # - - Ref: ApiGatewayRestApi + # - ".execute-api." + # - Ref: AWS::Region + # - ".amazonaws.com" + CustomOriginConfig: + HTTPPort: '80' + HTTPSPort: '443' + OriginProtocolPolicy: https-only + OriginSSLProtocols: ['TLSv1', 'TLSv1.1', 'TLSv1.2'] + OriginPath: '' Enabled: true HttpVersion: http2 Comment: cdn for api gateway @@ -27,16 +33,16 @@ Resources: PriceClass: PriceClass_All DefaultCacheBehavior: AllowedMethods: - - DELETE - - GET - - HEAD - - OPTIONS - - PATCH - - POST - - PUT + - DELETE + - GET + - HEAD + - OPTIONS + - PATCH + - POST + - PUT CachedMethods: - - HEAD - - GET + - HEAD + - GET ForwardedValues: QueryString: true Headers: [] @@ -44,7 +50,7 @@ Resources: Forward: all MinTTL: '0' DefaultTTL: '0' - TargetOriginId: ApiGateway + TargetOriginId: LambdaUrl ViewerProtocolPolicy: redirect-to-https CustomErrorResponses: [] ViewerCertificate: @@ -59,4 +65,4 @@ Resources: Outputs: ApiDistribution: Value: - Fn::GetAtt: [ ApiDistribution, DomainName ] + Fn::GetAtt: [ApiDistribution, DomainName]