// Include the AWS SDK
const AWS = require('aws-sdk')
const async = require('async')
const { Deploy } = require('./models')
// Create instances for ECR & CloudFormation
const ECR = new AWS.ECR()
const CloudFormation = new AWS.CloudFormation()
/**
* Handle a stack change event (as published from SNS topic)
* @param {object} event
*/
module.exports.stackChange = (event) => {
// Return a promise for the promise chain
return new Promise(
(resolve, reject) => {
// Ensure the SNS records have been parsed into messages
if (!event.parsed.messages || event.parsed.messages.length === 0) {
reject(new Error('No messages to be sent.'))
return
}
console.log('Messages:', JSON.stringify(event.parsed.messages, null, 2))
// Loop through all messages (async)
async.each(event.parsed.messages, (msg, next) => {
// Flag to determine whether this message is for parent stack or a stack resource
const isStackMessage = (msg.ResourceType === 'AWS::CloudFormation::Stack' && msg.LogicalResourceId === msg.StackName)
if (isStackMessage) {
console.log('Tracking deploy status from msg:', msg)
console.log('Current event object', JSON.stringify(event, null, 2))
next()
} else {
console.log('Not tracking deploy status for msg:', msg)
next()
}
}, err => {
if (err) {
// Reject on error
reject(err)
} else {
// Resolve with event
resolve(event)
}
})
}
)
}
/**
* Deploy build (from parsed deployments)
* @param {object} event
*/
module.exports.deployBuild = (event) => {
// Return a promise for the promise chain
return new Promise(
(resolve, reject) => {
// Ensure the deployments have been parsed
if (!event.parsed.deployments) {
console.log('Event does not contain parsed deployment details.')
resolve(event)
return
}
// Debug logs for deployments being handled
console.log('Parsed deployments', JSON.stringify(event.parsed.deployments, null, 2))
console.log('Current event object', JSON.stringify(event, null, 2))
// Loop through parsed deployments (async) and update CF stack(s)
async.each(event.parsed.deployments, (deployment, next) => {
// Construct ECR parameters to search for the specified tag
const ecrParams = {
// Use repo as specified in the parsed service parameters
repositoryName: process.env.ECR_REPO_NAME,
imageIds: [
{
// Search for an image tag as specified in the parsed service parameters
imageTag: deployment.tag
}
]
}
// Check if repo & tag exist
ECR.batchGetImage(ecrParams, (err, data) => {
// Reject on error
if (err) {
next(err)
return
}
// Skip deployment if tag does not exist in ECR
if (data.images.length === 0) {
console.log('Unable to find ECR image with params:', JSON.stringify(ecrParams))
next()
return
}
// Ensure at least 1 image, and no failures
// if (!data.images || data.images.length == 0 || data.failures.length > 0) {
// next(data)
// return
// }
// Describe the stack (verifies it exists, and can build parameter list)
CloudFormation.describeStacks({
// Get the stack name from the parsed service parameters
StackName: deployment.stack
}, (err, data) => {
// Reject on error
if (err) {
console.warn(err)
console.log('Unable to find CF Stack with name:', deployment.stack)
next()
return
}
// This should be impossible, but ensure only a single stack is being returned
if (data.Stacks.length > 1) {
next(new Error(`More than 1 stack matching name: ${deployment.stack}`))
return
}
// Filter current stack parameters except for "Tag"
const params = data.Stacks[0].Parameters.map(p => {
return {
ParameterKey: p.ParameterKey,
UsePreviousValue: true
}
}).filter(p => p.ParameterKey !== 'Tag')
// Add the "Tag" parameter with the tag as specified by the parsed service parameters
params.push({
ParameterKey: 'Tag',
ParameterValue: deployment.tag
})
// Update the CloudFormation stack with the reconstructed parameters
CloudFormation.updateStack({
// Get the stack name from the parsed service parameters
StackName: deployment.stack,
// Should include all previous parameters, and an updated "Tag" parameter
Parameters: params,
// Reuse the previous stack template
UsePreviousTemplate: true,
// Execute this stack update with the given role ARN
// - This is passed into the environment by serverless (see "serverless.yml")
RoleARN: process.env.UPDATE_ROLE_ARN,
// A stack that creates/changes IAM roles/policies/etc MUST provide this capability flag
Capabilities: [ 'CAPABILITY_IAM' ],
// Send stack change notifications to the SNS topic ARN
// - This is passed into the environment by serverless (see "serverless.yml")
// - This SNS topic is automatically hooked up to the "stack-change" function to send messages to Slack
NotificationARNs: [ process.env.NOTIFY_SNS_ARN ]
}, (err) => {
if (err) {
return next(err)
}
const newDeploy = new Deploy(Object.assign({}, deployment, {
service_name: process.env.SERVICE_NAME,
deploy_status: 'IN_PROGRESS',
deploy_timestamp: Date.now()
}))
newDeploy.save().then(data => {
console.log('Saved deploy data:', data)
next()
}).catch(next)
})
})
})
}, err => {
if (err) {
reject(err)
} else {
resolve(event)
}
})
}
)
}