Skip to content

Latest commit

 

History

History
1039 lines (520 loc) · 48 KB

File metadata and controls

1039 lines (520 loc) · 48 KB

五、简单的存储服务和内容交付

既然我们的应用已经在 Web 上运行,是时候构建一些核心功能了:图片上传。人们会认为我们可以从一开始就这样做,但事实并非如此。虽然我们已经使用了许多 AWS 服务,但只编写了很少的代码,这个功能是一个例外。我们正在基于我们的 AWS 架构构建我们的照片上传和查看功能。为此,我们将首次尝试使用 AWS SDK 以编程方式直接与 AWS 服务进行交互。

我们将在控制台中做一些工作,然后快速将 SDK 添加到我们的应用包中,并开始编码。我们将不得不创建一个 S3(简单存储服务) 1 桶,这个 AWS 服务旨在为静态资产提供文件存储。我们还必须配置 S3 存储桶和相应的 IAM 策略。

如果我们的应用开始运行,我们预计会有成千上万的文件上传和下载。如果我们将这些文件存储在 EC2 实例上,将会产生大量的问题。首先,我们的介质所消耗的巨大磁盘空间要求我们扩大实例存储容量。事实上,每个实例都需要每个映像的副本,因此这种冗余会造成大量资源浪费。其次,如果我们的实例负责向用户发送图像,纯粹是在检索和发送所有内容所使用的内存中,这可能会导致严重的瓶颈。

第三,这将产生一个主要的同步问题。存储在 EC2 实例上的所有数据都是短暂的,持久数据存储在我们的 RDS 数据库中。如果一个用户上传一张照片到一个实例,就会产生一种情况,即图像必须被复制到所有其他正在运行的实例。简而言之,使用实例来上传文件是个坏主意。

在这一章中,我们将通过使用 S3 存储静态内容来避开这些问题。因为 S3 提供了无限文件存储的高可用性和冗余性,所以我们可以将所有静态内容保存在一个地方。我们将修改我们的应用,以便它上传图像到 S3 桶。有了 AWS SDK,我们将能够以编程方式上传文件,设置它们的权限,并从公共 URL 访问它们。然后,我们将使用 CloudFront 将图像分发给我们的用户。

在应用中使用 S3

使用 S3 进行静态内容存储将省去我们前面描述的许多麻烦,并显著减轻应用的整体重量。此外,如果我们不得不更换实例、堆栈或数据库,我们可以依靠 S3 保持独立于我们的应用堆栈的服务。虽然 S3 存储桶是在特定区域创建的,但它们在该区域内是冗余的,从而最大限度地降低了可用性区域中断带来的问题风险。

当然,由于 S3 存储桶是在特定地区创建的,因此图像有可能存储在远离用户的地方。这就是 CloudFront 可以帮助我们的地方,它将我们的文件副本存储在边缘位置。为了最大化 CloudFront 的有效性,我们将在我们的文件命名中使用版本控制,并让我们的图像在 CloudFront 中有较长的生命周期。这两种服务将协同工作,作为通常所说的内容交付网络,或 CDN。

注意图 5-1 。这是我们之前看到的系统图的更新。这一次,它已被更新,以反映 S3 桶将发挥的新作用。虽然 CloudFront 收到的许多请求将被路由到负载均衡器,但对媒体的请求将完全绕过应用堆栈,转而到达 S3 存储桶。同样,我们的应用堆栈中的 EC2 实例将能够直接连接到 S3 存储桶,向其传输文件。

A978-1-4842-0653-9_5_Fig1_HTML.gif

图 5-1。

Our system, updated for this lesson

创建 S3 存储桶

首先,我们将在 AWS 控制台中创建一个 S3 存储桶。登录并导航到 S3。如果你以前没有来过这里,它有点类似于 Route 53 界面:左边是铲斗列表,右边是详细视图。点按左上角的“创建存储桶”按钮。将出现一个模态视图,提示您命名存储桶并选择一个区域。给你的桶起个名字,比如相册-cdn。

Note

S3 标识符是唯一的,因此您不能将您的存储桶命名为与前面的文本完全相同。尝试一些类似的也能反映你喜好的东西。

从地区下拉列表中,选择美国标准(地区名称不符合我们习惯的美国东、美国西的惯例),如图 5-2 所示。

A978-1-4842-0653-9_5_Fig2_HTML.jpg

图 5-2。

Selecting a bucket name and region

现在,您可以选择立即创建存储桶或为存储桶设置日志记录。对于生产应用,您可能会发现日志记录很有用,所以现在让我们启用它。单击设置日志记录➤.在下一个视图中,选择您的存储桶作为存储日志的目标存储桶(参见图 5-3 )。保持其他字段不变,然后单击“创建”。

A978-1-4842-0653-9_5_Fig3_HTML.jpg

图 5-3。

Setting up logging for your bucket

S3 桶本身很容易交互。每个存储桶都包含自己的目录结构。您可以根据需要创建任意多的目录,并根据需要对它们进行深度嵌套。就文件管理而言,在控制台中与 S3 存储桶进行交互有一些限制。您不能将文件从一个目录移动到另一个目录;你必须下载并重新上传到新的目录。除非先删除目录或存储桶的内容,否则不能删除目录或存储桶。相反,AWS 建议您在开始设计系统时就确定文件的生命周期。例如,如果您想在 30 天后删除日志文件,最好现在就确定并相应地配置存储桶的行为。但这本身就是一个完整的话题。

现在我们已经创建了我们的 bucket,我们需要一个地方来存储用户上传的照片。让我们简单地将这些文件存储在一个/uploads目录中。单击您的存储桶以访问存储桶详细信息视图。和以前一样,您将在左侧看到一个列表(这次是目录和文件的列表),在右侧看到属性/其他详细信息。

在左上角,点按“创建文件夹”按钮。一个未命名的文件夹将出现在列表中,其名称可以编辑,就像在 Finder for Mac 或 Windows Explorer for Windows 中一样。继续输入上传的名字。你现在应该在你的桶中看到两个目录:logsuploads(见图 5-4 )。

A978-1-4842-0653-9_5_Fig4_HTML.jpg

图 5-4。

Bucket contents list

在 IAM 中启用 S3 访问

到目前为止,我们需要转向身份和访问管理来配置我们的应用,以便将资产上传到我们的 S3 存储桶,这应该不足为奇。在第一章中,有一个可选课程,您可能已经创建了一个 IAM 用户,该用户具有从 S3 存储桶进行读/写的权限。我们再次需要类似的功能,尽管这里有不止一种可能的方法。

存储凭据

主要的问题不是我们是否可以通过 AWS SDK 让我们的实例访问 S3 桶,而是当我们这样做时,我们如何管理凭证。根据 Amazon 的说法,在代码中使用凭证时,有一个最佳实践的层次结构。在 JavaScript SDK 文档( http://docs.aws.amazon.com/AWSJavaScriptSDK/guide/node-configuring.html )中,亚马逊按如下顺序提供了他们的建议:

Loaded from IAM roles for Amazon EC2 (if running on EC2),   Loaded from the shared credentials file ( ∼/.aws/credentials ),   Loaded from environment variables,   Loaded from a JSON file on disk,   Hardcoded in your application  

前三个建议是特定于 AWS 环境的,而后两个是更常见的、与平台无关的技术。虽然我们希望遵循亚马逊的建议,但有一个问题是,这将削弱我们在当地环境中的开发能力。这不是亚马逊独有的缺陷,许多平台即服务(PaaS)提供商都会遇到。

第一个建议是“从 Amazon EC2 的 IAM 角色加载”,实现起来并不难。作为首选和最简单的建议,我们将很快实施这一策略。然而,我们希望在本地开发环境中保留尽可能多的功能。没有办法在您的本地环境中模拟 IAM EC2 角色,因此我们必须采用不同的方法来实现本地开发。但是,我们可以创建一个 IAM 用户,并在本地环境中通过 AWS SDK 使用其凭证。为此,我们也将支持第四项建议。

实施 IAM 角色

让我们首先实现 IAM 角色方法。首先,让我们回到 OpsWorks 来唤起你的记忆。导航至您的应用堆栈,并点击堆栈设置(参见图 5-5 )。

A978-1-4842-0653-9_5_Fig5_HTML.jpg

图 5-5。

Access application stack settings via the Stack Settings button at the top right

在 Stack Settings 视图中,您将看到我们在开始时做出的许多配置决策(参见图 5-6 )。您会发现一个名为 Default IAM instance profile 的设置,它应该设置为一个以 AWS-ops works-photo albums-ec2-role 结尾的长标识符。当我们在前面设置这个值时,我们创建了 IAM 角色,它被部署到应用堆栈中的每个 EC2 实例。这么多工作已经完成了!

A978-1-4842-0653-9_5_Fig6_HTML.jpg

图 5-6。

Stack Settings

导航到 IAM,并从左侧导航栏中选择角色。在角色列表中,选择 AWS-ops works-photo albums-ec2-role。在 Permissions 标题下,您将看到我们尚未为此角色创建任何策略。我们根本不需要。我们现在必须授予该角色对 S3 存储桶的读/写权限。为了清楚起见,我们将启用对 S3 的全局访问,但将来我们可能希望限制对特定 S3 存储桶的访问。正如你所看到的,我们再次面临对我们的实践和组织方法做出高度主观的决定。与其说是科学,不如说是艺术。

单击附加策略以转至策略选择视图。在策略表中向下滚动,直到找到 AmazonS3FullAccess 并选中它,如图 5-7 所示。

A978-1-4842-0653-9_5_Fig7_HTML.jpg

图 5-7。

Policy Template—S3 Full Access

单击右下角的应用策略。您将返回到用户详细信息视图,在该视图中,您将看到您的策略被列出(参见图 5-8 )。

A978-1-4842-0653-9_5_Fig8_HTML.jpg

图 5-8。

EC2 Service Role Permissions

就这样!现在,当我们使用 AWS SDK 时,它将自动检测 EC2 服务角色的权限,并使用这些凭证。您可以理解为什么 Amazon 推荐这种方法:凭证不会暴露给任何人,人为错误的风险可以忽略不计。

Note

稍后,我们可能希望我们的应用不仅仅拥有 S3 权限。我们将简单地返回到这个 IAM 角色,并附加额外的策略来扩展权限。

使用 IAM 用户凭据

对于我们的第二种方法,我们将创建可用于本地开发的凭证,但我们不会签入到我们的存储库中或部署到生产中。

回顾前面的 IAM 课程,我讨论过您总是可以选择创建 IAM 用户并配置其权限,或者创建组权限并将用户添加到组中。同样,我们可以选择任何一种方法。至此,我已经介绍了如何创建 IAM 用户、组和角色,因此几乎没有必要重复每一项。如果要创建 IAM 组,请为其提供适当的策略,并在该组中创建一个用户。随意这样做,锻炼你已经学到的东西。为了简洁起见,我们将简单地创建一个 IAM 用户,并将策略应用于用户本身。

在 IAM 左侧导航栏中选择“用户”,然后单击“创建新用户”。当提示输入名称时,输入 photoalbums-stack。确保选中了生成访问密钥的框,然后单击创建。在下一个屏幕上,单击下载凭据。文件下载时,保密;保管好它。然后,单击关闭返回到用户列表。

单击您的用户进入用户详细信息视图。我们有一个用户和它的凭证,现在我们只需要给它必要的权限。我们将遵循与为 IAM 角色创建策略时完全相同的步骤。在权限标题下,单击附加策略。我们回到了托管策略选择视图。向下滚动,直到找到 AmazonS3FullAccess。选择它旁边的框,然后单击“Attach Policy”按钮。您将返回到用户默认视图,在这里您将看到您的策略已被添加。

我们已经完成了这两个 IAM 方法,我们将很快在代码中使用它们。在此之前,我们必须在奥普斯沃斯做短暂停留。

添加 OpsWorks 环境变量

第 3 章中,我们通过将应用层连接到数据库层,将数据库凭证从源代码中移出,放入由 OpsWorks 生成的文件中。不幸的是,我们不能用 S3 桶做同样的事情。但是,您可能还记得,我们在代码中使用了 OpsWorks 环境变量来确定应该在哪里查找这些数据库凭证。我们将使用类似的方法连接到我们的 S3 桶。我们不必将 IAM 凭证存储在环境变量中,但是我们应该为 S3 存储区名称这样做。这样做的原因很简单:这将使更改我们的应用中的 S3 桶变得容易,并且使创建新的应用堆栈变得容易。如果我们必须创建一个开发堆栈,或者出于任何原因创建一个副本,我们也可以创建一个新的 S3 桶,并在 OpsWorks 中轻松地交换名称。

在 AWS 控制台中导航到 OpsWorks 并选择您的应用堆栈。使用导航菜单,选择应用。当您看到应用列表视图时,单击相册应用旁边的编辑(参见图 5-9 )。

A978-1-4842-0653-9_5_Fig9_HTML.jpg

图 5-9。

Return to OpsWorks Apps list view

在应用编辑视图中,向下滚动到环境变量标题(参见图 5-10 )。您应该会看到已经创建的名为“环境”的变量添加一个变量,其键为 S3BUCKET,值等于您之前创建的 S3 存储桶的名称。然后点击右下角的保存。

A978-1-4842-0653-9_5_Fig10_HTML.jpg

图 5-10。

New Environment Variables

当我们重新部署我们的应用时,环境变量将是可访问的。但是在开始编码之前,我们还有一个任务,那就是更改实例的默认服务器配置。

使用 AWS SDK 进行开发

到目前为止,我们的 AWS 课程完全依赖于 AWS 控制台。这并不意味着你必须使用它,但它只是更容易学习和更容易教。事实上,我们到目前为止所做的大部分工作也可以使用 AWS SDK 以编程方式实现。在某些情况下,在 AWS 控制台中工作要快得多。在其他情况下,特别是对于您希望自动化的任务,使用 SDK 并编写您想要的行为会更有意义。

AWS SDK 有多种语言和平台,包括 JavaScript,这将是我们的选择。你可以在这里找到 AWS SDK 工具的完整列表: http://aws.amazon.com/tools/ 。安装 AWS SDK 很容易。

更新相关性

我们必须将 AWS SDK 添加到我们的应用中,以及 multer 中间件包。对于以前用过 ExpressJS 的人来说,ExpressJS 第 4 版有点不一样。几个中间件依赖项已经被移除,我们必须根据我们需要支持的功能将它们分别添加到我们的包中。我们将使用 multer 来接受文件上传并将它们写入一个临时目录。

在代码编辑器中,打开根目录中的package.json。在依赖项列表的开始,您将添加 AWS SDK Node 模块和 multer,因此您的依赖项 JSON 应该类似于清单 5-1

Listing 5-1. package.json Dependencies

"dependencies": {

"aws-sdk": "2.0.*",

"multer": "^0.1.3",

"express": "∼4.8.6",

"body-parser": "∼1.6.6",

"cookie-parser": "∼1.3.2",

"mysql": "2.0.*",

"morgan": "∼1.2.3",

"serve-favicon": "∼2.0.1",

"debug": "∼1.0.4",

"jade": "∼1.5.0"

}

接下来,我们必须在本地重新安装我们的应用,以安装新的软件包。在命令行界面中,导航到项目目录并键入以下内容:

npm install

aws-sdkmulterNode 模块及其各自的依赖项应该开始下载,这将向控制台打印诸如清单 5-2 之类的内容。

Listing 5-2. AWS SDK Installing

npm http GET https://registry.npmjs.org/aws-sdk

npm http 200 https://registry.npmjs.org/aws-sdk

npm http GET https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.0.29.tgz

npm http 200 https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.0.29.tgz

npm http GET https://registry.npmjs.org/xml2js/0.2.6

npm http GET https://registry.npmjs.org/xmlbuilder/0.4.2

npm http 200 https://registry.npmjs.org/xml2js/0.2.6

npm http GET https://registry.npmjs.org/xml2js/-/xml2js-0.2.6.tgz

npm http 200 https://registry.npmjs.org/xmlbuilder/0.4.2

npm http GET https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.2.tgz

npm http 200 https://registry.npmjs.org/xml2js/-/xml2js-0.2.6.tgz

npm http 200 https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.2.tgz

npm http GET https://registry.npmjs.org/sax/0.4.2

npm http 200 https://registry.npmjs.org/sax/0.4.2

npm http GET https://registry.npmjs.org/sax/-/sax-0.4.2.tgz

npm http 200 https://registry.npmjs.org/sax/-/sax-0.4.2.tgz

aws-sdk@2.0.29 node_modules/aws-sdk

■t0]

ε──t0″

一旦安装完成,您就可以开始在代码中使用aws-sdk模块。下次部署代码时,它也会自动安装在 EC2 实例上。让我们开始编写上传代码吧!

首先,我们必须配置 express app 实例来使用 multer。打开/server.js,在顶部的变量声明中添加下面一行,如下所示:

var multer = require('multer');

“header”(不是真的,而是精神上的)现在看起来应该类似于清单 5-3

Listing 5-3. The server.js “Header”

var express = require('express');

var path = require('path');

var favicon = require('serve-favicon');

var logger = require('morgan');

var cookieParser = require('cookie-parser');

var bodyParser = require('body-parser');

var multer = require('multer');

var debug = require('debug')('photoalbums');

var routes = require('./routes/index');

var users = require('./routes/users');

var photos = require('./routes/photos');

var albums = require('./routes/albums');

var globals = require('./lib/globals');

var mysql = require('mysql');

var app = express();

接下来,我们将告诉express实例使用 multer 作为中间件,并将文件上传的目的地作为参数传入。在代码中再往下一点,您会看到一系列配置expressapp.use()语句。在app.use(bodyParser{...})之后,增加以下内容:

app.use(multer({dest: './tmp/'}));

您的app.use()块现在应该如清单 5-4 所示。

Listing 5-4. server.js Express App Configuration

app.use(logger('dev'));

app.use(bodyParser.json());

app.use(bodyParser.urlencoded({ extended: false }));

app.use(multer({dest: './tmp/'}));

app.use(cookieParser());

app.use(express.static(path.join(__dirname, 'public')));

就像我们在这里完成的那样,将您的更改保存到文件中。接下来,我们希望设置我们的应用来使用我们创建的 OpsWorks 环境变量。我们将使用类似的方法来访问数据库凭证。如果代码在生产环境中运行,我们将使用环境变量。否则,我们将默认使用凭据的本地副本。

访问环境变量

在本地凭证的情况下,这意味着凭证仍然会向开发人员公开。但是因为 IAM 凭证和 bucket 名称都存储在环境变量中,所以您可以很容易地为本地开发创建一个单独的 bucket 和 IAM 用户,限制该用户对单个 dev bucket 的访问。如果您对练习这种级别的安全性感兴趣,您可以自己尝试一下。

打开/lib/globals.js。我们将增加一个与database()几乎相同的功能,叫做awsVariables()。正如您所想象的,我们将再次检查ENVIRONMENT变量,如果它在那里,就使用我们创建的新变量。如果没有定义,我们将加载一个本地配置。您希望您的globals文件看起来像清单 5-5

Listing 5-5. Complete /lib/globals.js

module.exports = {

applicationPort  : 80,

database : function(){

if(process.env.ENVIRONMENT){

var opsworks = require('./../opsworks');

var opsWorksDB = opsworks.db;

var rdsConnection = {

host : opsWorksDB.host,

port : opsWorksDB.port,

database : opsWorksDB.database,

user : opsWorksDB.username,

password : opsWorksDB.password

};

return rdsConnection;

} else {

var local = require('./../config/local');

var localConnection = local.db;

return localConnection;

}

},

awsVariables : function(){

if(process.env.ENVIRONMENT){

var variables = {

bucket : process.env.S3BUCKET

}

return variables;

} else {

var local = require('./../config/local');

return local.awsVariables;

}

}

}

接下来,我们必须更新本地配置文件。打开/config/local.js,你将在其中添加一个awsVariables对象。这个对象的属性应该映射到/lib/globals.js中的属性,因此您的代码应该类似于清单 5-6 ,使用我们为 IAM 用户生成的密钥/秘密。虽然由您来决定如何管理这些凭证,但是请记住,如果您不愿意,您不需要将它们提交给存储库,或者您可以使用空字符串作为键和密码值来提交文件。

Listing 5-6. Local Config File

module.exports = {

db :  {

host : 'localhost',

port : 3306,

database : 'photoalbums',

user  : 'root',

password : 'root'

},

awsVariables : {

bucket : 'photoalbums-cdn',

key : 'AKIAINJDCDGH3TBMN7AA',

secret : '8RJHMIGriShsKjgs;8W3B8gIRXC/v0QXDhcVH2RwMAw'

}

}

处理文件上传

现在我们已经完成了所有的配置,可以开始编写上传代码了。幸运的是,我们不必对模型进行任何更改,尽管我们必须改变数据库模式。我们将在/routes/photos.js做出所有的改变。

这个文件中剩下的主要任务是重要的。当用户进行POST/photos/upload操作时,我们希望采取以下操作:

User input is validated (user ID, album ID, and image are required).   Image is written to /tmp folder.   Image is uploaded from /tmp folder to S3 bucket.   Image is deleted from /tmp folder.   Final image URL is generated.   Entry is created in database, including URL.   User receives success message.  

在这个过程中,有许多事情可能会出错,我们希望对此有所计划:用户可能会包含无效的输入;在 EC2 实例上读取/写入映像可能有问题;或者可能无法上传到 S3 或写入数据库。坏消息是,在复杂的 Node.js 应用中正确的错误处理看起来有点混乱。好消息是,我们可以用相对较少的代码完成所有这些步骤。

首先,我们必须允许路由访问globals以及fs模块。尽管文件系统模块内置在 Node.js 中,但您必须声明它才能直接访问它。路由的顶部现在看起来如下所示:

var express = require('express');

var router = express.Router();

var model = require('./../lib/model/model-photos');

var globals = require('./../lib/globals');

var fs = require('fs');

接下来,/upload的路线需要完全重写。将其替换为清单 5-7

Listing 5-7. New POST/upload Route

router.post('/upload', function(req, res) {

if(req.param('albumID') && req.param('userID') && req.files.photo){

var params = {

userID : req.param('userID'),

albumID : req.param('albumID')

}

if(req.param('caption')){

params.caption = req.param('caption');

}

fs.exists(req.files.photo.path, function(exists) {

if(exists) {

params.filePath = req.files.photo.path;

var timestamp = Date.now();

params.newFilename = params.userID + '/' + params.filePath.replace('tmp/', timestamp);

uploadPhoto(params, function(err, fileObject){

if(err){

res.status(400).send({error: 'Invalid photo data'});

} else {

params.url = fileObject.url;

delete params.filePath;

delete params.newFilename;

model.createPhoto(params, function(err, obj){

if(err){

res.status(400).send({error: 'Invalid photo data'});

} else {

res.send(obj);

}

});

}

});

} else {

res.status(400).send({error: 'Invalid photo data'});

}

});

} else {

res.status(400).send({error: 'Invalid photo data'});

}

});

这里发生了很多事情,让我们一步一步来。首先,验证表单数据。除了需要albumIDuserID之外,我们现在还需要提交一个名字为photo的文件。大部分代码都包装在这个条件中,如果失败,就会发送一个 HTTP 400 错误来响应请求。

像我们经常做的那样,我们接下来基于请求参数构造一个params对象。所需的albumIDuserID都包括在内,如果找到了一个标题,也包括在内。标题是可选的,我们从不通过这种方式直接访问它们。因为我们使用 multer,当一个文件被包含在POST中时,它会被自动写入到/tmp文件夹中(我们在server.js中指定)。存储在/tmp文件夹中的副本不保留其原始名称,而是分配一个随机标识符,以减轻对重复文件名的担忧。不难想象,两个从同一部智能手机上传照片的用户会有相同的图片名称。请求中包含的任何文件都会自动分配一个 path 属性,指向它们在服务器上的位置。这给我们省了不少麻烦!

接下来,我们开始使用fs模块。首先,我们使用fs.exists()来检查文件是否确实位于我们期望的路径,可以通过req.files.photo.path访问。如果在这里找不到,将向用户发送一个错误,我们的路线将被停止。如果找到了文件,那么我们将文件的路径添加到我们的params对象中。我们还创建了一个名为newFilenameparams属性,这将是文件上传到 S3 时的最终文件名。因为我们的应用同时在几个实例上运行,即使使用随机文件名,仍然有文件名冲突的可能。为了减轻这种情况,我们在文件名前添加了一个时间戳,使文件名更加独特。此外,我们还在路径中包含一个带有用户 ID 的目录。使用这些技术产生文件名冲突的可能性微乎其微。

现在我们的params对象已经准备好了,我们把它发送给uploadPhoto()方法,我们还没有检查它。如果成功,我们的图像将被写入 S3,我们的 params 对象将被分配一个url属性。最后,我们删除不再需要的params属性,并将完成的对象发送给model.createPhoto()函数。如果操作成功,我们向用户返回一个带有照片 ID 的 HTTP 200 状态。

/routes/photos.js中,向下滚动到路线的末端,但在底部的module.exports声明之前。我们将在这里添加私有函数,仅在该文件中使用。首先,我们将添加uploadPhoto()函数,如清单 5-8 所示。

Listing 5-8. uploadPhoto() Function

function uploadPhoto(params, callback){

fs.readFile(params.filePath, function (err, imgData) {

if(err){

callback(err);

} else {

var contentType = 'image/jpeg';

var uploadPath = 'uploads/' + params.newFilename;

var uploadData = {

Bucket: globals.awsVariables().bucket,

Key: uploadPath,

Body: imgData,

ACL:'public-read',

ContentType: contentType

}

putS3Object(uploadData, function(err, data){

if(err){

callback(err);

} else {

fs.unlink(params.filePath, function (err) {

if (err){

callback(err);

} else {

callback(null, {url: uploadPath});

}

});

}

});

}

});

}

首先,这个函数从/tmp目录中读取文件。然后使用来自params对象的文件名设置上传路径。使用 AWS SDK 所需的键值,构造了一个名为uploadData的对象。我们构建这个对象是为了准备将图像上传到 S3,在这一点上它将被称为一个对象。

Bucket键使用在我们的 globals 中声明的 bucket,它最终在 OpsWorks 环境变量中设置。Key就是 S3 桶中的路径。Body包含我们用fs.readFile(). ACL检索的图像数据,代表访问控制列表,代表对象在 S3 上创建时的权限。最后是ContentType,硬编码为'image/jpeg'

作为一个额外的练习,你可以动态地设置ContentType,通过用fs读取它并把它传递给params对象中的这个函数。

接下来,我们将uploadData对象传递给putS3Object()。上传完成后,使用fs.unlink()将图像从/tmp目录中移除。最后,在回调中返回 S3 对象路径。您会记得这个相对路径是传递给model.createPhoto()的路径,从那里它被写入数据库。

我们将在uploadPhoto()下面添加最后一个函数putS3Object()。这个函数(参见清单 5-9 )使用 AWS SDK 简单地处理到 S3 的上传。在/routes/photos.js中增加以下功能:

Listing 5-9. putS3Object() Function

function putS3Object(uploadData, callback){

var aws = require('aws-sdk');

if(globals.awsVariables().key){

aws.config.update({ accessKeyId: globals.awsVariables().key, secretAccessKey: globals.awsVariables().secret });

}

var s3 = new aws.S3();

s3.putObject(uploadData, function(err, data) {

if(err){

callback(err);

} else {

callback(null, data);

}

});

}

让我们一行一行地分解它。首先,加载aws-sdk。然后,我们检查globals.awsVariables().key是否被定义。您应该还记得,它只是在本地定义的,用于我们使用 IAM 用户凭证的用例。如果你不想使用这种方法,你可以完全删除这个if语句。但是如果您使用 IAM 用户获得 S3 权限,那么必须将密钥和秘密传递给aws.config.update()。如果我们转而依赖实例的 IAM 角色,那么 AWS SDK 会自动获取凭证,我们永远也不需要调用aws.config.update()

然后,我们就简单的叫s3.putObject()。如前所述,S3 存储桶的内容被含糊地称为对象,不管是什么类型。我们已经在这个函数之前构造了必要的参数,所以它很简单。

为了弄清楚这一切是如何工作的,让我们快速看一下model.createPhoto()。打开/lib/model/model-photos.js。在文件的顶部附近,您应该可以看到清单 5-10 中的代码。

Listing 5-10. Model createPhoto() Function

function createPhoto(params, callback){

var query = 'INSERT INTO photos SET ? ';

connection.query(query, params, function(err, rows, fields){

if(err){

callback(err);

} else {

var response = {

id : rows.insertId

};

callback(null, response);

}

});

}

我们没有对此功能进行任何更改。因为它根据params对象参数的内容设置值,所以对控制器和数据库的任何更改都会自动反映在这里。可以看到,返回的值只是照片的 ID。

但是,如果您查看模型中的其他方法,您会看到我们选择了特定的字段来输出给用户。我们必须对其他 SQL 语句进行一些修改。毕竟,拥有一个实际上不显示任何照片的相册 web 应用是很可笑的。无论如何,它可能会筹集到 5000 万美元的风险投资。

先找函数getPhotoByID()。将url添加到查询变量中,因此该函数现在如清单 5-11 所示。

Listing 5-11. Model getPhotoByID() Function

function getPhotoByID(params, callback){

var query = 'SELECT photoID, caption, url, albumID, userID FROM photos WHERE published=1 AND photoID=' + connection.escape(params.photoID);

connection.query(query, function(err, rows, fields){

if(err){

callback(err);

} else {

if(rows.length > 0){

callback(null, rows);

} else {

callback(null, []);

}

}

});

}

同样,我们希望在通过相册 ID 选择照片时包含 URL。同样,只更新 SQL 查询(参见清单 5-12 )。

Listing 5-12. Model getPhotosByAlbumID() Function

function getPhotosByAlbumID(params, callback){

var query = 'SELECT photoID, caption, url, albumID, userID FROM photos WHERE published=1 AND albumID=' + connection.escape(params.albumID);

connection.query(query, function(err, rows, fields){

if(err){

callback(err);

} else {

if(rows.length > 0){

callback(null, rows);

} else {

callback(null, []);

}

}

});

}

最后,我们希望包含通过搜索检索到的照片的 URL(参见清单 5-13 )。

Listing 5-13. Model getPhotosSearch() Function

function getPhotosSearch(params, callback){

var query = 'SELECT photoID, caption, url, albumID, userID FROM photos WHERE caption LIKE "%' + params.query + '%"';

connection.query(query, function(err, rows, fields){

if(err){

callback(err);

} else {

if(rows.length > 0){

callback(null, rows);

} else {

callback(null, []);

}

}

});

}

现在我们完成了编码!将您的更改提交到您的存储库中。返回 OpsWorks 并部署您的应用。你现在可以不需要指导就能做到。这次部署过程有很多工作要做。它将您的 OpsWorks 环境变量添加到每个实例中,更新 IAM 角色,并运行您的 Chef JSON。当您的代码从存储库中检索出来时,OpsWorks 会找到您的package.json文件中列出的新依赖项,并自动将它们与您的应用的其余部分一起安装。同时,在本课结束之前,我们还有一些任务要完成。

更新数据库模式

您可能已经注意到,我们的数据库模式不再反映我们需要的值。我们必须对照片表格进行快速更改。在您的 MySQL 客户端(理想情况下是 MySQL Workbench)中,连接到 RDS 实例。展开左侧导航中的相册数据库并展开表格以显示照片(参见图 5-11 )。

A978-1-4842-0653-9_5_Fig11_HTML.jpg

图 5-11。

RDS instance tables

Control -点击表格,从工具提示菜单中选择 Alter Table。您应该会看到表模式出现在中间的列中。添加一个 Varchar(250)类型的 url 列,如图 5-12 所示。如果您更喜欢执行原始 SQL 查询,这里是:

A978-1-4842-0653-9_5_Fig12_HTML.jpg

图 5-12。

The photos table schema

ALTER TABLE photoalbums.photos ADD url VARCHAR(250) NOT NULL;

如果要使本地环境保持最新,请确保对本地数据库进行相同的更改。

与 CloudFront 集成

在第 4 章中,我们创建了一个 CloudFront 实例来服务我们的应用。在此过程中,我们将应用堆栈的负载平衡器注册为对 CloudFront 实例的请求源。接下来,我们将对 S3 桶做同样的事情。S3 桶将是 CloudFront 实例的第二个来源,并自动将我们的资产副本存储在 CloudFront edge 位置。

创建云锋 S3 起源

使用服务菜单,在 AWS 控制台中导航到 CloudFront。从 CloudFront 登录页面的发行版列表中选择您的发行版。单击原点选项卡并创建原点。您将再次进入“创建源”视图,在该视图中,您将选择您的 S3 存储桶作为源域名。开始编辑字段时,会出现一个下拉列表。当您选择您的存储桶时,将自动生成一个起点 ID。你可以让它保持原样。

目前,我们不打算让我们的应用中的照片保密。然而,我们很可能希望在以后的某一天使用户内容成为私有的(或者至少是受限制的)。作为支持该功能的第一步,我们必须限制对 bucket 的直接访问。当我们将 Restrict Bucket Access 值设置为 Yes 时,S3 URL 将不再是公共的,用户将只能访问 CloudFront URLs 上的内容。

因为我们限制了 bucket 的访问,所以我们必须创建一个 CloudFront 访问身份,该身份具有访问 S3 bucket 的权限。虽然这听起来像是您在 IAM 中管理的东西,但是这些身份完全是在 CloudFront 中管理的。选择“创建新身份”以生成新的 CloudFront origin 访问身份。在 Comment 字段中,您可以输入一个字符串来标识您正在创建的身份,类似于 access-identity-photo albums-cloudfront。

接下来,将询问您是否要授予对 Bucket 的读取权限。这是一种更新 S3 存储桶策略的便捷方法,因此您不必手动执行此操作。选择是,更新存储桶策略。如果一切看起来如图 5-13 所示,点击创建继续。

A978-1-4842-0653-9_5_Fig13_HTML.jpg

图 5-13。

Creating a CloudFront Origin for the S3 bucket

云锋 S3 行为

既然我们已经为我们的 S3 桶创建了一个 CloudFront 源,我们必须创建将请求路由到该源的行为。选择行为选项卡。当我们在第 4 章中实验缓存行为时,你应该认识到这个观点。您会注意到,到目前为止,我们所有的行为都源于同一个地方:应用堆栈中的负载平衡器。但是如前所述,让我们的应用为用户提供图片上传服务是一种资源浪费。这一次,我们将创建一个源于 S3 桶的行为,从等式中完全删除应用堆栈。

单击左上角的“创建行为”。同样,您必须为行为输入一个路径模式。在“路径模式”字段中,输入/uploads/*以捕获对上传文件夹的所有请求。在“原点”字段中,展开下拉列表并选择您刚刚创建的原点,该原点对应于 S3 桶。

在我们到达对象缓存之前,不要管其他字段。因为我们不会从我们的应用栈发送定制的源头,所以我们希望 CloudFront 控制这些资产的 TTL。选择自定义,并在最小 TTL 字段中输入 43200(秒),持续 12 小时。从我们的 S3 桶中检索到的资产将在至少 12 小时后在 CloudFront 中刷新,如果您愿意,当然可以将其更改为任何其他值。

浏览其余选项,我们不会转发查询字符串或 cookies,也不会更改任何其他值。检查您的选择,确保它们与图 5-14 匹配,然后点击创建。

A978-1-4842-0653-9_5_Fig14_HTML.jpg

图 5-14。

Create CloudFront behavior for /uploads/*

您会注意到新行为出现在行为列表的底部,除了默认(*),它总是在底部。没有必要将此行为移到列表的顶部,因为它与我们现有的行为没有冲突。同样,CloudFront 中的变化需要一些时间来传播。您可以留意分发列表中的状态字段,等待您的状态从正在进行更改为已部署。

现在是真相大白的时候了!找到您想要用来创建照片的 jpeg 图像文件。我们假设你已经创建了一个用户和相册。如果您还没有,现在就提出这些请求,您应该得到 1 的userID和 1 的albumID。现在做一个POST请求到www.[ your domain ].com/photos/upload。在表单数据中包含以下参数:

albumID: 1

userID: 1

caption: 'my first real photo'

文件密钥应该是 photo,并且一定要将Content-Type设置为application/x-www-form-urlencoded。如果请求成功,您仍然会收到响应中的照片 ID。

{"id":21}

您的响应标题应该类似于清单 5-14 。您会注意到,X-Cache标题显示了来自 CloudFront 的一个未命中。响应不应该来自 CloudFront,否则您无疑会看到错误的数据。

Listing 5-14. Photo Upload Response Headers

Content-Type : application/json; charset=utf-8

Content-Length : 9

Connection : keep-alive

Date : Mon, 15 Dec 2014 21:02:57 GMT

X-Powered-By : Express

X-Cache : Miss from cloudfront

Via : 1.1 eadedd3fe9e82c51cc035044b3a5f3fa.cloudfront.net (CloudFront)

X-Amz-Cf-Id : yRWUsgyOTSh4xw5NcKfX-ne2-N7EU9yUIQYot9J82xcF1elqiRgBnw==

接下来,让我们验证我们上传到应用的数据存储正确,并且现在可以访问。打开您的浏览器到www.[ your domain ].com/photos/id/21(用您收到的 ID 替换21)。您应该会看到类似如下的 JSON:

[

{

"photoID":21,

"caption":"my first real photo",

"url":"uploads/1/141867737733318085bdee7f0a1577a57200e59c65306.jpg",

"albumID":1,

"userID":1

}

]

有图片的网址!接下来,只要 CloudFront 行为改变完成,您就应该能够访问您的域中的映像。复制 URL,将其添加到您的域中,并尝试在您的浏览器中查看。你应该看看你的形象!

收尾

恭喜你!现在您的应用中有了一个 CDN!这是应用的一个重大突破。这将是一个很好的时机,可以回顾并应用您所学到的一些经验来改进功能。一些小的改变可以大大改进我们的应用。

我们的 web app API 中的 URL 都是相对的。虽然这可能没问题,但在这种情况下,许多开发人员更喜欢绝对 URL。我们应该能够支持这两者。当前的设置也是本地开发的一个问题,因为我们仍然在上传文件到 S3,即使我们在本地数据库上运行应用。因此,虽然有一个可通过网络访问的版本uploads/1/141867737733318085bdee7f0a1577a57200e59c65306.jpg,但在http://localhost:8081/uploads/1/141867737733318085bdee7f0a1577a57200e59c65306.jpg却找不到该图像。因此,我们的第一个任务是将我们的图像转换为绝对 URL,并使 URL 正确,即使在本地环境中也是如此。

我们还可以改进 CloudFront 缓存图像的方式。默认情况下,/uploads/*路径中缓存的 URL 会在 CloudFront 中缓存 24 小时。然而,我们可以肯定的是,这些图像根本不会改变。我们不支持任何图像修饰或裁剪,即使我们支持,我们也会使用版本化文件命名。现在,24 小时没什么大不了的。但是,如果我们服务于成千上万的用户,为什么不从 CloudFront 中获益呢?因此,另一个收尾工作将是简单地将图像存储在 CloudFront 中,时间远远超过 24 小时。

绝对 URL

第一个任务虽然更难,但仍然很简单。如果您认为我们可以使用环境变量来存储域,那么您就错了!这不仅将允许我们在本地开发时访问我们的 S3 映像,还将使克隆我们的开发堆栈、更改域等变得容易。

首先,返回 OpsWorks 并选择您的堆栈。导航到堆栈中的应用视图,然后单击应用旁边的列中的编辑。向下滚动到环境变量标题,为关键域添加值http://www.[ your domain ].com(参见图 5-15 )。然后,单击页面底部的保存。

A978-1-4842-0653-9_5_Fig15_HTML.jpg

图 5-15。

Adding another Environment Variable to OpsWorks

既然已经设置好了,我们可以修改代码,然后开始新的部署。回到你的代码编辑器,我们将一个接一个地进行修改。我们要做的第一件事是将新变量添加到全局变量中。打开/libs/globals,我们将在其中添加域变量。awsVariables()函数看起来应该如清单 5-15 所示。

Listing 5-15. Adding Another Environment Variable to the Code Base

awsVariables : function(){

if(process.env.ENVIRONMENT){

var variables = {

bucket : process.env.S3BUCKET,

domain : process.env.DOMAIN

}

return variables;

} else {

var local = require('./../config/local');

return local.awsVariables;

}

}

我们还将在全球化方面更进一步。虽然我们现在只需要将照片的相对 URL 转换成绝对 URL,但是可以想象,我们可能在其他地方也需要这个功能。让我们给全局变量添加一个名为absoluteURL()的函数,这样我们就可以轻松地重用我们的代码。在awsVariables :function(){}之后,添加一个逗号,后面是清单 5-16 中的代码。

Listing 5-16. absoluteURL() Helper Function

absoluteURL : function(path){

if(this.awsVariables().domain){

return this.awsVariables().domain + '/' + path;

}

return path;

}

这个功能很简单,但是以后可能会省去我们很多复制粘贴的工作。它接受一个相对路径作为参数,并在域前面加上一个正斜杠(如果定义了域的话)。如果没有,它会无声地失败,不会崩溃。

我们还需要本地文件中的域变量。前往/config/local.js,将您的域名添加到那里的awsVariables对象中。

目前,我们没有对数据库中的数据进行任何查询后操作。我们只需检索我们需要的对象和属性,并将它们发送回各自的控制器。不幸的是,聚会已经结束了。我们不打算将域写入数据库,而是在数据库数据从模型返回之前将其添加到响应中。此外,可以想象,这将是我们对从数据库中检索的数据执行的许多查询后操作中的第一个。因此,我们可以向模型添加一个私有函数,从这个函数中我们可以组织对数据的修改。

打开/lib/model/model-photos.js并滚动到底部。在deletePhotoByID()方法之后,添加清单 5-17 中的代码。

Listing 5-17. formatPhotoData() Helper Function

function formatPhotoData(rows){

for(var i = 0; i < rows.length; i++){

var photo = rows[i];

if(photo.url){

photo.url = globals.absoluteURL(photo.url);

}

}

return rows;

}

这个函数很简单,遍历从数据库中检索到的照片,并对那些具有url属性的照片调用我们的absoluteURL()辅助函数。如果我们需要对所有照片进行任何其他查询后操作,我们可以稍后将它们添加到这个循环中。

接下来,我们必须确保每个检索照片的方法都使用这个函数,这意味着我们必须在三个地方进行更改。向上滚动到功能getPhotoByID(),找到如下一行:

callback(null, rows);

用以下内容替换:

callback(null, formatPhotoData(rows));

该函数现在应该如下所示:

function getPhotoByID(params, callback){

var query = 'SELECT photoID, caption, url, albumID, userID FROM photos WHERE published=1 AND photoID=' + connection.escape(params.photoID);

connection.query(query, function(err, rows, fields){

if(err){

callback(err);

} else {

if(rows.length > 0){

callback(null, formatPhotoData(rows));

} else {

callback(null, []);

}

}

});

}

最后,我们必须对getPhotosByAlbumID()getPhotosSearch()进行相同的更改。进行这些更改,然后将您的代码提交到存储库中。回到 OpsWorks 并部署您的应用。等待几分钟让部署完成,完成后,刷新浏览器中的/photos/id/21路径。现在,您应该可以看到图像的绝对路径,如下所示:

[

{

"photoID":27,

"caption":"test",

"url":" http://www.cloudyeyes.net/uploads/1/1418498633152f1fe581691cc3aa20577958626077976

.jpg",

"albumID":1,

"userID":1

}

]

增强型图像缓存

对于本章的最后一个任务,您将增加 CloudFront 中的/uploads/*路径的 TTL。在 AWS 控制台中返回 CloudFront,并从发行版列表中选择您的发行版。然后,单击“原点”选项卡。选择/uploads/*路径模式,如图 5-16 ,点击编辑。

A978-1-4842-0653-9_5_Fig16_HTML.jpg

图 5-16。

Edit CloudFront behavior

找到对象缓存字段,并将值从使用原始缓存头更改为自定义。使用原始缓存头没有任何好处。因为对/uploads/*的请求甚至从未到达我们的应用堆栈,所以我们不能像对其他路径那样以编程方式控制缓存行为。通过切换到 Customize,我们可以输入 CloudFront edge 位置中的缓存对象的最小 TTL,或者说生命周期。选择自定义后,在最小 TTL 字段中输入数字 604800。这将存储缓存的对象一周(秒)。我们可以很容易地把它设置为两周,或者一年!不过,就目前而言,一周时间似乎足够了。最后,单击 Yes,Edit,您的更改将开始生效。您可以通过观察发行状态来检查这一点,发行状态现在将被设置为 InProgress。记住:CloudFront 中的变化传播需要几分钟时间。

摘要

下一章,对我们的应用进行了一些大的修改。然而,我们只需编写很少的代码就可以为我们的软件添加一些强大的工具。我希望您已经习惯了使用 AWS 控制台对我们的基础设施进行大规模更改的想法。如此随意地行使这种权力可能会让人有些伤脑筋。考虑到这一点,我们仅仅触及了 AWS SDK 的皮毛。在接下来的几章中,我们将继续使用 SDK 来添加更多的特性。

Footnotes 1

也参见 S3 文献中的 http://aws.amazon.com/documentation/s3/