์๋ฒ๋ฆฌ์ค(serverless, server+less)
์คํ ๋ฆฌ์ง ์น์
์ S3๋ฅผ ์ ํ
๋ฒํท ๋ง๋ค๊ธฐ๋ ์์ํ๊ธฐ ๋ฒํผ ํด๋ฆญ
๋ฒํท ์ด๋ฆ์ ๊ณ ์ ํด์ผ ํ๋ฏ๋ก ๊ณ ์ ํ ์ด๋ฆ์ ์ฌ์ฉํ ๊ฒ
ํ๋ฉด์ด ๋จ๋ฉด nodebird ๋ฒํท ํด๋ฆญ
๊ถํ โ ๋ฒํท ์ ์ฑ ๋ฉ๋ด ์ ํ
์๋จ ๋ฉ๋ด์์ ๊ณ์ ์ด๋ฆ ํด๋ฆญ ํ ๋ด ๋ณด์ ์๊ฒฉ ์ฆ๋ช ๋ฉ๋ด ์ ํ
์ ์ก์ธ์ค ํค ๋ง๋ค๊ธฐ ๋ฒํผ ํด๋ฆญ
๋ณด์ ์ก์ธ์ค ํค๋ ๋ค์ ๋ณผ ์ ์์ผ๋ฏ๋ก ์๋ ๊ทธ๋ฆผ์ ํค ํ์ผ ๋ค์ด๋ก๋ ๋ฒํผ ๋๋ฌ ์ ์ฅ
multer-s3์ aws-sdk ํจํค์ง๋ฅผ ์ค์นํ ํ .env์ ๋ฐ๊ธ๋ฐ์ ๋ณด์ ํค ๊ธฐ์
npm i multer-s3 aws-sdk
๐ป.env
COOKIE_SECRET=nodebirdsecret
KAKAO_ID=5d4daf57becfd72fd9c919882552c4a6
SEQUELIZE_PASSWORD=nodejsbook
REDIS_HOST=redis-18954.c92.us-east-1-3.ec2.cloud.redislabs.com
REDIS_PORT=18954
REDIS_PASSWORD=JwTwGgKM4P0OFGStgQDgy2AcXvZjX4dc
S3_ACCESS_KEY_ID=AKIAID6RLNYHFCZEEODA
S3_SECRET_ACCESS_KEY=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
AWS.config.update๋ก AWS์ ๊ดํ ์ค์ ์ ํจ(ap-northeast-2๋ ์์ธ ๋ฆฌ์ )
multer๋ฅผ multerS3๋ก ๊ต์ฒดํจ(๋ฒํท์ ์ฌ๋ฌ๋ถ์ ๋ฒํท๋ช
์ ์ฌ์ฉํ ๊ฒ)
req.file.location์ S3 ๋ฒํท ์ด๋ฏธ์ง ์ฃผ์๊ฐ ๋ด๊ฒจ ์์
๐ปnodebird/routes/post.js
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const AWS = require('aws-sdk');
const multerS3 = require('multer-s3');
const { Post, Hashtag } = require('../models');
const { isLoggedIn } = require('./middlewares');
const router = express.Router();
try {
fs.readdirSync('uploads');
} catch (error) {
console.error('uploads ํด๋๊ฐ ์์ด uploads ํด๋๋ฅผ ์์ฑํฉ๋๋ค.');
fs.mkdirSync('uploads');
}
AWS.config.update({
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
region: 'ap-northeast-2',
});
const upload = multer({
storage: multerS3({
s3: new AWS.S3(),
bucket: 'nodebird',
key(req, file, cb) {
cb(null, `original/${Date.now()}${path.basename(file.originalname)}`);
},
}),
limits: { fileSize: 5 * 1024 * 1024 },
});
router.post('/img', isLoggedIn, upload.single('img'), (req, res) => {
console.log(req.file);
res.json({ url: req.file.location });
});
const upload2 = multer();
router.post('/', isLoggedIn, upload2.none(), async (req, res, next) => {
try {
console.log(req.user);
const post = await Post.create({
content: req.body.content,
img: req.body.url,
UserId: req.user.id,
});
const hashtags = req.body.content.match(/#[^\s#]*/g);
if (hashtags) {
const result = await Promise.all(
hashtags.map(tag => {
return Hashtag.findOrCreate({
where: { title: tag.slice(1).toLowerCase() },
})
}),
);
await post.addHashtags(result.map(r => r[0]));
}
res.redirect('/');
} catch (error) {
console.error(error);
next(error);
}
});
module.exports = router;
http://localhost:8001์ ์ ์ํ์ฌ ๋ก๊ทธ์ธ ํ ์ด๋ฏธ์ง ์ ๋ก๋
์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ง์ CPU๋ฅผ ๋ง์ด ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ๊ธฐ์กด ์๋ฒ๋ก ์์ ํ๋ฉด ๋ฌด๋ฆฌ๊ฐ ๊ฐ
aws-upload ํด๋ ๋ง๋ ํ package.json ์์ฑ
๐ปaws-upload/package.json
{
"name": "aws-upload",
"version": "1.0.0",
"description": "Lambda ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ง",
"main": "index.js",
"author": "ZeroCho",
"license": "ISC",
"dependencies": {
"aws-sdk": "^2.634.0",
"sharp": "^0.25.1"
}
}
๐ปaws-upload/.gitignore
node_modules
Sharp๋ ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ง์ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
๐ปaws-upload/index.js
const AWS = require('aws-sdk');
const sharp = require('sharp');
const s3 = new AWS.S3();
exports.handler = async (event, context, callback) => {
const Bucket = event.Records[0].s3.bucket.name;
const Key = event.Records[0].s3.object.key;
const filename = Key.split('/')[Key.split('/').length - 1];
const ext = Key.split('.')[Key.split('.').length - 1];
const requiredFormat = ext === 'jpg' ? 'jpeg' : ext; // sharp์์๋ jpg ๋์ jpeg ์ฌ์ฉํฉ๋๋ค.
console.log('name', filename, 'ext', ext);
try {
const s3Object = await s3.getObject({ Bucket, Key }).promise(); // ๋ฒํผ๋ก ๊ฐ์ ธ์ค๊ธฐ
console.log('original', s3Object.Body.length);
const resizedImage = await sharp(s3Object.Body) // ๋ฆฌ์ฌ์ด์ง
.resize(200, 200, { fit: 'inside' })
.toFormat(requiredFormat)
.toBuffer();
await s3.putObject({ // thumb ํด๋์ ์ ์ฅ
Bucket,
Key: `thumb/${filename}`,
Body: resizedImage,
}).promise();
console.log('put', resizedImage.length);
return callback(null, `thumb/${filename}`);
} catch (error) {
console.error(error);
return callback(error);
}
};
๋จผ์ GitHub์ aws-upload ๋ฆฌํ์งํ ๋ฆฌ๋ก ์ฌ๋ฆฐ ํ Lightsail ์ธ์คํด์ค์์ ํด๋ก
์์ถ ํ S3๋ก ์
๋ก๋
์์ถ ํ S3๋ก ์
๋ก๋
์ปดํจํ
-Lambda
ํจ์ ์์ฑ ๋ฒํผ ํด๋ฆญ
ํจ์๋ช ์ node-deploy๋ก, ๋ฐํ์์ Node.js 12.x์ผ๋ก
ํจ์ ์ฝ๋ ์น์ ์์ S3์ ์ฌ๋ฆฐ ํ์ผ์ ์ ํ
๊ธฐ๋ณธ ์ค์ ์น์ ์์ ํธ์ง ๋ฒํผ ํด๋ฆญ
์ข์ธก S3๋ฅผ ํธ๋ฆฌ๊ฑฐ๋ก ์ ํ
๋ชจ๋ ๊ฐ์ฒด ์์ฑ ์ด๋ฒคํธ๋ฅผ ์ ํํ๊ณ ์ ๋ฏธ์ฌ์ original/ ๋๋ฅธ ํ ์ ์ฅ
๊ธฐ์กด original ํด๋ ๋ถ๋ถ์ thumb(๋ฆฌ์ฌ์ด์ง ๋จ) ํด๋๋ก ๊ต์ฒด
๋๋ค๋ฅผ ํตํด ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ง ๋ ๊ฒ ํ์ธํ๊ธฐ
์ข์ธก ๋ฉ๋ด์์ Storage๋ฅผ ์ ํํ ํ ๋ฒํท ๋ง๋ค๊ธฐ ๋ฒํผ ํด๋ฆญ
)
๋ฒํท ์ด๋ฆ์ ๊ณ ์ ํด์ผ ํ๋ฏ๋ก ๊ณ ์ ํ ๋ฒํท ์ด๋ฆ ์ ํ ํ ๋ง๋ค๊ธฐ ๋ฒํผ ํด๋ฆญ
์ฐ์ธก ๋ฒํท ๋ฉ๋ด์์ ๋ฒํท ๊ถํ ์์ ์ ํ
https://console.cloud.google.com/apis/credentials ์ ์ ์ํ๊ธฐ
์์ฑ ๋ฒํผ์ผ๋ก json ํ์ผ ๋ค์ด๋ก๋ ํ NodeBird ํด๋๋ก ๋ณต์ฌ
multer-google-storage์ axios ์ค์น
๐ปnodebird/routes/post.js
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const multerGoogleStorage = require('multer-google-storage');
const { Post, Hashtag } = require('../models');
const { isLoggedIn } = require('./middlewares');
const router = express.Router();
try {
fs.readdirSync('uploads');
} catch (error) {
console.error('uploads ํด๋๊ฐ ์์ด uploads ํด๋๋ฅผ ์์ฑํฉ๋๋ค.');
fs.mkdirSync('uploads');
}
const upload = multer({
storage: multerGoogleStorage.storageEngine({
bucket: 'nodebird',
projectId: 'node-deploy-270114',
keyFilename: 'node-deploy-270114-b024dbed754a.json',
}),
limits: { fileSize: 5 * 1024 * 1024 },
});
router.post('/img', isLoggedIn, upload.single('img'), (req, res) => {
console.log(req.file);
res.json({ url: req.file.path });
});
const upload2 = multer();
router.post('/', isLoggedIn, upload2.none(), async (req, res, next) => {
try {
console.log(req.user);
const post = await Post.create({
content: req.body.content,
img: req.body.url,
UserId: req.user.id,
});
const hashtags = req.body.content.match(/#[^\s#]*/g);
if (hashtags) {
const result = await Promise.all(
hashtags.map(tag => {
return Hashtag.findOrCreate({
where: { title: tag.slice(1).toLowerCase() },
})
}),
);
await post.addHashtags(result.map(r => r[0]));
}
res.redirect('/');
} catch (error) {
console.error(error);
next(error);
}
});
module.exports = router;
http://localhost:8001์ ์ ์ํ์ฌ ๋ก๊ทธ์ธ ํ ์ด๋ฏธ์ง ์
๋ก๋
ํด๋ผ์ฐ๋ ์คํ ๋ฆฌ์ง ๋ฒํท์ ์ด๋ฏธ์ง๊ฐ ์
๋ก๋๋ ๊ฒ ํ์ธ
์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ง์ CPU๋ฅผ ๋ง์ด ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ๊ธฐ์กด ์๋ฒ๋ก ์์ ํ๋ฉด ๋ฌด๋ฆฌ๊ฐ ๊ฐ
gcp-upload ํด๋ ์์ package.json ์์ฑํ๊ธฐ
๐ปgcp-upload/package.json
{
"name": "gcp-upload",
"version": "1.0.0",
"description": "Cloud Functions ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ง",
"main": "index.js",
"author": "ZeroCho",
"license": "ISC",
"dependencies": {
"@google-cloud/storage": "^1.6.0",
"gm": "^1.23.1",
"sharp": "^0.25.1"
}
}
๐ปgcp-upload/.gitingnore
node_modules
resizeAndUpload ๋ฉ์๋์ ์ฝ๋ ์์ฑ
๐ปgcp-upload/index.js
const storage = require('@google-cloud/storage')();
const sharp = require('sharp');
exports.resizeAndUpload = (data, context) => {
const { bucket, name } = data;
const ext = name.split('.')[name.split('.').length - 1];
const requiredFormat = ext === 'jpg' ? 'jpeg' : ext; // sharp์์๋ jpg ๋์ jpeg์ฌ์ฉํฉ๋๋ค
console.log('name', name, 'ext', ext);
const file = storage.bucket(bucket).file(name);
const readStream = file.createReadStream();
const newFile = storage.bucket(bucket).file(`thumb/${name}`);
const writeStream = newFile.createWriteStream();
sharp(readStream)
.resize(200, 200, { fit: 'inside' })
.toFormat(requiredFormat)
.pipe(writeStream);
return new Promise((resolve, reject) => {
writeStream.on('finish', () => {
resolve(`thumb/${name}`);
});
writeStream.on('error', reject);
});
};
๋จผ์ GitHub์ gcp-upload ๋ฆฌํฌ์งํฐ๋ฆฌ๋ก ์ฌ๋ฆฐ ํ ์ปดํจํธ์์ง ์ธ์คํด์ค์์ ํด๋ก
์์ถ ํ ํด๋ผ์ฐ๋ ์คํ ๋ฆฌ์ง๋ก ์
๋ก๋
์์ถ ํ S3๋ก ์
๋ก๋
GCP ์น์ฌ์ดํธ ์ข์ธก ๋ฉ๋ด์์ Cloud Functions ์ ํํ ํ ํจ์ ๋ง๋ค๊ธฐ ๋ฒํผ ํด๋ฆญ
ํจ์ ๋ง๋ค๊ธฐ ๋ฒํผ ํด๋ฆญ
ํจ์ ์ด๋ฆ์ gcp-upload
๊ธฐ์กด original ํด๋ ๋ถ๋ถ์ thumb(๋ฆฌ์ฌ์ด์ง ๋จ) ํด๋๋ก ๊ต์ฒด
ํ์
์ ํตํด ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ง ๋ ๊ฒ ํ์ธํ๊ธฐ
๐์ถ์ฒ๐
Node.js ๊ต๊ณผ์ - ๊ธฐ๋ณธ๋ถํฐ ํ๋ก์ ํธ ์ค์ต๊น์ง
https://www.inflearn.com/course/%EB%85%B8%EB%93%9C-%EA%B5%90%EA%B3%BC%EC%84%9C/dashboard