diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index dabaf0779..5dee87723 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -27,6 +27,11 @@ jobs: JWT_SECRET: ${{ secrets.JWT_SECRET }} FRONT_URL: ${{secrets.FRONT_URL}} BACK_URL: ${{secrets.BACK_URL}} + BUCKET_NAME: ${{secrets.BUCKET_NAME}} + BUCKET_REGION: ${{secrets.BUCKET_REGION}} + IMG_BASE_URL: ${{secrets.BASE_URL}} + S3_ACCESS_KEY: ${{secrets.S3_ACCESS_KEY}} + S3_SECRET_KEY: ${{secrets.S3_SECRET_KEY}} steps: - name: Github Repository 파일 불러오기 diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 26939d2b9..629520d89 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -35,6 +35,11 @@ jobs: JWT_SECRET: ${{ secrets.JWT_SECRET }} FRONT_URL: ${{secrets.FRONT_URL}} BACK_URL: ${{secrets.BACK_URL}} + BUCKET_NAME: ${{secrets.BUCKET_NAME}} + BUCKET_REGION: ${{secrets.BUCKET_REGION}} + IMG_BASE_URL: ${{secrets.BASE_URL}} + S3_ACCESS_KEY: ${{secrets.S3_ACCESS_KEY}} + S3_SECRET_KEY: ${{secrets.S3_SECRET_KEY}} steps: diff --git a/build.gradle b/build.gradle index c49448165..ebb137d36 100644 --- a/build.gradle +++ b/build.gradle @@ -52,6 +52,9 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + // AWS + implementation(platform("software.amazon.awssdk:bom:2.29.20")) + implementation("software.amazon.awssdk:s3") // Web Layer implementation 'org.springframework.boot:spring-boot-starter-web' @@ -61,7 +64,6 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' implementation group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-ui', version: '2.6.0' - //test testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' diff --git a/src/main/java/com/somemore/global/configure/S3Config.java b/src/main/java/com/somemore/global/configure/S3Config.java new file mode 100644 index 000000000..b58ef6755 --- /dev/null +++ b/src/main/java/com/somemore/global/configure/S3Config.java @@ -0,0 +1,45 @@ +package com.somemore.global.configure; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; + +@Configuration +public class S3Config { + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AwsCredentials basicAWSCredentials() { + return AwsBasicCredentials.create(accessKey, secretKey); + } + + @Bean + public S3Presigner s3Presigner(AwsCredentials awsCredentials) { + return S3Presigner.builder() + .region(Region.of(region)) + .credentialsProvider(StaticCredentialsProvider.create(awsCredentials)) + .build(); + } + + @Bean + public S3Client s3Client(AwsCredentials awsCredentials) { + return S3Client.builder() + .region(Region.of(region)) + .credentialsProvider(StaticCredentialsProvider.create(awsCredentials)) + .build(); + } + +} diff --git a/src/main/java/com/somemore/global/exception/ExceptionMessage.java b/src/main/java/com/somemore/global/exception/ExceptionMessage.java index 2c3b040c7..de2ac538b 100644 --- a/src/main/java/com/somemore/global/exception/ExceptionMessage.java +++ b/src/main/java/com/somemore/global/exception/ExceptionMessage.java @@ -14,6 +14,11 @@ public enum ExceptionMessage { NOT_EXISTS_LOCATION("존재하지 않는 위치 ID 입니다."), NOT_EXISTS_RECRUIT_BOARD("존재하지 않는 봉사 모집글 ID 입니다."), UNAUTHORIZED_RECRUIT_BOARD("자신이 작성한 봉사 모집글이 아닙니다."), + UPLOAD_FAILED("파일 업로드에 실패했습니다."), + INVALID_FILE_TYPE("지원하지 않는 파일 형식입니다."), + FILE_SIZE_EXCEEDED("파일 크기가 허용된 한도를 초과했습니다."), + EMPTY_FILE("파일이 존재하지 않습니다."), + INSTANTIATION_NOT_ALLOWED("인스턴스화 할 수 없는 클래스 입니다.") ; private final String message; diff --git a/src/main/java/com/somemore/global/exception/ImageUploadException.java b/src/main/java/com/somemore/global/exception/ImageUploadException.java new file mode 100644 index 000000000..d314407ea --- /dev/null +++ b/src/main/java/com/somemore/global/exception/ImageUploadException.java @@ -0,0 +1,8 @@ +package com.somemore.global.exception; + +public class ImageUploadException extends RuntimeException{ + + public ImageUploadException(String message) { + super(message); + } +} diff --git a/src/main/java/com/somemore/global/handler/GlobalExceptionHandler.java b/src/main/java/com/somemore/global/handler/GlobalExceptionHandler.java index 742719d0d..3f6cc0736 100644 --- a/src/main/java/com/somemore/global/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/somemore/global/handler/GlobalExceptionHandler.java @@ -2,10 +2,13 @@ import com.somemore.global.exception.BadRequestException; +import com.somemore.global.exception.ImageUploadException; +import org.springframework.data.crossstore.ChangeSetPersister; import org.springframework.http.HttpStatus; import org.springframework.http.ProblemDetail; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; @@ -25,4 +28,15 @@ ProblemDetail handleBadRequestException(final BadRequestException e) { return problemDetail; } + @ExceptionHandler(ImageUploadException.class) + ProblemDetail handleImageUploadException(final ImageUploadException e) { + + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage()); + + problemDetail.setTitle("이미지 업로드 실패"); + problemDetail.setDetail("업로드 중 문제가 발생했습니다. 파일 크기나 형식이 올바른지 확인해 주세요."); + + return problemDetail; + } + } diff --git a/src/main/java/com/somemore/imageupload/dto/ImageUploadRequestDto.java b/src/main/java/com/somemore/imageupload/dto/ImageUploadRequestDto.java new file mode 100644 index 000000000..8cc01de84 --- /dev/null +++ b/src/main/java/com/somemore/imageupload/dto/ImageUploadRequestDto.java @@ -0,0 +1,8 @@ +package com.somemore.imageupload.dto; + +import org.springframework.web.multipart.MultipartFile; + +public record ImageUploadRequestDto( + MultipartFile imageFile +) { +} diff --git a/src/main/java/com/somemore/imageupload/service/ImageUploadService.java b/src/main/java/com/somemore/imageupload/service/ImageUploadService.java new file mode 100644 index 000000000..1542ac30a --- /dev/null +++ b/src/main/java/com/somemore/imageupload/service/ImageUploadService.java @@ -0,0 +1,63 @@ +package com.somemore.imageupload.service; + +import com.somemore.global.exception.ImageUploadException; +import com.somemore.imageupload.dto.ImageUploadRequestDto; +import com.somemore.imageupload.usecase.ImageUploadUseCase; +import com.somemore.imageupload.util.ImageUploadUtils; +import com.somemore.imageupload.validator.ImageUploadValidator; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +import java.io.IOException; + +import static com.somemore.global.exception.ExceptionMessage.UPLOAD_FAILED; + +@RequiredArgsConstructor +@Service +public class ImageUploadService implements ImageUploadUseCase { + + private final S3Client s3Client; + private final ImageUploadValidator imageUploadValidator; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + @Value("${cloud.aws.s3.base-url}") + private String baseUrl; + + @Override + public String uploadImage(ImageUploadRequestDto requestDto) { + imageUploadValidator.validateFileSize(requestDto.imageFile()); + imageUploadValidator.validateFileType(requestDto.imageFile()); + + try { + return uploadToS3(requestDto.imageFile()); + } catch (IOException e) { + throw new ImageUploadException(UPLOAD_FAILED.getMessage()); + } + } + + private String uploadToS3(MultipartFile file) throws IOException { + String fileName = ImageUploadUtils.generateUniqueFileName(file.getOriginalFilename()); + + PutObjectRequest request = createPutObjectRequest(file, fileName); + + s3Client.putObject(request, RequestBody.fromInputStream(file.getInputStream(), file.getSize())); + + return ImageUploadUtils.generateS3Url(baseUrl, fileName); + } + + private PutObjectRequest createPutObjectRequest(MultipartFile file, String fileName) { + return PutObjectRequest.builder() + .bucket(bucket) + .key(fileName) + .contentType(file.getContentType()) + .build(); + } + +} diff --git a/src/main/java/com/somemore/imageupload/usecase/ImageUploadUseCase.java b/src/main/java/com/somemore/imageupload/usecase/ImageUploadUseCase.java new file mode 100644 index 000000000..acfd3ed2c --- /dev/null +++ b/src/main/java/com/somemore/imageupload/usecase/ImageUploadUseCase.java @@ -0,0 +1,7 @@ +package com.somemore.imageupload.usecase; + +import com.somemore.imageupload.dto.ImageUploadRequestDto; + +public interface ImageUploadUseCase { + String uploadImage(ImageUploadRequestDto requestDto); +} diff --git a/src/main/java/com/somemore/imageupload/util/ImageUploadUtils.java b/src/main/java/com/somemore/imageupload/util/ImageUploadUtils.java new file mode 100644 index 000000000..c72e2c5b0 --- /dev/null +++ b/src/main/java/com/somemore/imageupload/util/ImageUploadUtils.java @@ -0,0 +1,27 @@ +package com.somemore.imageupload.util; + +import java.util.UUID; + +import static com.somemore.global.exception.ExceptionMessage.INSTANTIATION_NOT_ALLOWED; + +public class ImageUploadUtils { + + private ImageUploadUtils() { + throw new UnsupportedOperationException(INSTANTIATION_NOT_ALLOWED.getMessage()); + } + + public static String generateUniqueFileName(String originalFileName) { + String uuid = UUID.randomUUID().toString(); + String fileExtension = extractFileExtension(originalFileName); + return uuid + fileExtension; + } + + private static String extractFileExtension(String fileName) { + return fileName.substring(fileName.lastIndexOf(".")); + } + + public static String generateS3Url(String baseUrl, String fileName) { + return String.format("%s/%s", baseUrl, fileName); + } + +} diff --git a/src/main/java/com/somemore/imageupload/validator/DefaultImageUploadValidator.java b/src/main/java/com/somemore/imageupload/validator/DefaultImageUploadValidator.java new file mode 100644 index 000000000..865dd2844 --- /dev/null +++ b/src/main/java/com/somemore/imageupload/validator/DefaultImageUploadValidator.java @@ -0,0 +1,39 @@ +package com.somemore.imageupload.validator; + +import com.somemore.global.exception.ImageUploadException; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import static com.somemore.global.exception.ExceptionMessage.*; + +@Component +public class DefaultImageUploadValidator implements ImageUploadValidator { + + private static final long MAX_FILE_SIZE = 8L * 1024 * 1024; // 8MB + + public void validateFileSize(MultipartFile file) { + if (file == null || file.isEmpty()) { + throw new ImageUploadException(EMPTY_FILE.getMessage()); + } + + if (file.getSize() > MAX_FILE_SIZE) { + throw new ImageUploadException(FILE_SIZE_EXCEEDED.getMessage()); + } + } + + public void validateFileType(MultipartFile file) { + String contentType = file.getContentType(); + if (!isAllowedImageType(contentType)) { + throw new ImageUploadException(INVALID_FILE_TYPE.getMessage()); + } + } + + private boolean isAllowedImageType(String contentType) { + return contentType != null && ( + contentType.equals("image/jpeg") || + contentType.equals("image/png") || + contentType.equals("image/gif") || + contentType.equals("image/webp") + ); + } +} diff --git a/src/main/java/com/somemore/imageupload/validator/ImageUploadValidator.java b/src/main/java/com/somemore/imageupload/validator/ImageUploadValidator.java new file mode 100644 index 000000000..2481c5d48 --- /dev/null +++ b/src/main/java/com/somemore/imageupload/validator/ImageUploadValidator.java @@ -0,0 +1,9 @@ +package com.somemore.imageupload.validator; + +import org.springframework.web.multipart.MultipartFile; + +public interface ImageUploadValidator { + + void validateFileSize(MultipartFile file); + void validateFileType(MultipartFile file); +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 73394baa0..6621c5d18 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -2,6 +2,20 @@ app: front-url: ${FRONT_URL} back-url: ${BACK_URL} +# AWS S3 +cloud: + aws: + credentials: + access-key: ${S3_ACCESS_KEY} + secret-key: ${S3_SECRET_KEY} + region: + static: ${BUCKET_REGION} + s3: + bucket: ${BUCKET_NAME} + base-url: ${IMG_BASE_URL} + stack: + auto: false + spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver @@ -47,6 +61,12 @@ spring: locale: ko_KR locale-resolver: fixed + servlet: + multipart: + max-file-size: 8MB + max-request-size: 8MB + + #swagger springdoc: swagger-ui: diff --git a/src/test/java/com/somemore/imageupload/service/ImageUploadServiceTest.java b/src/test/java/com/somemore/imageupload/service/ImageUploadServiceTest.java new file mode 100644 index 000000000..31e2fbfd6 --- /dev/null +++ b/src/test/java/com/somemore/imageupload/service/ImageUploadServiceTest.java @@ -0,0 +1,79 @@ +package com.somemore.imageupload.service; + +import com.somemore.IntegrationTestSupport; +import com.somemore.global.exception.ImageUploadException; +import com.somemore.imageupload.dto.ImageUploadRequestDto; +import com.somemore.imageupload.validator.ImageUploadValidator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +import java.io.IOException; +import java.io.InputStream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class ImageUploadServiceTest extends IntegrationTestSupport { + + @Mock + private S3Client s3Client; + + @Mock + private ImageUploadValidator imageUploadValidator; + + @InjectMocks + private ImageUploadService imageUploadService; + + @Mock + private MultipartFile multipartFile; + + @BeforeEach + void setUp() throws IOException { + ReflectionTestUtils.setField(imageUploadService, "bucket", "test-bucket"); + ReflectionTestUtils.setField(imageUploadService, "baseUrl", "https://amazonaws.com/"); + + when(multipartFile.getOriginalFilename()).thenReturn("testImage.jpg"); + when(multipartFile.getContentType()).thenReturn("image/jpeg"); + when(multipartFile.getInputStream()).thenReturn(mock(InputStream.class)); + when(multipartFile.getSize()).thenReturn(1000L); + } + + @DisplayName("업로드 요청 이미지를 S3에 업로드 할 수 있다.") + @Test + void testUploadImage_success() { + // given + ImageUploadRequestDto requestDto = new ImageUploadRequestDto(multipartFile); + + when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))) + .thenReturn(null); + + // when + String result = imageUploadService.uploadImage(requestDto); + + // then + verify(s3Client, times(1)).putObject(any(PutObjectRequest.class), any(RequestBody.class)); + assertNotNull(result); + assertTrue(result.startsWith("https://amazonaws.com/")); + assertTrue(result.endsWith(".jpg")); + } + + @DisplayName("이미지 형식이 올바르지 않다면 업로드 할 수 없다.") + @Test + void testUploadImage_failure() throws IOException { + // given + when(multipartFile.getInputStream()).thenThrow(new IOException()); + + ImageUploadRequestDto requestDto = new ImageUploadRequestDto(multipartFile); + + // when, then + assertThrows(ImageUploadException.class, () -> imageUploadService.uploadImage(requestDto)); + } +} diff --git a/src/test/java/com/somemore/imageupload/util/ImageUploadUtilsTest.java b/src/test/java/com/somemore/imageupload/util/ImageUploadUtilsTest.java new file mode 100644 index 000000000..15180389b --- /dev/null +++ b/src/test/java/com/somemore/imageupload/util/ImageUploadUtilsTest.java @@ -0,0 +1,84 @@ +package com.somemore.imageupload.util; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +import static org.junit.jupiter.api.Assertions.*; + +class ImageUploadUtilsTest { + + @Test + void privateConstructorShouldThrowException() throws Exception { + // given + Constructor constructor = ImageUploadUtils.class.getDeclaredConstructor(); + constructor.setAccessible(true); + + // when + InvocationTargetException exception = assertThrows(InvocationTargetException.class, constructor::newInstance); + + // then + assertThrows(UnsupportedOperationException.class, () -> { throw exception.getCause(); }); + } + + @DisplayName("이미지 업로드시 유일한 이미지 이름을 만들어줄 수 있다.") + @Test + void testGenerateUniqueFileName() { + //given + String fileName = "image.png"; + + //when + String uniqueName = ImageUploadUtils.generateUniqueFileName(fileName); + + //then + assertTrue(uniqueName.endsWith(".png")); + assertNotEquals(fileName, uniqueName); + } + + @DisplayName("유니크한 파일 이름을 생성할 때 UUID는 정상적으로 생성된다.") + @Test + void testGenerateUniqueFileName_uuid() { + // given + String fileName = "image.png"; + + // when + String uniqueName = ImageUploadUtils.generateUniqueFileName(fileName); + + // then + assertNotNull(uniqueName); + assertTrue(uniqueName.contains("-")); + assertTrue(uniqueName.endsWith(".png")); + } + + + @DisplayName("이미지의 주소를 반환할 수 있다.") + @Test + void testGenerateS3Url() { + //given + String baseUrl = "https://amazonaws.com"; + String fileName = "unique-image.png"; + + //when + String url = ImageUploadUtils.generateS3Url(baseUrl, fileName); + + //then + assertEquals("https://amazonaws.com/unique-image.png", url); + } + + @DisplayName("baseUrl이 빈 문자열일 경우 URL을 생성할 수 있다.") + @Test + void testGenerateS3Url_emptyBaseUrl() { + // given + String baseUrl = ""; + String fileName = "unique-image.png"; + + // when + String url = ImageUploadUtils.generateS3Url(baseUrl, fileName); + + // then + assertEquals("/unique-image.png", url); + } + +} \ No newline at end of file diff --git a/src/test/java/com/somemore/imageupload/validator/DefaultImageUploadValidatorTest.java b/src/test/java/com/somemore/imageupload/validator/DefaultImageUploadValidatorTest.java new file mode 100644 index 000000000..03c2d6b0e --- /dev/null +++ b/src/test/java/com/somemore/imageupload/validator/DefaultImageUploadValidatorTest.java @@ -0,0 +1,86 @@ +package com.somemore.imageupload.validator; + +import com.somemore.global.exception.ImageUploadException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import static org.junit.jupiter.api.Assertions.*; + +class DefaultImageUploadValidatorTest { + + private DefaultImageUploadValidator imageUploadValidator; + + @BeforeEach + void setUp() { + // given + imageUploadValidator = new DefaultImageUploadValidator(); + } + + @Test + @DisplayName("파일이 비어있으면 예외가 발생한다.") + void shouldThrowExceptionWhenFileIsEmpty() { + //given + MultipartFile emptyFile = new MockMultipartFile("file", new byte[0]); + + //when + Throwable exception = assertThrows(ImageUploadException.class, () -> imageUploadValidator.validateFileSize(emptyFile)); + + //then + assertEquals(ImageUploadException.class, exception.getClass()); + } + + @Test + @DisplayName("파일 크기가 최대 8MB를 초과하는 경우, 예외가 발생한다.") + void shouldThrowExceptionWhenFileSizeExceeded() { + // given + MultipartFile largeFile = new MockMultipartFile("file", "largeImage.jpg", "image/jpeg", new byte[9 * 1024 * 1024]); + + // when + Throwable exception = assertThrows(ImageUploadException.class, () -> imageUploadValidator.validateFileSize(largeFile)); + + // then + assertEquals(ImageUploadException.class, exception.getClass()); + } + + @Test + @DisplayName("유효한 이미지 타입(JPEG) 파일은, 검증에 통과한다.") + void shouldNotThrowExceptionWhenFileTypeIsValidJpeg() { + // given + MultipartFile validFile = new MockMultipartFile("file", "validImage.jpg", "image/jpeg", new byte[1024]); + + // when + imageUploadValidator.validateFileType(validFile); + + // then + assertDoesNotThrow(() -> imageUploadValidator.validateFileType(validFile)); + } + + @Test + @DisplayName("유효하지 않은 이미지 타입 파일이 있을 경우, 예외가 발생한다.") + void shouldThrowExceptionWhenFileTypeIsInvalid() { + // given + MultipartFile invalidFile = new MockMultipartFile("file", "invalidFile.pdf", "application/pdf", new byte[1024]); + + // when + Throwable exception = assertThrows(ImageUploadException.class, () -> imageUploadValidator.validateFileType(invalidFile)); + + // then + assertEquals(ImageUploadException.class, exception.getClass()); + } + + @Test + @DisplayName("파일 타입이 올바르지 않을 경우, 예외가 발생한다.") + void shouldThrowExceptionWhenFileTypeIsNull() { + // given + MultipartFile nullContentTypeFile = new MockMultipartFile("file", "noContentTypeFile.jpg", null, new byte[1024]); + + // when + Throwable exception = assertThrows(ImageUploadException.class, () -> imageUploadValidator.validateFileType(nullContentTypeFile)); + + // then + assertEquals(ImageUploadException.class, exception.getClass()); + } +} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index fab7dd965..50d9922c6 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -44,3 +44,17 @@ server: charset: UTF-8 enabled: true force: true + +cloud: + aws: + credentials: + access-key: test-access-key + secret-key: test-secret-key + region: + static: ap-northeast-2 + s3: + bucket: somemore + base-url: https://somemore-image.s3.ap-northeast-2.amazonaws.com + stack: + auto: false +