서론
회사에서 개발하고 있는 서비스의 파일 저장소로써 AWS S3 서비스를 이용하려 하고 있다.
어드민 사이트에서 파일을 업로드하면, 대외 서비스에서 특정 고객들에게만 해당 파일 다운로드를 제공하는 서비스인데,
제한된 사용자에게만 파일을 제공하는 방법에 대해 고민하다 AWS 에서 제공하는 Presigned URL이란 방법을 알게 되었다.
Presigned URL을 서비스에 적용하기까지 알아본 내용들을 글로 정리해두려 한다.
Presigned URL
Presigned URL은 AWS S3에서 요청자의 권한을 제어하는데 제공하는 방법 중 하나이다.
Presigned URL은 접근 제어 정책을 따로 업데이트하지 않고도 URL을 통해 임시적으로 접근 권한을 제공하는 것이다. 사용자에게 접근권한을 부여하는 토큰인 셈이다.
S3에서 권한을 제어하는 방법들은 Presigned URL 외에도 IAM 사용자 정책, 액세스 포인트 등을 활용하는 방법들이 있다. 이 권한 제어 방법들은 별도 글에서 정리해보려 한다.
Presigned URL은 아래와 같이 생겼다.
https://<버킷명>.s3.<리전명>.amazonaws.com/<객체명>?response-content-disposition=inline&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Security-Token=<임시 인증 정보>&X-Amz-Algorithm=<서명 방식>&X-Amz-Credential=<AWS 인증 정보>&X-Amz-Date=20250506T043725Z&X-Amz-Expires=60&X-Amz-SignedHeaders=host&X-Amz-Signature=<서명 값>
생성
Presigned URL을 생성할 때에는 아래 정보들이 필요하다.
- 버킷 명
- 객체 key (파일 경로와 파일 명)
- 요청하려는 HTTP 메서드 (다운로드는 GET, 업로드는 PUT, 객체의 메타 데이터 읽는 것은 HEAD)
- URL 만료 시간
- AWS CLI 나 SDK로 URL을 생성한다면, 최대 7일까지 유효 기간을 설정할 수 있다.
- 만약 Presigned URL의 생성자의 권한이 수정/삭제/비활성화 처리 되면, 생성자의 Credentials로 만든 Presigned URL도 만료 된다.
생성된 Presigned URL을 받은 사용자는 제한 없이 Presigned URL을 이용해 특정 Operation을 할 수 있지만, URL 을 생성하려는 유저가 갖고 있는 Operation에 제한된다.
예를 들어, 버킷 A에 대해 파일 읽기 권한만 있는 생성자는 버킷 A에 대한 파일 읽기 작업을 수행하는 Presigned URL만 생성할 수 있고, 쓰기 작업을 수행하는 Presigned URL은 생성할 수 없다.
방법 1) SDK를 이용한 Presigned URL 생성
애플리케이션에서 AWS SDK를 이용해 Presigned URL을 생성하는 방법이다.
이 방법은 자격 증명을 거치고 나면 따로 AWS S3 와 통신하지 않고도 애플리케이션에서 Presigned URL을 생성할 수 있는 방법이다.
네트워크 통신을 하지 않아도 된다는 점이 좋다.
소스코드
Import (gradle) :
implementation(platform("software.amazon.awssdk:bom:2.27.21"))
implementation("software.amazon.awssdk:s3")
AWS 자격 증명 값 세팅 방법 3가지 :
만약 접근하려는 버킷/파일의 접근 제어 정책이 세팅되어 있다면
Presigned URL을 생성하는 역할의 객체를 Bean으로 등록할 때 인증/인가를 체크하기 위해
해당 버킷/파일의 Operation 권한이 있는 계정의 Credentials 정보를 세팅해야한다.
AWS Credentials 정보를 세팅하는 방법은 3가지가 있다.
1. 기본 AWS 자격 증명 파일 (루트/.aws/credentials
)에 설정된 AWS 자격 증명 값을 사용하여 인증하는 방법
- <루트 경로>\.aws\credentials 파일에 아래 정보를 넣어준다.
[default]
aws_access_key_id = <버킷의-access-key>
aws_secret_access_key = <버킷의-secret-key>
- 소스코드에서는 ProfileCredentialsProvider 객체를 생성하면서 위 credentials 파일에 작성한 프로파일 값 ('default') 을 지정한다.
@Configuration
public class AwsS3Config {
@Bean(name = "s3Presigner")
public S3Presigner s3PresignerWithCredentialFiles() {
return S3Presigner.builder()
.region(Region.of(region))
.credentialsProvider(ProfileCredentialsProvider.create("default"))
.build();
}
}
2. 설정 파일 (application.yaml 혹은 application.properties)에 설정된 AWS 자격 증명 값을 사용하여 인증하는 방법
- 다만, 설정 파일에 Access Key와 Secret Key를 직접 노출하여 세팅하는 것보다는 환경변수로 대체하는 것이 보안 상 안전하다.
@RequiredArgsConstructor
@Configuration
public class AwsS3Config {
@Value("${amazon.s3.access-key}")
private String accessKey;
@Value("${amazon.s3.secret-key}")
private String secretKey;
@Value("${amazon.s3.region}")
private String region; // ex: "ap-northeast-2"
@Bean(name = "s3Presigner")
public S3Presigner s3PresignerWithStatic() {
AwsBasicCredentials awsCredentials = AwsBasicCredentials.create(accessKey, secretKey);
return S3Presigner.builder()
.region(Region.of(region))
.credentialsProvider(StaticCredentialsProvider.create(awsCredentials))
.build();
}
}
3. 운영 체제의 환경 변수에 설정된 AWS 자격 증명 값을 사용하여 인증하는 방법
- 자격증명 정보를 세팅한 AWSStaticCredentialsProvider 객체를 AmazonS3ClientBuilder에 전달하여 빌드한다.
@Configurations
public class AmazonConfig {
@Value("${amazon.s3.access-key}")
private String accessKey;
@Value("${amazon.s3.secret-key}")
private String secretKey;
@Value("${amazon.s3.region}")
private String region; // ex: "ap-northeast-2"
@Bean
public AmazonS3 awsS3Client() {
BasicAWSCredentials basicAWSCredentials = AwsBasicCredentials.create(accessKey, secretKey);
ClientConfiguration clientConfiguration = new ClientConfiguration();
clientConfiguration.setProtocol(Protocol.HTTPS);
return S3Client.builder()
.region(region)
.credentialsProvider(StaticCredentialsProvider.create(basicAWSCredentials))
.clientConfiguration(clientConfiguration)
.build();
}
}
만약 Access Key가 잘못되거나 Secret Key가 잘못되어 세팅되면 403 상태 코드와 함께 아래와 같은 에러 코드를 반환받는다.
- InvalidAccessKeyId : Access Key를 잘못 세팅한 경우
- SignatureDoesNotMatch : Secret Key를 잘못 세팅한 경우
- Access Denied : 권한이 없는 경우 (Presigned URL이 만료된 경우 등 여러 권한에 문제되는 상황에서 반환된다.)
SDK 통한 생성 메서드 (java) :
GET 요청을 할 수 있는 Presigned URL을 생성하는 로직이다. S3Presigner 객체를 이용한다.
위에서 언급했던 버킷명, 객체명, 유효 시간, 요청 메서드 를 입력한다.
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.S3Exception;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;
import software.amazon.awssdk.utils.IoUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.time.Duration;
@Slf4j
@Service
@RequiredArgsConstructor
public class AmazonS3Utils {
private final S3Presigner s3Presigner;
/* Create a pre-signed URL to download an object in a subsequent GET request. */
public String createPresignedGetUrl(String bucketName, String keyName) {
GetObjectRequest objectRequest = GetObjectRequest.builder()
.bucket(bucketName)
.key(keyName)
.build();
GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(10)) // The URL will expire in 10 minutes.
.getObjectRequest(objectRequest)
.build();
PresignedGetObjectRequest presignedRequest = s3Presigner.presignGetObject(presignRequest);
log.info("Presigned URL: [{}]", presignedRequest.url().toString());
log.info("HTTP method: [{}]", presignedRequest.httpRequest().method());
return presignedRequest.url().toExternalForm();
}
방법 2) AWS Lambda를 이용한 Presigned URL 생성 (feat. python - boto3 라이브러리)
Presigned URL 생성 로직을 애플리케이션에서 하지 않고 AWS 에서 제공하는 Lambda 를 이용할 수 있다.
AWS Lambda는 서버를 프로비저닝하고 관리하는 것 없이 코드를 실행시킬 수 있도록 제공하는 AWS 서비스이다. (지원하는 언어는 Node.js, Python, Ruby, Typescript, Java, Go, C# 이다.)
Lambda는 필요할 때 함수를 실행시키고, 트래픽에 따라 자동 확장을 지원하기 때문에
실시간 처리가 필요한 기능에서 유용하게 사용된다.
우리 팀에서 Presigned URL 생성 로직 실행을 애플리케이션이 아닌 Lambda 서비스를 이용하게 된데에는 아래 세가지 정도의 이점이 있었다.
- 중앙 집중화
이번에 생성하는 S3 버킷은 여러 유관 부서 혹은 서비스에서도 호출할 가능성이 있다.
그래서 사용자의 권한 관리를 쉽게 하는 것 등 앞으로의 운영 편의성도 고려해야 했다.
별개로 Lambda 함수를 실행시키기 전, 호출 서비스들에 대한 검증 절차도 필요했는데
AWS의 API Gateway 를 이용하여 API Gateway에서 해당 요청을 검증하는 Authorizer 를 등록해 이를 수행하도록 했다.
Authorizer에는 다양한 인증 처리 방식을 사용할 수 있는데,
이번에 만드는 버킷의 경우, 요청 서비스들이 공통적으로 사용자 토큰 (jwt 토큰)을 가지고 있었기 때문에 JWT 토큰을 검증하는 로직을 Authorizer로 등록하여 요청 클라이언트를 인증하는 방식도 통일하였다.
Authorizer도 Lambda 로 구현하였다.
다만, 나의 상황과 달리
호출하는 서비스마다 인증하는 방식이 상이하다면, 인증 절차를 위한 Authorizer Lambda 함수를 서비스 갯수만큼 추가해야할 수 있다.
2. 보안 강화 (보안 중앙 관리)
S3 버킷에 접근해야하는 여러 사용자(부서/서비스) 별 IAM 정책을 생성해주어야 하는데,
S3 버킷의 Presigned URL을 생성하는 주체를 Lambda로 제한하게 되면 각 사용자 (부서/서비스)마다 IAM 권한을 부여/관리 하지 않아도 된다.
그리고 이를 사용하고자 하는 서비스들은 해당 Lambda를 호출할 수 있는 JWT 토큰만 갖고 있으면 된다.
3. 유지보수 용이: Presigned URL 생성 로직을 한곳에서 관리하면,
여러 서비스에서 중복으로 로직을 개발해야 하는 부담을 줄일 수 있고
Presigned URL 생성 로직을 수정하거나 인증 방식을 수정해야할 때 Lambda 함수에서만 수정하면 된다는 점에서 유지보수가 용이해진다.
4. 로깅 용이 : Presigned URL을 생성하며 발생하는 이슈에 대해 모니터링 시에는 CloudWatch Logs로 중앙 관리/확인할 수 있다.
- Lambda 함수 코드 내에서 출력하는 로그는 자동으로 CloudWatch Logs에 전송된다.
결론적으로, 호출자 검증 및 권한 관리를 위한 Authorizer 함수와 S3 Presigned URL 생성 함수를 Lambda에 등록하였다.
물론 Lambda 서비스를 사용할 때 고려해야할 점들도 존재한다.
Lambda 서비스 사용 시 고려해야할 단점.
- Cold Start
Lambda 서비스는 외부 트리거에 의해 Lambda 인스턴스가 함수를 실행하는 방식으로 동작하는데,
만약 인스턴스가 없거나, 기존 인스턴스가 만료되었다면, 함수 인스턴스를 생성하면서 컨테이너 준비, 초기화 작업을 거치는 과정을 수행해야 한다.
이로 인해 사용자 응답에 지연이 발생하게 된다.
특히 요청이 갑자기 몰렸을 때에는 이를 처리하기 위해 신규 인스턴스들을 생성하고 이때에도 초기화 작업을 수행하게 된다.
2. 제한된 환경 (실행 시간, 메모리 사용량, 환경 변수 크기)
Lambda 로 함수를 실행하는 것에는 몇가지 제한이 있다.
실행 시간은 최대 900초로 제한되고, 메모리도 본인이 설정한 값으로 제한되기 때문에 함수가 실행되는 동안의 메모리 사용량도 확인해야 한다.
Lambda는 환경 변수를 사용하여 함수에 설정 정보를 제공하는데, 제한 크기가 4KB이다.
그래서 Lambda 로 실행시키고자 하는 로직이 실행 시간이 너무 소요되거나, 많은 메모리를 필요로 한다면 Lambda를 사용하기 어렵다는 문제가 있다.
나의 선택은,
결론적으로 우리가 방법 2를 선택한데에는 아래와 같은 배경도 고려하였기 때문에, 운영하는 서비스와 팀의 상황에 맞게 다른 선택을 할 수 있을 것이다.
- Lambda 서비스를 사용하면 결국 네트워크도 타고, 비용도 발생하는 것이기 때문에 개인적으로는 방법 1) 이 낫지 않을까 란 생각이었는데
현재 요청 트래픽 수준에서는 비용이 감당 가능하며,
여러 서비스에서 중복 구현해야하는 이슈를 해결하는 것에 더 의의를 두고자 한다는 DevOps의 의견에 따르기로 했다. - 팀 내 DevOps에서 S3 서비스를 관리하는데, 여러 서비스가 호출할 버킷이다보니 관리/운영을 쉽게하고자 하는 니즈도 있었다.
- 제공할 서비스가 Cold Start 이슈를 고려할만큼 사용자 트래픽이 한번에 다량으로 발생하지는 않는다는 점도 이유이다.
추가) 서비스 구성 : API Gateway + Lambda
AWS의 API Gateway 서비스는 REST 및 WebSocket API를 생성, 게시, 유지, 모니터링 및 보호하기 위한 AWS 서비스이다.
우리 팀에서는 AWS API Gateway를 구성하여, AWS Lambda 서비스를 호출하는 요청을 검증하는 Authorizer를 추가하였다.
이외에도 기본적으로 설정된 공통 응답을 제공하고 (‘게이트웨이 응답’ 메뉴에서 확인 가능하다.) 사용량을 제어할 수 있는 등 기본적으로 게이트웨이에서 수행하는 기능들을 제공한다.
<참고 이미지: API Gateway 를 Lambda 서비스의 트리거로써 연동>
Cloud Front까지 구성한다고 하면 아래와 같은 모습이 될 수 있다.
전체 구성도이다.
'클라우드' 카테고리의 다른 글
[AWS] nohup을 이용하여 무중단 서비스 만들기 (0) | 2021.10.14 |
---|---|
[AWS] EC2 인스턴스 생성 방법 (Ubuntu 18.04) (0) | 2021.10.08 |
[AWS] node.js의 프로세스 매니저 PM2 사용 명령어 (0) | 2021.07.31 |
[AWS] Windows에서 외부 사용자가 EC2 인스턴스 접속하기 (cmd) (0) | 2021.07.06 |
[AWS] Windows에서 EC2 인스턴스 접속 방법 (Putty) (0) | 2021.07.06 |