외로운 Nova의 작업실

aws - 글로벌 사진 사이트 구축 본문

Cloud/aws

aws - 글로벌 사진 사이트 구축

Nova_ 2023. 10. 22. 19:39

- 글로벌 사진 사이트

글로벌 사진 사이트는 인스타그램같은 서비스를 떠올리면 이해하기 쉽습니다. 사용자가 사진을 올리면, 서버는 그 사진을 웹사이트에 맞게 조절해서 저장해놓고 그다음 클라이언트가 요청하면 이미지를 보내는 서비스입니다. 글로벌 사진 사이트를 구축하려면 어떻게 해야할까요? 사용할 AWS 리소스를 선택하기 전에 구축하려는 서비스의 요구사항을 먼저 파악해보겠습니다.

 

- 요구사항

  • 대용량의 이미지 파일을 저장해야합니다
  • 전 세계 어디서나 사이트 및 이미지를 보여주는 속도가 빨라야합니다.
  • 원본 사진의 크기를 웹사이트에 맞게 조절해야합니다.(요즘 사진들은 해상도가 높기떄문)
  • 사용자가 늘어났을때 대비할 수 있어야합니다.

이제 요구사항에 맞게 사용할 AWS 리소스와 서비스 구조를 설계해보겠습니다.

  • 대용량 이미지 파일을 저장해야하니 S3가 적합합니다.
  • 전세계에 빠르게 사이트와 이미지를 배포해야하니 CloudFront가 적합합니다.
  • 원본 사진의 크기를 조절할 때는 EC2를 사용해야합니다. 하지만, 이미지 변환을 웹서버에서 처리한다면 웹사이트 반응 속도에 영향을 미칩니다. 따라서 변환할 이미지 목록을 SQS에 메시지로 보낸뒤 변환 전용 EC2 인스턴스에서 처리하면 됩니다.
  • EC2에 웹서버를 구축하고 데이터는 RDS에 저장합니다. 사용자가 증가했을때는 ELB와 Auto Scaling으로 대응하면 되고, RDS의 DB인스터늣 클래스를 높이고 용량을 높이면됩니다.
  • 도메인 처리는 Route53을 사용합니다.(도메인이 없다면 사용안해도됩니다.)
  • 사이트 소스 파일은 S3에 저장하겠습니다. 

 

ExamplePhoto 서비스의 동작흐름입니다.

1. 사용자는 examplephoto.com에 이미지 파일을 올립니다.

2. 웹 서버 EC2 인스턴스는 이미지 파일을 S3 버킷에 저장하고, 이미지 파일명을 SQS 메시지로 보냅니다. 그리고 이미지 파일 정보를 RDS 데이터베이스에 저장합니다.

3. 이미지 변환 EC2 인스턴스에서는 SQS에서 이미지를 받은 뒤 이미지 파일의 해상도를 줄입니다. 그리고 해상도를 줄인 이미지파일을 S3 버킷에 저장하고 SQS 메시지를 삭제합니다.

4. 웹서버에서는 RDS 데이터베이스에서 이미지 파일 목록을 가져와서 보여줍니다.

 

 

- 이미지, 소스 저장용 S3 버킷 생성

S3버킷은 이미지를 담을 버킷하나와 웹서버의 소스코드를 담을 버킷하나를 만들어야합니다. 먼저, 이미지를 담을 버킷하나를 생성해보겠습니다.

버킷이름을 만들어줍니다. 이제 웹서버용 버킷을 만들어보겠습니다.

버킷이름을 잘 설정해줍니다.

나중에 사용할 폴더도 만들어줍니다.

 

- 이미지 정보 저장용 RDS DB 인스턴스 생성

이미지 파일의 목록을 저장하기 위해 Mysql 데이터베이스 엔진을 사용하는 RDS DB 인스턴스를 생성합니다.

이렇게하고 적절히 보안그룹이랑 VPC를 설정해줍니다.

 

- S3, SQS 접근용 IAM 역할 생성

EC2를 선택합니다.

이렇게 2개를 선택해줍니다.

이름은 위처럼 생성해줍니다.

초기 데이터베이스 이름도 생성해줍니다.

 

- 이미지 처리용 SQS 큐 생성

 

방금 만든 역할을 설정해줍니다.

 

- 웹서버용 ELB 생성

ELB를 생성하기전에 대상 그룹을 하나 만들어줍니다.

웹 서버의 부하를 분산하기 위한 ELB 로드 밸런서를 생성합니다. ELB 로드밸런서 이름은 examplephoto이고 80포트로 설정합니다. EC2 인스턴스는 아직 연결하지 않아도됩니다.

이름을 설정해줍니다.

아까만든 그룹을 넣어줍니다.

 

- CloudFront 배포 생성

웹서버용 CloudFront와 이미지용 CloudFront 배포를 생성해보겠습니다. 먼저, 웹서버용입니다.

웹서버용이기때문에 ELB를 할당해줍니다.

 

POST도 허용해줍니다. 그리고 생성해줍니다.

이제 이미지용 CloudFront를 생성해보겠습니다.

이미지 S3를 생성해줍니다.

나중에 버킷 정책을 업데이트해줘야합니다.

정책도 업데이트해줍니다.

 

- 웹서버 EC2 인스턴스 생성

먼저 웹서버 인스턴스는 auto scaling 기능으로 인스턴스가 계속 만들어지는데, 그때마다 소스코드를 S3버킷에서 받아올 것입니다. 따라서 S3 버킷에 먼저 웹서버 코드를 올려주겠습니다.

//app.js

var express = require('express')
  , multer = require('multer')
  , AWS = require('aws-sdk')
  , Sequelize = require('sequelize')
  , mime = require('mime')
  , http = require('http')
  , fs = require('fs')
  , app = express()
  , server = http.createServer(app)
  , s3 = new AWS.S3({ region: 'ap-northeast-2' })
  , sqs = new AWS.SQS({ region: 'ap-northeast-2' });

var s3Bucket = 'examplephoto-study.image';
var sqsQueueUrl = 'https://sqs.ap-northeast-2.amazonaws.com/396540618055/examplephoto';
var rdsEndpoint = {
  host: 'examplephoto.c3x38juuohni.ap-northeast-2.rds.amazonaws.com',
  port: 3306
};

// MySQL DB 이름, 계정, 암호
var sequelize = new Sequelize('examplephoto', 'admin', 'examplephoto', {
  host: rdsEndpoint.host,
  port: rdsEndpoint.port
});

// MySQL DB 테이블 정의
var Photo = sequelize.define('Photo', {
  filename: { type: Sequelize.STRING, allowNull: false, unique: true }
});

// MySQL DB 테이블 생성
sequelize.sync();

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

app.get(['/', '/index.html'], function (req, res) {
  fs.readFile('./index.html', function (err, data) {
    res.contentType('text/html');
    res.send(data);
  });
});

// 이미지 목록 출력
app.get('/images', function (req, res) {
  Photo.findAll().success(function (photoes) {
    var data = [];
    photoes.map(function (photo) { return photo.values; }).forEach(function (e) {
      data.push(e.filename);
    });

    res.header('Cache-Control', 'max-age=0, s-maxage=0, public');
    res.send(data);
  });
});

// 웹 브라우저에서 이미지 받기
app.post('/images', function (req, res) {
  fs.readFile(req.files.images.path, function (err, data) {
    var filename = req.files.images.name;
    s3.putObject({
      Bucket: s3Bucket,
      Key: 'original/' + filename,
      Body: data,
      ContentType: mime.lookup(filename)
    }, function (err, data) {
      if (err)
        console.log(err, err.stack);
      else {
        console.log(data);
        
        sqs.sendMessage({
          MessageBody: filename,
          QueueUrl: sqsQueueUrl
        }, function (err, data) {
          if (err)
            console.log(err, err.stack);
          else
            console.log(data);
        });
      }
    });
  });

  res.send();
});

server.listen(80);
//index.html

<!DOCTYPE HTML>
<html>
<head>
  <title>ExamplePhoto</title>
  <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
  <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/blueimp-file-upload/9.5.7/css/jquery.fileupload.min.css">
  
  <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
  <script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/jquery-ui.min.js"></script>
  <script src="//cdnjs.cloudflare.com/ajax/libs/blueimp-file-upload/9.5.7/jquery.fileupload.min.js"></script>
</head>
<body>
  <span class="btn btn-success fileinput-button">
    <i class="glyphicon glyphicon-plus"></i>
      <span>Select files...</span>
      <input id="fileupload" type="file" name="images" multiple>
  </span>
  
  <div id="progress" class="progress">
    <div class="progress-bar progress-bar-success"></div>
  </div>
  
  <div id="imagelist"></div>
  
  <script>
    $(function () {
      $('#fileupload').fileupload({
        url: '/images',
        dataType: 'json',
        progressall: function (e, data) {
          var progress = parseInt(data.loaded / data.total * 100, 10);
          $('#progress .progress-bar').css('width', progress + '%');
        }
      });
      
      $.getJSON('/images', function (data) {
        $.each(data, function (i, e) {
          var img = $('<img>');
          // 도메인을 구입하였다면 image 서브 도메인 입력
          img.attr('src', 'https://d2pdttatnl5t6o.cloudfront.net/resized/' + e)
          .attr({'width': '150px', 'height': '150px' })
          .addClass('img-thumbnail');
          // 도메인을 구입하지 않았다면 CloudFront 배포 도메인 입력
          //img.attr('src', 'http://d3fo0v5xpnp6x5.cloudfront.net/resized/' + e)
          //.attr({'width': '150px', 'height': '150px' })
          //.addClass('img-thumbnail');
          $('#imagelist').append(img);
        });
      });
    });
  </script>
</body>
</html>
//package.json

{
  "name": "ExamplePhotoWebServer",
  "version": "0.0.1",
  "description": "ExamplePhotoWebServer",
  "dependencies": {
    "express": "4.4.x",
    "multer": "0.1.x",
    "aws-sdk": "2.0.x",
    "mime": "1.2.x",
    "sequelize": "1.7.x",
    "mysql": "2.3.2"
  }
}

위 파일 3개를 S3에 올려줍니다.

이제 AMI를 만들기 위해서 EC2 인스턴스를 만들어줍니다. AMI는 Auto Scaling에서 사용할 것입니다.

 

역할을 EC2에 넣어줍니다.

이제 SSH로 연결해서 각종 웹서버와 패키지 모듈등을 설치해줍니다.

sudo yum install -y nodejs npm
sudo npm install -g forever //업데이트시 자동으로 Node.js를 다시시작해줍니다.
mkdir ExamplePhotoWebServer
aws s3 sync --region=ap-northeast-2 s3://examplephoto-study.src/ExamplePhotoWebServer ExamplePhotoWebServer 
cd ExamplePhotoWebServer
npm install

이제 다되었습니다.

 

- Auto Scaling 그룹 만들기

AMI를 만들어보겠습니다.

이제 Auto Scaling에서 사용할 시작 템플릿을 만들어주겠습니다.

이름을 설정해주고

AMI설정해줍니다.

IAM도 설정해줍니다.

#!/bin/bash
cd /home/ec2-user
aws s3 sync --region=ap-northeast-2 \
s3://examplephoto-study.src/ExamplePhotoWebServer ExamplePhotoWebServer
cd ExamplePhotoWebServer
npm install
node app.js

 

 

UserData도 넣어줍니다.

이제 auto scaling 그룹을 만들어줍니다.

 

템플릿을 선택해줍니다.

로드밸런서에 연결해줍니다. 그리고 생성해줍니다.

접속하면 잘됩니다.

 

- 이미지 변환서버 작성 및 구축

//app.js

const AWS = require('aws-sdk');
const Sequelize = require('sequelize');
const gm = require('gm').subClass({ imageMagick: true });
const mime = require('mime');
const s3 = new AWS.S3({ region: 'ap-northeast-2' });
const sqs = new AWS.SQS({ region: 'ap-northeast-2' });

const s3Bucket = 'examplephoto-study.image';
const sqsQueueUrl = 'https://sqs.ap-northeast-2.amazonaws.com/396540618055/examplephoto';
const rdsEndpoint = {
  host: 'examplephoto.c3x38juuohni.ap-northeast-2.rds.amazonaws.com',
  port: 3306
};

// MySQL DB 이름, 계정, 암호
const sequelize = new Sequelize('examplephoto', 'admin', 'adminpassword', {
  host: rdsEndpoint.host,
  dialect: 'mysql'
});

// MySQL DB 테이블 정의
const Photo = sequelize.define('Photo', {
  filename: { type: Sequelize.STRING, allowNull: false, unique: true }
});

// SQS 메시지 삭제
function deleteMessage(ReceiptHandle) {
  sqs.deleteMessage({
    QueueUrl: sqsQueueUrl,
    ReceiptHandle: ReceiptHandle
  }, function (err, data) {
    if (err)
      console.log(err, err.stack);
    else
      console.log(data);
  });
}

// MySQL에 데이터 저장
async function insertPhoto(filename) {
  await sequelize.sync();
  await Photo.create({
    filename: filename
  });
}

// SQS 메시지 받기
function receiveMessage() {
  sqs.receiveMessage({
    QueueUrl: sqsQueueUrl,
    MaxNumberOfMessages: 1,
    VisibilityTimeout: 10,
    WaitTimeSeconds: 10
  }, function (err, data) {
    if (!err && data.Messages && data.Messages.length > 0)
      resizeImage(data.Messages[0]);
    else if (err)
      console.log(err, err.stack);
    receiveMessage();
  });
}

// 이미지 해상도 변환
function resizeImage(Message) {
  const filename = Message.Body;
  s3.getObject({
    Bucket: s3Bucket,
    Key: 'original/' + filename
  }, function (err, data) {
    gm(data.Body)
      .resize(800)
      .toBuffer('PNG', function (err, buffer) {
        if (err) {
          console.error(err);
          return;
        }
        s3.putObject({
          Bucket: s3Bucket,
          Key: 'resized/' + filename,
          Body: buffer,
          ACL: 'public-read',
          ContentType: mime.lookup(filename)
        }, function (err, data) {
          if (err)
            console.error(err);
          else {
            console.log('Complete resize ' + filename);
            deleteMessage(Message.ReceiptHandle);
            insertPhoto(filename);
          }
        });
      });
  });
}

receiveMessage();
//package.json

{
  "name": "ExamplePhotoResizeServer",
  "version": "0.0.1",
  "description": "ExamplePhotoResizeServer",
  "dependencies": {
    "aws-sdk": "^2.1149.0",
    "mime": "^1.4.1",
    "sequelize": "1.7.x",
    "mysql2": "^2.3.2",
    "gm": "^1.24.0"
  }
}

위 2파일을 s3버킷에 폴더를 만들고 업로드해줍니다.

이제 EC2를 만들어줍니다. 그리고 SSH로 들어가서 아래 명령어를 쳐줍니다.

sudo yum install -y nodejs npm
aws s3 sync --region=ap-northeast-2 s3://examplephoto-study.src/ExamplePhotoResizeServer ExamplePhotoResizeServer
cd ExamplePhotoResizeServer
npm install
sudo node app.js

 

- 사진 사이트 동작 확인

이제 auto scaling으로 만든 웹사이트에 접속합니다.

select File을 눌러 아무 파일이나 업로드해줍니다.

오류가 뜨는데 무슨 오류인지 모르겠습니다. 그래도 aws 아키텍처나 콘솔 사용에관해서 좋은 경험이라고 생각합니다.

'Cloud > aws' 카테고리의 다른 글

aws - Lambda  (1) 2023.10.26
aws - CLI(추가 예정)  (1) 2023.10.21
aws - Elastic Transcoder  (2) 2023.10.21
aws - SQS  (0) 2023.10.21
aws - SNS  (0) 2023.10.20
Comments