UNPKG

mup-aws-beanstalk

Version:

Deploy apps to AWS Elastic Beanstalk using Meteor Up

774 lines (666 loc) 26.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.setup = setup; exports.deploy = deploy; exports.logs = logs; exports.logsNginx = logsNginx; exports.logsEb = logsEb; exports.start = start; exports.stop = stop; exports.restart = restart; exports.clean = clean; exports.reconfig = reconfig; exports.events = events; exports.status = status; exports.ssl = ssl; exports.shell = shell; exports.debug = debug; var _chalk = _interopRequireDefault(require("chalk")); var _ssh = require("ssh2"); var _aws = require("./aws"); var _certificates = _interopRequireDefault(require("./certificates")); var _policies = require("./policies"); var _upload = _interopRequireWildcard(require("./upload")); var _prepareBundle = require("./prepare-bundle"); var _utils = require("./utils"); var _versions = require("./versions"); var _envSettings = require("./env-settings"); var _ebConfig = require("./eb-config"); var _envReady = require("./env-ready"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } async function setup(api) { const config = api.getConfig(); const appConfig = config.app; const { bucket: bucketName, app: appName, instanceProfile, serviceRole: serviceRoleName, trailBucketPrefix, trailName, deregisterRuleName, environment: environmentName, eventTargetRole: eventTargetRoleName, eventTargetPolicyName, eventTargetPassRoleName, automationDocument } = (0, _utils.names)(config); (0, _utils.logStep)('=> Setting up'); // Create bucket if needed const { Buckets } = await _aws.s3.listBuckets().promise(); const beanstalkBucketCreated = await (0, _utils.ensureBucketExists)(Buckets, bucketName, appConfig.region); if (beanstalkBucketCreated) { console.log(' Created Bucket'); } (0, _utils.logStep)('=> Ensuring IAM Roles and Instance Profiles are setup'); // Create role and instance profile await (0, _utils.ensureRoleExists)(instanceProfile, _policies.rolePolicy); await (0, _utils.ensureInstanceProfileExists)(config, instanceProfile); await (0, _utils.ensurePoliciesAttached)(config, instanceProfile, ['arn:aws:iam::aws:policy/AWSElasticBeanstalkWebTier', 'arn:aws:iam::aws:policy/AWSElasticBeanstalkMulticontainerDocker', 'arn:aws:iam::aws:policy/AWSElasticBeanstalkWorkerTier', ...(appConfig.gracefulShutdown ? ['arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM'] : [])]); await (0, _utils.ensureRoleAdded)(config, instanceProfile, instanceProfile); // Create role used by enhanced health await (0, _utils.ensureRoleExists)(serviceRoleName, _policies.serviceRole); await (0, _utils.ensurePoliciesAttached)(config, serviceRoleName, ['arn:aws:iam::aws:policy/service-role/AWSElasticBeanstalkEnhancedHealth', 'arn:aws:iam::aws:policy/service-role/AWSElasticBeanstalkService']); if (appConfig.gracefulShutdown) { const accountId = await (0, _utils.getAccountId)(); const policy = (0, _policies.eventTargetRolePolicy)(accountId, environmentName, appConfig.region || 'us-east-1'); const passPolicy = (0, _policies.passRolePolicy)(accountId, eventTargetRoleName); await (0, _utils.ensureRoleExists)(eventTargetRoleName, _policies.eventTargetRole, true); await (0, _utils.ensureInlinePolicyAttached)(eventTargetRoleName, eventTargetPolicyName, policy); await (0, _utils.ensureInlinePolicyAttached)(eventTargetRoleName, eventTargetPassRoleName, passPolicy); } // Create beanstalk application if needed const { Applications } = await _aws.beanstalk.describeApplications().promise(); if (!Applications.find(app => app.ApplicationName === appName)) { const params = { ApplicationName: appName, Description: `App "${appConfig.name}" managed by Meteor Up` }; await _aws.beanstalk.createApplication(params).promise(); console.log(' Created Beanstalk application'); } if (appConfig.gracefulShutdown) { (0, _utils.logStep)('=> Ensuring Graceful Shutdown is setup'); const existingBucket = (0, _utils.findBucketWithPrefix)(Buckets, trailBucketPrefix); const trailBucketName = existingBucket ? existingBucket.Name : (0, _utils.createUniqueName)(trailBucketPrefix); const region = appConfig.region || 'us-east-1'; const accountId = await (0, _utils.getAccountId)(); const policy = (0, _policies.trailBucketPolicy)(accountId, trailBucketName); const trailBucketCreated = await (0, _utils.ensureBucketExists)(Buckets, trailBucketName, appConfig.region); await (0, _utils.ensureBucketPolicyAttached)(trailBucketName, policy); if (trailBucketCreated) { console.log(' Created bucket for Cloud Trail'); } const params = { trailNameList: [trailName] }; const { trailList } = await _aws.cloudTrail.describeTrails(params).promise(); if (trailList.length === 0) { const createParams = { Name: trailName, S3BucketName: trailBucketName }; await _aws.cloudTrail.createTrail(createParams).promise(); console.log(' Created CloudTrail trail'); } const createdDocument = await (0, _utils.ensureSsmDocument)(automationDocument, (0, _policies.gracefulShutdownAutomationDocument)()); if (createdDocument) { console.log(' Created SSM Automation Document'); } const createdRule = await (0, _utils.ensureCloudWatchRule)(deregisterRuleName, 'Used by Meteor Up for graceful shutdown', _policies.DeregisterEvent); if (createdRule) { console.log(' Created Cloud Watch rule'); } const target = (0, _policies.deregisterEventTarget)(environmentName, eventTargetRoleName, accountId, region); const createdTarget = await (0, _utils.ensureRuleTargetExists)(deregisterRuleName, target, accountId); if (createdTarget) { console.log(' Created target for Cloud Watch rule'); } } } async function deploy(api) { await api.runCommand('beanstalk.setup'); const config = api.getConfig(); const { app, bucket, bundlePrefix, environment } = (0, _utils.names)(config); const version = await (0, _versions.largestVersion)(api); const nextVersion = version + 1; // Mutates the config, so the meteor.build command will have the correct build location config.app.buildOptions.buildLocation = config.app.buildOptions.buildLocation || (0, _utils.tmpBuildPath)(config.app.path, api); const bundlePath = api.resolvePath(config.app.buildOptions.buildLocation, 'bundle.zip'); const willBuild = (0, _utils.shouldRebuild)(bundlePath, api.getOptions()['cached-build']); if (willBuild) { await api.runCommand('meteor.build'); (0, _prepareBundle.injectFiles)(api, app, nextVersion, config.app); await (0, _prepareBundle.archiveApp)(config.app.buildOptions.buildLocation, api); } (0, _utils.logStep)('=> Uploading bundle'); const key = `${bundlePrefix}${nextVersion}`; await (0, _upload.default)(config.app, bucket, `${bundlePrefix}${nextVersion}`, bundlePath); (0, _utils.logStep)('=> Creating version'); await _aws.beanstalk.createApplicationVersion({ ApplicationName: app, VersionLabel: nextVersion.toString(), Description: (0, _utils.createVersionDescription)(api, config.app), SourceBundle: { S3Bucket: bucket, S3Key: key } }).promise(); await api.runCommand('beanstalk.reconfig'); await (0, _envReady.waitForEnvReady)(config, true); (0, _utils.logStep)('=> Deploying new version'); const { toRemove, toUpdate } = await (0, _ebConfig.prepareUpdateEnvironment)(api); if (api.verbose) { console.log('EB Config changes:'); console.dir({ toRemove, toUpdate }); } await _aws.beanstalk.updateEnvironment({ EnvironmentName: environment, VersionLabel: nextVersion.toString(), OptionSettings: toUpdate, OptionsToRemove: toRemove }).promise(); await (0, _envReady.waitForEnvReady)(config, true); const { Environments } = await _aws.beanstalk.describeEnvironments({ ApplicationName: app, EnvironmentNames: [environment] }).promise(); await api.runCommand('beanstalk.clean'); await api.runCommand('beanstalk.ssl'); // Check if deploy succeeded const { Environments: finalEnvironments } = await _aws.beanstalk.describeEnvironments({ ApplicationName: app, EnvironmentNames: [environment] }).promise(); if (nextVersion.toString() === finalEnvironments[0].VersionLabel) { console.log(_chalk.default.green(`App is running at ${Environments[0].CNAME}`)); } else { console.log(_chalk.default.red`Deploy Failed. Visit the Aws Elastic Beanstalk console to view the logs from the failed deploy.`); process.exitCode = 1; } } async function logs(api) { const logsContent = await (0, _utils.getLogs)(api, ['web.stdout.log', 'nodejs/nodejs.log']); logsContent.forEach(({ instance, data }) => { console.log(`${instance} `, data[0] || data[1]); }); } async function logsNginx(api) { const logsContent = await (0, _utils.getLogs)(api, ['nginx/error.log', 'nginx/access.log']); logsContent.forEach(({ instance, data }) => { console.log(`${instance} `, data[0]); console.log(`${instance} `, data[1]); }); } async function logsEb(api) { const logsContent = await (0, _utils.getLogs)(api, ['eb-engine.log', 'eb-activity.log']); logsContent.forEach(({ data, instance }) => { console.log(`${instance} `, data[0] || data[1]); }); } async function start(api) { const config = api.getConfig(); const { environment } = (0, _utils.names)(config); (0, _utils.logStep)('=> Starting App'); const { EnvironmentResources } = await _aws.beanstalk.describeEnvironmentResources({ EnvironmentName: environment }).promise(); const autoScalingGroup = EnvironmentResources.AutoScalingGroups[0].Name; const { minInstances, maxInstances } = config.app; await _aws.autoScaling.updateAutoScalingGroup({ AutoScalingGroupName: autoScalingGroup, MaxSize: maxInstances, MinSize: minInstances, DesiredCapacity: minInstances }).promise(); await (0, _envReady.waitForHealth)(config); } async function stop(api) { const config = api.getConfig(); const { environment } = (0, _utils.names)(config); (0, _utils.logStep)('=> Stopping App'); const { EnvironmentResources } = await _aws.beanstalk.describeEnvironmentResources({ EnvironmentName: environment }).promise(); const autoScalingGroup = EnvironmentResources.AutoScalingGroups[0].Name; await _aws.autoScaling.updateAutoScalingGroup({ AutoScalingGroupName: autoScalingGroup, MaxSize: 0, MinSize: 0, DesiredCapacity: 0 }).promise(); await (0, _envReady.waitForHealth)(config, 'Grey'); } async function restart(api) { const config = api.getConfig(); const { environment } = (0, _utils.names)(config); (0, _utils.logStep)('=> Restarting App'); await _aws.beanstalk.restartAppServer({ EnvironmentName: environment }).promise(); await (0, _envReady.waitForEnvReady)(config, false); } async function clean(api) { const config = api.getConfig(); const { app, bucket } = (0, _utils.names)(config); (0, _utils.logStep)('=> Finding old versions'); const { versions } = await (0, _versions.oldVersions)(api); const envVersions = await (0, _versions.oldEnvVersions)(api); (0, _utils.logStep)('=> Removing old versions'); const promises = []; for (let i = 0; i < versions.length; i++) { promises.push(_aws.beanstalk.deleteApplicationVersion({ ApplicationName: app, VersionLabel: versions[i].toString(), DeleteSourceBundle: true }).promise()); } for (let i = 0; i < envVersions.length; i++) { promises.push(_aws.s3.deleteObject({ Bucket: bucket, Key: `env/${envVersions[i]}.txt` }).promise()); } // TODO: remove bundles await Promise.all(promises); } async function reconfig(api) { const config = api.getConfig(); const { app, environment, bucket } = (0, _utils.names)(config); const deploying = !!api.commandHistory.find(entry => entry.name === 'beanstalk.deploy'); (0, _utils.logStep)('=> Configuring Beanstalk environment'); // check if env exists const { Environments } = await _aws.beanstalk.describeEnvironments({ ApplicationName: app, EnvironmentNames: [environment] }).promise(); if (!Environments.find(env => env.Status !== 'Terminated')) { const desiredEbConfig = (0, _ebConfig.createDesiredConfig)(api.getConfig(), api.getSettings(), config.app.longEnvVars ? 1 : false); if (config.app.longEnvVars) { const envContent = (0, _envSettings.createEnvFile)(config.app.env, api.getSettings()); await (0, _upload.uploadEnvFile)(bucket, 1, envContent); } const platformArn = await (0, _utils.selectPlatformArn)(); const [version] = await (0, _versions.ebVersions)(api); await _aws.beanstalk.createEnvironment({ ApplicationName: app, EnvironmentName: environment, Description: `Environment for ${config.app.name}, managed by Meteor Up`, VersionLabel: version.toString(), PlatformArn: platformArn, OptionSettings: desiredEbConfig.OptionSettings }).promise(); console.log(' Created Environment'); await (0, _envReady.waitForEnvReady)(config, false); } else if (!deploying) { // If we are deploying, the environment will be updated // at the same time we update the environment version const { toRemove, toUpdate } = await (0, _ebConfig.prepareUpdateEnvironment)(api); if (api.verbose) { console.log('EB Config changes:'); console.dir({ toRemove, toUpdate }); } if (toRemove.length > 0 || toUpdate.length > 0) { await (0, _envReady.waitForEnvReady)(config, true); await _aws.beanstalk.updateEnvironment({ EnvironmentName: environment, OptionSettings: toUpdate, OptionsToRemove: toRemove }).promise(); console.log(' Updated Environment'); await (0, _envReady.waitForEnvReady)(config, true); } } const { ConfigurationSettings } = await _aws.beanstalk.describeConfigurationSettings({ EnvironmentName: environment, ApplicationName: app }).promise(); if ((0, _ebConfig.scalingConfigChanged)(ConfigurationSettings[0].OptionSettings, config)) { (0, _utils.logStep)('=> Configuring scaling'); await _aws.beanstalk.updateEnvironment({ EnvironmentName: environment, OptionSettings: (0, _ebConfig.scalingConfig)(config.app).OptionSettings }).promise(); await (0, _envReady.waitForEnvReady)(config, true); } } async function events(api) { const { environment } = (0, _utils.names)(api.getConfig()); const { Events: envEvents } = await _aws.beanstalk.describeEvents({ EnvironmentName: environment }).promise(); console.log(envEvents.map(ev => `${ev.EventDate}: ${ev.Message}`).join('\n')); } async function status(api) { const { environment } = (0, _utils.names)(api.getConfig()); let result; try { result = await _aws.beanstalk.describeEnvironmentHealth({ AttributeNames: ['All'], EnvironmentName: environment }).promise(); } catch (e) { if (e.message.includes('No Environment found for EnvironmentName')) { console.log(' AWS Beanstalk environment does not exist'); return; } throw e; } const { InstanceHealthList } = await _aws.beanstalk.describeInstancesHealth({ AttributeNames: ['All'], EnvironmentName: environment }).promise(); const { RequestCount, Duration, StatusCodes, Latency } = result.ApplicationMetrics; console.log(`Environment Status: ${result.Status}`); console.log(`Health Status: ${(0, _utils.coloredStatusText)(result.Color, result.HealthStatus)}`); if (result.Causes.length > 0) { console.log('Causes: '); result.Causes.forEach(cause => console.log(` ${cause}`)); } console.log(''); console.log(`=== Metrics For Last ${Duration || 'Unknown'} Minutes ===`); console.log(` Requests: ${RequestCount}`); if (StatusCodes) { console.log(' Status Codes'); console.log(` 2xx: ${StatusCodes.Status2xx}`); console.log(` 3xx: ${StatusCodes.Status3xx}`); console.log(` 4xx: ${StatusCodes.Status4xx}`); console.log(` 5xx: ${StatusCodes.Status5xx}`); } if (Latency) { console.log(' Latency'); console.log(` 99.9%: ${Latency.P999}`); console.log(` 99% : ${Latency.P99}`); console.log(` 95% : ${Latency.P95}`); console.log(` 90% : ${Latency.P90}`); console.log(` 85% : ${Latency.P85}`); console.log(` 75% : ${Latency.P75}`); console.log(` 50% : ${Latency.P50}`); console.log(` 10% : ${Latency.P10}`); } console.log(''); console.log('=== Instances ==='); InstanceHealthList.forEach(instance => { console.log(` ${instance.InstanceId}: ${(0, _utils.coloredStatusText)(instance.Color, instance.HealthStatus)}`); }); if (InstanceHealthList.length === 0) { console.log(' 0 Instances'); } } async function ssl(api) { const config = api.getConfig(); await (0, _envReady.waitForEnvReady)(config, true); if (!config.app || !config.app.sslDomains) { (0, _utils.logStep)('=> Updating Beanstalk SSL Config'); await (0, _certificates.default)(config); return; } (0, _utils.logStep)('=> Checking Certificate Status'); const domains = config.app.sslDomains; const { CertificateSummaryList } = await _aws.acm.listCertificates().promise(); let found = null; for (let i = 0; i < CertificateSummaryList.length; i++) { const { DomainName, CertificateArn } = CertificateSummaryList[i]; if (DomainName === domains[0]) { const { Certificate } = await _aws.acm.describeCertificate({ // eslint-disable-line no-await-in-loop CertificateArn }).promise(); if (domains.join(',') === Certificate.SubjectAlternativeNames.join(',')) { found = CertificateSummaryList[i]; } } } let certificateArn; if (!found) { (0, _utils.logStep)('=> Requesting Certificate'); const result = await _aws.acm.requestCertificate({ DomainName: domains.shift(), SubjectAlternativeNames: domains.length > 0 ? domains : null }).promise(); certificateArn = result.CertificateArn; } if (found) { certificateArn = found.CertificateArn; } let emailsProvided = false; let checks = 0; let certificate; /* eslint-disable no-await-in-loop */ while (!emailsProvided && checks < 5) { const { Certificate } = await _aws.acm.describeCertificate({ CertificateArn: certificateArn }).promise(); const validationOptions = Certificate.DomainValidationOptions[0]; if (typeof validationOptions.ValidationEmails === 'undefined') { emailsProvided = true; certificate = Certificate; } else if (validationOptions.ValidationEmails.length > 0 || checks === 6) { emailsProvided = true; certificate = Certificate; } else { checks += 1; await new Promise(resolve => { setTimeout(resolve, 1000 * 10); }); } } if (certificate.Status === 'PENDING_VALIDATION') { console.log('Certificate is pending validation.'); certificate.DomainValidationOptions.forEach(({ DomainName, ValidationEmails, ValidationDomain, ValidationStatus }) => { if (ValidationStatus === 'SUCCESS') { console.log(_chalk.default.green(`${ValidationDomain || DomainName} has been verified`)); return; } console.log(_chalk.default.yellow(`${ValidationDomain || DomainName} is pending validation`)); if (ValidationEmails) { console.log('Emails with instructions have been sent to:'); ValidationEmails.forEach(email => { console.log(` ${email}`); }); } console.log('Run "mup beanstalk ssl" after you have verified the domains, or to check the verification status'); }); } else if (certificate.Status === 'ISSUED') { console.log(_chalk.default.green('Certificate has been issued')); (0, _utils.logStep)('=> Updating Beanstalk SSL config'); await (0, _certificates.default)(config, certificateArn); } } async function shell(api) { const { selected, description } = await (0, _utils.pickInstance)(api.getConfig(), api.getArgs()[2]); if (!selected) { console.log(description); console.log('Run "mup beanstalk shell <instance id>"'); process.exitCode = 1; return; } const { sshOptions, removeSSHAccess } = await (0, _utils.connectToInstance)(api, selected, 'mup beanstalk shell'); const conn = new _ssh.Client(); conn.on('ready', () => { conn.exec('sudo node /home/webapp/meteor-shell.js', { pty: true }, (err, stream) => { if (err) { throw err; } stream.on('close', async () => { conn.end(); await removeSSHAccess(); process.exit(); }); process.stdin.setRawMode(true); process.stdin.pipe(stream); stream.pipe(process.stdout); stream.stderr.pipe(process.stderr); stream.setWindow(process.stdout.rows, process.stdout.columns); process.stdout.on('resize', () => { stream.setWindow(process.stdout.rows, process.stdout.columns); }); }); }).connect(sshOptions); } async function debug(api) { const config = api.getConfig(); const { selected, description } = await (0, _utils.pickInstance)(config, api.getArgs()[2]); if (!selected) { console.log(description); console.log('Run "mup beanstalk debug <instance id>"'); process.exitCode = 1; return; } const { sshOptions, removeSSHAccess } = await (0, _utils.connectToInstance)(api, selected, 'mup beanstalk debug'); const conn = new _ssh.Client(); conn.on('ready', async () => { const result = await (0, _utils.executeSSHCommand)(conn, 'sudo pkill -USR1 -u webapp -n node || sudo pkill -USR1 -u nodejs -n node'); if (api.verbose) { console.log(result.output); } const server = _objectSpread(_objectSpread({}, sshOptions), {}, { pem: api.resolvePath(config.app.sshKey.privateKey) }); let loggedConnection = false; api.forwardPort({ server, localAddress: '0.0.0.0', localPort: 9229, remoteAddress: '127.0.0.1', remotePort: 9229, onError(error) { console.error(error); }, onReady() { console.log('Connected to server'); console.log(''); console.log('Debugger listening on ws://127.0.0.1:9229'); console.log(''); console.log('To debug:'); console.log('1. Open chrome://inspect in Chrome'); console.log('2. Select "Open dedicated DevTools for Node"'); console.log('3. Wait a minute while it connects and loads the app.'); console.log(' When it is ready, the app\'s files will appear in the Sources tab'); console.log(''); console.log('Warning: Do not use breakpoints when debugging a production server.'); console.log('They will pause your server when hit, causing it to not handle methods or subscriptions.'); console.log('Use logpoints or something else that does not pause the server'); console.log(''); console.log('The debugger will be enabled until the next time the app is restarted,'); console.log('though only accessible while this command is running'); }, onConnection() { if (!loggedConnection) { // It isn't guaranteed the debugger is connected, but not many // other tools will try to connect to port 9229. console.log(''); console.log('Detected by debugger'); loggedConnection = true; } } }); }).connect(sshOptions); process.on('SIGINT', async () => { await removeSSHAccess(); process.exit(); }); } //# sourceMappingURL=command-handlers.js.map