将具有集群功能的 Express 应用程序部署到 Elastic Beanstalk - AWS Elastic Beanstalk

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

将具有集群功能的 Express 应用程序部署到 Elastic Beanstalk

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

注意

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

先决条件

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

  • Node.js 运行时

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

  • Express 命令行生成器

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

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

有关安装和配置 EB CLI 的详细信息,请参阅 安装 EB CLI配置 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 应用程序的 Amazon Elastic Compute Cloud(Amazon EC2)虚拟机。

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

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

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

    • 负载均衡器安全组 – 配置为允许端口 80 上的入站流量的 Amazon EC2 安全组。利用此资源,HTTP 流量可从 Internet 到达负载均衡器。默认情况下,其他端口不允许流量进入。

    • 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.com 域注册到 公共后缀列表(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 环境中的 Amazon 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 配置命名空间。此配置文件会创建一个 IAM 用户(该用户拥有发现 ElastiCache 节点所需的权限),只要缓存发生变化就向某个文件写入数据。您也可以从 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 > 缓存节点指标,然后选择您的缓存节点以查看缓存中的项目数量。

      注意

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

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

    2. 制作日志的快照。有关检索日志的更多信息,请参阅查看您的 Elastic Beanstalk 环境中的 Amazon 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 会立即显示一条消息。