https://kyeum-d.tistory.com/32
[Object Storage] - Java SDK를 활용한 S3 Upload
https://kyeum-d.tistory.com/31 [Object Storage] 객체 저장소, 왜 선택되었나 ? 들어가며... 사이드 프로젝트로 강의 플랫폼 프로젝트를 진행하면서 가장 핵심이 되는 강의 파일 업로드에 대한 처리를 어떻게
kyeum-d.tistory.com
(이전글)
들어가며
지난 시간에 Java SDK를 활용해 Object Storage에 객체를 업로드 해봤습니다.
그런데 여기서 SDK만을 활용하여 업로드하면 어떻게 될까요?
총 2가지의 문제가 발생합니다.
- 클라이언트에서 전달하는 데이터를 서버에 저장하고 저장된 데이터를 다시 Object Storage로 전송해야합니다.
- 크기가 큰 데이터를 전달하는 과정에서 네트워크에 장애가 생긴다면 처음부터 다시 전송해야합니다.
크기가 크지 않은 데이터라면 문제가 없지만 영상같은 대용량 데이터는 한번의 네트워크 전송을 더 거친다는 것은 굉장히 부담스러운 작업입니다.
따라서 1번 문제를 해결하기 위해 REST API를 활용하고
2번 문제를 해결하기 위해 MultiPartUpload를 활용해 보겠습니다.(분산처리로 업로드 효율 증가는 덤)
전체 흐름
S3의 REST API MultiPartUpload는 다음과 같은 순서로 진행이 됩니다.
- MultiPartUpload할 파일 분할
- S3서버로부터 MultiPartUpload를 위한 Upload Id 발급
- Upload Id를 기반으로 객체 업로드를 위한 Authorization 정보 발급
- S3서버에 Upload Id와 Authorization을 포함한 정보로 업로드 요청
- S3서버에서 part 별로 ETag 반환
- ETag를 기준으로 S3서버에 part별 Upload Complete 요청
- 업로드 완료
여기서 6번 과정의 Upload Complete 요청이 무슨 과정인지 처음에 잘 이해가 가지 않을 수 있는데(제가 그랬습니다)
쉽게 말해서 RDBMS의 Commit과 비슷한 역할을 한다고 보면 됩니다.
MultiPartUpload는 part별로 분할해서 데이터를 업로드 하기 때문에 누락된 part가 존재할 수도 있습니다.
이에 S3는 각 part 별로 완료된 업로드에 대해 ETag를 반환하고 ETag를 종합하여 최종적으로 Upload Complete 요청을 보내서 하나의 객체로 결합시키는 과정을 통해 객체의 무결성과 일관성을 보장합니다.
역할 분리
클라이언트
- MultiPartUpload할 파일 분할
- Infrun 서버로 Authorization 정보 요청
- Infrun 서버로부터 받은 Authorization 정보와 분할된 파일을 S3서버에 업로드 요청
- 반환받은 Part별 ETag와 강의 정보 Infrun 서버에 전달
Infrun 서버
- S3로부터 Upload Id 발급
- 발급된 Upload Id를 기반으로 Authorization 정보 생성 후 클라이언트에 반환
- 클라이언트로부터 받은 ETag 정보를 기반으로 S3서버에 객체 Upload Complete 요청
- 강의 정보 저장
*클라이언트 역할은 PostMan을 통해서 진행했습니다
REST API를 통한 객체 업로드
1. 파일 분할
업로드 할 파일을 분할합니다.
저는 Mac에서 간단하게 split 명령어를 통해 파일을 분할했습니다.
split -n 3 test.mp4 test_
*주의 : 분할된 파일의 각 크기가 5MB가 넘지 않으면 S3 Upload 요청이 거절됩니다.
👉🏼 분할된 파일의 크기가 최소 5MB 이상이 되도록 분할해주세요
2. S3로부터 Upload Id 발급
지난시간에 살펴본 Client를 통한 객체 업로드 방법과 유사합니다.
public static String getUploadId(String bucketName, String objectKey, String profileName,
Region region) {
// String bucketName = "infrun";
// String objectKey = "test.mp4";
// String profileName = "InfrunManager";
// Region region = Region.AP_NORTHEAST_2; -> seoul
ProfileCredentialsProvider credentialsProvider = ProfileCredentialsProvider.create(
profileName);
S3Client s3 = S3Client.builder()
.region(region)
.credentialsProvider(credentialsProvider)
.build();
CreateMultipartUploadRequest createRequest = CreateMultipartUploadRequest.builder()
.bucket(bucketName)
.key(objectKey)
.build();
CreateMultipartUploadResponse createResponse = s3.createMultipartUpload(createRequest);
String uploadId = createResponse.uploadId();
System.out.println("Upload ID: " + uploadId);
return uploadId;
}
- 저장되어있는 프로필에 대한 AccessKey, SecretKey를 공급받기 위해 ProfileCredentialsProvider를 생성합니다.
- 생성한 ProfileCredentialsProvider와 region 정보를 가지고 S3Client를 build합니다.
- 어떤 Bucket에 어떤 Object를 업로드 할 것인지에 대한 Request 정보를 생성하기 위해 MultipartUploadRequest를 빌드합니다.
- S3 Client를 통해 생성한 Request를 기반으로 MultipartUpload를 요청합니다.
- S3 서버로부터 Response를 받고 받은 Response 객체의 Upload Id를 반환합니다.
생성된 Upload Id는 AWS CLI를 통해 간단하고 쉽게 확인 할 수 있습니다.
- 확인 명령어 : aws s3api list-multipart-uploads --bucket infrun
- 삭제 명령어 : aws s3api abort-multipart-upload --bucket infrun --key test.mp4 --upload-id asdasd--
3. Upload Id를 기반으로 Authorization 정보 생성
S3 업로드를 하면서 가장 까다로웠고 많이 헤맸던 부분인데
객체를 업로드 하기 위해서는 허가된 인증 정보가 필요합니다.
SDK에서 직접 upload 한다면 Credential Provider를 사용해서 권한을 부여하면 되지만
REST API를 통해 요청을 보내기 위해서는 이런 인증에 대한 Authorization정보를 직접 생성해야합니다.
물론 클라이언트 코드에서 Credential Provider 등을 활용한 인증정보를 직접 작성하는 방법도 있지만
이 경우 AccessKey와 SecretKey가 노출 될 위험이 크고
책임과 역할을 분리하는 관점에서 인증 정보에 대한 책임을 클라이언트에서 부담하는 것은 적절하지 않다고 판단했습니다.
따라서 서버에서 인증정보를 작성해야하는데 인증정보를 작성하는 방법에는 크게 2가지가 있습니다.
- AWS Signature V4를 활용한 Authorization 서명 정보 생성
- PreSignedURL을 생성하여 업로드에 대한 URL을 생성
특별한 상황이 아니라면 간단하고 쉽게 사용 할 수 있는 2번 PreSignedURL 생성하는 방법을 추천드립니다.
두가지에 대해서 동작 방식을 설명드리자면
AWS Signature V4
AWS Signature V4를 활용한 Authorization은 REST API를 통해 통신 할 Request의 Header에 들어가는 Authorization의 signature를 생성하는 작업입니다.
즉, 결과적으로 Header에 들어갈 값 자체를 생성하는 것이고 나머지 요청에 대한 정보를 전부 다 개별로 관리하고 작성해야합니다.
https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html
Authenticating Requests (AWS Signature Version 4) - Amazon Simple Storage Service
Amazon S3 supports Signature Version 4, a protocol for authenticating inbound API requests to AWS services, in all AWS Regions. At this time, AWS Regions created before January 30, 2014 will continue to support the previous protocol, Signature Version 2. A
docs.aws.amazon.com
AWS 공식 문서에 자세한 내용이 나와있는데 요약해보면
HMAC-SHA256 알고리즘을 통해
- "AWSS4"를 Message로 SecretAccesKey를 Key로 암호화 하여 값을 생성
- 1번에서 생성된 값을 다시 Message로 aws-region(ap-northeast-2)을 Key로 암호화 하여 값을 생성
- 2번에서 생성된 값을 다시 Message로 aws-service(s3)를 Key로 암호화 하여 값을 생성
- 3번에서 생성된 값을 다시 Message로 "aws4_request" 를 Key로 암호화 하여 최종 Signature를 생성
이런 과정을 거치기 때문에 무엇보다 이 HMAC-SH256 알고리즘을 서버에서 직접 구현을 해야하는 불편함이 있습니다.
(물론 라이브러리를 통해 생성할 수도 있습니다.)
AWS Signature V4를 활용하면 upload id를 받아오는 것 또한 클라이언트에서 진행 할 수 있습니다.
그렇게 된다면 결과적으로 이런 과정을 거칩니다.
- Upload Id를 요청하기 위한 Request 요청의 Header에 들어갈 서명 정보를 Infrun서버에 요청
- 반환 받은 서명 정보를 통해 클라이언트에서 S3에 Upload Id 요청
- 반환 받은 Upload Id를 통해 Upload 하기 위한 Request 요청의 Header에 들어갈 서명 정보를 Infrun 서버에 요청
- 반환 받은 서명 정보를 통해 클라이언트에서 S3에 Upload 요청
- S3에서 완료된 part 별 ETag 값 반환
- 반환된 ETag 값을 Infrun 서버에 전달
- Upload Complete수행
여기서 Upload Id를 요청하기 위한 서명 정보 생성에는 Body에 아무런 값이 들어가지 않습니다.
(그래서 ""을 SHA-256 알고리즘으로 해싱한 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"를 입력합니다.)
그런데 Upload를 요청하기 위한 서명 정보 생성에는 각 part별 checkSum 값이 들어가야 합니다.
이렇게 생성된 서명 정보를 S3서버에 요청하면 S3에서도 같은 방법으로 서명정보를 생성하고
제가 직접 생성한 서명 정보와 비교해서 일치하는 경우에만 허가를 하는 구조인데
아무래도 서명 정보를 직접 작성하다보니 저의 경우 실수도 많고 생성한 서명 정보가 틀리는 경우도 많아서 시간을 굉장히 많이 잡아먹었습니다.(여기서 문제는 생성된 Hash가 비교는 가능하지만 어떤 값으로 만들어졌는지에 대한 디코딩이 불가능하기 때문에 제가 어떤 정보를 잘못 입력했는지 알아내기가 정말 어렵다는 것입니다.)
결국 Upload Id 요청의 서명정보까지는 문제 없이 생성했으나, part별 checkSum값에 어떤 값이 들어가야하는지를 아직 못 찾아서 Upload 요청을 위한 서명 정보를 생성하지 못하고 있는 상황입니다.
이 part별 checksum에 대한 값에 대한 정보를 알고 계시다면 공유해주시면 감사하겠습니다.
추가로 AWS V4 Signature관련 코드는 해당 링크에 업로드 해뒀습니다. AWS V4 Signature로 구현을 원하시는 분은
두가지 자료를 참고하시기 바랍니다.
https://wave1994.tistory.com/152
Spring boot :: Multipart upload API using Amazon S3 API 구현 과정 정리
이번에 사내에서 S3 업로드 방식을 멀티파트(Multipart) 업로드 방식으로 변경하는 일을 맡게 되었다. 해당 기능 구현 중 SDK를 사용한 예제는 많았지만 S3에서 지원하는 REST API를 사용하는 예제는 찾
wave1994.tistory.com
https://github.com/kyeumd/aws_s3_multipart_upload/commit/59e9f111e7615aa4b0b61b331de7a8bc4ea54bc8
feat(#2) - Publish AWS V4 Signature · kyeumd/aws_s3_multipart_upload@59e9f11
kyeumd committed Sep 3, 2023
github.com
PresignedURL
PresignedURL을 활용한 Authorization 정보 생성은 결과적으로 이미 서명이 완료된 URL만 반환하고
클라이언트에서는 Body에 분할된 파일을 담고 이 URL로 호출하기만 하면 완료됩니다.
이 PresignedURL은 SHA-256 알고리즘을 통한 해싱도 필요 없고 SDK 레벨에서 CredentialProvider 정보로 생성한 Presigner(이전의 Client와 비슷한 역할)를 통해 간편하게 발급 받을 수 있습니다.
따라서, PresignedURL을 활용한 방법을 통해 구현해보겠습니다.
public static String getPreSigned(String bucketName, String objectKey,
String profileName, Region region, String uploadId, int partNum, Duration duration) {
// String bucketName = "infrun";
// String keyName = "test.mp4";
// Region region = Region.AP_NORTHEAST_2; -> seoul
// String uploadId = "your upload Id";
ProfileCredentialsProvider credentialsProvider = ProfileCredentialsProvider.create(
profileName);
S3Presigner presigner = S3Presigner.builder()
.region(region)
.credentialsProvider(credentialsProvider)
.build();
UploadPartRequest uploadPartRequest = UploadPartRequest.builder()
.bucket(bucketName)
.uploadId(uploadId)
.key(objectKey)
.partNumber(partNum)
.build();
UploadPartPresignRequest preSignedRequest = UploadPartPresignRequest.builder()
.signatureDuration(duration)
.uploadPartRequest(uploadPartRequest)
.build();
PresignedUploadPartRequest preSignedUploadPartRequest = presigner.presignUploadPart(
preSignedRequest);
String resultURL = preSignedUploadPartRequest.url().toString();
System.out.println("Presigned URL to upload a file to: " + resultURL);
return resultURL;
}
- 저장되어있는 프로필에 대한 AccessKey, SecretKey를 공급받기 위해 ProfileCredentialsProvider를 생성합니다.
- 생성한 ProfileCredentialsProvider와 region 정보를 가지고 이번에는 S3Presigner를 build합니다.
- Bucket 이름, Upload Id, ObjectKey, PartNum 등 PartUpload를 위한 정보를 입력하고 Request를 build합니다.
- 3번에서 생성한 Request를 가지고 만료 일자를 설정하고 Presigned URL을 요청하기 위한 Request를 build합니다.
- S3 서버로부터 PresignedRequest를 응답받고 해당 객체에서 url을 반환합니다.
* PresignedURL 을 발급 받을 때 Expire에 관한 정책을 주의해주세요
생성된 PresignedURL 예시를 살펴보겠습니다.
https://infrun.s3.ap-northeast-2.amazonaws.com/
test.mp4?partNumber=1&
uploadId=FLFv5rN0V8OpQ92YPeaUnEnxSVd55xdBLmKLQhfG1Sk8Z60twgoXiGf.eY6W4f5_H1.UNUJk9VryWqHu3NpnYKbz7Q6VbweRt4sqXeNudpkxlDNNhdbVijqm0xoqEpuzLwsTqUH0DPUZJvl1mVf_OQ--
&X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Date=20230903T095153Z
&X-Amz-SignedHeaders=host
&X-Amz-Expires=3600
&X-Amz-Credential=AKIATTZI3I5475ZRBK5Y%2F20230903%2Fap-northeast-2%2Fs3%2Faws4_request
&X-Amz-Signature=87e4aa36c18076e87dda34e40bf4141a51ef2cc93616bed6b538c31b0b970ed0
익숙한 구조가 보이는데 결국 AWS Signature V4를 S3서버에서 대신 생성해서 URL 정보에 전부 포함시켜서 반환해주는 것이라고 볼 수 있습니다.
4. PresignedURL로 Part별 업로드 요청
PostMan을 통해 Part 별 PUT 요청을 보냅니다.
URL란에 PresignedURL로 반환받은 URL 작성하고
Body에 part별 들어갈 분할된 파일을 선택하고 요청을 보냅니다.
*주의 : partNum과 Body에 추가 할 분할한 파일의 순서가 일치해야합니다!
업로드가 완료되면 해당 part에 대한 ETag값이 반환됩니다.
남은 part에 대해서 동일하게 처리하고 반환된 ETag 값을 수집합니다.
(ETag 값 기록 안했는데 다음 요청을 보냈으면 그냥 다시 해당 part에 대한 요청 보내면 동일한 ETag 값 반환됩니다.)
5. Part Upload Complete 요청
Part 별 ETag 값을 전부 받았으니 Upload를 완료했다는 요청을 보냅니다.
public static void CompleteUpload(String bucketName, String objectKey, String profileName,
Region region, String uploadId, List<String> etag) {
// String bucketName = "infrun";
// String key = "test.mp4";
// Region region = Region.AP_NORTHEAST_2;
// String uploadId = "sample"; // Multipart Upload의 업로드 ID
ProfileCredentialsProvider credentialsProvider = ProfileCredentialsProvider.create(
profileName);
S3Client s3 = S3Client.builder()
.region(region)
.credentialsProvider(credentialsProvider)
.build();
// String etag1 = "c6462a92be8d7d1f6868d7338d8c5ad4";
// String etag2 = "9892cdd487e4da8d0e3e0c2902f8cf92";
// String etag3 = "de951bd3aef1f409316e7abac9edd140";
List<CompletedPart> partList = new ArrayList<>();
for (int i = 1; i <= etag.size(); i++) {
partList.add(CompletedPart.builder().partNumber(i).eTag(etag.get(i - 1)).build());
}
// Finally call completeMultipartUpload operation to tell S3 to merge all uploaded
// parts and finish the multipart operation.
CompletedMultipartUpload completedMultipartUpload = CompletedMultipartUpload.builder()
.parts(partList)
.build();
CompleteMultipartUploadRequest completeMultipartUploadRequest =
CompleteMultipartUploadRequest.builder()
.bucket(bucketName)
.key(objectKey)
.uploadId(uploadId)
.multipartUpload(completedMultipartUpload)
.build();
s3.completeMultipartUpload(completeMultipartUploadRequest);
}
- Part별 CompletePart List 생성하기 위해 Part 번호와 해당 Part의 ETag정보를 활용해 build 합니다.
- 1번에서 생성한 CompletePart List를 가지고 CompleteMultipartUpload를 build합니다.
- 2번에서 생성한 CompleteMultipartUpload로 서버에 보낼 CompleteMultipartUploadRequest를 생성합니다.
- Client로 3번에서 생성한 Request를 S3서버에 complete 요청합니다.
6. 객체 업로드 완료
Complete 요청을 하면 최종적으로 객체 업로드가 완료된 것을 직접 확인 할 수 있습니다.
마치며
이번 포스팅에서 REST API를 통한 Multipart 객체 업로드를 알아봤습니다.
그런데 현재 구조에서는 클라이언트에서 ETag를 반환받기까지 상당한 시간이 걸리고 이를 기다려야 한다는 것이 문제가 되는 상황입니다.
다음 포스팅에서는 이러한 업로드 완료 결과를 Event 방식을 통해 서버에서 ETag 값을 반환 받는 방법에 대해서 알아보고 적용에 대한 고민을 해보겠습니다.
전체 코드는 제 Git에서 확인하실 수 있습니다.
https://github.com/kyeumd/aws_s3_multipart_upload
GitHub - kyeumd/aws_s3_multipart_upload
Contribute to kyeumd/aws_s3_multipart_upload development by creating an account on GitHub.
github.com
실제 Spring을 활용한 강의 시스템에서 Multipart 업로드를 통한 업로드
https://github.com/f-lab-edu/infrun
GitHub - f-lab-edu/infrun: 교육 강의 플랫폼
교육 강의 플랫폼. Contribute to f-lab-edu/infrun development by creating an account on GitHub.
github.com
'Data' 카테고리의 다른 글
[Object Storage] - Java SDK를 활용한 S3 Upload (0) | 2023.08.26 |
---|---|
[Object Storage] - 객체 저장소, 왜 선택되었나 ? (0) | 2023.08.20 |
[전문검색] - 형태소 분석의 이해 (0) | 2023.08.13 |
[전문검색] - Like 검색 vs 전문검색(n-gram) (0) | 2023.07.23 |