본문 바로가기

BackEnd/Node

[노드교과서] 섹션 15. 16장 AWS 서버리스(S3+Lambda) 사용하기

16.1 서버리스

 1. 서버리스 컴퓨팅 이해하기

  ⇒ 서버리스는 영어로 'serverless'이다. 'server(서버) + less(없는)' 이지만, 사실 서버가 없다는 것이 아니다.

     클라우드 서비스가 대신 관리함으로 개발자/운영자가 서버를 관리하는 부담이 줄어든다는 의미이다.

   - 서버리스 컴퓨팅을 할 때는 AWS ES2나 구글 컴퓨트 엔진(Google Compute Engine : GCE)과는 다르게

    WM 인스턴스를 미리 구매하지 않아도 된다.

   - 단순히 코드를 업로드 한 뒤, 사용량에 따라 요금을 지불하면 된다.

    (함수처럼 호출할 때만 실행됨, FaaS(Function as a Service)

   - 24시간 작동할 필요가 없는 서버인 경우, 서버리스 컴퓨팅을 사용하면 필요한 경우에만 실행되어 요금 절약 가능

   - AWS : 람다(Lambda)나 게이트웨어(API Gateway), S3 등

   - GCP : 클라우드 런(Cloud Run), 파이어베이스(Firebase), 클라우드 펑션스(Cloud Functions)

              , 클라우드 스토리지(Clod Storage)

16.2 AWS S3 사용하기

 1. AWS S3 사용해보기

노드교과서

 2. 버킷 만들기 ( 버킷 만들기나 시작하기 버튼 클릭)

노드교과서

 3. 버킷 리전 설정하기

  ⇒ 버킷 이름은 고유한 이름을 사용할 것

   - 이름, 리전 정의하기.  권한에서 모든 퍼블릭 액세스 차단 체크박스 해제

   - 실제 운영되는 서비스는 S3 앞에 다른 서비스를 둬 S3에 있는 파일에 접근해도 되는지 권한을 체크해

    해당 파일에 접근할 수 있게 한다.

노드교과서

 4. 버킷 생성 확인하기 ( 화면이 뜨면 생성된 버킷 클릭)

노드교과서

 5. 버킷 정책 수정하기 (권한 - 버킷 정책 - 편집 메뉴 선택)

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AddPerm",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:GetObject",
                "s3:PutObject"
            ],
            "Resource": "arn:aws:s3:::nodejsbook9488/*"
        }
    ]
}

 6. 내 보안 자격 증명하기(상단 메뉴 계정이름 클릭 - 내 보안 자격증명 메뉴 선택)

노드교과서

 7. 액세스 키 발급 받기 (엑세스 키 섹션에서 새 엑세스 키 만들기 버튼 클릭)

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

노드교과서

9. aws-sdk로 S3 도입하기

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

  - 과금요소 : 1년간 저장용량 5GB, 데이터 로드 2만건, 데이터 업로드 2천건까지 무료(보안엑세스키 주의)

// .env
...
S3_ACCESS_KEY_ID=DKWOPJIDFOISJ1234JIOIJWF
S3_SECRET_ACCESS_KEY_ID=SDLKDFJOIWDJFP2IO3J1ORIFJ2OIEJ2ASDFQWSDS1

 10. aws-sdk로 S3 도입하기

  ⇒ S3 Client로 AWS에 관한 설정을 한다.(ap-northeast-2는 서울 리전)

// routes/post.js

const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');

const { afterUploadImage, uploadPost } = require('../controller/post');
const { isLoggedIn } = require('../middlewares');
const { S3Client } = require('@aws-sdk/client-s3');
const multerS3 = require('multer-s3');


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_ID,
    },
    region : 'ap-northeast-2',  // 지역코드
});


const upload = multer({
    /*
    storage : multer.diskStorage({
        destination(req, file, cb) {
            cb(null, 'uploads/');
        },
        filename(req, file, cb) {
            const ext = path.extname(file.originalname);
            console.log("ext : " + ext);
            console.log("file.originalname : " + ext);
            console.log("path.basename(file.originalname, ext) : " + path.basename(file.originalname, ext));
            cb(null, path.basename(file.originalname, ext)+ Date.now() + ext);
        },
    }),
    */
    storage : multerS3({
       s3,
       bucket : 'nodejsbook9488',
       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);

// form의 enctype이 multipart/form-data 일경우 빈 multer를 전달해줘야 submit이 정상처리된다.
const upload2 = multer();
router.post('/', isLoggedIn, upload2.none(), uploadPost); // csurf 처리

module.exports = router;

 11. 이미지 업로드 시도하기 (http://localhost:8001에 접속해 로그인 후 이미지 업로드)

  ⇒ S3 버킷에 이미지가 업로드 된 것을 확인한다.

노드교과서

16.3 AWS Lambda 사용하기

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

  ⇒ 이미지 리사이징은 CPU를 많이 사용하기 때문에 기존 서버로 작업 시 무리가 간다.

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

   - Lambda는 데이터 비용을 줄이기 위해 화질을 저하 시키고 용량을 대폭 줄인다.

노드교과서

 2. 람다용 package.json과 .gitgnore 작성하기

// aws-upload/package.json
{
  "name": "aws-upload",
  "version": "0.0.1",
  "description": "Lambda 이미지 리사이징",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "JaeikKim",
  "license": "ISC",
  "dependencies": {
    "@aws-sdk/client-s3": "^3.504.0",
    "sharp": "^0.33.2"
  },
  "devDependencies": {
    "prettier": "^3.2.4"
  }
}

 

// aws-upload/.gitgnore
node_modules

 3. sharp로 리사이징하기 ( Sharp는 이미지 리사이징을 위한 라이브러리이다.)

// aws-upload/index.js

const sharp = require('sharp'); // 이미지 리사이징 라이브러리
const { S3Client, GetObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3');

const s3 = new S3Client();

// 람다 실행부분(event에 버킷과 데이터 정보가 들어있다)
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();

    // sharp에서는 확장자가 jpg면 jpeg로 변경해줘야 한다.
    const requiredFormat = ext === 'jpg' ? 'jpeg' : ext;
    console.log('name', filename, 'ext', ext);
    try {
        // s3.send(new GetObjectCommand({ Bucket, Key})) 로 버킷에서 이미지를 가져온다.
        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();               // 버퍼로 변환
        // s3.send(new PutObjectCommand({ ... }); 로 버킷에 이미지 데이터 저장
        await s3.send(new PutObjectCommand({
            Bucket,
            Key : `thumb/${filename}`,
            Body : resizedImage,
        }));
        // 람다 종료 및 응답데이터(첫번째자리는 에러자리, 두번째자리는 응답값자리) 전달
        return callback(null, `thumb/${filename}`);
    } catch (error) {
        console.error(error);
        return callback(error);
    }
};

 4. 코드 깃 허브로 전송하기

  ⇒ 먼저 GitHub에 aws-upload 리파지토리를 생성 및 업로드 후 Lightsail 인스턴스에서 클론한다.

// 콘솔
$ git init
$ git add .
$ git commit -m "Initial commit"
$ git remote add origin https://아이디:토큰@github.com/아이디/aws-upload
$ git push origin master

// ssh
$ git clone https://github.com/아이디/aws-upload
$ cd aws-uplaod
$ npm i

 5. 코드 압축해서 S3으로 보내기

// ssh
$ sudo zip -r aws-upload.zip ./*
$ ls
aws-upload.zip ...

// ssh (S3에 파일을 업로드할 수 있는 권한 설정)
$ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
$ sudo unzip awscliv2.zip
$ sudo ./aws/install
$ aws configure
AWS Access Key ID [None] : [키 아이디]
AWS Secret Access Key [None] : [시크릿 키 아이디]
Default region name [None] : ap-northeast-2
Default output format [None] : json

$ aws s3 cp "aws-uplaod.zip" s3://버킷명

 6. 람다 서비스 설정하기 (컴퓨팅-Lambda)

노드교과서

 7. 새함수 만들기(함수 생성 버튼 클릭)

노드교과서

  ⇒ 함수명은 node-deploy로, 런타임은 Node.js 20.x로 설정

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

함수생성 화면

 9. zip 파일 업로드하기

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

 

노드교과서

 11. S3를 트리거로 설정하기 (좌측 S3를 트리거로 선택)

노드교과서

 12. 트리거 상세 설정하기

노드교과서

 13. NodeBird에 람다 연결하기

  ⇒ 기존 original 폴더 부분을 thumb(리사이징) 폴더로 교체처리한다.

// controller/post.js

const { Post, Hashtag } = require('../models');

exports.afterUploadImage = (req, res) => {
    // res.json({ url : `/img/${req.file.filename}`})
    console.log(req.file.location);
    const originalUrl = req.file.location;  // s3의 주소가 들어있다.
    const url = originalUrl.replace(/\/original\//, '/thumb/');
    res.json({ url, originalUrl });
};

...

 

// views/main.html

...
        {% for twit in twits %}
          <div class="twit">
            <input type="hidden" value="{{twit.User.id}}" class="twit-user-id">
            <input type="hidden" value="{{twit.id}}" class="twit-id">
            <div class="twit-author">{{twit.User.nick}}</div>
            {% if not followingIdList.includes(twit.User.id) and twit.User.id !== user.id %}
              <button class="twit-follow">팔로우하기</button>
            {% endif %}
            <div class="twit-content">{{twit.content}}</div>
            {% if twit.img %}
              <div class="twit-img"><img src="{{twit.img}}" onerror="this.src = this.src.replace(/\/thumb\//, '/original/');" alt="섬네일"></div>
            {% endif %}
          </div>
        {% endfor %}
...

<script>
    if (document.getElementById('img')) {
      document.getElementById('img').addEventListener('change', function(e) {
        const formData = new FormData();
        console.log(this, this.files);
        formData.append('img', this.files[0]);
        axios.post('/post/img', formData)
          .then((res) => {
            document.getElementById('img-url').value = res.data.url;
            // 리사이징되는 람다함수의 오류가 발생하거나 리사이징되는데 시간이 걸리기 때문에
            // 프리뷰는 오리지널로 설정한다.
            document.getElementById('img-preview').src = res.data.originalUrl;
            document.getElementById('img-preview').style.display = 'inline';
          })
          .catch((err) => {
            console.error(err);
          });
      });
    }
    ...
</script>

 

 14. 이미지 업로드하기

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

노드교과서