├── .gitignore ├── .npmignore ├── Gruntfile.js ├── LICENSE-MIT ├── README.md ├── event_handle.json ├── event_monitor.json ├── index.js ├── lib ├── aws_sdks.js ├── handle_job.js ├── job_functions.js └── monitor_job.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | dist 3 | *.iml 4 | node_modules -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | event.json 2 | Gruntfile.js 3 | dist 4 | *.iml 5 | .idea 6 | node_modules -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | var grunt = require('grunt'); 2 | grunt.loadNpmTasks('grunt-aws-lambda'); 3 | 4 | grunt.initConfig({ 5 | lambda_invoke: { 6 | task: { 7 | options: { 8 | event: 'event_handle.json' 9 | } 10 | }, 11 | monitor: { 12 | options: { 13 | event: 'event_monitor.json' 14 | } 15 | } 16 | }, 17 | lambda_deploy: { 18 | default: { 19 | arn: 'arn:aws:lambda:us-east-1:608866947342:function:codedeploy-opsworks-runner' 20 | } 21 | }, 22 | lambda_package: { 23 | default: { 24 | } 25 | } 26 | }); 27 | 28 | grunt.registerTask('deploy', ['lambda_package', 'lambda_deploy']); 29 | grunt.registerTask('lambda_invoke_monitor', ['lambda_invoke:monitor']); 30 | grunt.registerTask('lambda_invoke_task', ['lambda_invoke:task']); -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Tim-B 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodePipeline OpsWorks Deployer 2 | 3 | **Update: CodePipeline now has built-in support for both [Lambda](https://docs.aws.amazon.com/codepipeline/latest/userguide/how-to-lambda-integration.html) 4 | and [OpsWorks](https://docs.aws.amazon.com/opsworks/latest/userguide/other-services-cp.html) so you should take a look at the built-in functionality before using this project.** 5 | 6 | ## What is this? 7 | 8 | This is a Lambda function which allows you to deploy to OpsWorks using [CodePipeline](http://aws.amazon.com/codepipeline/) by 9 | implementing a [custom action](http://docs.aws.amazon.com/codepipeline/latest/userguide/how-to-create-custom-action.html). 10 | 11 | **You can find a full guide on setting it up [on my blog](http://hipsterdevblog.com/blog/2015/07/28/deploying-from-codepipeline-to-opsworks-using-a-custom-action-and-lambda/)** 12 | 13 | When configured you can use it just like the inbuilt stage actions: 14 | ![Diagram](http://hipsterdevblog.com/images/posts/opsworks_codepipeline/actionopts.png) 15 | 16 | ## How does it work? 17 | 18 | It uses S3 put notifications to trigger the Lambda function, then SNS retries to implement polling. 19 | 20 | Here's a general diagram of how it works: 21 | 22 | ![Diagram](http://hipsterdevblog.com/images/posts/opsworks_codepipeline/codepipelineopsworks-diagram.png) 23 | 24 | ## How do I develop on this? 25 | 26 | You might find the following two grunt functions useful: 27 | 28 | `grunt lambda_invoke_monitor` 29 | `grunt lambda_invoke_task` 30 | 31 | You may also want to monitor the two event_something.json files to reference resources within your own account. 32 | 33 | Finally, if you're using this as a base for another action you might want to change the `buildProvider` variable in 34 | `lib/handle_job.js` to a different name. -------------------------------------------------------------------------------- /event_handle.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "EventSource": "aws:sns", 5 | "EventVersion": "1.0", 6 | "EventSubscriptionArn": "arn:aws:sns:us-east-1:623423423:test:8a1787b2-5234234-4fb6-9e12-2342342", 7 | "Sns": { 8 | "TopicArn":"arn:aws:sns:us-east-1:123456789012:lambda_topic", 9 | "Timestamp":"2015-04-02T07:36:57.451Z", 10 | "Message": "{\"Records\": [\n {\n \"eventVersion\": \"2.0\",\n \"eventSource\": \"aws:s3\",\n \"awsRegion\": \"us-east-1\",\n \"eventTime\": \"2015-07-26T05:24:44.333Z\",\n \"eventName\": \"ObjectCreated:CompleteMultipartUpload\",\n \"userIdentity\": {\n \"principalId\": \"AWS:623423423423432423:652342342342\"\n },\n \"requestParameters\": {\n \"sourceIPAddress\": \"10.127.202.55\"\n },\n \"responseElements\": {\n \"x-amz-request-id\": \"82954680B93F21B9\",\n \"x-amz-id-2\": \"4bVxcKdiiirCh83jnagnmpunfKC28IIgvy6Ny4/ocbTkpXEaHyDYV3sdWlStuIoF\"\n },\n \"s3\": {\n \"s3SchemaVersion\": \"1.0\",\n \"configurationId\": \"Putnotif\",\n \"bucket\": {\n \"name\": \"codepipeline-us-east-1-703432432423\",\n \"ownerIdentity\": {\n \"principalId\": \"A2J34324324324324\"\n },\n \"arn\": \"arn:aws:s3:::codepipeline-us-east-1-72432562342342341\"\n },\n \"object\": {\n \"key\": \"test-pipeline/opsworks/reOLsL1.zip\",\n \"size\": 18759,\n \"eTag\": \"b5484d0fcf7c813cfe40b407cedbd641-1\"\n }\n }\n }\n ]}"} 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /event_monitor.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "EventSource": "aws:sns", 5 | "EventVersion": "1.0", 6 | "EventSubscriptionArn": "arn:aws:sns:us-east-1:608866947342:test:8a1787b2-2665-4fb6-9e12-432356234", 7 | "Sns": { 8 | "TopicArn":"arn:aws:sns:us-east-1:123456789012:lambda_topic", 9 | "Timestamp":"2015-04-02T07:36:57.451Z", 10 | "Message": "{\"Action\":\"monitorJob\",\"DeploymentId\":\"5442343-f683-48a4-bd14-fbf2f3b7b866\",\"Job\":{\"accountId\":\"231414213\",\"data\":{\"actionConfiguration\":{\"configuration\":{\"ProjectName\":\"sagasdas\"}},\"actionTypeId\":{\"category\":\"Build\",\"owner\":\"Custom\",\"provider\":\"My-Build-Provider-Name\",\"version\":\"1\"},\"artifactCredentials\":{\"accessKeyId\":\"dfsdfqdqwdqw\",\"secretAccessKey\":\"dagfgffsdaf\",\"sessionToken\":\"oisdjoasdjosaidjosasladjasojdoiasjdoasjfhgaiuosdasdsa==\"},\"inputArtifacts\":[{\"location\":{\"s3Location\":{\"bucketName\":\"codepipeline-us-east-1-7066678721\",\"objectKey\":\"test-pipeline/opsworks/bhZ5flf.zip\"},\"type\":\"S3\"},\"name\":\"opsworks\"}],\"outputArtifacts\":[{\"location\":{\"s3Location\":{\"bucketName\":\"codepipeline-us-east-1-42625434\",\"objectKey\":\"test-pipeline/adasdasd/wVwUx4t\"},\"type\":\"S3\"},\"name\":\"fasdasdasd\"}],\"pipelineContext\":{\"action\":{\"name\":\"hjedsfasd\"},\"pipelineName\":\"test-pipeline\",\"stage\":{\"name\":\"spacestation\"}}},\"id\":\"0e42167f-3454-4a70-4354-5bf13e75db45\",\"nonce\":\"3\"}}"} 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | console.log('Loading function'); 2 | 3 | var actions = { 4 | handleJob: require('./lib/handle_job.js'), 5 | monitorJob: require('./lib/monitor_job.js') 6 | }; 7 | 8 | exports.handler = function (event, context) { 9 | 10 | 11 | var topicARN = event.Records[0].Sns.TopicArn; 12 | var messageSent = Date.parse(event.Records[0].Sns.Timestamp); 13 | var now = Date.now(); 14 | var minsElapsed = Math.ceil((now - messageSent) / 60000); 15 | 16 | var message = JSON.parse(event.Records[0].Sns.Message); 17 | 18 | if(typeof message.Action == 'undefined') { 19 | message.Action = 'handleJob'; 20 | } 21 | 22 | if(typeof actions[message.Action] != 'undefined') { 23 | actions[message.Action](message, topicARN, minsElapsed, context); 24 | } 25 | } -------------------------------------------------------------------------------- /lib/aws_sdks.js: -------------------------------------------------------------------------------- 1 | 2 | var AWS = require('aws-sdk'); 3 | 4 | AWS.config.update({ 5 | region: 'us-east-1', 6 | apiVersions: { 7 | codepipeline: '2015-07-09', 8 | s3: '2006-03-01' 9 | }, 10 | S3Config: { 11 | UseSignatureVersion4: true 12 | } 13 | }); 14 | var exp = {}; 15 | 16 | exp.codepipeline = new AWS.CodePipeline(); 17 | exp.opsworks = new AWS.OpsWorks(); 18 | exp.s3 = new AWS.S3({ 19 | signatureVersion: 'v4' 20 | }); 21 | exp.sns = new AWS.SNS(); 22 | 23 | module.exports = exp; -------------------------------------------------------------------------------- /lib/handle_job.js: -------------------------------------------------------------------------------- 1 | var buildProvider = 'OpsWorks-Via-Lambda'; 2 | 3 | var AWS = require('aws-sdk'); 4 | var async = require('async'); 5 | var jobHelper = require('./job_functions.js'); 6 | var SDKs = require('./aws_sdks.js'); 7 | 8 | module.exports = function (message, topicARN, minsElapsed, context) { 9 | 10 | var key = message.Records[0].s3.object.key; 11 | var bucket = message.Records[0].s3.bucket.name; 12 | 13 | var codepipeline = SDKs.codepipeline; 14 | var s3 = SDKs.s3; 15 | var opsworks = SDKs.opsworks; 16 | var sns = SDKs.sns; 17 | 18 | var publishMonitorMessage = function(job, deployment, topicARN, callback) { 19 | 20 | var message = JSON.stringify({ 21 | Action: 'monitorJob', 22 | DeploymentId: deployment.DeploymentId, 23 | Job: job 24 | }); 25 | 26 | var params = { 27 | Message: message, 28 | Subject: 'Monitor OpsWorks Deployment', 29 | TargetArn: topicARN 30 | }; 31 | sns.publish(params, function(err, data) { 32 | if(err) { 33 | console.log('Could not publish to monitoring topic.'); 34 | console.log(err); 35 | jobHelper.markFail(job, 'Could not publish to monitoring topic.', callback); 36 | return; 37 | } 38 | console.log('Published to monitoring topic'); 39 | callback(); 40 | }); 41 | } 42 | 43 | var getLayerInstances = function(job, stackID, callback) { 44 | var params = { 45 | StackId: stackID 46 | }; 47 | opsworks.describeInstances(params, function(err, data) { 48 | if(err) { 49 | console.log('Could not list instances.'); 50 | console.log(err); 51 | jobHelper.markFail(job, 'Could not list instances.', callback); 52 | return; 53 | } 54 | 55 | var returnArr = []; 56 | 57 | data.Instances.forEach(function(item) { 58 | returnArr.push(item.InstanceId); 59 | }); 60 | 61 | callback(returnArr); 62 | }); 63 | } 64 | 65 | var deployOpsworks = function(job, actionConfig, topicARN, callback) { 66 | getLayerInstances(job, actionConfig['OpsWorks-Stack-ID'], function(instances) { 67 | opsworks.createDeployment({ 68 | Command: { 69 | Name : 'deploy' 70 | }, 71 | StackId: actionConfig['OpsWorks-Stack-ID'], 72 | AppId: actionConfig['OpsWorks-App-ID'], 73 | InstanceIds: instances 74 | }, function(err, data) { 75 | if(err) { 76 | console.log('Deployment to OpsWorks failed.'); 77 | console.log(err); 78 | jobHelper.markFail(job, 'Deploying to OpsWorks failed.', callback); 79 | return; 80 | } 81 | console.log('OpsWorks deployment triggered.'); 82 | publishMonitorMessage(job, data, topicARN, callback); 83 | }); 84 | }); 85 | } 86 | 87 | var copyArtifact = function (jobS3, actionConfig) { 88 | return function (artifact, callback) { 89 | var artifactBucket = artifact.location.s3Location.bucketName; 90 | var artifactKey = artifact.location.s3Location.objectKey; 91 | var artifactName = artifact.name; 92 | 93 | jobS3.getObject({ 94 | Bucket: artifactBucket, 95 | Key: artifactKey 96 | }, function (err, data) { 97 | if (err) { 98 | console.log(err); 99 | callback(err); 100 | return; 101 | } 102 | 103 | var body = data.Body; 104 | var artifactKey = artifactName + '.zip'; 105 | s3.putObject({ 106 | Bucket: actionConfig['Deploy-Bucket'], 107 | Key: artifactKey, 108 | Body: body 109 | }, function (err, data) { 110 | if (err) { 111 | console.log('Upload error: ' + err); 112 | callback(err); 113 | return; 114 | } 115 | console.log('Uploaded ' + artifactKey); 116 | callback(); 117 | }); 118 | }); 119 | } 120 | } 121 | 122 | var ackJob = function (job, callback) { 123 | codepipeline.acknowledgeJob({ 124 | jobId: job.id, 125 | nonce: job.nonce 126 | }, function (err, data) { 127 | if (err) { 128 | console.log('Job ack failed.'); 129 | console.log(err); 130 | } 131 | callback(); 132 | }); 133 | } 134 | 135 | var handleArtifacts = function (job, jobS3, actionConfig, callback) { 136 | async.each(job.data.inputArtifacts, copyArtifact(jobS3, actionConfig), function (err) { 137 | if (err) { 138 | console.log(err); 139 | } 140 | callback(); 141 | }); 142 | } 143 | 144 | var dispatchJob = function (job, callback) { 145 | var credentials = job.data.artifactCredentials; 146 | var jobS3 = new AWS.S3({ 147 | signatureVersion: 'v4', 148 | credentials: credentials 149 | }); 150 | var actionConfig = job.data.actionConfiguration.configuration; 151 | 152 | async.series([ 153 | function (callback) { 154 | ackJob(job, callback); 155 | }, 156 | function (callback) { 157 | handleArtifacts(job, jobS3, actionConfig, callback); 158 | }, 159 | function (callback) { 160 | deployOpsworks(job, actionConfig, topicARN, callback); 161 | } 162 | ], 163 | function (err, results) { 164 | callback(); 165 | } 166 | ); 167 | } 168 | 169 | var pollResults = function (err, data) { 170 | if (err) { 171 | console.log(err); 172 | context.fail("Could not poll results"); 173 | return; 174 | } 175 | 176 | console.log(data.jobs.length + " jobs waiting."); 177 | async.each(data.jobs, dispatchJob, function (err) { 178 | if (err) { 179 | console.log(err); 180 | } 181 | 182 | if(data.jobs.length === 0) { 183 | context.fail("No messages"); 184 | } else { 185 | context.succeed("Done"); 186 | } 187 | }); 188 | } 189 | 190 | codepipeline.pollForJobs({ 191 | actionTypeId: { 192 | category: 'Deploy', 193 | owner: 'Custom', 194 | provider: buildProvider, 195 | version: '1' 196 | }, 197 | maxBatchSize: 1 198 | }, pollResults); 199 | 200 | }; -------------------------------------------------------------------------------- /lib/job_functions.js: -------------------------------------------------------------------------------- 1 | var SDKs = require('./aws_sdks.js'); 2 | 3 | var codepipeline = SDKs.codepipeline; 4 | 5 | var exp = {}; 6 | 7 | exp.markFail = function(job, message, callback) { 8 | var params = { 9 | jobId: job.id, 10 | failureDetails: { 11 | message: message, 12 | type: 'JobFailed', 13 | externalExecutionId: '1' 14 | } 15 | }; 16 | codepipeline.putJobFailureResult(params, function(err, data) { 17 | if (err) { 18 | console.log('Marking fail failed.'); 19 | console.log(err); 20 | callback(); 21 | return; 22 | } 23 | console.log('Task marked as failed'); 24 | callback(); 25 | }); 26 | } 27 | 28 | exp.markSuccess = function(job, callback) { 29 | var params = { 30 | jobId: job.id, 31 | currentRevision: { 32 | changeIdentifier: '1', 33 | revision: '1' 34 | }, 35 | executionDetails: { 36 | externalExecutionId: '1', 37 | percentComplete: 100, 38 | summary: 'Deployed to OpsWorks' 39 | } 40 | }; 41 | codepipeline.putJobSuccessResult(params, function(err, data) { 42 | if (err) { 43 | console.log('Marking done failed.'); 44 | console.log(err); 45 | callback(); 46 | return; 47 | } 48 | console.log('Task marked as succeeded'); 49 | callback(); 50 | }); 51 | } 52 | 53 | module.exports = exp; -------------------------------------------------------------------------------- /lib/monitor_job.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | var jobHelper = require('./job_functions.js'); 3 | var SDKs = require('./aws_sdks.js'); 4 | var opsworks = SDKs.opsworks; 5 | var jobHelper = require('./job_functions.js'); 6 | 7 | module.exports = function (message, topicARN, minsElapsed, context) { 8 | 9 | var deployID = message.DeploymentId; 10 | 11 | var getDeployStatus = function (deployID, callback) { 12 | var params = { 13 | DeploymentIds: [deployID] 14 | }; 15 | opsworks.describeDeployments(params, function (err, data) { 16 | if (err) { 17 | console.log('Could not check deploy status.'); 18 | console.log(err); 19 | jobHelper.markFail(message.Job, 'Could not check deploy statusc.', callback); 20 | return; 21 | } 22 | 23 | if (data.Deployments.length > 0) { 24 | var deployStatus = data.Deployments[0].Status; 25 | switch (deployStatus) { 26 | case 'successful': 27 | console.log('Deploy succeeded'); 28 | jobHelper.markSuccess(message.Job, callback); 29 | break; 30 | case 'failed': 31 | console.log('Deploy failed'); 32 | jobHelper.markFail(message.Job, 'OpsWorks deploy failed', callback); 33 | break; 34 | default: 35 | console.log('Deploy still running, elapsed: ' + minsElapsed); 36 | if(minsElapsed > 9) { 37 | jobHelper.markFail(message.Job, 'OpsWorks deploy timed out', function() { 38 | context.succeed("OpsWorks deploy timed out"); 39 | }); 40 | } else { 41 | context.fail("Still waiting"); 42 | } 43 | } 44 | } else { 45 | callback(); 46 | } 47 | }); 48 | } 49 | 50 | getDeployStatus(deployID, function () { 51 | context.succeed("Status updated"); 52 | }); 53 | 54 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codepipeline-opsworks-deployer", 3 | "version": "0.0.0", 4 | "description": "Deploys from Code Pipeline to OpsWorks", 5 | "private": "true", 6 | "devDependencies": { 7 | "grunt": "0.4.*", 8 | "grunt-aws-lambda": "0.*" 9 | }, 10 | "dependencies": { 11 | "async" : "1.4.0", 12 | "aws-sdk": "2.1.40" 13 | }, 14 | "bundledDependencies": [ 15 | "async", 16 | "aws-sdk" 17 | ], 18 | "author": "", 19 | "license": "BSD" 20 | } --------------------------------------------------------------------------------