ToB企服应用市场:ToB评测及商务社交产业平台
标题:
【云计算】基于AWS和Node.js的作业提交体系
[打印本页]
作者:
星球的眼睛
时间:
2024-6-14 21:47
标题:
【云计算】基于AWS和Node.js的作业提交体系
简介
application: Node.js的Express框架
custom ami:Packer
AWS和GCP资源:Pulumi-iac(js)
domain:AWS Route53
email:Mailgun
Lambda函数:Node.js
流程
webapp哀求处理流程
用户发送POST哀求到:http://mydomain.tld/v1/assignments/:id/submission
域名配置A record指向AWS中的load balancer,load balancer将哀求转发到EC2实例中webapp运行的端口上
webapp运行在AWS EC2上,在cloud watch上监控CPU使用率,通过auto scaling调解EC2实例数目
webapp与RDS通信处理数据库变更和查询
webapp写入log和自定义metrics,cloud watch监控logs和metrics
webapp向SNS发送包括用户邮箱和提交url的message
SNS调用Lambda函数
Lambda函数中从用户提交的url中下载zip然后上传到GCP的bucket
Lambda函数中使用mailgun的api发送邮件到用户告知提交状态,并在DynamoDB中记录邮件发送
体系构建流程
注册域名并为域名添加email service(Mailgun / AWS SES)
使用Packer创建配置好webapp和cloud-watch-agent主动运行的custom ami
使用Pulumi创建AWS和GCP资源
Web applicaiton
User story
用户可以新建assignment,指定assignment的可提交次数和deadline。
用户可以查看所有assignments,或者按id查看指定assignment。
用户可以修改自己创建的assignment。
用户可以删除自己创建的assignment。
用户可以提交submission到指定assignment。
用户可以收到提交状态的邮件反馈。
数据库
使用mariadb
创建database
const mariadb = require('mariadb');
const mariaPool = await mariadb.createPool({
host: "DATABASE_HOST",
user: "DATABASE_USER",
password : "DATABASE_PASSWORD"
});
const conn = await mariaPool.getConnection();
await conn.query(`CREATE DATABASE IF NOT EXISTS ${"DATABASE_NAME"}`);
conn.end();
复制代码
使用Sequelize连接mariadb
const Sequelize = require('sequelize');
const mariadb = require('mariadb');
const db = new Sequelize("DATABASE_NAME","DATABASE_USER","DATABASE_PASSWORD",{
host: "DATABASE_HOST",
port:3306,
dialect:"mariadb",
pool:{
max:5,
min:0,
idle:10000
},
debug:true,
})
复制代码
注:创建 Sequelize 实例时,该操作自己并不立即建立数据库连接,只是配置 Sequelize 实例,准备与数据库连接的相干参数。
Tables布局
使用sequelize.define()方法定义表布局。
使用sequelize.sync()方法创建或更新({alter: true})定义的表。
User Keyidfirst_namelast_nameemailpasswordTypeUUID STRING()
STRING()
STRING()
STRING()
#
Assignment Keyid user_id
namepoints num_of_attemps
deadline
TypeUUIDUUID STRING()
INTEGER
INTEGER
DATE
Submission Keyid assignment_id
user_id submission_url
TypeUUIDUUIDUUID STRING()
注:创建 Sequelize 实例时,该操作自己并不立即建立数据库连接,只是配置 Sequelize 实例,准备与数据库连接的相干参数。
注:UUID的范例为sequelize.DataTypes.UUID,默认值为sequelize.DataTypes.UUIDV4。
导入user信息
将存储在csv文件中的用户信息导入到数据库表,存储加密后的密码。
const fs = require('fs');
const parse = require('csv-parser');
const bcrypt = require('bcrypt');
const salt = bcrypt.genSaltSync(10);
fs.createReadStream("CSV_FILE_PATH")
.pipe(parse())
.on('data', async (row) => {
try {
const hashPass = bcrypt.hashSync(row.password, salt);
const [user, created] = await User.findOrCreate({
where:{
first_name: row.first_name,
last_name: row.last_name,
email: row.email
},defaults:{
password: hashPass
}})
} catch (error) {
console.log(error);
}
})
复制代码
Functions
getUser
根据哀求header中的Basic Authorization信息,从User表中获得该user的UUID。
如果header中没有Basic Auth信息或者密码不正确,response发送401 Unauthorized,返回0;
通过验证,返回user的UUID。
checkContent
检查哀求body中的所需字段是否存在和范例正确。
如果url不正确,body中须要的字段缺失或者范例不正确,response发送400 Bad Request,返回false;
通过检查返回true。
Api接口
GET /healthz
检查数据库连接
try{
db.authenticate(); //sequelize.authenticate
res.status(200).send(); //200 OK
} catch(e){
res.status(503).send(); //503 Service Unavailable
}
复制代码
GET /v1/assignments
查看所有assignment,返回一个list
try{
const userId = await getUser(basicAuth(req),res); //检查Authorization
if(userId == 0 || !await checkContent(req,res)){ //如果Authorization没通过或者请求body格式不对
return;
}else{
const result = await Assignment.findAll();
res.status(200).json(result); //200 OK
}
} catch(e){
res.status(503).send(); //503 Service Unavailable
}
复制代码
GET /v1/assignments/:id
查看id对应的assignment
try{
const userId = await getUser(basicAuth(req),res); //检查Authorization
if(userId == 0 || !checkContent(req,res)){ //如果Authorization没通过或者请求body格式不对
return;
}else{
const result = await Assignment.findOne({
where: {
id:req.params.id //根据id查找assignment
}
});
if(result == null){
res.status(404).send(); //404 Not Found
}
else{
res.status(200).json(getResult(result)); //200 OK
}
}
} catch(e){
res.status(503).send(); //503 Service Unavailable
}
复制代码
POST /v1/assignment
创建assignment
try {
const userId = await getUser(basicAuth(req),res); //检查Authorization
if(userId == 0 || !checkContent(req,res)){ //如果Authorization没通过或者请求body格式不对
return;
}else{
Assignment.create({
user_id: userId,
name: req.body.name,
points: req.body.points,
num_of_attemps: req.body.num_of_attemps,
deadline: req.body.deadline
}).then((created) => res.status(201).json(created)); //201 Created
}
} catch (e) {
if (e.name === 'SequelizeUniqueConstraintError') {
res.status(409).json(result); //409 Conflict
} else {
res.status(503).send(); //503 Service Unavailable
}
}
复制代码
注:定义模型的时间name字段设置为unique,如果试图创建名字一样的assignment,会返回sequelize的错误。
POST /v1/assignment/:id/submission
向id对应的assignment举行提交
try{
const userId = await getUser(basicAuth(req),res); //检查Authorization
if(userId == 0){
return;
}else if(!req.body.submission_url){ //检查body字段
logger.info(`No submission content.`);
res.status(400).send(); //400 Bad Request
} else{
const result = await Assignment.findOne({ //找到id对应的assignment
where: {
id:req.params.id
}
});
if(result == null){
res.status(404).send(); //404 Not Found
}
else{
const now = new Date();
if(now > result.deadline){ //提交时间晚于deadline
res.status(403).send(); //403 Forbidden
}else{
const subs = await Submission.findAll({where: {//得到该user在该assignment上的所有提交
assignment_id:req.params.id,
user_id: userId,
}});
if(subs.length == result.num_of_attemps){ //如果达到最大可提交次数
res.status(403).send(); //403 Forbidden
}else{
Submission.create({ //201 Created
assignment_id: req.params.id,
user_id: userId,
submission_url: req.body.submission_url,
}).then((created) => res.status(201).json(getResult(created)));
}
}
}
}
} catch(e){
res.status(503).send(); //503 Service Unavailable
}
复制代码
PUT /v1/assignment/:id
修改assignment
try {
const userId = await getUser(basicAuth(req),res); //检查Authorization
if(userId == 0 || !checkContent(req,res)){ //如果Authorization没通过或者请求body格式不对
return;
}else{
Assignment.findOne({ //根据id查找要修改的assignment
where: {
id: req.params.id,
}
}).then((result) => {
if(result == null){
res.status(404).send(); //404 Not Found
}
else if(result.user_id == userId){ //如果是请求用户创建的assignment,有修改权限
Assignment.update({
user_id: userId,
name: req.body.name,
points: req.body.points,
num_of_attemps: req.body.num_of_attemps,
deadline: req.body.deadline
},
{
where: {
id: req.params.id
}
}).then(ok => res.status(204).send()); //204 No Content
}
else{
res.status(403).send(); //403 Forbidden
}
});
}
} catch (e) {
res.status(503).send(); //503 Service Unavailable
}
复制代码
DELETE /v1/assignment/:id
删除该id对应的assignment
try {
const userId = await getUser(basicAuth(req),res); //检查Authorization
if(userId == 0 || !checkContent(req,res)){ //如果Authorization没通过或者请求body格式不对
return;
}else{
await Assignment.findOne({ //根据id找到要删除的assignment
where: {
id:req.params.id
}
}).then((result) => {
if(result == null){
res.status(404).send(); //404 Not Found
}
else if(result.userId == userId){ //assignment是请求用户创建的,有删除权限
Assignment.destroy(
{
where: {id: req.params.id}
}).then(ok =>res.status(204).send()); //204 No Content
}
else{
res.status(403).send(); //403 Forbidden
}
});
}
} catch (e) {
res.status(503).send(); //503 Service Unavailable
}
复制代码
其他哀求
对于其他method好比PATCH,返回405 Method Not Avaliable。
对于其他哀求url,返回400 Bad Request。
Test
使用mocha举行api测试
const request = require('supertest');
const app = require('./app'); // my webapp in app.js
const chai = require('chai');
const expect = chai.expect;
const describe = require('mocha').describe;
describe('GET /healthz', () => {
it('should return a 200 status code if the MySQL connection is healthy', (done) => {
request(app)
.get('/healthz')
.expect(200)
.end((err, res) => {
if (err) return done(err);
done();
});
});
});
复制代码
Packer
Packer作用
使用Packer创建一个可以主动运行webapp的ami。
source ami:Debian 12
subnet id:该地域中默认vpc的一个public subnet
build:
在本地打包webapp的所有文件到zip
将zip传入
安装unzip、node和amazon-cloudwatch-agent
创建一个user用来运行webapp
将webapp的zip解压到创建的user的home目录
将配置webapp主动运行的webapp.service文件移动到/lib/systemd/system/下
设置webapp和amazon-cloudwatch-agent开机自启(sudo systemctl enable webapp)
新建cloudwatch配置文件中要监控的log文件并配置权限
Packer指令
其中fmt如果不加-check会主动改正,加-check如果有格式错误会直接报错。
packer init ami.pkr.hcl
packer fmt -check ami.pkr.hcl
packer validate ami.pkr.hcl
packer build ami.pkr.hcl
复制代码
GitHub Workflow
从forked堆栈向主堆栈提交pull request时,workflow包罗对于packer格式的检查,如果检查不通过将拒绝merge。
on:
pull_request_target:
types:
- opened
- synchronize
复制代码
merge到主堆栈后,触发workflow举行api测试,创建webapp的artifact并创建ami
on:
push:
branches:
- main
复制代码
Domain
在AWS Route53中申请域名,并创建Hosted Zone。
之后会在Pulumi中为域名添加A record,指向webapp地点的ec2实例,这样就可以从域名访问webapp而不须要每次使用新的ip地点。
Mailgun(AWS SES)
使用mailgun配置应用程序邮件服务,将注册的domain添加到mailgun,然后将须要添加的记录在Hosted Zone中创建,通过verify之后即可使用mailgun的api。
const formData = require('form-data');
const Mailgun = require('mailgun.js');
const mailgun = new Mailgun(formData);
const mg = mailgun.client({username: 'api', key: "MY_API_KEY"});
mg.messages.create('sandbox-123.mailgun.org', {
from: "Excited User <mailgun@MY_DOMAIN>",
to: [email],
subject: "Hello",
text: "Testing some Mailgun awesomeness!",
html: "<h1>Testing some Mailgun awesomeness!</h1>"
})
.then(msg => console.log(msg)) // logs response data
.catch(err => console.log(err)); // logs any error
复制代码
AWS CloudWatch
使用cloudwatch监控webapp的log和自定义matrics统计api调用。
cloudwatch的配置可以在终端举行也可以直接写入cloudwatch-config.json文件,在终端配置也是生成文件,如果该配置文件不存在则无法运行cloud-watch-agent。
const StatsD = require('hot-shots');
const { createLogger, format, transports } = require('winston');
const logger = createLogger({
format: format.combine(
format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
format.json()
),
transports: [
new transports.File({ filename: '/var/log/myLog.log' })
]
});
const statsd = new StatsD({
port: 8125,
errorHandler: (error) => {
logger.info("StatsD error: ", error);
}
});
statsd.increment(`api_call.${req.method}.${req.path}`);
logger.info("log content");
复制代码
Lambda函数
lambda函数由sns调用,负责从用户提交的url链接下载文件然后上传到gcp的bucket中,邮件告知用户下载状态,并将邮件发送状态存储到DynamoDB。
AWS & Pulumi
AWS资源是通过Pulumi摆设的,Pulumi 是一种开源的基础设施即代码(Infrastructure as Code, IaC)工具。
Pulumi官网AWS Package Api Documentation:AWS Classic Package | Pulumi Registry
配置AWS和Pulumi
创建AWS账号,启用MFA(多因素认证,增强账号安全性)。
下载AWS CLI和Pulumi并安装。
创建IAM user并创建访问密钥,在本地终端配置AWS profile,如果不指定profile名字将会配置到名为default的profile。
aws configure
aws configure --profile profile_name
复制代码
使用Pulumi创建AWS资源
主要用到的库:
const pulumi = require('@pulumi/pulumi');
const aws = require('@pulumi/aws');
复制代码
Pulumi配置文件:
Pulumi.stackName.yaml
config:
aws:profile: dev
aws:region: us-east-1
key: value
复制代码
const config = new pulumi.Config();
const value = config.require("key");
复制代码
媒介
每个创建的资源都用tags参数设置了名字,并且将将名字等设置放在配置文件里,为了节省篇幅代码中不再列出,例如
const vpc = new aws.ec2.Vpc(config.require('vpcName'), {
cidrBlock: cidrBlock,
tags: { Name: config.require("vpcName") }
});
复制代码
VPC&Subnet
const vpc = new aws.ec2.Vpc("myVPC", {
cidrBlock: cidrBlock,
});
const internetGateway = new aws.ec2.InternetGateway("myInternetGateway", {
vpcId: vpc.id,
});
const publicRouteTable = new aws.ec2.RouteTable("myPublicRouteTable", {
vpcId: vpc.id,
});
const privateRouteTable = new aws.ec2.RouteTable("myPrivateRouteTable", {
vpcId: vpc.id,
});
//create subnet of my VPC
const createSubnet = (index, cidrBlock, availabilityZone, isPublic) => {
const subnetName = isPublic? `public-subnet-${index}`:`private-subnet-${index}`;
const associationName = isPublic ? `myPublicRouteTableAssociation-${index}` : `myPrivateRouteTableAssociation-${index}`;
const routeTableId = isPublic ? publicRouteTable.id : privateRouteTable.id;
const subnet = new aws.ec2.Subnet(subnetName, {
vpcId: vpc.id,
cidrBlock: cidrBlock,
availabilityZone: availabilityZone,
mapPublicIpOnLaunch: isPublic,
});
new aws.ec2.RouteTableAssociation(associationName, {
subnetId: subnet.id,
routeTableId: routeTableId,
});
}
//get AZ
const availabilityZones = await aws.getAvailabilityZones({state: "available"});
//得到已经被存在的subnet使用中的CIDR
const existingSubnetCIDR = [];
const subnets = await aws.ec2.getSubnets();
for(let id of subnets.ids){
await aws.ec2.getSubnet({id: id}).then(r => existingSubnetCIDR.push(r.cidrBlock));
}
//用subnet-cidr-calculator包得到可用的CIDR
const probabal_subnets = await SubnetCIDRAdviser.calculate(cidrBlock.split('/')[0] , cidrBlock.split('/')[1],existingSubnetCIDR);
//根据AZ数得到能创建的subnet数,最多使用三个AZ
const azLength = Math.min(3, availabilityZones.names.length);
//在每个az上创建一个public subnet和一个private subnet
for (let i = 0; i < azLength; i++) {
await createSubnet(i+1,probabal_subnets.subnets[2*i + 1].value,availabilityZones.names[i],true);
await createSubnet(i+1,probabal_subnets.subnets[2*i + 2].value,availabilityZones.names[i],false);
}
//为public路由表创建路由,指向互联网网关
new aws.ec2.Route(config.require("routeName"), {
routeTableId: publicRouteTable.id,
destinationCidrBlock: config.require("destinationCidrBlock"),
gatewayId: internetGateway.id,
});
复制代码
注:没有给private subnet配置NAT网关,这个项目中摆设在private subnet上的资源不须要访问互联网。
RDS
EC2
LoadBalancer
AutoScaling
Lambda
SNS
Domain
CloudWatch
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/)
Powered by Discuz! X3.4