16. 서버리스 노드 개발
포스트
취소

16. 서버리스 노드 개발

기존 블로그에 작성했던 포스트를 이전한 글입니다.

해당 포스트는 NODEJS를 학습하며 정리한 내용에 대한 포스트입니다.




🌈 서버리스 노드 개발

💻 서버리스 이해하기

서버리스(serverless, server+less)

서버가 없다는 뜻이지만 서버가 없는 것은 아니고, 서버를 직접 운영하지 않아도 된다는 뜻

개발자는 자신의 서비스 로직 작성에만 집중할 수 있음

단순히 코드를 업로드한 뒤, 사용량에 따라 요금을 지불하면 됨(함수처럼 호출할 때만 실행됨, FaaS(Function as a Service))

24시간 작동할 필요가 없는 서버인 경우, 서버리스 컴퓨팅을 사용하면 요금 절약

AWS는 Lambda, GCP에서는 Cloud Functions나 Firebase가 유명함

이를 활용해 NodeBird에서 업로드하는 이미지를 리사이징 및 저장할 것임




💻 AWS S3 사용하기

🍳 AWS S3 사용해보기

스토리지 섹션의 S3를 선택

image

🍳 버킷 만들기

버킷 만들기나 시작하기 버튼 클릭

image


🍳 버킷 리전 설정하기

버킷 이름은 고유해야 하므로 고유한 이름을 사용할 것

  • 이름만 정하고 계속 다음 버튼을 눌러 넘어감

  • 권한에서 모든 퍼블릭 액세스 차단 체크박스 해제(실무에서는 해제하지 않는 것이 좋음)

image


🍳 버킷 생성 확인하기

화면이 뜨면 nodebird 버킷 클릭

image


🍳 버킷 정책 수정하기

권한 – 버킷 정책 메뉴 선택

image

  • 다음 코드를 입력 후 저장
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Sid": "AddPerm",
			"Effect": "Allow",
			"Principal": "*",
			"Action": [
			    "s3:GetObject",
			    "s3:PutObject"
			    ],
			"Resource": "arn:aws:s3:::버킷명/*"
		}
	]
}

s3:GetObject는 S3로부터 데이터를 가져오는 권한 s3:PutObject는 S3에 데이터를 넣는 권한

image


🍳 액세스 키 발급받기

상단 메뉴에서 계정 이름 클릭 후 내 보안 자격 증명 메뉴 선택

  • 보안 자격 증명으로 계속 버튼 클릭

  • 실 서비스에서는 IAM 사용자 시작하기 버튼 누를것

액세스 키 만들기 버튼 클릭

보안 액세스 키는 다시 볼 수 없으므로 키 파일 다운로드 버튼 눌러 저장

image

image

image


🍳 aws-sdk로 S3 도입하기

multer-s3와 aws-sdk 패키지를 설치한 후 .env에 발급받은 보안 키 기입

1
npm i multer-s3 aws-sdk

🔻.env

1
2
3
4
5
6
7
8
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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


image

AWS.config.update로 AWS에 관한 설정을 함(ap-northeast-2는 서울 리전)

multer를 multerS3로 교체함(버킷은 여러분의 버킷명을 사용할 것)

req.file.location에 S3 버킷 이미지 주소가 담겨 있음

🔻 nodebird/routes/post.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { S3Client } = require('@aws-sdk/client-s3');
const multerS3 = require('multer-s3');

const { afterUploadImage, uploadPost } = require('../controllers/post');
const { isLoggedIn } = require('../middlewares');

const router = express.Router();

try {
  fs.readdirSync('uploads');
} catch (error) {
  console.error('uploads 폴더가 없어 uploads 폴더를 생성합니다.');
  fs.mkdirSync('uploads');
}

const s3 = new S3Client({
  credentials: {
    accessKeyId: process.env.S3_ACCESS_KEY_ID,
    secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
  },
  region: 'ap-northeast-2',
});
const upload = multer({
  storage: multerS3({
    s3,
    bucket: 'nodebird03',
    key(req, file, cb) {
      cb(null, `original/${Date.now()}_${file.originalname}`);
    },
  }),
  limits: { fileSize: 5 * 1024 * 1024 },
});

// POST /post/img
router.post('/img', isLoggedIn, upload.single('img'), afterUploadImage);

// POST /post
const upload2 = multer();
router.post('/', isLoggedIn, upload2.none(), uploadPost);

module.exports = router;

🔻 nodebird/controllers/post.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const { Post, Hashtag } = require('../models');

exports.afterUploadImage = (req, res) => {
  console.log(req.file);
  res.json({ url: req.file.location });
};

exports.uploadPost = async (req, res, next) => {
  try {
    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);
  }
};

🍳 이미지 업로드 시도하기

http://localhost:8001에 접속하여 로그인 후 이미지 업로드

S3 버킷에 이미지가 업로드된 것 확인

image




💻 AWS LAMBDA 사용하기

🍳 이미지 리사이징을 위해 람다 사용

이미지 리사이징은 CPU를 많이 사용하기 때문에 기존 서버로 작업하면 무리가 감

Lambda라는 기능을 사용해 필요할 때만 서버를 실행해서 리사이징

image


🍳 람다용 package.json 작성하기

aws-upload 폴더 만든 후 package.json 작성

Lambda라는 기능을 사용해 필요할 때만 서버를 실행해서 리사이징 🔻aws-upload/package.json

1
2
3
4
5
6
7
8
9
10
11
12
{
  "name": "aws-upload",
  "version": "0.0.1",
  "description": "Lambda 이미지 리사이징",
  "main": "index.js",
  "author": "leekoby",
  "license": "ISC",
  "dependencies": {
    "aws-sdk": "^2.634.0",
    "sharp": "^0.25.1"
  }
}

🔻 aws-upload/.gitignore

1
node_modules


🍳 sharp로 리사이징하기

Sharp는 이미지 리사이징을 위한 라이브러리

  • exports.handler가 람다 실행 부분

  • event에 버킷과 데이터 정보가 들어 있음

  • s3.getObject로 이미지를 버킷에서 가져옴

  • sharp로 리사이징

  • resize(가로, 세로, 모드), 모드는 inside(비율 유지하면서 꽉 차게)

  • toFormat으로 확장자 지정, toBuffer로 버퍼로 변환

  • S3.putObject로 버킷에 이미지 데이터 저장

  • callback으로 람다 종료 및 응답 데이터 전달

🔻aws-upload/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const sharp = require('sharp');
const { S3Client, GetObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3');

const s3 = new S3Client();

// 고양이.png
// %CD%AE%AW.png
exports.handler = async (event, context, callback) => {
    const Bucket = event.Records[0].s3.bucket.name;
    const Key = decodeURIComponent(event.Records[0].s3.object.key); // original/고양이.png
    const filename = Key.split('/').at(-1);
    const ext = Key.split('.').at(-1).toLowerCase();
    const requiredFormat = ext === 'jpg' ? 'jpeg' : ext;
    // sharp에서는 jpg 대신 jpeg 사용합니다.
    console.log('name', filename, 'ext', ext);

    try {
        const getObject = await s3.send(new GetObjectCommand({ Bucket, Key }));
        const buffers = [];
        for await (const data of getObject.Body) {
            buffers.push(data);
        }
        const imageBuffer = Buffer.concat(buffers); // 버퍼로 가져오기
        console.log('put', imageBuffer.length);
        const resizedImage = await sharp(imageBuffer) // 리사이징
            .resize(200, 200, { fit: 'inside' })
            .toFormat(requiredFormat)
            .toBuffer();
        await s3.send(new PutObjectCommand({
            Bucket,
            Key: `thumb/${filename}`, // thumb/고양이.png
            Body: resizedImage,
        }))
        console.log('put', resizedImage.length);
        return callback(null, `thumb/${filename}`);
    } catch (error) {
        console.error(error);
        return callback(error);
    }
}


🍳 코드 깃허브로 전송하기

먼저 GitHub에 aws-upload 리파지토리로 올린 후 Lightsail 인스턴스에서 클론

image

압축 후 S3로 업로드

image


🍳 람다 서비스 설정하기

image


🍳 새 함수 만들기

함수 생성 버튼 클릭

image

함수명은 node-deploy로, 런타임은 Node.js 18.x으로

역할은 템플릿에서 새 역할 생성 선택, S3 객체 읽기 전용 권한 부여

image


🍳 zip 파일 업로드하기

함수 코드 섹션에서 S3에 올린 파일을 선택

https://버킷명.s3.지역명.amazonaws.com/파일명

image

image


🍳 트리거 설정하기

image

모든 객체 생성 이벤트를 선택하고 접두사에 original/ 누른 후 저장

image


🍳 NodeBird에 람다 연결하기

기존 original 폴더 부분을 thumb(리사이징 됨) 폴더로 교체

🔻 nodebird/controllers/post.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const { Post, Hashtag } = require('../models');

exports.afterUploadImage = (req, res) => {
  console.log(req.file);
  const originalUrl = req.file.location;
  const url = originalUrl.replace(/\/original\//, '/thumb/');
  res.json({ url, originalUrl });
};

exports.uploadPost = async (req, res, next) => {
  try {
    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);
  }
};

이미지 리사이징이 오래걸려서 리사이징된 이미지가 일정 기간 동안 표시되지 않는 경우를 대비해 img 태그에 onerror 속성을 붙여서 리사이징 이미지를 로딩하는 데 실패하면 원본 이미지를 사용하도록 수정

🔻 nodebird/views/main.html

html


🍳 이미지 업로드하기

람다를 통해 이미지 리사이징 된 것 확인하기

image




💻 Google Cloud Storage 사용하기

🍳 Cloud Storage 이용하기

좌측 메뉴에서 Storage를 선택한 후 버킷 만들기 버튼 클릭

image


🍳 Cloud Storage 버킷 만들기

버킷 이름은 고유해야 하므로 고유한 버킷 이름 정한 후 만들기 버튼 클릭

image


🍳 버킷 권한 수정하기

우측 버킷 메뉴에서 액세스 수정 선택

구성원 추가에 allUsers를 입력하고 저장소 개체 뷰어 선택 후 추가

image

image

image

image

  • 위 단계에서 오류가 발생할 경우 공개 액세스 방지를 삭제해주면 됨

image

image


🍳 클라우드 스토리지 키 발급받기

https://console.cloud.google.com/apis/credentials 에 접속하기

사용자 인증 정보 화면에서 서비스 계정 키 선택

image

image

image

image

image

image

image

image

image


🍳 클라우드 스토리지 연결하기

multer-google-storage와 axios 설치

  • routes/post.js에 multer-google-storage를 multer 대신 연결

  • 아까 다운받은 json 파일 이름을 keyFilename에 입력, projectId는 프로젝트의

  • 아이디 입력(홈 메뉴의 프로젝트 정보 섹션에 있음)

image

1
npm i multer-google-storage

🔻 nodebird/routes/post.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const multerGoogleStorage = require('multer-google-storage');

const { afterUploadImage, uploadPost } = require('../controllers/post');
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: 'nodebird3',
    projectId: 'node-deploy-358509',
    keyFilename: 'node-deploy-358509-a2917cd5849c.json',
    filename: (req, file, cb) => {
      cb(null, `original/${Date.now()}_${file.originalname}`);
    },
  }),
  limits: { fileSize: 5 * 1024 * 1024 },
});

// POST /post/img
router.post('/img', isLoggedIn, upload.single('img'), afterUploadImage);

// POST /post
const upload2 = multer();
router.post('/', isLoggedIn, upload2.none(), uploadPost);

module.exports = router;

🔻 nodebird/controllers/post.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const { Post, Hashtag } = require('../models');

exports.afterUploadImage = (req, res) => {
  console.log(req.file);
  res.json({ url: req.file.path });
};

exports.uploadPost = async (req, res, next) => {
  try {
    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);
  }
};


🍳 이미지 업로드 시도하기

http://localhost:8001에 접속하여 로그인 후 이미지 업로드

클라우드 스토리지 버킷에 이미지가 업로드된 것 확인

image




💻 Google Cloud Functions 사용하기

🍳 이미지 리사이징을 위해 펑션 사용

이미지 리사이징은 CPU를 많이 사용하기 때문에 기존 서버로 작업하면 무리가 감

Cloud Functions라는 기능을 사용해 필요할 때만 서버를 실행해서 리사이징

image


🍳 펑션용 package.json 작성하기

gcp-upload 폴더 안에 package.json 작성하기

🔻 gcp-upload/package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
{
  "name": "gcp-upload",
  "version": "0.0.1",
  "description": "Cloud Functions 이미지 리사이징",
  "main": "index.js",
  "author": "leekoby",
  "license": "ISC",
  "dependencies": {
    "@google-cloud/storage": "^6.4.1",
    "gm": "^1.23.1",
    "sharp": "^0.30.5"
  }
}

🔻 gcp-upload/.gitingnore

1
node_modules


🍳 펑션으로 이미지 리사이징하기

resizeAndUpload 메서드에 코드 작성

  • storage.bucket(버킷명).file(파일명)
  • readStream으로 파일 읽어들임
  • sharp로 이미지 리사이징
  • writeStream으로 파일 출력
  • resolve로 응답 마무리

🔻 gcp-upload/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const storage = require('@google-cloud/storage')();
const sharp = require('sharp');

exports.resizeAndUpload = (data, context) => {
  const { bucket, name } = data;
  const ext = name.split('.').at(-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 리포지터리로 올린 후 컴퓨트엔진 인스턴스에서 클론

image

image

압축 후 클라우드 스토리지로 업로드


🍳 코드 압축해서 클라우드 스토리지로 보내기

압축 후 S3로 업로드

image

image


🍳 펑션 이용하기

image

image

image

image

image


🍳 NodeBird에 펑션 연결하기

🔻 nodebird/controllers/post.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const { Post, Hashtag } = require('../models');

exports.afterUploadImage = (req, res) => {
  console.log(req.file);
  const filePath = req.file.path.split('/').splice(0, 3).join('/');
  const originalUrl = `${filePath}/${req.file.filename}`;
  const url = originalUrl.replace(/\/original\//, '/thumb/');
  res.json({ url, originalUrl });
};

exports.uploadPost = async (req, res, next) => {
  try {
    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);
  }
};

🔻 nodebird/views/main.html

html


🍳 이미지 업로드하기

펑션을 통해 이미지 리사이징 된 것 확인하기

image

image




💻 함께 보면 좋은 자료

S3

람다

Google Cloud Storage

Google Functions

sharp




📚 레퍼런스

조현영. Node.js 교과서 = Node.js Textbook / 조현영 지음 (2022). Print.

[리뉴얼] Node.js 교과서 - 기본부터 프로젝트 실습까지

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.