목차

  1. 소개
  2. $UnionWith의 지원 버전 확인
  3. 네이티브 쿼리를 이용한 Union 연산
    • 네이티브 쿼리 실행 방법
    • MongoCollection.aggregate를 사용한 네이티브 쿼리 실행
  4. 코드 예시
    • MongoClient 설정
    • MongoCollection 가져오기
    • 매치 쿼리 생성
    • 매치 쿼리를 List<Document>로 변환
    • $unionWith로 쿼리 감싸기
    • 집계 연산 쿼리 추가
    • 최종 쿼리 실행

1. 소개

MongoDB 4.4부터 SQL의 Union all과 유사한 연산인 $UnionWith를 지원합니다. 이 기능은 데이터베이스의 여러 컬렉션을 합치는 데 사용됩니다. 그러나 Spring Data MongoDB의 최신 버전인 spring-boot-start-data-mongodb:3.0.7은 spring-data-mongodb:4.0.6에 의존하고 있어 $UnionWith와 관련된 기능을 사용할 수 없습니다.

따라서 네이티브 쿼리를 사용해야 합니다. 네이티브 쿼리는 MongoDatabase 인터페이스의 runCommand 메서드나 MongoCollectionaggregate 메서드를 사용하여 실행할 수 있습니다. 이를 통해 네이티브 쿼리 요청을 MongoDB 쿼리로 전달할 수 있습니다.

이 글에서는 MongoCollectionaggregate 메서드를 사용하여 네이티브 쿼리를 실행하는 방법을 알아보겠습니다.

2. $UnionWith의 지원 버전 확인

$UnionWith 연산을 사용하기 전에 MongoDB 서버의 버전을 확인해야 합니다. $UnionWith는 MongoDB 4.4 이상에서 지원되므로 해당 버전 이상이어야 합니다.

3. 네이티브 쿼리를 이용한 Union 연산

3.1. 네이티브 쿼리 실행 방법

네이티브 쿼리를 실행하는 방법은 MongoDatabaserunCommand 메서드나 MongoCollectionaggregate 메서드를 사용하는 것입니다. 이 중 MongoCollection.aggregate를 사용하여 네이티브 쿼리를 실행해보겠습니다.

3.2. MongoCollection.aggregate를 사용한 네이티브 쿼리 실행

MongoTemplate에서 사용할 컬렉션을 지정하여 MongoCollection 인터페이스의 객체를 가져옵니다. 그런 다음, 네이티브 쿼리를 작성하고 List<Document> 형식으로 변환한 후, MongoCollection.aggregate(List<Document>) 메서드에 전달하여 쿼리를 실행합니다.

4. 코드 예시

아래는 Spring Data MongoDB에서 Union 연산을 수행하는 예시 코드입니다.

4.1. MongoClient 설정

먼저, MongoClient를 생성하여 Spring Data MongoDB가 접근해야 할 데이터베이스를 확인합니다.

@Configuration
public class MongoConfig {

    @Bean
    public MongoClient mongoClient() {
        ConnectionString connectionString = new ConnectionString("mongodb://localhost:27017/test");
        MongoClientSettings mongoClientSettings = MongoClientSettings.builder()
            .applyConnectionString(connectionString)
            .build();

        return MongoClients.create(mongoClientSettings);
    }
}

4.2. MongoCollection 가져오기

MongoTemplate에서 사용할 컬렉션을 지정하여 MongoCollection 인터페이스 객체를 가져옵니다.

mongoTemplate.getDb().getCollection(COLLECTION_NAME)

4.3. 매치 쿼리 생성

Union에 사용할 매치 쿼리를 생성하는 메서드를 작성합니다.

private List<String> generateMatchQuery(List<Target> targets) {
    return targets.stream().map(target -> {
        return "{\n"
            + "  $match :{\n"
            + "    where : \"" + target.getPageUrl() + "\",\n"
            + "    \"what.uniqueSelector\" : \"" + target.getHtmlElement() + "\",\n"
            + "    when : {\n"
            + "      $gte : ISODate(\"" + target.getApplyStart() + "\"),\n"
            + "      $lte : ISODate(\"" + target.getApplyEnd() + "\"),\n"
            + "    },\n"
            + "    \"what.eventType\" : \"" + target.getEventType() + "\"\n"
            + "  }\n"
            + "}";
    }).toList();
}

4.4. 매치 쿼리를 List<Document>로 변환

생성된 여러 개의 매치 쿼리를 바탕으로 List<Document>를 생성합니다. 첫 번째 쿼리는 그대로 추가하고, 나머지 쿼리는 $unionWith로 감싸서 리스트에 추가합니다.

private List<Document> convert(List<Target> targets){
    if(targets.size()==0){
        throw new AnalyzeServiceException();
    }
    List<String> matchQuery= generateMatchQuery(targets);
    List<Document> documents=new ArrayList<>();
    documents.add(Document.parse(matchQuery.get(0)));
    for(int idx=1;idx<targets.size();idx++){
        documents.add(Document.parse(wrapUnionCommand(matchQuery.get(idx))));
    }
    return documents;
}

private String wrapUnionCommand(String query){
    return "{ $unionWith : { coll : \""+COLLECTION_NAME+"\", pipeline: ["
        +query+"]}}";
}

4.5. 집계 연산 쿼리 추가

중복 데이터를 제거하기 위해 집계 연산 쿼리를 추가하는 기능을 구현합니다. $unionWith 쿼리는 중복 데이터를 제거하지

않으므로 집계 연산을 추가하여 중복을 제거해야 합니다.

public String query(List<Document> convertResult, String aggregationQueryJson) {
    List<Document> fullQuery= new ArrayList<>(convertResult.size()+2);
    convertResult.forEach(document -> fullQuery.add(document));
    fullQuery.add(Document.parse("{ $group: {_id: \"$$ROOT\"} }"));
    fullQuery.add(Document.parse(aggregationQueryJson));
    return convert(runAggregate(fullQuery));
}

이제 네이티브 String 쿼리를 List<Document> 형식으로 변환하는 과정을 마쳤습니다. 이를 MongoCollection.aggregate(List<Document>)에 전달하여 Union 연산을 수행할 수 있습니다.

목차

소개

이 글은 Gitlab에 러너를 등록하고 설정하는 방법에 대해 안내합니다. Gitlab의 CI/CD를 활용하기 위해서는 러너를 등록해야 하며, 이를 통해 자동화된 빌드, 테스트, 배포 등의 작업을 실행할 수 있습니다.

러너 등록하기

  1. Gitlab 러너 설치 문서를 참고하여 해당 환경에 맞게 러너를 설치합니다.
  2. Gitlab 레포지토리로 이동하여 “Settings” 메뉴에서 “CI/CD” 탭으로 이동합니다.
  3. “Runner” 항목에서 등록할 레포지토리 URL 및 등록 토큰을 확인합니다.
  4. 터미널에서 gitlab-runner register 명령을 실행합니다.
  5. URL과 토큰을 입력하고, executor는 원하는 설정으로 선택합니다. Docker를 사용하는 것을 추천합니다.
  6. 기본 이미지로 ubuntu:latest를 설정합니다.
  7. gitlab-runner verify 명령을 실행하여 러너가 정상적으로 등록되었는지 확인합니다.
  8. gitlab-runner start 명령을 실행하여 러너 서비스를 시작합니다.
  9. 다시 Gitlab 레포지토리의 “Settings” 메뉴에서 “CI/CD” 탭으로 이동합니다.
  10. 초록색 불이 나타나는지 확인합니다. 성공적으로 등록된 경우 아래 이미지와 같은 화면이 표시됩니다.
    Runner Configuration
  11. “Edit” 버튼을 클릭하여 러너를 편집합니다.
  12. “Run untagged jobs” 옵션을 선택합니다. 이 옵션은 태그가 없는 작업도 실행할 수 있도록 해줍니다.

러너 구성하기

  1. 레포지토리의 루트 디렉토리에서 .gitlab-ci.yml 파일을 생성합니다.
  2. 다음 코드를 입력하여 테스트 작업을 실행해보세요.
stages:
  - deploy

deploy:
  stage: deploy
  script:
    - echo 'hello runner'
  only:
    - main
  1. 위 코드는 간단한 테스트용 작업을 실행하는 것입니다. 다음과 같은 결과가 나타나면 성공입니다.
    Runner Test

문제 해결

만약 러너가 정상적으로 작동하지 않는다면, 다음 단계를 다시 확인해보세요:

  • Gitlab 러너 설치 문서를 참고하여 올바르게 설치되었는지 확인합니다.
  • 등록할 레포지토리 URL 및 토큰을 정확하게 입력했는지 확인합니다.
  • 편집한 러너 구성이 올바른지 다시 확인합니다.

이 글을 통해 Gitlab에 러너를 등록하고 설정하는 방법에 대해 알아보았습니다. Gitlab의 CI/CD를 효율적으로 활용하여 개발 작업을 자동화할 수 있습니다.

1. 서버 구조

  1. 사설 인터넷망에 3개의 클러스터용 노드가 있습니다.
  2. 사설 인터넷망에 1개의 프로젝트 배포용 노드가 있습니다.

2. 앤서블 설정하기

2.1 배포 서버 DNS 설정하기

배포 서버를 정의하기 위해 /etc/hosts 파일에 호스트를 설정해야 합니다. 호스트 설정은 각 운영 체제마다 다를 수 있습니다. 리눅스 기반 시스템에서는 다음과 같이 /etc/hosts 파일을 수정합니다:

//... 10.12.204.13 peter-zk01.foo.bar 10.12.204.13 peter-kafka01.foo.bar 10.12.204.14 peter-zk02.foo.bar 10.12.204.14 peter-kafka02.foo.bar 10.12.204.17 peter-zk03.foo.bar 10.12.204.17 peter-kafka03.foo.bar 

2.2 SSH 허용하기

앤서블을 사용하여 서버에 접속할 때마다 비밀번호를 입력하는 것은 번거로울 수 있습니다. 따라서 배포 서버에 대한 SSH 접속을 허용하는 것이 좋습니다.

2.2.1 SSH 키 생성

ssh-keygen 명령을 사용하여 SSH 키를 생성합니다. 생성된 키 데이터는 ~/.ssh/id_rsa.pub 파일에 저장됩니다. 다음 단계에서 이 키 데이터를 사용하게 됩니다.

  1. ssh-keygen 명령을 실행하여 키를 생성합니다.
  2. 생성된 데이터를 ~/.ssh/id_rsa.pub 파일로부터 복사합니다.
  3. SSH로 접속하고자 하는 서버에 접속합니다.
  4. vi ~/.ssh/authorized_keys 명령을 사용하여 인증 키 파일을 수정합니다.
  5. 새로운 라인에 이전에 복사한 ~/.ssh/id_rsa.pub 데이터를 붙여넣습니다.
  6. chmod 600 ~/.ssh/authorized_keys 명령을 실행하여 권한을 설정합니다.
  7. 이제 로컬에서 ssh 명령을 사용하여 비밀번호 없이 서버에 접속할 수 있습니다.

2.3 앤서블 스크립트 다운로드

다음 명령을 사용하여 앤서블 스크립트를 다운로드합니다:

git clone https://github.com/onlybooks/kafka2 

그리고 kafka2/chapter2/ansible_playbook 디렉토리로 이동합니다.

hosts 파일을 다음과 같이 수정합니다. 보통 사용자는 root를 사용하므로 ansible_user를 root로 설정합니다:

[zkhosts] peter-zk01.foo.bar peter-zk02.foo.bar peter-zk03.foo.bar  [kafkahosts] peter-kafka01.foo.bar peter-kafka02.foo.bar peter-kafka03.foo.bar  [kerberoshosts] peter-zk01.foo.bar  [all:vars] ansible_user=root 

2.4 주키퍼 클러스터 설치하기

다음 명령을 사용하여 주키퍼 클러스터를 설치합니다:

ansible-playbook -i hosts zookeeper.yml 

Kafka ZooKeeper

2.5 카프카 클러스터 설치하기

다음 명령을 사용하여 카프카 클러스터를 설치합니다:

ansible-playbook -i hosts kafka.yml 

Kafka Cluster Installation

카프카는 실행되지만 내부적으로 주키퍼와의 연결을 실패하여 일정 시간 후에 타임아웃으로 종료될 수 있습니다. 서버에서 systemctl status kafka-server 명령을 실행하여 아무런 에러가 없는지 확인하면 성공입니다.

2.6 카프카 테스트하기

카프카가 올바르게 작동하는지 확인하기 위해 토픽을 생성하고 샘플 데이터를 프로듀스하고 컨슈밍해보겠습니다.

2.6.1 토픽 생성하기

서버에 접속하여 다음 명령을 실행합니다:

/usr/local/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic test-topic --partitions 1 --replication-factor 3 

출력으로 다음과 같이 "Created topic test-topic."이 나오면 성공입니다.

2.6.2 메시지 전송하기

샘플 메시지를 프로듀스해보겠습니다. 다음 명령을 실행하세요:

/usr/local/kafka/bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic test-topic 

> 기호가 나타나면 원하는 메시지를 입력하고 엔터 키를 눌러 메시지를 전송할 수 있습니다.

2.6.3 메시지 수신하기

2번 서버에서 메시지를 수신해보겠습니다. 다음 명령을 실행하세요:

/usr/local/kafka/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test-topic 

이제 이전에 실행한 프로듀서에서 테스트 메시지를 입력하면 출력으로 확인할 수 있습니다.

Kafka Consumer Output

 

Kafka Producer Output

2.7 스프링 프로젝트와 연결하기

스프링 프로젝트와 카프카 프로젝트를 연결해보겠습니다.

  1. 먼저 리액터 카프카 패키지를 설치합니다:
implementation 'io.projectreactor.kafka:reactor-kafka:1.3.18' 
  1. application.yml 파일에 다음과 같이 카프카 서버에 대한 정보를 설정합니다:
spring:   kafka:     bootstrap-servers: peter-kafka01.foo.bar:9092,peter-kafka02.foo.bar:9092,peter-kafka02.foo.bar:9092 
  1. 프로젝트를 실행하고 다음 로그가 출력되면 그룹에 성공적으로 조인했음을 의미합니다:
2023-07-12T17:59:46.090+09:00  INFO 3367 --- [y-trace-group-1] o.a.k.c.c.internals.ConsumerCoordinator  : [Consumer clientId=consumer-user-activity-trace-group-1, groupId=user-activity-trace-group] Successfully joined group with generation Generation{generationId=1, memberId='consumer-user-activity-trace-group-1-67094391-d771-4ec8-ba5e-d56cdf0e8083', protocol='range'} 

이제 앤서블을 사용하여 카프카 클러스터를 구축하고 테스트하는 방법에 대해 알게 되었습니다. 카프카를 사용하여 안정적이고 확장 가능한 메시지 시스템을 구축할 수 있습니다.

최근 서버 환경을 쿠버네틱스로 변경했기 때문에 지속적 배포 방법을 변경해야 합니다. 오늘은 Github Action으로 구현해 볼 것입니다.

쿠버네틱스에 배포하는 방법은 간단합니다. 원하는 배포사항을 YAML 파일로 작성해 kubectl로 등록해주면 됩니다. 이후에 Github Action에서 SSH로 접속해 YAML 파일을 전달하고 kubectl을 실행해줄 것입니다.

도커 빌드 및 레지스트리 저장

레포지토리명과 브랜치 이름을 바탕으로 이미지 이름을 정할 것입니다. Github Secret 에는 도커 허브 로그인에 사용할 DOCKER_USERNAME와 DOCKER_PASSWORD를 등록하면 됩니다.

스크립트는 다음과 같습니다.

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: <https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle>

name: Spring/Kotlin Coverage Test on main

on:
  push:
    branches: [ "main","dev","qa" ]

permissions:
  contents: write

jobs:
  Deploy-docker-image:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 11
        uses: actions/setup-java@v3
        with:
          java-version: '11'
          distribution: 'temurin'

      - name: Setup Gradle
        uses: gradle/gradle-build-action@v2

      - name: Build
        run: ./gradlew build

      - name: Create Test Coverage Report
        run: ./gradlew jacocoTestReport

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3.1.1
        with:
          file: ./build/reports/jacoco/test/jacocoTestReport.xml

      - name: Unpack JAR
        run: |
          mkdir -p build/dependency && (cd build/dependency; jar -xf ../libs/*.jar)

      - name: Set lowercase repository name without owner environment variable
        run: |
          echo "REPOSITORY_NAME=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]' | cut -d'/' -f2)" >> $GITHUB_ENV

      - name: Get lowercase branch name
        run: |
          echo "BRANCH_NAME=$(echo $GITHUB_REF_NAME | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV

      - name: Get docker image name
        run: |
          echo "IMAGE_NAME=${{ secrets.DOCKER_USERNAME }}/${{ env.REPOSITORY_NAME }}:${{ env.BRANCH_NAME }}" >> $GITHUB_ENV

      - name: Build docker image
        run: |
          docker build -t ${{ env.IMAGE_NAME }} .
          docker tag ${{ env.IMAGE_NAME }} ${{ secrets.DOCKER_USERNAME }}/${{ env.REPOSITORY_NAME }}:latest

      - name: Push docker image
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
          docker push ${{ env.IMAGE_NAME }}
          docker push ${{ secrets.DOCKER_USERNAME }}/${{ env.REPOSITORY_NAME }}:latest

Plain JAR 생성은 억제해야 합니다. 코틀린의 경우 build.gradle.kts에 아래와 같이 입력합니다.

tasks.getByName<Jar>("jar") {
    enabled = false
}

자바인 경우 build.gradle에 아래와 같이 입력합니다.

jar{
	enabled = false
}

이렇게 하면 dev 브랜치에 새로운 커밋이 생성될 때마다 그 커밋을 기준으로 도커 이미지를 빌드하고 푸시합니다. 그럼 latest와 dev 두 개가 만들어져 있을 것입니다. 꼭 latest를 만들어야 하는 것은 아닙니다.

EC2에 SSH로 접속하기

이제 SSH에 접속해보겠습니다. ssh-action을 사용하면 상당히 쉽습니다.

접속할 때는 ec2를 생성할 때 적용한 .pem 키 파일을 사용할 예정입니다.

1. 깃헙 시크릿 등록하기

깃헙 시크릿에는 총 3개를 등록해야 합니다.

  1. 사용자 이름 (AWS_USERNAME)
  2. EC2 호스트 주소(AWS_HOST)
  3. 시크릿 키(AWS_SECRET_ACCESS_KEY)

사용자 이름은 기본적으로 ec2-user 입니다.

호스트의 경우 ip 주소 또는 이를 가리키는 도메인을 의미합니다.

시크릿 키는 .pem 파일 값입니다.

메모장 또는 cat 명령어로 .pem 파일 내부의 값을 출력해 복사 후 시크릿으로 등록합니다.

2. 배포 스크립트 작성

k8s에 배포를 위한 스크립트를 작성합니다.

서비스와 배포에 대한 명세를 deploy.yml에 작성합니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: spring-demo-deploy
  labels:
    app: spring-demo
    tier: backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: spring-demo
  template:
    metadata:
      labels:
        app: spring-demo
    spec:
      containers:
        - name: spring-demo
          image: 'lee01042000/beamworks-backend:dev'
          ports:
            - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: spring-service
spec:
  type: NodePort
  selector:
    app: spring-demo
  ports:
    - protocol: TCP
      port: 8080
      targetPort: 8080
      nodePort: 30007

3. Github Action k8s 배포 스크립트 작성

이제 deploy.yaml을 서버에 전달해 kubectl로 적용하는 스크립트를 작성합니다.

scp, ssh 액션을 사용하면 쉽게 가능합니다.

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: <https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle>

name: Spring/Kotlin Coverage Test on main

on:
  push:
    branches: [ "main","dev","qa" ]

permissions:
  contents: write

jobs:
  Deploy:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 11
        uses: actions/setup-java@v3
        with:
          java-version: '11'
          distribution: 'temurin'

      - name: Setup Gradle
        uses: gradle/gradle-build-action@v2

      - name: Build
        run: ./gradlew build

      - name: Create Test Coverage Report
        run: ./gradlew jacocoTestReport

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3.1.1
        with:
          file: ./build/reports/jacoco/test/jacocoTestReport.xml

      - name: Unpack JAR
        run: |
          mkdir -p build/dependency && (cd build/dependency; jar -xf ../libs/*.jar)

      - name: Set lowercase repository name without owner environment variable
        run: |
          echo "REPOSITORY_NAME=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]' | cut -d'/' -f2)" >> $GITHUB_ENV

      - name: Get lowercase branch name
        run: |
          echo "BRANCH_NAME=$(echo $GITHUB_REF_NAME | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV

      - name: Get docker image name
        run: |
          echo "IMAGE_NAME=${{ secrets.DOCKER_USERNAME }}/${{ env.REPOSITORY_NAME }}:${{ env.BRANCH_NAME }}" >> $GITHUB_ENV

      - name: Build docker image
        run: |
          docker build -t ${{ env.IMAGE_NAME }} .
          docker tag ${{ env.IMAGE_NAME }} ${{ secrets.DOCKER_USERNAME }}/${{ env.REPOSITORY_NAME }}:latest

      - name: Push docker image
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
          docker push ${{ env.IMAGE_NAME }}
          docker push ${{ secrets.DOCKER_USERNAME }}/${{ env.REPOSITORY_NAME }}:latest

      - name: Transport deploy.yaml to server
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.AWS_HOST }}
          username: ${{ secrets.AWS_USERNAME }}
          key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          overwrite: true
          source: ./deploy.yaml
          target: ~/

      - name: Connect to AWS EC2 with SSH
        uses: appleboy/ssh-action@v0.1.8
        with:
          host: ${{ secrets.AWS_HOST }}
          username: ${{ secrets.AWS_USERNAME }}
          key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          script: kubectl apply -f deploy.yaml

4. 배포 및 확인

지금까지 작성한 커밋을 dev 브랜치에 커밋하면 Github Action이 실행됩니다.

위와 같이 배포가 잘 이루어지면 서버에 가서 상태를 확인합니다.

아래처럼 새로운 파드가 돌아가면 성공입니다.

쿠버네틱스에 배포하는 방법은 간단합니다. 원하는 배포사항을 YAML 파일로 작성해 kubectl로 등록해주면 됩니다. 이후에 Github Action에서 SSH로 접속해 YAML 파일을 전달하고 kubectl을 실행해줄 것입니다.

도커 빌드 및 레지스트리 저장

레포지토리명과 브랜치 이름을 바탕으로 이미지 이름을 정할 것입니다. Github Secret 에는 도커 허브 로그인에 사용할 DOCKER_USERNAME와 DOCKER_PASSWORD를 등록하면 됩니다.

스크립트는 다음과 같습니다.

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: <https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle>

name: Spring/Kotlin Coverage Test on main

on:
  push:
    branches: [ "main","dev","qa" ]

permissions:
  contents: write

jobs:
  Deploy-docker-image:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 11
        uses: actions/setup-java@v3
        with:
          java-version: '11'
          distribution: 'temurin'

      - name: Setup Gradle
        uses: gradle/gradle-build-action@v2

      - name: Build
        run: ./gradlew build

      - name: Create Test Coverage Report
        run: ./gradlew jacocoTestReport

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3.1.1
        with:
          file: ./build/reports/jacoco/test/jacocoTestReport.xml

      - name: Unpack JAR
        run: |
          mkdir -p build/dependency && (cd build/dependency; jar -xf ../libs/*.jar)

      - name: Set lowercase repository name without owner environment variable
        run: |
          echo "REPOSITORY_NAME=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]' | cut -d'/' -f2)" >> $GITHUB_ENV

      - name: Get lowercase branch name
        run: |
          echo "BRANCH_NAME=$(echo $GITHUB_REF_NAME | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV

      - name: Get docker image name
        run: |
          echo "IMAGE_NAME=${{ secrets.DOCKER_USERNAME }}/${{ env.REPOSITORY_NAME }}:${{ env.BRANCH_NAME }}" >> $GITHUB_ENV

      - name: Build docker image
        run: |
          docker build -t ${{ env.IMAGE_NAME }} .
          docker tag ${{ env.IMAGE_NAME }} ${{ secrets.DOCKER_USERNAME }}/${{ env.REPOSITORY_NAME }}:latest

      - name: Push docker image
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
          docker push ${{ env.IMAGE_NAME }}
          docker push ${{ secrets.DOCKER_USERNAME }}/${{ env.REPOSITORY_NAME }}:latest

Plain JAR 생성은 억제해야 합니다. 코틀린의 경우 build.gradle.kts에 아래와 같이 입력합니다.

tasks.getByName<Jar>("jar") {
    enabled = false
}

자바인 경우 build.gradle에 아래와 같이 입력합니다.

jar{
	enabled = false
}

이렇게 하면 dev 브랜치에 새로운 커밋이 생성될 때마다 그 커밋을 기준으로 도커 이미지를 빌드하고 푸시합니다. 그럼 latest와 dev 두 개가 만들어져 있을 것입니다. 꼭 latest를 만들어야 하는 것은 아닙니다.

EC2에 SSH로 접속하기

이제 SSH에 접속해보겠습니다. ssh-action을 사용하면 상당히 쉽습니다.

접속할 때는 ec2를 생성할 때 적용한 .pem 키 파일을 사용할 예정입니다.

1. 깃헙 시크릿 등록하기

깃헙 시크릿에는 총 3개를 등록해야 합니다.

  1. 사용자 이름 (AWS_USERNAME)
  2. EC2 호스트 주소(AWS_HOST)
  3. 시크릿 키(AWS_SECRET_ACCESS_KEY)

사용자 이름은 기본적으로 ec2-user 입니다.

호스트의 경우 ip 주소 또는 이를 가리키는 도메인을 의미합니다.

시크릿 키는 .pem 파일 값입니다.

메모장 또는 cat 명령어로 .pem 파일 내부의 값을 출력해 복사 후 시크릿으로 등록합니다.

2. 배포 스크립트 작성

k8s에 배포를 위한 스크립트를 작성합니다.

서비스와 배포에 대한 명세를 deploy.yml에 작성합니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: spring-demo-deploy
  labels:
    app: spring-demo
    tier: backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: spring-demo
  template:
    metadata:
      labels:
        app: spring-demo
    spec:
      containers:
        - name: spring-demo
          image: 'lee01042000/beamworks-backend:dev'
          ports:
            - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: spring-service
spec:
  type: NodePort
  selector:
    app: spring-demo
  ports:
    - protocol: TCP
      port: 8080
      targetPort: 8080
      nodePort: 30007

3. Github Action k8s 배포 스크립트 작성

이제 deploy.yaml을 서버에 전달해 kubectl로 적용하는 스크립트를 작성합니다.

scp, ssh 액션을 사용하면 쉽게 가능합니다.

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: <https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle>

name: Spring/Kotlin Coverage Test on main

on:
  push:
    branches: [ "main","dev","qa" ]

permissions:
  contents: write

jobs:
  Deploy:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 11
        uses: actions/setup-java@v3
        with:
          java-version: '11'
          distribution: 'temurin'

      - name: Setup Gradle
        uses: gradle/gradle-build-action@v2

      - name: Build
        run: ./gradlew build

      - name: Create Test Coverage Report
        run: ./gradlew jacocoTestReport

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3.1.1
        with:
          file: ./build/reports/jacoco/test/jacocoTestReport.xml

      - name: Unpack JAR
        run: |
          mkdir -p build/dependency && (cd build/dependency; jar -xf ../libs/*.jar)

      - name: Set lowercase repository name without owner environment variable
        run: |
          echo "REPOSITORY_NAME=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]' | cut -d'/' -f2)" >> $GITHUB_ENV

      - name: Get lowercase branch name
        run: |
          echo "BRANCH_NAME=$(echo $GITHUB_REF_NAME | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV

      - name: Get docker image name
        run: |
          echo "IMAGE_NAME=${{ secrets.DOCKER_USERNAME }}/${{ env.REPOSITORY_NAME }}:${{ env.BRANCH_NAME }}" >> $GITHUB_ENV

      - name: Build docker image
        run: |
          docker build -t ${{ env.IMAGE_NAME }} .
          docker tag ${{ env.IMAGE_NAME }} ${{ secrets.DOCKER_USERNAME }}/${{ env.REPOSITORY_NAME }}:latest

      - name: Push docker image
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
          docker push ${{ env.IMAGE_NAME }}
          docker push ${{ secrets.DOCKER_USERNAME }}/${{ env.REPOSITORY_NAME }}:latest

      - name: Transport deploy.yaml to server
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.AWS_HOST }}
          username: ${{ secrets.AWS_USERNAME }}
          key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          overwrite: true
          source: ./deploy.yaml
          target: ~/

      - name: Connect to AWS EC2 with SSH
        uses: appleboy/ssh-action@v0.1.8
        with:
          host: ${{ secrets.AWS_HOST }}
          username: ${{ secrets.AWS_USERNAME }}
          key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          script: kubectl apply -f deploy.yaml

4. 배포 및 확인

지금까지 작성한 커밋을 dev 브랜치에 커밋하면 Github Action이 실행됩니다.

위와 같이 배포가 잘 이루어지면 서버에 가서 상태를 확인합니다.

아래처럼 새로운 파드가 돌아가면 성공입니다.

+ Recent posts