회사에서 프로젝트 진행 중 이미지를 저장하고 조회하기 위해 S3를 사용하기로 했습니다.
기존의 방식은 Spring Boot 서버에서 S3로 직접 이미지를 업로드 하는 방식이었습니다.
하지만 이 방식은 Spring Boot 가 직접 이미지를 요청을 받아 S3로 업로드 하는 방식이라 만약 이미지 크기가 크면 서버에 부담을 줄 수 있습니다.(물론 사용자가 적당하고 이미지 크기가 크지 않으면 상관없습니다.)
하지만 Presigned url을 발급받아 이를 클라이언트 측에서 업로드를 하면 서버의 부담을 줄일 수 있게 됩니다.
추가적으로 Presigned url의 경우 접근 시간을 설정할 수 있어 url을 탈취당해도 만료가 되면 접근이 불가능하기 때문에 보안상 이점을 챙길 수 있습니다.
※ S3 버킷 생성 이전에 IAM 계정을 생성해야 합니다.
IAM 권한 설정 시 S3관련 ROLE 설정을 해주어야 합니다. (물론 개인적으로 하는거면 S3FullAccess로 해도 됩니다만)
버킷 만들기에 들어가 줍니다.
S3 region, 이름 입력 후 버킷 만들기를 클릭합니다.
이후 생성한 버킷에 들어간 후 권한에 들어가서 버킷 정책과 CORS 정책을 설정해주어야 합니다.
,
로 구분하여 입력, 전체 허용시 *
입력이후 Add statement 클릭 후 Generate Policy 클릭하면
아래와 같이 정책이 나오는데 이를 복사하여
정책 화면에 붙여넣기 하면 됩니다.
CORS 정책
application.yml
spring:
cloud:
aws:
s3:
credentials:
accessKey: ${AWS_S3_ACCESSKEY}
secretKey: ${AWS_S3_SECRETKEY}
bucket: ${AWS_S3_BUCKET}
bucket.url: ${AWS_S3_BUCKET_URL}
region:
static: ${AWS_REGION}
stack:
auto: false
이때 accessKey와 secretKey는 퍼블릭 레포에 올리면 여러분의 서버는 모두의 것이 됩니다.
accessKey와 secretKey를 application.yml에 올리지 않고도 사용하는 방법은 여러가지가 있으나 여기에선 환경변수를 설정하는 방법을 사용하겠습니다.
다른 좋은 방법에 대해서는 여기로
accessKey와 secretKey를 각각 AWS_ACCESS_KEY_ID
와 AWS_SECRET_ACCESS_KEY
로 환경변수를 설정해주면 EnvironmentVariableCredentialsProvider
에 주입됩니다.
이를 S3Config에 활용하면 됩니다.
@Configuration
public class AWSS3Config {
@Bean
public S3Client s3Client() {
return S3Client.builder()
.credentialsProvider(EnvironmentVariableCredentialsProvider.create())
.region(Region.AP_NORTHEAST_2)
.build();
}
@Bean
public S3Presigner presigner() {
return S3Presigner.builder()
.credentialsProvider(EnvironmentVariableCredentialsProvider.create())
.region(Region.AP_NORTHEAST_2)
.build();
}
}
AwsService
@RequiredArgsConstructor
@Component
public class AwsService {
private final S3Client s3Client;
private final S3Presigner presigner;
// 이미지 업로드를 위한 Presigned url 발급
public String getUploadImagePresignedUrl(String imageName, String contentType) {
PutObjectRequest putObjectRequest =
PutObjectRequest.builder()
.bucket(bucketName)
.key(imageName)
.contentType(contentType)
.build();
PutObjectPresignRequest putObjectPresignRequest =
PutObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(5))
.putObjectRequest(putObjectRequest)
.build();
PresignedPutObjectRequest presignedPutObjectRequest =
presigner.presignPutObject(putObjectPresignRequest);
presigner.close();
return presignedPutObjectRequest.url().toString();
}
// 이미지 조회를 위한 Presigned url 발급
public String getPresignUrl(String filename) {
if(filename == null || filename.equals("")) {
return null;
}
GetObjectRequest getObjectRequest = GetObjectRequest.builder()
.bucket(bucketName)
.key(filename)
.build();
GetObjectPresignRequest getObjectPresignRequest = GetObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(5)) // presignedURL 5분간 접근 허용
.getObjectRequest(getObjectRequest)
.build();
PresignedGetObjectRequest presignedGetObjectRequest = presigner
.presignGetObject(getObjectPresignRequest);
String url = presignedGetObjectRequest.url().toString();
presigner.close(); // presigner를 닫고 획득한 모든 리소스를 해제
return url;
}
}
AController
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/abc")
@RestController
public class AController {
private final AwsService awsService;
@GetMapping("/file/{filename}")
public ResponseEntity<String> getFile(@PathVariable(value = "filename") String fileName) throws IOException {
String url = awsService.getPresignUrl(fileName);
return new ResponseEntity<>(url, HttpStatus.OK);
}
}
결과
+) 이후 이미지 조회의 경우 CloudFront를 사용하게 되었습니다. 이유는 이미지 조회를 위해 계속 S3에 접근하기 보단, CDN을 통해 이미지 캐싱이 가능하고, DDoS 공격 방어(WAF) 등 보안에 이점이 있기 때문입니다.
멋있어요^^