将带有集群功能的 Node.js Express 应用程序部署到 Elastic Beanstalk - AWS Elastic Beanstalk

本文属于机器翻译版本。若本译文内容与英语原文存在差异,则一律以英文原文为准。

将带有集群功能的 Node.js Express 应用程序部署到 Elastic Beanstalk

本教程将引导您使用 Elastic Beanstalk 命令行CLI界面 (EB) 将示例应用程序部署到 Elastic Beanstalk,然后更新应用程序以使用 Express 框架、Amazon 和集群。 ElastiCache集群功能增强了 Web 应用程序的高可用性、性能和安全性。要了解有关亚马逊的更多信息 ElastiCache,请访问什么是亚马逊 ElastiCache (Memcached)? 在亚马逊 ElastiCache (Memcached)用户指南中。

注意

此示例创建了 AWS 资源,您可能需要为此付费。有关 AWS 定价的更多信息,请参阅https://aws.amazon.com/pricing/。有些服务属于 AWS 免费使用套餐的一部分。如果您是新客户,则可免费试用这些服务。请参阅https://aws.amazon.com/free/了解更多信息。

先决条件

本教程需要以下先决条件:

  • Node.js 运行时

  • 默认 Node.js 程序包管理器软件 npm

  • Express 命令行生成器

  • Elastic Beanstalk 命令行界面 (EB) CLI

有关安装列出的前三个组件和设置本地开发环境的详细信息,请参阅 为 Elastic Beanstalk 设置 Node.js 开发环境。在本教程中,您无需安装适用 AWS SDK于 Node.js 的,参考主题中也提到了这一点。

有关安装和配置 EB 的详细信息CLI,请参阅安装 Elastic Beanstalk 命令行界面配置 EB CLI

创建 Elastic Beanstalk 环境

您的应用程序目录

本教程为应用程序源包使用名为 nodejs-example-express-elasticache 的目录。为本教程创建 nodejs-example-express-elasticache 目录。

~$ mkdir nodejs-example-express-elasticache
注意

本章中的每个教程都为应用程序源包使用自己的目录。该目录名称与教程使用的示例应用程序的名称相匹配。

将您当前的工作目录更改为 nodejs-example-express-elasticache

~$ cd nodejs-example-express-elasticache

现在,让我们设置运行 Node.js 平台和示例应用程序的 Elastic Beanstalk 环境。我们将使用 Elastic Beanstalk 命令行界面 (EB)。CLI

为您的应用程序配置 EB CLI 存储库并创建运行 Node.js 平台的 Elastic Beanstalk 环境
  1. 使用 eb init 命令创建存储库。

    ~/nodejs-example-express-elasticache$ eb init --platform node.js --region <region>

    此命令在名为 .elasticbeanstalk 的文件夹中创建配置文件,该配置文件指定用于为您的应用程序创建环境的设置;并创建以当前文件夹命名的 Elastic Beanstalk 应用程序。

  2. 使用 eb create 命令创建运行示例应用程序的环境。

    ~/nodejs-example-express-elasticache$ eb create --sample nodejs-example-express-elasticache

    此命令使用 Node.js 平台的默认设置以及以下资源来创建负载均衡环境:

    • EC2实例 — 配置为在您选择的平台上运行 Web 应用程序的亚马逊弹性计算云 (AmazonEC2) 虚拟机。

      各平台运行一组特定软件、配置文件和脚本以支持特定的语言版本、框架、Web 容器或其组合。大多数平台都使用 Apache 或NGINX作为反向代理,它位于您的 Web 应用程序前面,向其转发请求,提供静态资产,并生成访问和错误日志。

    • 实例安全组 — 配置为允许端口 80 上的入站流量的 Amazon EC2 安全组。此资源允许来自负载均衡器的HTTP流量到达运行您的 Web 应用程序的EC2实例。默认情况下,其他端口不允许流量进入。

    • 负载均衡器 – 配置为向运行您的应用程序的实例分配请求的 Elastic Load Balancing 负载均衡器。负载均衡器还使您无需将实例直接公开在 Internet 上。

    • 负载均衡器安全组-配置为允许端口 80 上的入站流量的 Amazon EC2 安全组。此资源允许来自互联网的HTTP流量到达负载均衡器。默认情况下,其他端口不允许流量进入。

    • Auto Scaling 组 – 配置为在实例终止或不可用时替换实例的 Auto Scaling 组。

    • Amazon S3 存储桶 – 使用 Elastic Beanstalk 时创建的源代码、日志和其他构件的存储位置。

    • Amazon CloudWatch CloudWatch 警报 — 两个警报,用于监控您环境中实例的负载,并在负载过高或过低时触发。警报触发后,您的 Auto Scaling 组会扩展或收缩以进行响应。

    • AWS CloudFormation 堆栈 — Elastic AWS CloudFormation Beanstalk 用于在您的环境中启动资源并传播配置更改。这些资源在您可通过 AWS CloudFormation 控制台查看的模板中定义。

    • 域名-以以下形式路由到您的 Web 应用程序的域名 subdomain.region.elasticbeanstalk.com。

      注意

      为了增强 Elastic Beanstalk 应用程序的安全性,elasticbeanstalk.co m 域已在公共后缀列表 () 中注册。PSL为进一步增强安全性,如果您需要在 Elastic Beanstalk 应用程序的默认域名中设置敏感 Cookie,我们建议您使用带 __Host- 前缀的 Cookie。这种做法将有助于保护您的域名免受跨站请求伪造企图的侵害()CSRF。要了解更多信息,请参阅 Mozilla 开发者网络中的 Set-Cookie 页面。

  3. 环境创建完成后,使用eb open命令在默认浏览器URL中打开环境。

    ~/nodejs-example-express-elasticache$ eb open

您现在已经使用示例应用程序创建了 Node.js Elastic Beanstalk 环境。您可以使用自己的应用程序对其进行更新。接下来,我们会更新示例应用程序,以使用 Express 框架。

更新应用程序以使用 Express

更新 Elastic Beanstalk 环境中的示例应用程序以使用 Express 框架。

您可以从 nodejs-example-express-elasticache.zip 下载最终源代码。

更新您的应用程序以使用 Express

在创建具有示例应用程序的环境后,可将其更新为自己的应用程序。在此过程中,首先运行 expressnpm install 命令,以在您的应用程序目录中设置 Express 框架。

  1. 运行 express 命令。这将生成 package.jsonapp.js,以及几个目录。

    ~/nodejs-example-express-elasticache$ express

    在系统提示您是否要继续时,键入 y

    注意

    如果 express 命令不起作用,则您可能没有按照前面的先决条件部分所述安装 Express 命令行生成器。或者,可能需要设置本地计算机的目录路径设置才能运行 express 命令。有关设置开发环境的详细步骤,请参阅先决条件部分,以便您可以继续学习本教程。

  2. 设置本地依赖项。

    ~/nodejs-example-express-elasticache$ npm install
  3. (可选)验证 Web 应用程序服务器已启动。

    ~/nodejs-example-express-elasticache$ npm start

    您应该可以看到类似于如下所示的输出内容:

    > nodejs@0.0.0 start /home/local/user/node-express > node ./bin/www

    默认情况下,服务器在端口 3000 上运行。要对其进行测试,请在另一台终端上运行,或者curl http://localhost:3000在本地计算机上打开浏览器并输入URL地址http://localhost:3000

    Ctrl+C 以停止该服务器。

  4. nodejs-example-express-elasticache/app.js 重命名为 nodejs-example-express-elasticache/express-app.js

    ~/nodejs-example-express-elasticache$ mv app.js express-app.js
  5. nodejs-example-express-elasticache/express-app.js 中的行 var app = express(); 更新为以下内容:

    var app = module.exports = express();
  6. 在本地计算机上,使用以下代码创建一个名为 nodejs-example-express-elasticache/app.js 的文件。

    /** * Module dependencies. */ const express = require('express'), session = require('express-session'), bodyParser = require('body-parser'), methodOverride = require('method-override'), cookieParser = require('cookie-parser'), fs = require('fs'), filename = '/var/nodelist', app = express(); let MemcachedStore = require('connect-memcached')(session); function setup(cacheNodes) { app.use(bodyParser.raw()); app.use(methodOverride()); if (cacheNodes.length > 0) { app.use(cookieParser()); console.log('Using memcached store nodes:'); console.log(cacheNodes); app.use(session({ secret: 'your secret here', resave: false, saveUninitialized: false, store: new MemcachedStore({ 'hosts': cacheNodes }) })); } else { console.log('Not using memcached store.'); app.use(session({ resave: false, saveUninitialized: false, secret: 'your secret here' })); } app.get('/', function (req, resp) { if (req.session.views) { req.session.views++ resp.setHeader('Content-Type', 'text/html') resp.send(`You are session: ${req.session.id}. Views: ${req.session.views}`) } else { req.session.views = 1 resp.send(`You are session: ${req.session.id}. No views yet, refresh the page!`) } }); if (!module.parent) { console.log('Running express without cluster. Listening on port %d', process.env.PORT || 5000) app.listen(process.env.PORT || 5000) } } console.log("Reading elastic cache configuration") // Load elasticache configuration. fs.readFile(filename, 'UTF8', function (err, data) { if (err) throw err; let cacheNodes = [] if (data) { let lines = data.split('\n'); for (let i = 0; i < lines.length; i++) { if (lines[i].length > 0) { cacheNodes.push(lines[i]) } } } setup(cacheNodes) }); module.exports = app;
  7. nodejs-example-express-elasticache/bin/www 文件的内容替换为以下内容:

    #!/usr/bin/env node /** * Module dependencies. */ const app = require('../app'); const cluster = require('cluster'); const debug = require('debug')('nodejs-example-express-elasticache:server'); const http = require('http'); const workers = {}, count = require('os').cpus().length; function spawn() { const worker = cluster.fork(); workers[worker.pid] = worker; return worker; } /** * Get port from environment and store in Express. */ const port = normalizePort(process.env.PORT || '3000'); app.set('port', port); if (cluster.isMaster) { for (let i = 0; i < count; i++) { spawn(); } // If a worker dies, log it to the console and start another worker. cluster.on('exit', function (worker, code, signal) { console.log('Worker ' + worker.process.pid + ' died.'); cluster.fork(); }); // Log when a worker starts listening cluster.on('listening', function (worker, address) { console.log('Worker started with PID ' + worker.process.pid + '.'); }); } else { /** * Create HTTP server. */ let server = http.createServer(app); /** * Event listener for HTTP server "error" event. */ function onError(error) { if (error.syscall !== 'listen') { throw error; } const bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; // handle specific listen errors with friendly messages switch (error.code) { case 'EACCES': console.error(bind + ' requires elevated privileges'); process.exit(1); break; case 'EADDRINUSE': console.error(bind + ' is already in use'); process.exit(1); break; default: throw error; } } /** * Event listener for HTTP server "listening" event. */ function onListening() { const addr = server.address(); const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; debug('Listening on ' + bind); } /** * Listen on provided port, on all network interfaces. */ server.listen(port); server.on('error', onError); server.on('listening', onListening); } /** * Normalize a port into a number, string, or false. */ function normalizePort(val) { const port = parseInt(val, 10); if (isNaN(port)) { // named pipe return val; } if (port >= 0) { // port number return port; } return false; }
  8. 使用 eb deploy 命令将更改部署到您的 Elastic Beanstalk 环境。

    ~/nodejs-example-express-elasticache$ eb deploy
  9. 您的环境将在几分钟后进行更新。环境变为绿色并准备就绪后,请刷新URL以验证其是否正常运行。您应看到一个显示“欢迎使用 Express”的网页。

您可以访问运行您的应用程序的EC2实例的日志。有关访问日志的说明,请参阅在 Elastic Beanstalk 环境中查看来自亚马逊EC2实例的日志

接下来,让我们更新 Express 应用程序以使用亚马逊 ElastiCache。

更新您的 Express 应用程序以使用亚马逊 ElastiCache
  1. 在本地计算机上的源包顶级目录中,创建 .ebextensions 目录。在此示例中,我们使用的是 nodejs-example-express-elasticache/.ebextensions

  2. 使用以下代码段创建配置文件 nodejs-example-express-elasticache/.ebextensions/elasticache-iam-with-script.config。有关配置文件的更多信息,请参阅Node.js 配置命名空间。这将创建一个拥有发现 elasticache 节点所需权限的IAM用户,并在缓存发生变化时向文件写入内容。您也可以从 nodejs-example-express-elasticache.zip 中复制该文件。有关 ElastiCache属性的更多信息,请参阅示例:ElastiCache

    注意

    YAML依赖于一致的缩进。当替换示例配置文件中的内容时,应匹配缩进级别,并且确保您的文本编辑器使用空格而不是字符来进行缩进。

    Resources: MyCacheSecurityGroup: Type: 'AWS::EC2::SecurityGroup' Properties: GroupDescription: "Lock cache down to webserver access only" SecurityGroupIngress: - IpProtocol: tcp FromPort: Fn::GetOptionSetting: OptionName: CachePort DefaultValue: 11211 ToPort: Fn::GetOptionSetting: OptionName: CachePort DefaultValue: 11211 SourceSecurityGroupName: Ref: AWSEBSecurityGroup MyElastiCache: Type: 'AWS::ElastiCache::CacheCluster' Properties: CacheNodeType: Fn::GetOptionSetting: OptionName: CacheNodeType DefaultValue: cache.t2.micro NumCacheNodes: Fn::GetOptionSetting: OptionName: NumCacheNodes DefaultValue: 1 Engine: Fn::GetOptionSetting: OptionName: Engine DefaultValue: redis VpcSecurityGroupIds: - Fn::GetAtt: - MyCacheSecurityGroup - GroupId AWSEBAutoScalingGroup : Metadata : ElastiCacheConfig : CacheName : Ref : MyElastiCache CacheSize : Fn::GetOptionSetting: OptionName : NumCacheNodes DefaultValue: 1 WebServerUser : Type : AWS::IAM::User Properties : Path : "/" Policies: - PolicyName: root PolicyDocument : Statement : - Effect : Allow Action : - cloudformation:DescribeStackResource - cloudformation:ListStackResources - elasticache:DescribeCacheClusters Resource : "*" WebServerKeys : Type : AWS::IAM::AccessKey Properties : UserName : Ref: WebServerUser Outputs: WebsiteURL: Description: sample output only here to show inline string function parsing Value: | http://`{ "Fn::GetAtt" : [ "AWSEBLoadBalancer", "DNSName" ] }` MyElastiCacheName: Description: Name of the elasticache Value: Ref : MyElastiCache NumCacheNodes: Description: Number of cache nodes in MyElastiCache Value: Fn::GetOptionSetting: OptionName : NumCacheNodes DefaultValue: 1 files: "/etc/cfn/cfn-credentials" : content : | AWSAccessKeyId=`{ "Ref" : "WebServerKeys" }` AWSSecretKey=`{ "Fn::GetAtt" : ["WebServerKeys", "SecretAccessKey"] }` mode : "000400" owner : root group : root "/etc/cfn/get-cache-nodes" : content : | # Define environment variables for command line tools export AWS_ELASTICACHE_HOME="/home/ec2-user/elasticache/$(ls /home/ec2-user/elasticache/)" export AWS_CLOUDFORMATION_HOME=/opt/aws/apitools/cfn export PATH=$AWS_CLOUDFORMATION_HOME/bin:$AWS_ELASTICACHE_HOME/bin:$PATH export AWS_CREDENTIAL_FILE=/etc/cfn/cfn-credentials export JAVA_HOME=/usr/lib/jvm/jre # Grab the Cache node names and configure the PHP page aws cloudformation list-stack-resources --stack `{ "Ref" : "AWS::StackName" }` --region `{ "Ref" : "AWS::Region" }` --output text | grep MyElastiCache | awk '{print $4}' | xargs -I {} aws elasticache describe-cache-clusters --cache-cluster-id {} --region `{ "Ref" : "AWS::Region" }` --show-cache-node-info --output text | grep '^ENDPOINT' | awk '{print $2 ":" $3}' > `{ "Fn::GetOptionSetting" : { "OptionName" : "NodeListPath", "DefaultValue" : "/var/www/html/nodelist" } }` mode : "000500" owner : root group : root "/etc/cfn/hooks.d/cfn-cache-change.conf" : "content": | [cfn-cache-size-change] triggers=post.update path=Resources.AWSEBAutoScalingGroup.Metadata.ElastiCacheConfig action=/etc/cfn/get-cache-nodes runas=root sources : "/home/ec2-user/elasticache" : "https://elasticache-downloads.s3.amazonaws.com/AmazonElastiCacheCli-latest.zip" commands: make-elasticache-executable: command: chmod -R ugo+x /home/ec2-user/elasticache/*/bin/* packages : "yum" : "aws-apitools-cfn" : [] container_commands: initial_cache_nodes: command: /etc/cfn/get-cache-nodes
  3. 在您的本地计算机上,使用以下代码段创建配置文件nodejs-example-express-elasticache/.ebextensions/elasticache_settings.config进行配置 ElastiCache。

    option_settings: "aws:elasticbeanstalk:customoption": CacheNodeType: cache.t2.micro NumCacheNodes: 1 Engine: memcached NodeListPath: /var/nodelist
  4. 在本地计算机上,使用以下代码段替换 nodejs-example-express-elasticache/express-app.js。此文件会从磁盘读取节点列表 (/var/nodelist) 并配置 Express,以便在节点存在的情况下将 memcached 用作会话存储。您的文件应类似以下内容。

    /** * Module dependencies. */ var express = require('express'), session = require('express-session'), bodyParser = require('body-parser'), methodOverride = require('method-override'), cookieParser = require('cookie-parser'), fs = require('fs'), filename = '/var/nodelist', app = module.exports = express(); var MemcachedStore = require('connect-memcached')(session); function setup(cacheNodes) { app.use(bodyParser.raw()); app.use(methodOverride()); if (cacheNodes) { app.use(cookieParser()); console.log('Using memcached store nodes:'); console.log(cacheNodes); app.use(session({ secret: 'your secret here', resave: false, saveUninitialized: false, store: new MemcachedStore({'hosts': cacheNodes}) })); } else { console.log('Not using memcached store.'); app.use(cookieParser('your secret here')); app.use(session()); } app.get('/', function(req, resp){ if (req.session.views) { req.session.views++ resp.setHeader('Content-Type', 'text/html') resp.write('Views: ' + req.session.views) resp.end() } else { req.session.views = 1 resp.end('Refresh the page!') } }); if (!module.parent) { console.log('Running express without cluster.'); app.listen(process.env.PORT || 5000); } } // Load elasticache configuration. fs.readFile(filename, 'UTF8', function(err, data) { if (err) throw err; var cacheNodes = []; if (data) { var lines = data.split('\n'); for (var i = 0 ; i < lines.length ; i++) { if (lines[i].length > 0) { cacheNodes.push(lines[i]); } } } setup(cacheNodes); });
  5. 在本地计算机上,使用以下内容更新 package.json

    "dependencies": { "cookie-parser": "~1.4.4", "debug": "~2.6.9", "express": "~4.16.1", "http-errors": "~1.6.3", "jade": "~1.11.0", "morgan": "~1.9.1", "connect-memcached": "*", "express-session": "*", "body-parser": "*", "method-override": "*" }
  6. 运行 npm install

    ~/nodejs-example-express-elasticache$ npm install
  7. 部署更新的应用程序。

    ~/nodejs-example-express-elasticache$ eb deploy
  8. 您的环境将在几分钟后进行更新。在环境变为绿色并准备就绪后,验证代码是否正常。

    1. 查看 A mazon CloudWatch 控制台以查看您的 ElastiCache 指标。要查看您的 ElastiCache 指标,请在左侧窗格中选择指标,然后搜索CurrItems。选择 ElastiCache > 缓存节点指标,然后选择您的缓存节点以查看缓存中的项目数量。

      CloudWatch dashboard showing CurrItems metric for an ElastiCache node over time.
      注意

      确保您查看的是您的应用程序所部署到的相同地区。

      如果您URL将应用程序复制并粘贴到其他 Web 浏览器中并刷新页面,则应该会在 5 分钟后看到 CurrItem 计数上升。

    2. 制作日志的快照。有关检索日志的更多信息,请参阅在 Elastic Beanstalk 环境中查看来自亚马逊EC2实例的日志

    3. 检查日志文件包中的文件 /var/log/nodejs/nodejs.log。您应看到类似如下所示的内容:

      Using memcached store nodes: [ 'aws-my-1oys9co8zt1uo.1iwtrn.0001.use1.cache.amazonaws.com:11211' ]

清理

如果不再希望运行您的应用程序,您可通过终止环境并删除应用程序进行清除。

请使用 eb terminate 命令终止环境并使用 eb delete 命令删除应用程序。

终止环境

从您在其中创建本地存储库的目录中,运行 eb terminate

$ eb terminate

此过程可能耗时数分钟。成功终止环境后,Elastic Beanstalk 会立即显示一条消息。