├── .gitignore ├── iam-policy.json ├── serverless.yml ├── LICENSE ├── package.json ├── README.md ├── test └── event.json └── handler.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .serverless/ 3 | 4 | -------------------------------------------------------------------------------- /iam-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "codepipeline:PutJobFailureResult", 8 | "codepipeline:PutJobSuccessResult", 9 | "cloudfront:CreateInvalidation" 10 | ], 11 | "Resource": "*" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: cloudfront-invalidate-dist 2 | 3 | provider: 4 | name: aws 5 | runtime: ${opt:runtime, 'nodejs10.x'} 6 | memorySize: 128 7 | timeout: 30 8 | stage: ${opt:stage, 'dev'} 9 | region: ${opt:region, 'us-east-1'} 10 | 11 | iamRoleStatements: 12 | - Effect: "Allow" 13 | Action: 14 | - "codepipeline:PutJobFailureResult" 15 | - "codepipeline:PutJobSuccessResult" 16 | - "cloudfront:CreateInvalidation" 17 | Resource: "*" 18 | 19 | functions: 20 | invalidate: 21 | handler: handler.handler 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018, Jardel Weyrich 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | 7 | Source: http://opensource.org/licenses/ISC -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudfront-invalidate-dist", 3 | "version": "1.0.0", 4 | "description": "Lambda function to be invoked by AWS CodePipeline to invalidate specified paths from a CloudFront distribution.", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/jweyrich/cloudfront-invalidate-dist.git" 15 | }, 16 | "keywords": [ 17 | "aws", 18 | "codepipeline", 19 | "cloudfront", 20 | "invalidation", 21 | "distribution" 22 | ], 23 | "author": "Jardel Weyrich ", 24 | "license": "ISC", 25 | "bugs": { 26 | "url": "https://github.com/jweyrich/cloudfront-invalidate-dist/issues" 27 | }, 28 | "homepage": "https://github.com/jweyrich/cloudfront-invalidate-dist#readme", 29 | "dependencies": { 30 | "serverless-python-requirements": "^4.2.5" 31 | }, 32 | "devDependencies": { 33 | "aws-sdk": "^2.353.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CloudFront Invalidate Distribution 2 | 3 | Make your AWS CodePipeline invoke this Lambda function to invalidate a CloudFront distribution. 4 | 5 | ### Prerequisites 6 | 7 | Install the [Serverless Framework](https://serverless.com/) if you don't have it. 8 | 9 | npm install -g serverless 10 | 11 | ### Deploy 12 | 13 | git clone https://github.com/jweyrich/cloudfront-invalidate-dist.git 14 | cd cloudfront-invalidate-dist 15 | serverless deploy [--aws-profile yourProfile] 16 | 17 | ### Configure your CodePipeline 18 | 19 | 1. Open your CodePipeline 20 | 2. Create a new stage 21 | 3. Add a new action 22 | 4. In 'Action Provider' select 'AWS Lambda' 23 | 5. In 'Function name' select the deployed function 24 | 6. In 'User parameters' specify the desired CloudFront distribution and paths to be invalidated. Example: 25 | 26 | `{ "distributionId": "FP7AWS1WBJLKSX", "objectPaths": [ "/*" ] }` 27 | 28 | 7. Save the action 29 | 8. Test 30 | 31 | ### Examples of User Parameters 32 | 33 | { "distributionId": "FP7AWS1WBJLKSX", "objectPaths": [ "/*" ] } 34 | { "distributionId": "FP7AWS1WBJLKSX", "objectPaths": [ "/foo", "/bar/baz.jpg", "/bar/baz/*" ] } 35 | -------------------------------------------------------------------------------- /test/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "CodePipeline.job": { 3 | "id": "11111111-abcd-1111-abcd-111111abcdef", 4 | "accountId": "111111111111", 5 | "data": { 6 | "actionConfiguration": { 7 | "configuration": { 8 | "FunctionName": "MyLambdaFunctionForAWSCodePipeline", 9 | "UserParameters": "some-input-such-as-a-URL" 10 | } 11 | }, 12 | "inputArtifacts": [ 13 | { 14 | "location": { 15 | "s3Location": { 16 | "bucketName": "the name of the bucket configured as the pipeline artifact store in Amazon S3, for example codepipeline-us-east-2-1234567890", 17 | "objectKey": "the name of the application, for example CodePipelineDemoApplication.zip" 18 | }, 19 | "type": "S3" 20 | }, 21 | "revision": null, 22 | "name": "ArtifactName" 23 | } 24 | ], 25 | "outputArtifacts": [], 26 | "artifactCredentials": { 27 | "secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", 28 | "sessionToken": "MIICiTCCAfICCQD6m7oRw0uXOjANBgkqhkiG9w 29 | 0BAQUFADCBiDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZ 30 | WF0dGxlMQ8wDQYDVQQKEwZBbWF6b24xFDASBgNVBAsTC0lBTSBDb25zb2xlMRIw 31 | EAYDVQQDEwlUZXN0Q2lsYWMxHzAdBgkqhkiG9w0BCQEWEG5vb25lQGFtYXpvbi5 32 | jb20wHhcNMTEwNDI1MjA0NTIxWhcNMTIwNDI0MjA0NTIxWjCBiDELMAkGA1UEBh 33 | MCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZBb 34 | WF6b24xFDASBgNVBAsTC0lBTSBDb25zb2xlMRIwEAYDVQQDEwlUZXN0Q2lsYWMx 35 | HzAdBgkqhkiG9w0BCQEWEG5vb25lQGFtYXpvbi5jb20wgZ8wDQYJKoZIhvcNAQE 36 | BBQADgY0AMIGJAoGBAMaK0dn+a4GmWIWJ21uUSfwfEvySWtC2XADZ4nB+BLYgVI 37 | k60CpiwsZ3G93vUEIO3IyNoH/f0wYK8m9TrDHudUZg3qX4waLG5M43q7Wgc/MbQ 38 | ITxOUSQv7c7ugFFDzQGBzZswY6786m86gpEIbb3OhjZnzcvQAaRHhdlQWIMm2nr 39 | AgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAtCu4nUhVVxYUntneD9+h8Mg9q6q+auN 40 | KyExzyLwaxlAoo7TJHidbtS4J5iNmZgXL0FkbFFBjvSfpJIlJ00zbhNYS5f6Guo 41 | EDmFJl0ZxBHjJnyp378OD8uTs7fLvjx79LjSTbNYiytVbZPQUQ5Yaxu2jXnimvw 42 | 3rrszlaEXAMPLE=", 43 | "accessKeyId": "AKIAIOSFODNN7EXAMPLE" 44 | }, 45 | "continuationToken": "A continuation token if continuing job" 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /handler.js: -------------------------------------------------------------------------------- 1 | // 2 | // Parts of this source code were borrowed from the AWS official documentation. 3 | // 4 | // REFERENCE: https://docs.aws.amazon.com/codepipeline/latest/userguide/actions-invoke-lambda-function.html#actions-invoke-lambda-function-add-action 5 | // 6 | 7 | const assert = require('assert'); 8 | const AWS = require('aws-sdk'); 9 | const http = require('http'); 10 | 11 | async function put_job_success(job_id, message) { 12 | // 13 | // Notify CodePipeline of a successful job 14 | // 15 | // Args: 16 | // job: The CodePipeline job ID 17 | // message: A message to be logged relating to the job status 18 | // Raises: 19 | // Exception: Any exception thrown by .putJobSuccessResult() 20 | // 21 | console.log('Reporting job success'); 22 | console.log(message); 23 | 24 | var codepipeline = new AWS.CodePipeline(); 25 | var params = { 26 | jobId: job_id, 27 | }; 28 | return codepipeline.putJobSuccessResult(params).promise(); 29 | } 30 | 31 | async function put_job_failure(job_id, invoke_id, message) { 32 | // 33 | // Notify CodePipeline of a failed job 34 | // 35 | // Args: 36 | // job: The CodePipeline job ID 37 | // invoke_id: The current Lambda execution Invoke ID 38 | // message: A message to be logged related to the job status 39 | // Raises: 40 | // Exception: Any exception thrown by .putJobFailureResult() 41 | // 42 | console.log('Reporting job failure'); 43 | console.log(message); 44 | 45 | var codepipeline = new AWS.CodePipeline(); 46 | var params = { 47 | failureDetails: { 48 | message: message, 49 | type: 'JobFailed', 50 | externalExecutionId: invoke_id, 51 | }, 52 | jobId: job_id, 53 | }; 54 | return codepipeline.putJobFailureResult(params).promise(); 55 | } 56 | 57 | function get_user_params(job_data) { 58 | // 59 | // Decodes the JSON user parameters and validates the required properties. 60 | // 61 | // Args: 62 | // job_data: The job data structure containing the UserParameters string which should be a valid JSON structure 63 | // Returns: 64 | // The JSON parameters decoded as a dictionary. 65 | // Raises: 66 | // Exception: The JSON can't be decoded or a property is missing. 67 | // 68 | try { 69 | // Get the user parameters which contain the stack, artifact and file settings 70 | user_parameters = job_data['actionConfiguration']['configuration']['UserParameters']; 71 | console.log("UserParameters:"); 72 | console.log(user_parameters); 73 | decoded_parameters = JSON.parse(user_parameters); 74 | } catch (ex) { 75 | // We're expecting the user parameters to be encoded as JSON 76 | // so we can pass multiple values. If the JSON can't be decoded 77 | // then fail the job with a helpful message. 78 | throw new Error('UserParameters could not be decoded as JSON'); 79 | } 80 | 81 | const EXAMPLE_OF_USER_PARAMETERS = `{ 82 | "distributionId": "FP7AWS1WBJLKSX", 83 | "objectPaths": [ 84 | "/foo", 85 | "/bar/baz.jpg", 86 | "/bar/baz/*" 87 | ] 88 | }`; 89 | 90 | if (!('distributionId' in decoded_parameters)) { 91 | // Validate that the distribution ID is provided, otherwise fail the job 92 | // with a helpful message. 93 | throw new Error( 94 | 'Your UserParameters JSON must include the CloudFront Distribution ID to be invalidated.\n' 95 | + 'Example of a valid UserParameters:\n' 96 | + EXAMPLE_OF_USER_PARAMETERS 97 | ); 98 | } 99 | 100 | if (!('objectPaths' in decoded_parameters)) { 101 | // Validate that the object paths are provided, otherwise fail the job 102 | // with a helpful message. 103 | throw new Error( 104 | 'Your UserParameters JSON must include the object paths to invalidate.\n' 105 | + 'Example of a valid UserParameters:\n' 106 | + EXAMPLE_OF_USER_PARAMETERS 107 | ); 108 | } 109 | 110 | return decoded_parameters; 111 | } 112 | 113 | async function invalidate_cloudfront_distribution(distribution_id, object_paths) { 114 | const cloudfront = new AWS.CloudFront(); 115 | return cloudfront.createInvalidation({ 116 | DistributionId: distribution_id, 117 | InvalidationBatch: { 118 | CallerReference: `cloudfront-invalite-dist-${new Date().getTime()}`, 119 | Paths: { 120 | Quantity: object_paths.length, 121 | Items: object_paths, 122 | }, 123 | }, 124 | }).promise(); 125 | } 126 | 127 | async function lambda_handler(event, context) { 128 | var response = {}; 129 | 130 | // Extract the Job ID 131 | const job_id = event['CodePipeline.job']['id']; 132 | 133 | // Extract the Job Data 134 | const job_data = event['CodePipeline.job']['data']; 135 | 136 | try { 137 | // Extract the user parameters 138 | const params = get_user_params(job_data); 139 | 140 | const distribution_id = params['distributionId']; 141 | const object_paths = params['objectPaths']; 142 | 143 | await invalidate_cloudfront_distribution(distribution_id, object_paths); 144 | 145 | await put_job_success(job_id, 'Completed'); 146 | 147 | const body = { 148 | "message": "Success!", 149 | "input": event, 150 | }; 151 | 152 | response = { 153 | "statusCode": 200, 154 | "body": JSON.stringify(body), 155 | }; 156 | } catch (ex) { 157 | // If any exception is raised, fail the job and log the exception message. 158 | console.error('Function failed due to exception.'); 159 | console.error(ex); 160 | 161 | await put_job_failure(job_id, context.invokeid, 'Function exception: ' + JSON.stringify(ex)); 162 | 163 | const body = { 164 | "message": JSON.stringify(ex), 165 | "input": event 166 | }; 167 | 168 | response = { 169 | "statusCode": 500, 170 | "body": json.dumps(body) 171 | }; 172 | } 173 | 174 | return response; 175 | } 176 | 177 | exports.handler = lambda_handler; 178 | --------------------------------------------------------------------------------