Spring Boot로 AWS의 EC2 서버, RDS, S3 연결하기

유승욱·2024년 2월 26일
0

Amazon의 AWS를 이용해서 데이터베이스나 파일 업로드, 실제 배포를 진행해보자.

EC2 생성과 접속

1. OS 선택

AWS의 EC2를 이용하기 위해서는 EC2 서비스에 인스턴스를 생성해주어야한다.
신규 생성은 [인스턴트 시작] 메뉴에서 원하는 운영체제를 선택할 수 있다. 이때 EC2는 항상 비용이 발생할 수 있으므로 간단한 실습은 처음 가입할 때 사용할 수 있는 '프리티어'로 이용 가능한 항목을 선택하도록 한다.

2. 인스턴스 유형

마찬가지로 '프리티어'로 이용 가능한 항목을 선택한다.

3. 키 페어 생성

인스턴스를 시작하는 과정 중에 외부에서 접속할 때 필요한 보안 키를 생성할 수 있는 화면이 있다. [새 키 페어 생성]을 선택하면 '키 페어 이름'을 지정하는 부분이 나오는데 이는 나중에 파일 이름이 되므로 기억해 두어야 한다. 생성한 키는 반드시 내려받아 두도록 한다.(터미널 환경으로 연결할 때 이 파일을 이용한다.

4. 네트워크 설정

5. 스토리지 구성


[인스턴스 시작]을 눌러준다.

6. API 서비스를 위한 보안 정책 변경

API 서비스를 제공하기 위해서는 외부에서 호출 가능한 80이나 8080 포트 등을 열어 두어야만 접속이 가능하게된다. 이를 위해서 생성한 인스턴스의 상세 정보 중에 보안 부분을 수정해야한다.

보안그룹 정보를 보면 '인바운드(외부에서 들어오는 연결) 규칙'과 '아웃바운드(외부로 나가는 연결) 규칙'으로 구성되어 있는데 기본적으로 22번 포트(SSH 터미널 연결)만이 존재한다.

인바운드 규칙을 편집해서 80 포트와 8080 포트를 외부에서 사용할 수 있도록 지정한다.

외부에서 데이터를 보내는 '아웃바운드 규칙'도 마찬가지로 수정해준다.

Putty를 이용한 터미널 연결

EC2에 만들어둔 Linux를 이용하려면 Putty와 같은 Windows용 SSH 연결 프로그램을 이용하는 것이 편리하다.

설치된 프로그램에서 PuTTYgen을 실행하고 내려받은 '키 파일'은 [Load]를 통해서 로딩한다.

'키 파일'을 찾을 때 파일 확장자로 '.ppk'를 기본값으로 찾기 때문에 다음과 같이 모든 확장자를 검색하도록 변경해야만 찾을 수 있다.

정상적으로 로딩된 후에는 [Save private key] 버튼을 눌러 파일을 저장한다.

Putty를 이용한 연결

Putty 프로그램을 통해서 EC2에 연결하려면 EC2 인스턴스의 퍼블릿 Ipv4 DNS를 지정한다.


연결할 때는 카테고리의 [Connection - SSH - Auth -Credentials] 항목으로 이동해서 가지고 있는 ppk키를 이용하도록 지정한다.

연결이 된 후에 EC2 환경의 계정인 'ec2-user'라는 이름으로 로그인한다.

JDK 설치

만들어진 EC2 환경에는 JDK가 없거나 있더라도 JDK 8인 경우가 많으므로 개발 환경과 동일하게 Amazon의 Coretto JDK11 버전을 설치하도록 한다.
'sudo yum install java-11-amazon-corretto.x86_64' 명령어로 JDK11을 설치한다.

프로젝트 실행 확인

AWS에서 스프링 부트로 작성된 프로젝트를 실행하는 가장 기본적인 방식은 프로젝트를 실행가능한 jar 파일로 빌드해서 실행하는 방식이다.
이를 위해 로컬 환경에서 작성하는 프로젝트를 jar 파일로 빌드하고 해당 파일을 EC2에서 가져다가 실행하거나, 프로젝트의 빌드 자체를 EC2 환경에서 진행하는 방법을 사용할 수 있다.
예제에서는 깃허브로 프로젝트를 인터넷으로 올리고 EC2에서는 이 깃을 이용해서 내려받은 후에 빌드해서 실행하는 기초적인 방법을 이용할 것이다.

EC2 환경에서는 [sudo yum install git]을 통해서 깃 프로그램을 설치한다.

깃 설치 후 프로젝트를 실행할 수 있는 폴더를 다음 그림과 같이 mkdir로 생성하고 해당 폴더로 이동한다.

깃을 이용해 프로젝트를 클론하고 코드를 EC2로 복사한다.

복사한 후에는 gradlew 파일을 실행하기 위해 프로젝트 폴더 내부로 이동한다.

'./gradlew build'를 실행해 보면 실행 권한이 없어서 문제가 생기는 것을 볼 수 있다.

권한을 강제로 다음과 같이 변경한다.

변경된 후에는 정상적으로 프로젝트가 빌드된다.

빌드가 실행된 후에는 다음 그림과 같이 'build' 폴더 아래 'libs' 폴더로 이동한다.

'java -jar jar파일명' 명령어를 통해 프로젝트를 실행한다.

브라우저에서 EC2 인스턴스의 퍼블릭 IPv4 DNS 값과 포트 번호를 통해서 확인해보자.


프로젝트 종료는 Putty에서 Ctrl+C를 이용해서 종료할 수 있다.

AWS의 RDS 서비스

RDS는 AWS의 여러 서비스들 중에서 '데이터베이스' 항목에 존재한다.
필요한 데이터베이스를 생성하기 위해 [데이터베이스] 메뉴에서 [데이터베이스 생성]을 선택한다.


자격 증명에서 게정의 이름과 패스워드를 지정할 수 있다. 마스터 사용자 정보로 데이터베이스에 접근한다.

퍼블릭 액세스를 "예"로 선택한다. "아니요"로 선택하면 EC2에서만 데이터베이스에 접근가능하므로 개인PC에서 데이터베이스에 접근이 불가능하다. 보안 그룹을 새로 생성하고 이름을 입력해준다.

데이터베이스를 생성해준다.

데이터베이스가 생성된 후에는 보안 그룹에서 '3306' 포트가 외부 아이피에서 자유롭게 연결할 수 있도록 수정해 주어야 한다.

데이터베이스 연결 확인과 시간 설정

RDS의 인스턴스 구성이 완료되었다면 외부에서 데이터베이스 연결이 가능한지 확인하도록 한다. 이 작업을 위해서 우선 현재 구성된 인스턴스의 엔드포인트를 파악한다.

데이터베이스의 연결은 개발 중인 프로젝트의 [DataSource] 메뉴를 이용해서 확인할 수 있다.

• Host에는 엔드포인트 값을 입력
• User/Password는 인스턴스 생성 시에 작성된 계정 정보를 입력
• Database는 MariaDB의 경우 mysql로 작성


time_zone과 UTF-8 설정

데이터베이스의 시간을 보면 기준 시간이 다를 수 있는데 이런 경우 서울을 기준시로 했을 때 9시간 차이가 나게 된다. 이것은 서울이 UTC와 9시간 차이가 나기 때문인데 데이터베이스를 서울 시간에 맞추어 두는 것이 시간 관련된 문제를 예방할 수 있다.
데이터베이스 옵션을 조정하는 김에 한글 저장에 문제가 없도록 UTF-8 세팅도 같이해두는 것이 좋다.

우선 RDS에 적용할 파라미터 그룹을 다음과 같이 생성한다. 파라미터 그룹을 생성할 때는 생성한 데이터베이스 인스턴스의 '구성' 정보를 이용해서 버전을 확인해주고 해당 버전에 맞는 파라미터를 생성하도록 주의한다.


생성된 파라미터 그룹에서 검색으로 'time_zone'을 찾아 'Asia/Seoul'로 지정한다.

한글 처리를 위해서 파라미터의 이름을 'char'로 검색하고 다음 목록들을 'utf8'로 지정한다.

생성된 파라미터 그룹은 RDS 인스턴스의 수정 단계에서 처리할 수 있다.

수정의 마지막 단계에서는 [즉시 적용]을 선택해서 수정 내용을 반영할 수 있다.

즉시 적용 하더라도 확실하게 하기 위해서는 RDS 인스턴스를 재부팅하도록 한다.

프로젝트의 [DataSource] 연결 정보에는 [options] 항목의 'Time zone'을 지정한다.

설정을 변경한 후에는 반드시 [Deactivate]를 실행해서 변경된 설정이 반영될 수 있도록 한다.

RDS에 새 계정 추가

RDS 인스턴스에 연결괸 계정은 RDS 인스턴스를 생성할 때 작성한 admin(혹은 직접 지정한) 계정이고 인스턴스에 데이터베이스 스키마는 mysql로 지정되어 있다.

연결된 계정을 살펴보면 이미 많은 테이블이 생성되어 있어서 원하는 대로 실습하기에는 적합하지 않으므로 이전 예제들과 같이 webdb를 생성하고 새로운 사용자 webuser를 생성해서 사용하는 것이 안전하다.

계정 추가

우선 'create database webdb'라는 SQL을 실행해서 새로운 데이터베이스를 생성한다.

webuser 계정을 생성한다.

생성한 webuser 계정을 사용하려면 webdb 내에 권한을 주어야만 한다. 로컬 환경과 달리 RDS를 이용할 때는 현재 접속한 admin 조차도 권한의 제한이 있으므로 기존과 같이 권한을 부여하면 에러가 발생한다.

이를 해결하려면 전체 권한이 아닌 개별 권한을 다음과 같이 부여해 주어야 한다.

DataSource 연결 정보의 계정과 데이터베이스를 다음과 같이 변경해서 사용하면 된다.

EC2와 RDS 연동 확인

로컬 개발 환경에서 RDS 연동을 확인했다면 EC2 환경에서도 프로젝트가 문제없이 연동되고 동작하는지 확인할 필요가 있다. 이를 위해서 별도의 프로젝트를 하나 작성해서 테스트해보자.

application.properties

프로젝트의 설정 정보를 보관하는 application.properties에서는 DataSource를 구성할 때 RDS를 이용하도록 수정한다. DataSource의 url 정보에 RDS 정보와 serverTimezone 파라미터를 추가해 주어야 한다.

spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://zerock-database-1.cykarvkk14c5.ap-northeast-2.rds.amazonaws.com/webdb?serverTimezone=Asia/Seoul
spring.datasource.username=yso
spring.datasource.password=yso3865def

spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.show-sql=true

DataSource를 통한 시간 확인

테스트 해보자.

@SpringBootTest
class AppRdsApplicationTests {

    @Autowired
    private DataSource dataSource;

    @Test
    void contextLoads() {
        try(Connection connection = dataSource.getConnection();
            PreparedStatement preparedStatement = connection.prepareStatement("select now()");
            ResultSet resultSet = preparedStatement.executeQuery();
        ){
            resultSet.next();

            System.out.println(resultSet.getString(1));

        }catch(Exception e){
            e.printStackTrace();
        }
    }

}

TimeController 작성

EC2에서 프로젝트 배포 및 실행 결과를 확인하기 위해서 컨트롤러를 작성해 두면 프로젝트의 실행 여부를 외부에서 브라우저로 확인할 수 있다.

@RestController
@RequestMapping("/api/time")
@Log4j2
@RequiredArgsConstructor
public class TimeController {

    private final DataSource dataSource;

    @GetMapping("/now")
    public Map<String, String> getNow(){

        String now = "";

        try(Connection connection = dataSource.getConnection();
            PreparedStatement preparedStatement = connection.prepareStatement("select now()");
            ResultSet resultSet = preparedStatement.executeQuery();
        ){
            resultSet.next();

            now = resultSet.getString(1);

            log.info("NOW: " + now);

        }catch(Exception e){
            e.printStackTrace();
        }

        return Map.of("NOW", now);
    }
}

깃허브에 배포 후 테스트

프로젝트 실행에 문제가 없는지 로컬 개발 환경에서 확인이 끝났다면 현재 프로젝트를 깃허브에 올리고 EC2에서 클론해서 실행해보도록 하자.


프로젝트가 정상적으로 작동하는 것을 확인할 수 있다.

AWS의 S3 서비스

AWS의 S3 서비스는 스토리지 서비스로 다양한 종류의 데이터를 업로드하거나 내려받을 수 있다. S3로 콘텐츠를 배포하고 이를 운용하게 되면 서버에서 파일에 대한 관리 부담을 줄일 수 있다. 실제 운영 중인 서비스들은 S3로 파일이나 정적 데이터를 서비스하고 EC2로 동적인 리소스를 처리하는 방식으로 구성한다.

S3 서비스 구성

S3 서비스를 구성하기 위해서는 버킷(bucket)이라는 객체를 먼저 생성하고 버킷에 권한을 부여해서 파일을 올리고 내려받을 수 있다.



엑세스 설정은 외부에서 사용 가능하도록 구성한다.


버킷 생성 시 가장 중요한 작업은 '권한'설정이다. 생성된 버킷을 선택하고 [권한] 항목을 선택한다. 권한에서는 '버킷 정책'의 [편집]을 선택한다.

버킷 정책을 문자열로 작성하는 것은 번거롭기 때문에 [정책 생성기]를 이용한다.

'정책 생성기'는 별도의 페이지로 동작하는데 첫 단계에서는 [S3 Bucket Policy]를 선택한다.

정책을 생성할 때는 다음과 같은 부분을 주의해서 작성한다.

Principal 항목에는 '*'를 입력한다.
Actions 항목에는 'GetObject'와 'PutObject', 'DeleteObject'를 선택한다.


앞의 그림에서 'Amazon Resource Name' 항목은 이전 단계에서 생성된 버킷의 'ARN' 값에 반드시 '/*'를 추가해 주어야 한다.


다음 단계로 [Generate Policy]를 실행한다.

실행된 결과는 다음과 같은 JSON 이 만들어지게 된다. 생성된 JSON 문자열을 복사해서 버킷 정책으로 지정하고 [변경 사항 저장]을 눌러 완료한다.

생성된 버킷 테스트

버킷 생성이 완료되면 버킷에 파일을 업로드하는 테스트가 가능하다.
간단한 이미지 파일을 업로드 해보자.



업로드된 파일을 선택하면 나중에 사용할 수 있는 URL이 출력된다.(객체 URL)

브라우저에서 '객체 URL'로 접근하면 업로드된 파일을 볼 수 있다.

프로그램을 통한 S3 제어 설정

실제 프로젝트에서는 스프링 부트와 같은 프로그램으로 S3에 접근해서 파일 업로드를 진행하므로 이를 위한 추가적인 보안 설정이 필요하다.
AWS의 여러 서비스 중에 [보안, 자격 증명 및 규정 준수]에서 [IAM] 항목을 선택한다.

IAM 서비스의 [사용자] 항목을 선택하고 [사용자 추가]를 선택한다.


검토 단계에서는 'AmazonS3FullAccess'가 지정되어 있는지 확인한다.



프로그램을 이용해서 S3에 접근하기 위해서는 '버킷 정책' 바로 밑의 '객체 소유권'을 [ACL 활성화됨]으로 편집해 주어야 한다.

스프링 부트 설정에 필요한 '액세스 키'와 '비밀 액세스 키'를 생성한다.


프로그램을 통한 S3 업로드 확인

최종적으로 원하는 모습은 프로젝트를 통해서 S3에 업로드가 가능한지 확인하고 EC2로 실행하는 것이므로 우선 S3를 이용하는 새로운 프로젝트를 생성한다.

생성된 프로젝트는 AWS와 스프링 부트를 연동할 때 사용하는 'Spring Cloud AWS Starter' 라이브러리를 build.gradle에 추가한다.

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

프로젝트의 application.properties 파일에는 S3의 엑세스 키와 비밀 엑세스 키를 지정하고 파일 업로드와 관련된 설정을 추가한다.

cloud.aws.credentials.access-key=AKIAQZWT653INPOA4W6H
cloud.aws.credentials.secret-key=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

cloud.aws.s3.bucket=zerock-s3

cloud.aws.region.static=ap-northeast-2
cloud.aws.stack.auto=false

org.zerock.upload.path=/home/ec2-user/upload

spring.servlet.multipart.enabled=true
spring.servlet.multipart.location=/home/ec2-user/upload
spring.servlet.multipart.max-request-size=30MB
spring.servlet.multipart.max-file-size=10MB


logging.level.com.amazonaws.util.EC2MetadataUtils=error

참고
cloud.aws.credentials.access-key=AKIAQZWT653INPOA4W6H: 이 줄은 AWS에 접근하기 위한 액세스 키를 설정하는 것입니다. 이 키는 AWS에서 제공하는 특정 서비스를 사용할 수 있는 권한을 부여받은 것을 나타냅니다.
cloud.aws.credentials.secret-key=YXXpzwXZ1s3Hw1IdUQNEjB7aOPHxRVohoDa2hkCb: 이 줄은 액세스 키와 함께 사용되는 비밀 키를 설정하는 것입니다. 이 키는 액세스 키와 함께 사용하여 AWS 서비스에 대한 요청이 유효한지를 확인하는 데 사용됩니다.
cloud.aws.s3.bucket=zerock-s3: 이 줄은 AWS의 S3 서비스에 생성된 버킷의 이름을 설정하는 것입니다. S3 버킷은 객체 스토리지에서 데이터를 저장하고 검색하는 기본 컨테이너입니다.
cloud.aws.region.static=ap-northeast-2: 이 줄은 사용할 AWS 리전을 설정하는 것입니다. 'ap-northeast-2'는 서울 리전을 나타냅니다.
cloud.aws.stack.auto=false: 이 줄은 AWS CloudFormation 스택을 자동으로 생성할지 여부를 결정하는 것입니다. 'false'로 설정되어 있으므로 자동 생성은 비활성화된 상태입니다.
org.zerock.upload.path=/home/ec2-user/upload: 이 줄은 파일 업로드 경로를 설정하는 것입니다. 이 경로는 서버에서 파일이 업로드 될 때 파일이 저장되는 위치를 나타냅니다.
spring.servlet.multipart.enabled=true: 이 줄은 스프링에서 멀티파트 요청의 지원을 활성화하는 것입니다. 멀티파트 요청은 주로 파일 업로드를 처리할 때 사용됩니다.
spring.servlet.multipart.location=/home/ec2-user/upload: 이 줄은 멀티파트 파일이 업로드 될 때 파일이 저장되는 위치를 나타냅니다.

S3Config

@Configuration
public class S3Config {

    @Bean
    public AmazonS3 amazonS3Client() {
        AWSCredentials credentials = new BasicAWSCredentials("AKIAQZWT653INPOA4W6H", "YXXpzwXZ1s3Hw1IdUQNEjB7aOPHxRVohoDa2hkCb");

        return AmazonS3ClientBuilder
                .standard()
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .withRegion("ap-northeast-2")
                .build();
    }
}

참고
AmazonS3
AmazonS3는 인터페이스이고, AmazonS3Client가 구체클래스이다.
AmazonS3Client는 AWS S3 서비스와 상호 작용하기 위한 클라이언트 클래스이다

AWSCredentials 객체
AWS 자격 증명(AWS Credentials)을 나타내는 객체이다.
accessKey, secretKey, region 필드로부터 값을 가져와서 AWSCredentials 객체를 생성하고 있다.
BasicAWSCredentials는 AWSCredentials인터페이스의 구체클래스로, 다형성을 활용해 객체를 생성하고 있다.

AmazonS3ClientBuilder
AWS S3 서비스에 대한 클라이언트를 구성하고 빌드하기 위한 빌더 클래스이다.
standard() 메서드를 호출하여 기본 빌더를 생성한 뒤, 자격 증명 객체와 region을 지정한다.
build() 메서드를 호출하여 AmazonS3 객체를 생성하여 반환한다.

S3Uploader

@Component
@RequiredArgsConstructor
@Log4j2
public class S3Uploader {

    private final AmazonS3 amazonS3Client;

    @Value("${cloud.aws.s3.bucket}")
    public String bucket; // S3 버킷 이름

    // S3로 파일 업로드하기
    public String upload(String filePath)throws RuntimeException {
        File targetFile = new File(filePath);
        String uploadImageUrl = putS3(targetFile, targetFile.getName()); // s3로업로드
        removeOriginalFile(targetFile);
        return uploadImageUrl;
    }
    // S3로 업로드
    private String putS3(File uploadFile, String fileName)throws RuntimeException
    {
        amazonS3Client.putObject(new PutObjectRequest(bucket, fileName,
                uploadFile)
                .withCannedAcl(CannedAccessControlList.PublicRead));
        return amazonS3Client.getUrl(bucket, fileName).toString();
    }
    //S3 업로드 후 원본 파일 삭제
    private void removeOriginalFile(File targetFile) {
        if (targetFile.exists() && targetFile.delete()) {
            log.info("File delete success");
            return;
        }
        log.info("fail to remove");
    }
    public void removeS3File(String fileName){
        final DeleteObjectRequest deleteObjectRequest = new
                DeleteObjectRequest(bucket, fileName);
        amazonS3Client.deleteObject(deleteObjectRequest);
    }
}

참고
putObject()
s3Client를 사용하여 S3 버킷에 파일을 업로드하는 역할을 수행한다.
PutObjectRequest 타입 즉시 반환 객체를 생성하여 업로드할 파일의 버킷 이름, 파일 이름, 파일 내용(inputStream), 메타데이터(objectMetadata)를 설정한다.
withCannedAcl(CannedAccessControlList.PublicRead)는 업로드된 파일이 공개 읽기 권한을 가지도록 설정한다.

테스트 코드

@SpringBootTest
public class S3UploaderTest {

    @Autowired
    private S3Uploader s3Uploader;

    @Test
    public void testUpload() {
        try{
            String filePath ="C:\\upload\\sa.png";
            String uploadName = s3Uploader.upload(filePath);
            System.out.println("uploadName = " + uploadName);
        }catch(Exception e){
            System.out.println(e.getMessage());
        }
    }

    @Test
    public void testRemove() {
        try {
            s3Uploader.removeS3File("sa.png");
        }catch(Exception e){
            System.out.println(e.getMessage());
        }
    }
}

testUpload()는 C:\upload 폴더의 sa.png 파일을 S3Uploader를 통해서 업로드한다.

Ec2에서 업로드 확인

ECc2에서 파일 업로드와 S3의 연동을 확인하려면 로컬 환경에서 업로드되는 경로를 EC2 서버 경로로 변경해주어야 한다. 예제의 경우 C:\upload 폴더에 파일들을 업로드하는데 EC2 환경은 현재 Linux 환경이므로 현재 사용자(ec2-user) 폴더 내에 upload 폴더를 생성한다.

application.properties 파일의 업로드 경로는 '/home/ec2-user/upload'로 변경한다.


제대로 작동되는 것을 확인할 수 있다.

0개의 댓글