• yundream
  • 2018-06-16 17:36:39
  • 2018-06-13 08:08:05
  • 74303

Contents

클라우드 스토리지

서비스에서 다루는 데이터의 크기와 갯수가 늘어나면서, "고가용성의 확장가능한 클라우드 스토리지"에 대한 수요도 늘어나고 있다. 단일 서비스에서도 드롭박스와 같은 기능을 일반적으로 제공할 수 있어야하는 시대라는 거다.

s3를 이용해서 드롭박스와 같은 서비스를 만들어보려고 한다. s3를 이용하는 이유는 아래와 같다.
  • 가용성과 확장성을 고민 할 필요가 없다.
  • OpenStack Swift등을 이용해서 비교적 쉽게? 구축 할 수 있을 것이다. 하지만 굉장히 고달픈 작업이라는 것을 깨닫게 될 것이다.
  • 그냥 s3 기반으로 빠르게 구성하고 서비스에 신경쓰는게 정신건강, 서비스 품질 모두에 좋을 것이다.

파일 메타 정보

 파일 메타 데이터

파일의 정보를 담기 위한 데이터베이스를 구성한다. 여기에는 유저, 그룹 권한, 파일크기, 업데이트 시간, 디렉토리 등의 정보가 저장된다.

디렉토리 구성

s3에 디렉토리를 만들어서 파일을 배치하는 것은 쉬워보이지만 파일의 검색에서 단점이 있다. s3 에서 재귀적으로 찾을 수는 없는 노릇이니, 메타 데이터베이스에서 찾아야 할 텐데 디렉토리 위치를 찾는데 어려움이 있다. 메타 데이터베이스에 디렉토리 정보까지 넣는 방법을 생각 해볼 수 있겠지만, 디렉토리의 이동, 이름 변경등이 있을 수 있어서 좋은 방법이 아니다.

어차피 디렉토리 정보를 유지해야 한다면, 디렉토리도 메타정보 형식으로 관리를 하는게 낫겠다. 결국 s3에는 디렉토리를 사용하지 않고 Flat 하게 구성한다.

디렉토리 구조를 저장하기 위한 4가지 주요 모델은 아래와 같다.
  • adjacency lists
  • path enumeration
  • nested sets
  • closur tables
나는 adjacency list 모델을 이용하기로 했다. 대략 테이블 구조는 아래와 같다.
CREATE TABLE files (
  id int(10) unsigned NOT NULL AUTO_INCREMENT,
  name varchar(255) NOT NULL,
  parent_id int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (id),
  FOREIGN KEY (parent_id) REFERENCES category (id) 
    ON DELETE CASCADE ON UPDATE CASCADE
);
parent_id를 이용해서 아래와 같은 디렉토리 구조를 만들 수 있다.

 Directory

파일 권한관리

이제 파일에 대한 소유/권한을 관리해야 한다. 리눅스 파일 시스템을 참고해 볼 수 있겠다.
  • Owner : 파일을 만든 사람이다. 파일 읽기/수정/삭제 등 모든 작업을 할 수 있다.
  • Group : 그룹 단위로 권한을 제어 할 수 있다. Owner과는 달리 수정/삭제 권한이 제한 될 수 있다.
  • Other : 다른 유저에게 권한을 줄 수 있다. 역시 수정/삭제 권한이 제한될 수 있다.
권한은 "읽기", "쓰기", "실행" 조합을 가진다. 파일 시스템에서는 이 둘의 조합을 이용해서 파일의 모든 소유/권한 관계를 설정 할 수 있다. 이제 오브젝트 스토리지에 일반 파일 시스템의 소유/권한 관계를 사용 할 수 있을지 살펴보자.

  • 권한 측면에서 "실행"은 필요 없을테다. 빼자.
  • 파일의 Owner는 명확하다. 생성, 삭제, 업데이트 모든 권한을 가질 수 있다. 고민 할게 없다.
  • Group은 필요하다. 파일을 여러 명에게 공유할 때 사용할 수 있다. 새로운 Group ID를 만들어야 한다.
  • 특정 개인에게의 공유도 Group를 사용하면 된다. 유저의 ID를 Group ID로 만든다.
임의의 유저에게 파일을 공유 할 수 있다는 점 때문에, Group 관리가 복잡해 진다. 위의 유저의 ID를 Group ID로 할 경우, 순수한 Group ID와 구분하는데 문제가 생길 수 있다. 그냥 Group도 유저로 관리하면 된다.
  • 일반유저 : 일반 서비스 사용자
  • 그륩유저 : 그룹으로 존재하는 사용자
 Group User 관계

파일은 하나 이상의 Owner를 가지고 있다. 만약 Owner이 group 타입이라면, 멤버 테이블을 조회해서 권한을 확인 할 수 있다.

파일 업로드 & 다운로드 서비스

모델 - 1

일반적으로 생각 할 수 있는 방법은 업로드, 다운로드를 위한 API 서버를 개발하는 것이다. 이 API 서버는 유저의 요청을 읽어서 권한을 확인 한 다음 S3 SDK를 이용해서 파일을 업로드 하거나 다운로드 하는 일을 한다. 파일변환등의 작업이 있을 경우, 업로드 과정이 비동기적으로 진행될 수 있을 것이다. 동기적으로 업로드 할 경우 구성은 아래와 같을 것이다.

 파일 다운로드 모델

단순하고 구현이 쉽다. 반면 중간에 위치하는 WAS 때문에 스케일 관리에 이슈가 있다. 적당한 크기의 서비스에서는 문제가 없겠으나 규모가 커질 경우 적당한 방법은 아니다.

모델 - 2

 모델 - 2

Private S3에 접근하기 위해서 signed url을 발급 할 수 있다. Signed URL은 HMAC구현으로 유효시간과 URL이 포함된 시그니처를 만들어서 유저에게 배포한다.

s3 cli로 presigned URL을 만들어보자. 사용법은 아래와 같다.
# aws s3 presign --expires-in <유효시간-초> S3Uri
--expires-in을 설정하지 않은 경우 3600(1시간)이 기본 설정된다.

s3://s3.joinc.co.kr/test.txt를 위한 signed url을 만들어봤다. 원래는 한 줄인데, 가독성을 위해서 줄을 나눴다.
# aws s3 presign s3://s3.joinc.co.kr/test.txt
https://s3.ap-northeast-2.amazonaws.com/s3.joinc.co.kr/s3.go? \
X-Amz-Algorithm=AWS4-HMAC-SHA256& \
X-Amz-Expires=3600& \
X-Amz-Credential=AKIxxxxxxx%2Fxxxxxx%2Fap-northeast-2%2Fs3%2Faws4_request& \
X-Amz-SignedHeaders=host& \
X-Amz-Date=20180616T064638Z& \
X-Amz-Signature=d9dee529faddd6da48389d019becce129763498xxxxxxxxxxxxxxxxxxx
  • X-Amz-Algorithm: 시그니처를 만들기 위해서 사용한 알고리즘이다.
  • X-Amz-Expires : URL의 유효시간
  • X-Amz-Credential : AWS Access Key
  • X-Amz-Date : URL을 만든 시간
  • X-Amz-Signature : X-Amz-Algorithm과 다른 값들을 조합해서 만든 시그니처
AWS는 HTTP 요청에 대한 X-Amz-Signature를 검증해서 파일에 대한 업로드/다운로드를 허용한다. 프로세스를 정리해 보자.
  1. 클라이언트가 파일 요청 한다.
  2. 이 요청을 받은 Lambda 함수는 S3 Presigne URL 생성 요청을 한다.
  3. AWS는 S3 Presigne URL을 반환 한다. 이 URL은 실정한 제한 시간 안에서만 사용 할 수 있다.
  4. 클라이언트에게 Presigne URL이 전달된다.
  5. 클라이언트는 HTTP로 Presigne URL로 파일을 다운로드 한다.

S3 SDK로 테스트

설명은 주석으로 대신한다.
package main

import (
    "fmt"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/s3"
    "log"
    "os"
    "time"
)

func main() {
    // 서울리전(ap-northeast-2)에 api 요청을 위한 세션을 만든다. 
    // ~/.aws/credentials 를 기본 값으로 사용한다.
    sess, err := session.NewSession(&aws.Config{
        Region: aws.String("ap-northeast-2")},
    )

    // s3 서비스 클라이언트를 만든다.
    svc := s3.New(sess)
    req, _ := svc.GetObjectRequest(&s3.GetObjectInput{
        Bucket: aws.String("s3.joinc.co.kr/test.txt"),  // 접근할 버킷
        Key:    aws.String("AXXXXXXXXXXXX"),   // AWS KEY
    })

    // 30분 동안 사용 할 수 있는 sign url을 만든다.
    urlStr, err := req.Presign(30 * time.Minute)
    if err != nil {
        log.Println("Error ", err)
        os.Exit(1)
    }
    fmt.Println("Signed Url is ", urlStr)
}