[Infra] Spring + EB + Github Actions + Docker + ACM을 통한 CI/CD 자동화 및 https 적용 방법 (feat. 내도메인 한국) - 1편

Hyunjoon Choi·2023년 8월 23일
1

Infra

목록 보기
1/3
post-thumbnail

개요

스프링을 계속 공부하던 도중, CI/CD 자동화를 겨우겨우 했음에도 불구하고 https 적용에 계속 실패했었다. 그러다가 후배의 도움을 통해 최종적으로 완료가 되었기에, 두고두고 활용하기 위해 벨로그 글로 남겨두려고 한다. (아마 가장 최신판이지 않을까 싶다)

최종 구조 소개

최종 구조는 다음과 같다.

프로젝트 환경

프로젝트에 쓰인 환경은 다음과 같다.

  • Spring Boot 3.1.2
  • AWS RDS (MySQL)
  • AWS Certificate Manager
  • 내도메인.한국
  • Github Actions
  • Docker
  • AWS Elasticbeanstalk

RDS 생성하기

우선 데이터베이스도 사용할 것이기 때문에, AWS RDS를 생성하도록 하자.

보안 그룹

RDS에 대한 보안 그룹이 필요하다. 보안 그룹은 다음과 같이 생성한다. (EC2 > 네트워크 및 보안 > 보안 그룹 > 보안 그룹 생성)

AWS 접속 > 데이터베이스 생성

  • 데이터베이스 생성 방식 선택: 표준 생성
  • 엔진 옵션: MySQL
  • 엔진 버전: 기본값 (23.08.23 현재 MySQL 8.0.33)
  • 템플릿: 프리 티어
  • DB 인스턴스 식별자: RDS 이름
  • 마스터 사용자 이름: 관리자 이름
  • 마스터 암호: 암호 설정
  • DB 인스턴스 클래스: 버스터블 클래스 (db.t3.micro)
  • 스토리지 유형: 범용 SSD (gp2)
  • 할당된 스토리지: 20GiB
  • 스토리지 자동 조정 활성화: 비활성화 (활성화 할 경우 과금의 원인)
  • 컴퓨팅 리소스: EC2 컴퓨팅 리소스에 연결 안 함
  • VPC, DB 서브넷 그룹: 기본값
  • 퍼블릭 액세스: 예
  • 기존 VPC 보안 그룹: 위에서 생성한 보안 그룹 선택
  • 가용 영역: 기본 설정 없음
  • RDS 프록시: 해제
  • 인증 기관: 기본값
  • 데이터베이스 인증: 암호 인증
  • 모니터링: 해제
  • 추가 구성 중 자동 백업 활성화 해제, 마이너 버전 자동 업그레이드 사용 해제

생성 후 RDS의 엔드포인트를 확인할 수 있다.

스프링 프로젝트 생성하기

이제 CI/CD를 적용할 프로젝트를 만들어보자.
간단하게 멤버를 등록하고, 멤버를 조회할 수 있는 프로젝트를 만들었다.
작성한 스프링부트 환경은 아래 사진과 같다.

프로젝트 구성

.gitignore 수정

.gitignoreapplication.yml.DS_Store (맥 환경이기 때문에)를 추가로 붙였다.

### MAC ###
.DS_Store

### YAML ###
*.yml

application.yml 작성

기본적으로 생성된 application.propertiesapplication.yml로 바꾸고, 다음과 같이 작성하였다.

spring:
  datasource:
    url: jdbc:mysql://{RDS 엔드포인트}:{RDS 포트}/{DB 이름}?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
    username: {관리자 이름}
    password: {관리자 비번}
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: create # 처음에는 DDL이 만들어져야 하므로 create

로컬에서 RDS 접속하여 데이터베이스 만들어두기

맥 기준으로 터미널에서 RDS에 접속하는 방법은 아래 코드를 입력하면 된다.

mysql -u {관리자 이름} -p -h {RDS 엔드포인트}

Enter password: {비밀번호 입력}

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 16
Server version: 8.0.33 Source distribution

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> create database {데이터베이스 이름};
Query OK, 1 row affected (0.01 sec)

코드 작성

임시로 작성해 본 코드이기 때문에 효율적이라고 할 수는 없을 것 같습니다. 이 점 참고 바랍니다.

Member

package devholic.devops;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;
    private String name;

    public static Member from(String name) {
        return new Member(name);
    }

    private Member (String name) {
        this.name = name;
    }
}

MemberController

package devholic.devops;

import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/")
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;

    @GetMapping("/")
    public String hello() {
        return "hello";
    }

    @GetMapping("/members/{id}")
    public ResponseEntity<?> find(@PathVariable Long id) {
        try {
            return ResponseEntity.ok(memberService.find(id));
        } catch (IllegalStateException e) {
            return ResponseEntity.badRequest().body(e.getMessage());
        }
    }

    @PostMapping("/members")
    public ResponseEntity<MemberResponse> create(@RequestBody MemberRequest request) {
        return ResponseEntity.ok(memberService.create(request));
    }
}

MemberRepository

package devholic.devops;

import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long> {
}

MemberService

package devholic.devops;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;

    public MemberResponse create(MemberRequest request) {

        Member newMember = Member.from(request.getName());
        memberRepository.save(newMember);

        return MemberResponse.from(newMember.getId(), newMember.getName());
    }

    public MemberResponse find(Long id) {

        Member findMember = memberRepository.findById(id)
                .orElseThrow(() -> new IllegalStateException("멤버가 없습니다."));

        return MemberResponse.from(findMember.getId(), findMember.getName());
    }
}

MemberRequest

package devholic.devops;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class MemberRequest {

    private String name;
}

MemberResponse

package devholic.devops;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MemberResponse {

    private Long id;
    private String name;

    public static MemberResponse from(Long id, String name) {
        return new MemberResponse(id, name);
    }

    private MemberResponse (Long id, String name) {
        this.id = id;
        this.name = name;
    }
}

포스트맨 결과

Member 생성

Member 조회

기본

이제 다음 편에서는 Docker 연결, Elasticbeanstalk 설정 등 CI/CD에 관한 작업을 해 보겠다.


부족하거나 보완할 점이 있다면 댓글 부탁드립니다 😃

profile
개발을 좋아하는 워커홀릭

0개의 댓글