deployer.js

// 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)
        }
      })
    }
  )
}