버킷 : 저장소

객체 : 버킷에 업로드된 파일

S3 버킷 생성하기

퍼블릭 엑세스 차단 해제

 

그리고 그대로 생성하면 됨

 

버킷에 정책 ( 권한(Permission)을 정의하는 JSON 문서)추가

여기서 정책 추가

 

s3 -> gerobject

 

리소스 추가하기

 

S3에 파일 업로드 할 수 있도록 IAM에서 액세스 키 발급받기

백엔드 서버가 S3에 접근해서 파일을 업로드할 수 있어야 한다. S3에 접근할 수 있는 권한을 받기 위해 IAM이라는 곳에서 권한을 부여받아야 한다.

 

사용자 생성

 

 

정책 생성

 

엑세스 키 만들기

 

발급 완료, 저 두 키는 따로 저장해 놔야 함

 

스프링부트로 S3에 이미지 저장하기

  • users 테이블에 imageUrl필드를 추가하여 저장된 프로필 이미지의 url을 저장하여 user 조회시 함께 조회
  • 이미지 제목이 중복이 될 수 있기 때문에 원래 원본파일명 + UUID를 붙여서 저장
  • "jpg", "jpeg", "png", "gif"인 파일만 저장할 수 있도록 검증

 

이미지 저장 시도 중 문제 발생 : AccessControlListNotSupported

com.amazonaws.services.s3.model.AmazonS3Exception: The bucket does not allow ACLs (Service: Amazon S3; Status Code: 400; Error Code: AccessControlListNotSupported;

 

S3 버킷이 ACL(Access Control List)을 지원하지 않아서 발생하는 문제라고 한다

 

ACL

  • 사용자(User) 또는 그룹(Group)에 대해 특정 권한을 부여하는 방식
  • 버킷 정책(Bucket Policy) 및 IAM 정책보다 간단한 권한 관리 방식
  • 현재는 AWS는 ACL을 비활성화하는 것을 기본값(Default) 으로 변경

이미 위에서 버킷 정책을 적용했으니 ACL을 사용하는 코드를 제거해서 해결하면 된다

 

기존 코드

PutObjectRequest putObjectRequest =
    new PutObjectRequest(bucketName, s3FileName, byteArrayInputStream, metadata)
        .withCannedAcl(CannedAccessControlList.PublicRead); //ACL 사용
amazonS3.putObject(putObjectRequest);

 

수정된 코드

PutObjectRequest putObjectRequest =
    new PutObjectRequest(bucketName, s3FileName, byteArrayInputStream, metadata);
amazonS3.putObject(putObjectRequest);

 

스프링부트 코드

poroperties 파일 수정

aws.access.key=
aws.secret.access.key=
aws.s3.bucket=
aws.region.static=ap-northeast-2
aws.stack.auto-=false

 

라이브러리 추가

// s3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

 

S3Config 생성

@Configuration
public class S3Config {
    @Value("${aws.access.key}")
    private String accessKey;
    @Value("${aws.secret.access.key}")
    private String secretKey;
    @Value("${aws.region.static}")
    private String region;

    @Bean
    public AmazonS3 amazonS3() {
        AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);

        return AmazonS3ClientBuilder
                .standard()
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .withRegion(region)
                .build();
    }

}

 

이미지 업로드 및 삭제 컨트롤러

// 사용자 프로필 이미지 업로드
    @PostMapping("/users/image")
    public ResponseEntity<UserProfileResponse> uploadImage(@AuthenticationPrincipal AuthUser authUser, @RequestPart MultipartFile image) throws IOException {
        UserProfileResponse user = userService.uploadImage(authUser.getId(), image);
        return ResponseEntity.ok(user);
    }

    // 사용자 프로필 이미지 삭제
    @DeleteMapping("/users/image")
    public void imageDelete(@AuthenticationPrincipal AuthUser authUser, @RequestParam String imageUrl) {
        userService.deleteImage(authUser.getId(), imageUrl);
    }

 

이미지 업로드 검증 메서드

// 이미지 업로드 검증
    private static void validImage(MultipartFile image) {
        // 이미지가 비어있는지 확인
        if(image.isEmpty() || Objects.isNull(image.getOriginalFilename())){
            throw new InvalidRequestException("이미지 처리중 오류가 발생했습니다.");
        }
        int lastDotIndex = image.getOriginalFilename().lastIndexOf(".");
        if (lastDotIndex == -1) {
            throw new InvalidRequestException("이미지 처리중 오류가 발생했습니다.");
        }

        // 확장자 검증
        String extention = image.getOriginalFilename().substring(lastDotIndex + 1).toLowerCase();
        List<String> allowedExtentionList = Arrays.asList("jpg", "jpeg", "png", "gif");

        if (!allowedExtentionList.contains(extention)) {
            throw new InvalidRequestException("이미지 처리중 오류가 발생했습니다.");
        }
    }

 

이미지 업로드 서비스

// 이미지 업로드
    @Transactional
    public UserProfileResponse uploadImage(long userId, MultipartFile image) throws IOException {
        User user = userRepository.findById(userId).orElseThrow(() -> new InvalidRequestException("User not found"));
        validImage(image);

        String originalFilename = image.getOriginalFilename(); //원본 파일 명
        String extention = originalFilename.substring(originalFilename.lastIndexOf(".")); //확장자 명

        String s3FileName = UUID.randomUUID().toString().substring(0, 10) + originalFilename; //변경된 파일 명

        InputStream is = image.getInputStream();
        byte[] bytes = IOUtils.toByteArray(is);

        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentType("image/" + extention);
        metadata.setContentLength(bytes.length);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);

        try{
            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, s3FileName, byteArrayInputStream, metadata);
            amazonS3.putObject(putObjectRequest); // put image to S3
        }catch (Exception e){
            throw new InvalidRequestException("이미지 처리중 오류가 발생했습니다.");
        }finally {
            byteArrayInputStream.close();
            is.close();
        }

        String imageUrl = amazonS3.getUrl(bucketName, s3FileName).toString();
        user.updateImageUrl(imageUrl);
        User svaedUser = userRepository.findById(userId).orElseThrow(() -> new InvalidRequestException("User not found"));
        return new UserProfileResponse(svaedUser.getId(), svaedUser.getEmail(), svaedUser.getNickName(), svaedUser.getImageUrl());
    }

 

이미지 삭제 서비스

@Transactional
    public void deleteImage(Long userId, String imageUrl) {
        User user = userRepository.findById(userId).orElseThrow(() -> new InvalidRequestException("User not found"));
        try{
            URL url = new URL(imageUrl);
            String decodingKey = URLDecoder.decode(url.getPath(), "UTF-8");
            String key = decodingKey.substring(1); // 맨 앞의 '/' 제거
            amazonS3.deleteObject(new DeleteObjectRequest(bucketName, key));
        }catch (MalformedURLException | UnsupportedEncodingException e){
            throw new InvalidRequestException("이미지 처리중 오류가 발생했습니다.");
        }
        user.updateImageUrl(null);
    }

 

이미지 저장 및 삭제 테스트

저장테스트

 

삭제테스트

 

잘 삭제된다

 

 

RDS 생성

RDS에 들어가서 데이터베이스 생성을 누르자

 

. 설정

 

퍼블릭 엑세스 예로 변경

 

추가구성에서 백업 끄고 이름 설정하기

 

RDS 보안그룹 생성하기

 

생성한 보안그룹을 RDS에 붙이기

해당 RDS로 이동 후에 보안그룹 수정

 

RDS 파라미터 그룹 추가하기

1. 아래 속성 전부 utf8mb4로 설정하기

  • character_set_client
  • character_set_connection
  • character_set_database
  • characater_set_filesystem
  • characater_set_results
  • character_set_server

참고) utf8 대신에 utf8mb4를 사용하는 이유는 ‘한글’ 뿐만 아니라 ‘이모티콘’도 지원이 가능하도록 하기 위해서이다.

2. 아래 속성 전부 utf8mb4_unicode_ci로 설정하기

  • collation_connection
  • collation_server

참고) utf8mb4_unicode_ci은 정렬, 비교 방식을 나타낸다.

3. time_zone을 Asia/Seoul로 설정하기

 

RDS 파라미터 그룹 변경

 

앤드포인트로 접속하면 된다!

아까 생성할때 쓴 username과 비밀번호를 입력하면 된다

 

 

스프링 서버에 RDS 연결하기

propertise 파일을 수정하면 된다

아래와 같은 형식으로!

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://{엔드포인트}:{포트번호}/{(초기)데이터베이스}?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
spring.datasource.username={마스터 사용자 이름}
spring.datasource.password={마스터 암호}

 

스프링으로 데이터베이스 연결 시도 중 문제 발생 : CannotGetJdbcConnectionException

org.springframework.jdbc.CannotGetJdbcConnectionException: 에러가 뜨면서 디비가 붙지 않았다

spring.datasource.url=jdbc:mysql://{엔드포인트}:{포트번호}/{(초기)데이터베이스} 부분에서 도대체 초기 데이터베이스 명이 뭔지를 몰라서 RDS 설정하는 부분을 찾아봤다

데이터베이스명을 RDS 식별자명으로 작성하고 RDS 추가구성에서 초기 데이터베이스 이름을 작성하지 않은 것이다

사이트를 보니 초기 데이터베이스이름을 적어주지 않으면 데이터베이스가 생성되지 않는다고 한다

 

RDS 추가 구성에서 초기 데이터베이스 이름 지정하여 해결 

이름을 설정해주니 연결이 잘 됐다!

(참고로 아래 백업 설정도 꼭 끄자)

 

드디어 돌아간다!

 

 

하하 드디어 붙였다 너무 행복하네 ㅜ

인스턴스 생성

먼저 지역을 선택한다!

 

 

아래와 같이 설정

 

키 페어 만들어 주고 보안그룹 설정하기

보안그룹 주의사항

-> 만약 내가 8080포트로 요청을 보내고 싶다면 유형 - 사용자 지정 - 포트 직접입력을 해줘야 한다!

 

스토리지 구성

 

시작하기 누르면 끝

 

탄력적 IP 적용하기

그냥 기본값으로 적용하면 된다

 

만들어진 탄력적 ip주소를 내 인스턴스와 연결한다

 

인스턴스 선택 누르고 연결 누르면 끝!

 

EC2 인스턴스에서 어플리케이션 실행하기

Ubuntu 환경에서 JDK 설치

$ sudo apt update && /
sudo apt install openjdk-17-jdk -y

 

잘 설치됐는지 확인

$ java -version

잘 설치됐다고 나온다!

 

Spring Boot 프로젝트 clone하기

$ git clone 주소
$ cd 해당 파일

 

설정 파일 만들어주기

cd src/
cs main/
ls
mkdir -p resources
cd resources/
vi application.properties

 

내용 붙여넣기 해주고 esc + :wq

 

루트로 이동 후 build 재시작 -> cd .. 을 해주며 상위 폴더로 가고 ls를 치면서 build파일이 있는 곳 까지 이동하면 됨

./gradlew clean build

 

예외가 뜬다! 

gradle-wrapper.properties 파일이 없어서 Gradle Wrapper를 실행할 수 없다

gradle-wrapper.properties를 직접 만들어 주자

cd gradle/wrapper
vi gradle-wrapper.properties

 

여기서 i를 눌러줘야 값을 입력할 수 있음!!

다른 프로젝트에 있는 설정을 복붙해주고 저장!

./gradlew clean build -x test # 기존 빌드된 파일을 삭제하고 새롭게 JAR로 빌드
$ cd ~/프로젝트명/build/libs
$ sudo java -jar 프로젝트명-0.0.1-SNAPSHOT.jar

 

참고) 백그라운드에서 Spring Boot 실행시키기

$ sudo nohup java -jar ec2-spring-boot-sample-0.0.1-SNAPSHOT.jar &

 

참고

 

  • expert-0.0.1-SNAPSHOT.jar → Spring Boot 애플리케이션 실행 파일 (✅ 실행해야 함)
  • expert-0.0.1-SNAPSHOT-plain.jar → 의존성이 없는 단순한 JAR 파일 (🚫 실행 불가능)

 

드디어 실행 완료! 근데 이제 RDS를 적용 안해서 에러가 났지만 어서 적용해봅시다!

 

인스턴스 설정 및 보안 설정 바꿔주기

Nginx를 시작하기 위해서 먼저 인스턴스를 만들고 보안 설정을 변경해줬다

처음엔 22만 열려 있기 때문에 HTTP와 HTTPS 포트를 추가하고 ip는 모든 ip로 설정하기!

 

그 다음 인스턴스에 connection 후 서버를 켜기 위해서 Nginx 관련 명령어를 입력해준다

 

설치

$ sudo apt-get update
$ sudo apt-get install nginx

 

Nginx 시작 및 활성화 상태 보기 : 실패

$ sudo service nginx start
$ sudo service nginx status

 

활성화 중이라고 아주 잘 뜬다!

 

그 러 나......

 

아래와 같이 뜨는 것이다.....

분명히 보안 설정에서 http랑 https를 둘다 추가 해줬는데 말이다 😂

 

Nginx에서 현재 열려있는 포트 확인

sudo ss -tulnp | grep nginx

 

확인해보니 포트가 80번만 열려있었던 것이다...

찾아보니 나처럼 보안 설정에서 https를 추가해줘도 막상 열려있는 포트를 확인해보면 열려있지 않다고 하는 분도 있다는 것을 알아냈다

 

그렇다면 보안인증서를 추가해주고 443번도 열리도록 설정을 바꿔주자!

 

자체 서명된 SSL 인증서 만들기

sudo mkdir -p /etc/nginx/ssl
sudo openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 \
    -keyout /etc/nginx/ssl/nginx.key \
    -out /etc/nginx/ssl/nginx.crt

 

이렇게 하면 아래와 같이 입력해야 할 값들이 나온다 값을 입력해서 인증서를 생성했다

Country Name (2 letter code) [AU]: KR
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []: 
Organization Name (eg, company) [Internet Widgits Pty Ltd]: 
Organizational Unit Name (eg, section) []: 
Common Name (e.g. server FQDN or YOUR name) []: 
Email Address []:

 

Nginx 설정 파일 열고 수정하기

443포트를 열고 SSL 설정을 추가해준다

 

혹시 모를 nano 편집기사용법 : 수정하고 저장하는 법

1. Ctrl + O 눌러서 저장

2. Enter

3. Ctrl + X

혹은

1. esc

2. :wq

 

다시 재시작하고 상태 보기

$ sudo service nginx restart
$ sudo ss -tulnp | grep nginx

 

 

그러면 이렇게 443 포트도 열려있다!

 

재접속

그러면 이렇게 잘 뜨는걸 볼 수 있다

하지만 ERR_CERT_AUTHORITY_INVALID이 발생하게 된다

이것은 SSL 인증서가 유효하지 않거나 신뢰할 수 있는 인증 기관에서 발급되지 않았기 때문에 발생하는 오류인데 자체 서명된 인증서를 발급해서 발생한 문제라서 그냥 마저 접속했다!

 

신뢰할 수 있는 인증기관에서 발급받으려면 도메인이 있어야 한다고 해서 그냥 이번은 인스턴스 생성 연습이기 때문에 넘어갔다

 

실무에서는 인증서를

1. aws에서 구입하면 만들어주는게 있다고 한다 

2. 혹은 회사에서 데브옵스 팀이 자체적으로 만들어서 설정해준다고 한다!

+ Recent posts