├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── LICENSE.txt ├── README.md ├── commitlint.config.js ├── index.js ├── package-lock.json ├── package.json └── prettier.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: 'prettier', 4 | plugins: ['import', 'prettier'], 5 | env: { 6 | es6: true, 7 | node: true, 8 | jest: true 9 | }, 10 | parserOptions: { 11 | ecmaVersion: 2017, 12 | sourceType: 'module' 13 | }, 14 | rules: { 15 | 'array-bracket-spacing': [ 16 | 'error', 17 | 'never', 18 | { 19 | objectsInArrays: false, 20 | arraysInArrays: false 21 | } 22 | ], 23 | 'arrow-parens': ['error', 'always'], 24 | 'comma-dangle': ['error', 'never'], 25 | 'func-names': 'off', 26 | 'no-use-before-define': 'off', 27 | 'prefer-destructuring': 'off', 28 | 'no-console': 'error', 29 | 'no-shadow': 'error', 30 | 'no-undef': 'error', 31 | 'object-curly-newline': 'off', 32 | 'no-unused-vars': 'error', 33 | 'semi': 'off', 34 | 'object-shorthand': 'off', 35 | 'prettier/prettier': 'error', 36 | 'prefer-const': 'error' 37 | } 38 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | npm-debug.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | dist 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | .nyc_output 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 29 | node_modules 30 | 31 | # IDE stuff 32 | **/.idea 33 | 34 | # OS stuff 35 | .DS_Store 36 | .tmp 37 | 38 | # Serverless stuff 39 | admin.env 40 | .env 41 | tmp 42 | .coveralls.yml 43 | tmpdirs-serverless 44 | .eslintcache 45 | .serverless 46 | .vscode -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Serverless Operations, inc 4 | 5 | The following license applies to all parts of this software except as 6 | documented below: 7 | 8 | ==== 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | 28 | ==== 29 | 30 | All files located in the node_modules and external directories are 31 | externally maintained libraries used by this software which have their 32 | own licenses; we recommend you read them, as their terms may differ from 33 | the terms above. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless Lambda Edge PreExisting CloudFront 2 | A Serverless Framework plugin which associates Lambda@Edge against pre-existing CloudFront distributions. 3 | 4 | ## Install 5 | 6 | You can install this plugin from npm registry. 7 | 8 | ```shell 9 | $ npm install --save-dev serverless-lambda-edge-pre-existing-cloudfront 10 | ``` 11 | 12 | ## How it works 13 | 14 | Configure serverless.yml 15 | 16 | ```yaml 17 | functions: 18 | viewerRequest: 19 | handler: lambdaEdge/viewerRequest.handler 20 | events: 21 | - preExistingCloudFront: 22 | # ---- Mandatory Properties ----- 23 | distributionId: xxxxxxx # CloudFront distribution ID you want to associate 24 | eventType: viewer-request # Choose event to trigger your Lambda function, which are `viewer-request`, `origin-request`, `origin-response` or `viewer-response` 25 | pathPattern: '*' # Specifying the CloudFront behavior 26 | includeBody: false # Whether including body or not within request 27 | # ---- Optional Property ----- 28 | stage: dev # Specify the stage at which you want this CloudFront distribution to be updated 29 | 30 | plugins: 31 | - serverless-lambda-edge-pre-existing-cloudfront 32 | ``` 33 | 34 | Run deploy 35 | ``` 36 | $ serverless deploy 37 | ``` 38 | 39 | You can specify additional configurations a `lambdaEdgePreExistingCloudFront` value in the custom section of your serverless.yml file. 40 | A `validStages` value allows you to specify valid stage names for deploy Lambda@Edge. 41 | 42 | ```yaml 43 | lambdaEdgePreExistingCloudFront: 44 | validStages: 45 | - staging 46 | - production 47 | ``` 48 | 49 | ### How `validStages` and `stage` properties work 50 | This plugin will first check for `validStages` property defined in the `custom` section. If `validStages` is used, then all the `preExistingCloudFront` events are only possible to be updated at the `validStages`. If not used, all the `preExistingCloudFront` events are possible to be updated at any stage. 51 | 52 | Then at all valid stages, the plugin checks - for each `preExistingCloudFront` event - if the provider's stage is the same as the `stage` property defined for each `preExistingCloudFront` event. If they match, then that particular `preExistingCloudFront` event will be updated. 53 | 54 | If `stage` is not used for a `preExistingCloudFront` event, then that event will be updated at all `validStages` or all stages if `validStages` is not used. -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] } 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class ServerlessLambdaEdgePreExistingCloudFront { 4 | constructor(serverless, options) { 5 | this.serverless = serverless 6 | this.options = options || {} 7 | this.provider = this.serverless.getProvider('aws') 8 | this.service = this.serverless.service.service 9 | this.region = this.provider.getRegion() 10 | this.stage = this.provider.getStage() 11 | 12 | this.hooks = { 13 | 'after:aws:deploy:finalize:cleanup': async () => { 14 | await this.serverless.service 15 | .getAllFunctions() 16 | .filter((functionName) => { 17 | const functionObj = this.serverless.service.getFunction(functionName) 18 | return functionObj.events 19 | }) 20 | .reduce((promiseOutput, functionName) => { 21 | return promiseOutput.then(async () => { 22 | const functionObj = this.serverless.service.getFunction(functionName) 23 | const events = functionObj.events.filter( 24 | (event) => event.preExistingCloudFront && this.checkAllowedDeployStage() 25 | ) 26 | for (let idx = 0; idx < events.length; idx += 1) { 27 | const event = events[idx] 28 | 29 | if (event.preExistingCloudFront.stage !== undefined && 30 | event.preExistingCloudFront.stage != `${serverless.service.provider.stage}`) { continue } 31 | 32 | const functionArn = await this.getlatestVersionLambdaArn(functionObj.name) 33 | const resolvedDistributionId = await (event.preExistingCloudFront.distributionId['Fn::ImportValue'] 34 | ? this.resolveCfImportValue(this.provider, event.preExistingCloudFront.distributionId['Fn::ImportValue']) 35 | : event.preExistingCloudFront.distributionId 36 | ) 37 | this.serverless.cli.consoleLog( 38 | `${functionArn} (Event: ${event.preExistingCloudFront.eventType}, pathPattern: ${event.preExistingCloudFront.pathPattern}) is associating to ${resolvedDistributionId} CloudFront Distribution. waiting for deployed status.` 39 | ) 40 | 41 | let retryCount = 5 42 | 43 | const updateDistribution = async () => { 44 | const config = await this.provider.request('CloudFront', 'getDistribution', { 45 | Id: resolvedDistributionId 46 | }) 47 | 48 | if (event.preExistingCloudFront.pathPattern === '*') { 49 | config.DistributionConfig.DefaultCacheBehavior.LambdaFunctionAssociations = await this.associateFunction( 50 | config.DistributionConfig.DefaultCacheBehavior.LambdaFunctionAssociations, 51 | event, 52 | functionObj.name, 53 | functionArn 54 | ) 55 | } else { 56 | config.DistributionConfig.CacheBehaviors = await this.associateNonDefaultCacheBehaviors( 57 | config.DistributionConfig.CacheBehaviors, 58 | event, 59 | functionObj.name, 60 | functionArn 61 | ) 62 | } 63 | 64 | await this.provider 65 | .request('CloudFront', 'updateDistribution', { 66 | Id: resolvedDistributionId, 67 | IfMatch: config.ETag, 68 | DistributionConfig: config.DistributionConfig 69 | }) 70 | .catch(async (error) => { 71 | if (error.providerError.code === 'PreconditionFailed' && retryCount > 0) { 72 | this.serverless.cli.consoleLog( 73 | `received precondition failed error, retrying... (${retryCount}/5)` 74 | ) 75 | retryCount -= 1 76 | await new Promise((res) => setTimeout(res, 5000)) 77 | return updateDistribution() 78 | } 79 | this.serverless.cli.consoleLog(error) 80 | throw error 81 | }) 82 | } 83 | 84 | await updateDistribution() 85 | } 86 | }) 87 | }, Promise.resolve()) 88 | } 89 | } 90 | 91 | if (this.serverless.configSchemaHandler) { 92 | this.serverless.configSchemaHandler.defineCustomProperties({ 93 | type: 'object', 94 | properties: { 95 | lambdaEdgePreExistingCloudFront: { 96 | type: 'object', 97 | properties: { 98 | validStages: { 99 | type: 'array', 100 | items: { type: 'string' }, 101 | uniqueItems: true 102 | } 103 | } 104 | } 105 | } 106 | }) 107 | 108 | this.serverless.configSchemaHandler.defineFunctionEvent('aws', 'preExistingCloudFront', { 109 | type: 'object', 110 | properties: { 111 | distributionId: { 112 | anyOf: [{ type: 'string' }, { type: 'object' }], 113 | }, 114 | eventType: { type: 'string' }, 115 | pathPattern: { type: 'string' }, 116 | includeBody: { type: 'boolean' }, 117 | stage: { type: 'string' } 118 | }, 119 | required: ['distributionId', 'eventType', 'pathPattern', 'includeBody'] 120 | }) 121 | } 122 | } 123 | 124 | checkAllowedDeployStage() { 125 | if ( 126 | this.serverless.service.custom && 127 | this.serverless.service.custom.lambdaEdgePreExistingCloudFront && 128 | this.serverless.service.custom.lambdaEdgePreExistingCloudFront.validStages 129 | ) { 130 | if ( 131 | this.serverless.service.custom.lambdaEdgePreExistingCloudFront.validStages.indexOf( 132 | this.stage 133 | ) < 0 134 | ) { 135 | return false 136 | } 137 | } 138 | return true 139 | } 140 | 141 | async associateNonDefaultCacheBehaviors(cacheBehaviors, event, functionName, functionArn) { 142 | for (let i = 0; i < cacheBehaviors.Items.length; i++) { 143 | if (event.preExistingCloudFront.pathPattern === cacheBehaviors.Items[i].PathPattern) { 144 | cacheBehaviors.Items[i].LambdaFunctionAssociations = await this.associateFunction( 145 | cacheBehaviors.Items[i].LambdaFunctionAssociations, 146 | event, 147 | functionName, 148 | functionArn 149 | ) 150 | } 151 | } 152 | return cacheBehaviors 153 | } 154 | 155 | async associateFunction(lambdaFunctionAssociations, event, functionName, functionArn) { 156 | const originals = lambdaFunctionAssociations.Items.filter( 157 | (x) => x.EventType !== event.preExistingCloudFront.eventType 158 | ) 159 | lambdaFunctionAssociations.Items = originals 160 | lambdaFunctionAssociations.Items.push({ 161 | LambdaFunctionARN: functionArn, 162 | IncludeBody: event.preExistingCloudFront.includeBody, 163 | EventType: event.preExistingCloudFront.eventType 164 | }) 165 | lambdaFunctionAssociations.Quantity = lambdaFunctionAssociations.Items.length 166 | return lambdaFunctionAssociations 167 | } 168 | 169 | async getlatestVersionLambdaArn(functionName, marker) { 170 | const args = { 171 | FunctionName: functionName, 172 | MaxItems: 50 173 | } 174 | 175 | if (marker) { 176 | args['Marker'] = marker 177 | } 178 | 179 | const versions = await this.provider.request('Lambda', 'listVersionsByFunction', args) 180 | 181 | if (versions.NextMarker !== null) { 182 | return await this.getlatestVersionLambdaArn(functionName, versions.NextMarker) 183 | } 184 | let arn 185 | versions.Versions.forEach(async (functionObj) => { 186 | arn = functionObj.FunctionArn 187 | }) 188 | return arn 189 | } 190 | 191 | resolveCfImportValue(provider, name, sdkParams = {}) { 192 | return provider.request('CloudFormation', 'listExports', sdkParams).then(result => { 193 | const targetExportMeta = result.Exports.find(exportMeta => exportMeta.Name === name); 194 | if (targetExportMeta) return targetExportMeta.Value; 195 | if (result.NextToken) { 196 | return this.resolveCfImportValue(provider, name, { NextToken: result.NextToken }); 197 | } 198 | 199 | throw new Error( 200 | `Could not resolve Fn::ImportValue with name ${name}. Are you sure this value is exported ?` 201 | ); 202 | }); 203 | } 204 | } 205 | module.exports = ServerlessLambdaEdgePreExistingCloudFront 206 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-lambda-edge-pre-existing-cloudfront", 3 | "version": "1.1.6", 4 | "description": "The Serverless Framework plugin which creates Lambda@Edge against pre-existing CloudFront.", 5 | "main": "index.js", 6 | "author": "serverless-operations", 7 | "repository": "serverless-operations/serverless-lambda-edge-pre-existing-cloudfront", 8 | "license": "MIT", 9 | "scripts": { 10 | "lint": "eslint .", 11 | "fmt": "eslint . --fix --cache", 12 | "semantic-release": "semantic-release" 13 | }, 14 | "husky": { 15 | "hooks": { 16 | "pre-commit": "lint-staged", 17 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 18 | } 19 | }, 20 | "lint-staged": { 21 | "*.js": [ 22 | "eslint" 23 | ] 24 | }, 25 | "devDependencies": { 26 | "@commitlint/cli": "^8.3.5", 27 | "@commitlint/config-conventional": "^8.3.4", 28 | "eslint": "^6.8.0", 29 | "eslint-config-prettier": "^6.10.0", 30 | "eslint-plugin-import": "^2.20.1", 31 | "eslint-plugin-prettier": "^3.1.2", 32 | "husky": "^4.2.3", 33 | "lint-staged": "^10.0.8", 34 | "prettier": "^1.19.1", 35 | "semantic-release": "^17.0.4" 36 | }, 37 | "dependencies": {} 38 | } 39 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'always', 3 | printWidth: 100, 4 | semi: false, 5 | singleQuote: true, 6 | trailingComma: 'none' 7 | } 8 | --------------------------------------------------------------------------------