728x90
반응형

workflow를 사용하는 방법은 매우 간단하다

프로젝트/.github/workflows/ 경로 아래에 yml 파일을 만들면 인식해서 실행시켜준다.

 

1. /.github/workflows/auto_labeling.yml 생성

name: Auto Labeling

on:
  pull_request:
    types: [ opened, reopened, synchronize ]

permissions:
  contents: write
  pull-requests: write
  packages: write

jobs:
  update_release_draft:
    runs-on: self-hosted
    steps:
      - uses: release-drafter/release-drafter@v6
        with:
          commitish: main
          config-name: auto_labeling_config.yml
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

 

pull_request가 실행 될 때마다 작동하게 설계했으며, config 파일의 경로는 .github/ 이어야 한다.

권한 또한 중요하니 잊지 않도록 한다.

 

runs-on은 환경에 맞는 코드를 쓰면 된다.

 

2. /.github/auto_labeling_config.yml 작성

template: |
  ## What’s Changed

  $CHANGES
autolabeler:
  - label: 'Component: Client'
    files:
      - 'client/**'
  - label: 'Type: Bug'
    title:
      - '/^fix(\([a-zA-Z][a-zA-Z]\))?:/i'

template 은 필수 코드 이므로 추가하고, 경로와 PR title을 사용한 규칙을 지정하도록 한다.

사실상 template은 해당 작업에서 사용되진 않으며, 릴리즈 자동화에서 쓰일 예정이다.

title의 정규 표현식은 원하는대로 바꿀 수도 있다.

728x90
반응형
728x90
반응형

Pull Request를 올릴 때 자동으로 Label을 달아주는 workflow를 만들어보자.

 

먼저 .github/workflows에 workflow를 만들어준다.

name: Auto Labeling

on:
  pull_request:
    types: [ opened, reopened, synchronize ]

permissions:
  contents: write
  pull-requests: write
  packages: write

jobs:
  update_release_draft:
    runs-on: self-hosted
    steps:
      - uses: release-drafter/release-drafter@v6
        with:
          commitish: main
          config-name: auto_labeling.yml
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

 

그 다음 config 파일인 auto_labeling.yml 파일을 .github/ 경로에 만들어준다.

config 파일은 main 브랜치에 병합이 된 후 인식이 가능하니 알아두도록 하자.

 

template: |
  ## What’s Changed

  $CHANGES
autolabeler:
  - label: 'Component: Client'
    files:
      - 'client/**'
  - label: 'Component: DB'
    files:
      - 'docker/postgres/**'
  - label: 'Component: Server'
    files:
      - 'server/**'
  - label: 'Type: Bug'
    title:
      - '/^fix(\([a-zA-Z][a-zA-Z]\))?:/i'
  - label: 'Type: Build/CI/CD'
    title:
      - '/^build(\([a-zA-Z][a-zA-Z]\))?:/i'
      - '/^ci(\([a-zA-Z][a-zA-Z]\))?:/i'
      - '/^cd(\([a-zA-Z][a-zA-Z]\))?:/i'
  - label: 'Type: Change'
    title:
      - '/^change(\([a-zA-Z][a-zA-Z]\))?:/i'
  - label: 'Type: Chore'
    title:
      - '/^chore(\([a-zA-Z][a-zA-Z]\))?:/i'
  - label: 'Type: Documentation'
    title:
      - '/^docs(\([a-zA-Z][a-zA-Z]\))?:/i'
  - label: 'Type: Feature'
    title:
      - '/^feat(\([a-zA-Z][a-zA-Z]\))?:/i'
  - label: 'Type: Style'
    title:
      - '/^style(\([a-zA-Z][a-zA-Z]\))?:/i'
  - label: 'Type: Test'
    title:
      - '/^test(\([a-zA-Z][a-zA-Z]\))?:/i'
728x90
반응형
728x90
반응형

 

1. 액세스 토큰 생성

Personal Access Tokens (Classic) (github.com)  에서 패키지에 대한 권한을 가진 토큰을 생성한다.

생성되는 토큰 값을 저장한 뒤 원하는 곳에 txt 파일로 만들어두고, 아래 명령어 중 하나로 로그인 가능하다.

$ docker login https://ghcr.io -u outsideris // 입력 후 패스워드 토큰값 입력
$ cat TOKEN.txt | docker login https://ghcr.io -u outsideris --password-stdin

 

2. self_host runner를 만들고 등록해준다.

About self-hosted runners - GitHub Docs

 

About self-hosted runners - GitHub Docs

You can host your own runners and customize the environment used to run jobs in your GitHub Actions workflows.

docs.github.com

 

3. workflow를 만들어준다.

# 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 package using Gradle and then publish it to GitHub packages when a release is created
# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#Publishing-using-gradle

name: Deploy

permissions:
  contents: read
  packages: write

# 어떤 이벤트가 발생하면 workflow 실행할 지 명시
on:
  # workflow 수동 실행
  workflow_dispatch:
    inputs:
      logLevel:
        description: 'Log level'
        required: true
        default: 'warning'
        type: choice
        options:
          - info
          - warning
          - debug


# 실행될 작업들
jobs:
  # 빌드 후 Container Registry에 image 등록
  push_to_registry:
    # VM의실행 환경 지정 => self-hosted
    runs-on: self-hosted

    # 실행될 jobs를 순서대로 명시
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: .env 파일 생성
        run: echo "# Autogenerated .env file" > .env &&
          echo "BUILD_ENV=production" >> .env &&
          echo "TZ=Asia/Seoul" >> .env

      - name: .env.production 파일 생성
        run: echo "# Autogenerated .env.production file" > .env.production

      - name: Server Properties 생성
        run: mv ./server/src/main/resources/application-production-sample.yaml ./server/src/main/resources/application-production.yaml

      - name: Docker compose down
        run: docker compose down --rmi all -v

      - name: Docker compose up
        run: docker compose up --build -d

      # GitHub Container Registry 로그인
      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ secrets.GHUB_USERNAME }}
          password: ${{ secrets.GHUB_TOKEN }}

      # Docker image 빌드 및 push
      - name: Push to container registry
        run:
          docker push ghcr.io/teepo/test:${{ secrets.RELEASE_VERSION }} &&
          docker image prune -a -f

      # github actions 네트워크 확인
      - name: Check DockerHub Access
        run: |
          nslookup index.docker.io 8.8.8.8

      # ssh 키 확인
      - name: Test SSH Connection
        run: |
          echo "${{ secrets.DEPLOY_KEY }}" > deploy_key
          chmod 600 deploy_key
          ssh -p 2022 -i deploy_key -o StrictHostKeyChecking=no ${{ secrets.DEPLOY_USERNAME }}@${{ secrets.DEPLOY_HOST }} 

      - name: Copy Files to Server using SCP
        run: |
          scp -i deploy_key -P ${{ secrets.DEPLOY_PORT }} -o StrictHostKeyChecking=no \
          .env .env.production compose.yaml compose.base.yaml compose.production.yaml \
          ${{ secrets.DEPLOY_USERNAME }}@${{ secrets.DEPLOY_HOST }}:/home

      - name: Run Commands on Server via SSH
        run: |
          ssh -i deploy_key -p ${{ secrets.DEPLOY_PORT }} -o StrictHostKeyChecking=no ${{ secrets.DEPLOY_USERNAME }}@${{ secrets.DEPLOY_HOST }} << 'EOF'
          echo ${{ secrets.GHUB_TOKEN }} | sudo docker login ghcr.io --username ${{ secrets.GHUB_USERNAME }} --password-stdin &&
          cd /home
          sudo docker compose down --rmi all -v
          sudo docker pull ghcr.io/teepo/test:${{ secrets.RELEASE_VERSION }}
          sudo docker compose up -d
          sudo docker image prune -a -f
          EOF
          
      - name: Docker compose down
        run: docker compose down --rmi all -v

 

docker login 계정 확인 - sudo cat /root/.docker/config.json

4. Dockefile

ghcr(컨테이너 레지스트리)에서 image를 pull 하므로 배포용 Docker 파일을 다음과같이 바꿔야 한다.

    app:
        image: ghcr.io/${IMAGE_REPO}:${RELEASE_VERSION}

 

5. secrets

github repository -> Settings -> Secrets and variables에서 secrets 항목들을 추가해준다.

참고로 github username은 반드시 소문자로 해야된다.


self-hosted 로 사용할 경우 서버의 사용자에 Docker 권한을 부여해주어야 한다.

 

How to fix docker: Got permission denied issue - Stack Overflow

 

How to fix docker: Got permission denied issue

I installed Docker in my machine where I have Ubuntu OS. When I run: sudo docker run hello-world All is ok, but I want to hide the sudo command to make the command shorter. If I write the command

stackoverflow.com

 

 

참고

GitHub Action Docker Compose deployments via SSH (servicestack.net)

 

728x90
반응형
728x90
반응형

이번 포스트에서는 Dockerfile의 환경 ( dev or prod )에 따라 Nginx를 다르게 빌드하는 법을 알아보겠다.

 

1. dev.default.conf 생성

upstream client{
  server client:3000;
}

upstream server{
  server server:8080;
}

server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;

    #access_log  /var/log/nginx/host.access.log  main;
    location /api {
        proxy_pass http://server;
        proxy_redirect off;
        rewrite ^/api/(.*)$ /$1 break;
    }

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        proxy_pass http://client;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

 

2,  prod.default.conf 생성

upstream client{
  server client:3000;
}

upstream server{
  server server:8080;
}

server {
    listen       80;
    listen  [::]:80;
    server_name  [도메인이름];

    location ^~ /.well-known/acme-challenge/ {
      default_type "text/plain";
      root /var/www/certbot;
    }

    location / {
        return 301 https://[도메인이름]$request_uri;
    }
}

server {

        listen 443 ssl default_server;
        listen [::]:443 ssl default_server;
        server_name  [도메인이름];

        ssl_certificate /etc/nginx/ssl/live/[도메인이름]/fullchain.pem;
        ssl_certificate_key /etc/nginx/ssl/live/[도메인이름]/privkey.pem;

        #access_log  /var/log/nginx/host.access.log  main;
        location /api {
            proxy_pass http://server;
            proxy_redirect off;
            rewrite ^/api/(.*)$ /$1 break;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $host;
        }

        location / {
            proxy_pass http://client;
            root   /usr/share/nginx/html;
            index  index.html index.htm;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $host;
        }

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   /usr/share/nginx/html;
        }

        #allow deny ip
        #allow 12.0.0.0;
        #deny all;
}

 

3. .env 변수 추가

# Build 환경 변수 설정
BUILD_ENV=dev

 

4. docker compose 파일 수정

services:
  nginx:
    build:
      context: ./docker/nginx
      args:
        BUILD_ENV: ${BUILD_ENV} // 이 부분 추가
    ports:
      - "80:80"
      - "443:443"
    restart: always
    environment:
      TZ: Asia/Seoul
      BUILD_ENV: ${BUILD_ENV}
    volumes:
      - ${CERTBOT_WWW_DIR}:/var/www/certbot/:ro
      - ${CERTBOT_CONF_DIR}:/etc/nginx/ssl/:ro
    depends_on:
      - client
      - server

 

5. Dockerfile 수정

FROM nginx:latest

ARG BUILD_ENV=BUILD_ENV

COPY ./$BUILD_ENV.default.conf /etc/nginx/conf.d/default.conf

CMD ["nginx", "-g", "daemon off;"]
728x90
반응형
728x90
반응형

이전 포스트와 다르게 Docker를 활용한 SSL 인증을 구현해보았다. Cerbot 컨테이너를 만들고, docker compose 명령어를 통해 동작

1. /docker/nginx/default.conf 생성

최초 certbot 인증 시에는 아래와 같이 기본 세팅으로 해야한다.

server {
    listen 80;
    listen [::]:80;

    server_name 도메인 이름;
    server_tokens off;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

}

 

2. /docker/nginx/ Dockerfile 생성

FROM nginx:latest
COPY ./default.conf /etc/nginx/conf.d/default.conf

CMD ["nginx", "-g", "daemon off;"]

 

3. docker-compose.yml 추가

services:
  nginx:
    build: ./docker/nginx
    ports:
      - "80:80"
      - "443:443"
    restart: always
    environment:
      TZ: Asia/Seoul
    volumes:
      - ${CERTBOT_WWW_DIR}:/var/www/certbot/:ro
      - ${CERTBOT_CONF_DIR}:/etc/nginx/ssl/:ro
    depends_on:
      - client
      - server

  certbot:
    image: certbot/certbot:latest
    volumes:
      - ${CERTBOT_WWW_DIR}:/var/www/certbot/:rw
      - ${CERTBOT_CONF_DIR}:/etc/letsencrypt/:rw
      
  client:
  	...
  server:
  	...

 

4. .env파일 작성

...

# CERTBOT
CERTBOT_WWW_DIR=/data/certbot/www
CERTBOT_CONF_DIR=/data/certbot/conf

5. Docker compose up

$ docker compose up -d

인증서 발급을 위해 Nginx 서버를 켜준다.

6. 인증서 발급

$ docker compose run --rm  certbot certonly --webroot --webroot-path /var/www/certbot/ -d 도메인이름

옵션설명

--register-unsafely-without-email 이메일 없이 계정 등록 (주의 필요!)
--agree-tos Let's Encrypt 서비스 약관에 자동으로 동의
--non-interactive 모든 입력 없이 비대화식 실행 (자동화에 필수)

pem 키 위치 확인

 

와일드 카드 포함일 경우

$ docker compose run --rm certbot certonly \
   --manual \
   --preferred-challenges dns  \
   -d '*.도메인'  \
   --register-unsafely-without-email \
   --agree-tos

7. default.conf 파일 수정

upstream client{
  server client:3000;
}

upstream server{
  server server:8080;
}

server {
    listen       80;
    listen  [::]:80;
    server_name  [도메인이름];

    location ^~ /.well-known/acme-challenge/ {
      default_type "text/plain";
      root /var/www/certbot;
    }

    location / {
        return 301 https://[도메인이름]$request_uri;
    }
}

server {

        listen 443 ssl default_server;
        listen [::]:443 ssl default_server;
        server_name  [도메인이름];

        ssl_certificate /etc/nginx/ssl/live/[도메인이름]/fullchain.pem;
        ssl_certificate_key /etc/nginx/ssl/live/[도메인이름]/privkey.pem;

        #access_log  /var/log/nginx/host.access.log  main;
        location /api {
            proxy_pass http://server;
            proxy_redirect off;
            rewrite ^/api/(.*)$ /$1 break;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $host;
        }

        location / {
            proxy_pass http://client;
            root   /usr/share/nginx/html;
            index  index.html index.htm;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $host;
        }

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   /usr/share/nginx/html;
        }

        #allow deny ip
        #allow 12.0.0.0;
        #deny all;
}

 

8. docker 재시작

$ docker compose up -d

Nginx Service에 restart:always 옵션이 존재하기 때문에 Nginx Service는 재시작된다.

 

9. 인증서 관련 명령어

 

- 인증서 확인

$ docker compose run --rm certbot certificates

 

- 인증서 갱신

$ docker compose run --rm certbot renew

 

10. 크론 탭 활용 (crontab)

- /bin/letsencrypt.sh 파일 작성

cd /home
date >> /home/certbot_renew.log
sudo docker compose run --rm certbot renew >> /home/certbot_renew.log
sudo docker compose restart nginx

 

- 실행 권한 부여

$ sudo chmod +x /bin/letsencrypt.sh

 

- 크론탭 열고 편집 

$ sudo crontab -e

 

- 아래 배치잡 생성

30 4 * * 0 /bin/letsencrypt.sh

 

- 저장 후 배치잡 확인

$ sudo crontab -l

 

-저장하고 크론 다시 실행

$ sudo service cron restart
728x90
반응형
728x90
반응형

1. rsync 다운로드

$ sudo yum install rsync

# 혹은

$ sudo apt install rsync

 

2. 사용법

$ rsync [OPTIONS] [SOURCE] [TARGET]

 

3. 예제

# 로컬 데이터를 로컬에 복사
$ rsync -avh /home/user/data /home/new_user/backup

# 로컬의 데이터를 리모트로 복사 
$ rsync -avh /home/user/data remote_user@remotehost:/home/remote_user/backup

# ssh 포트가 다른 경우
$ rsync -avh -e "ssh -p 123" /home/user/data remote_user@remotehost:/home/remote_user/backup

# 리모트 데이터를 로컬로 가져옴
$ rsync -avh remote_user@remotehost:/home/remote_user/backup /home/user/data

 

-ravPh 옵션을 주면 Progress도 확인할 수 있다.

 

4. ssh 파일 전송 예제

rsync -ravPh abc.tar Server:~/
728x90
반응형
728x90
반응형

1. ~/.ssh/config 생성

Host Server
  HostName IP주소
  User 유저이름
  Port 22
  IdentityFile pem파일위치

 

 

2. pem 파일 권한 수정

chmod 400 pem파일위치

 

3. 이름으로 ssh 접속

$ssh Server
728x90
반응형
728x90
반응형

Kafka SASL에 관한 정보는 구글링해도 잘 나온다.

하지만 세팅하는 부분은 생각보다 별로 없어서 여기 적어놔야겠다.

1. Kafka Broker Docker Compose

  kafka:
    image: bitnami/kafka:3.4
    hostname: kafka
    ports:
      - 9092:9092
    environment:
      KAFKA_CLIENT_USERS: user
      KAFKA_CLIENT_PASSWORDS: pass
      ALLOW_PLAINTEXT_LISTENER: yes
      KAFKA_BROKER_ID: 1
      KAFKA_CFG_PROCESS_ROLES: broker,controller
      KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_CFG_INTER_BROKER_LISTENER_NAME: CLIENT
      KAFKA_CFG_LISTENERS: CONTROLLER://:9093,CLIENT://:9092
      KAFKA_CFG_ADVERTISED_LISTENERS: CLIENT://host.docker.internal:9092
      KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,CLIENT:SASL_PLAINTEXT
      KAFKA_CFG_SASL_MECHANISM_INTER_BROKER_PROTOCOL: PLAIN
      KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: true
      KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
      KAFKA_CFG_DELETE_TOPIC_ENABLE: true
      KAFKA_CFG_BROKER_ID: 1
      KAFKA_CFG_NODE_ID: 1
      KRAFT_MODE: true
      KAFKA_ENABLE_KRAFT: yes
      KAFKA_MESSAGE_MAX_BYTES: 2000000000
      TZ: Asia/Seoul
    volumes:
      - ${KAFKA_DATA_DIR}:/bitnami/kafka/data

위와 같이 Users, Passwords를 추가해주고 ( 여러개여도 상관 없음. )

SASL에 관한 필드를 추가해준다.

 

2. Producer

  kafka:
    producer:
      bootstrap-servers: host.docker.internal:9092
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
    properties:
      security.protocol: SASL_PLAINTEXT
      sasl.mechanism: PLAIN
      sasl.jaas.config: org.apache.kafka.common.security.plain.PlainLoginModule required username="user" password="pass!";

 

3. Consumer

  kafka:
    consumer:
      bootstrap-servers: kafka:9092
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
    properties:
      security.protocol: SASL_PLAINTEXT
      sasl.mechanism: PLAIN
      sasl.jaas.config: org.apache.kafka.common.security.plain.PlainLoginModule required username="user" password="pass!";
728x90
반응형

'Kafka' 카테고리의 다른 글

Kafka | Verify Server Connection  (0) 2024.02.22
728x90
반응형

kafkaTemplate.send() 메소드에는 훅이 없다. 혹여나 Kafka 서버가 다운될 경우, kafkaTemplate.send() 메소드를 호출 자체를 막는게 좋은데, 마땅한 조건이 없을 경우 다음과 같은 함수를 만들어 검증해보자.

 

public boolean verifyConnection() throws ExecutionException, InterruptedException {
        Properties props = new Properties();
        props.put("bootstrap.servers", bootstrap);
        props.put("request.timeout.ms", 3000);
        props.put("connections.max.idle.ms", 5000);

        AdminClient client = AdminClient.create(props);
        Collection<Node> nodes = client.describeCluster()
            .nodes()
            .get();
        return nodes != null && nodes.size() > 0;
    }

 

또한, try catch절로 예외처리를 할 수도 있다.

    public boolean verifyConnection(String message, MessageType type) {
        
        Properties props = new Properties();
        props.put("bootstrap.servers", bootstrap);
        props.put("request.timeout.ms", 500);
        props.put("connections.max.idle.ms", 900);

        AdminClient client = AdminClient.create(props);
        try {
            client.describeCluster()
                .nodes()
                .get();
        } catch (InterruptedException | ExecutionException e) {
            
            return false;
        }
        return true;
    }

 

이 방법이 마음에 안들면 정석은 아니지만 Sockek 객체로 해당 포트가 살아있는지 확인하는 방법도 있다.

Socket socket = new Socket();
        String[] bootstrapAddress = bootstrapServers.split(":");
        try {
            socket.connect(new InetSocketAddress(bootstrapAddress[0],
                Integer.parseInt(bootstrapAddress[1])));
            socket.close();
            return true;
        } catch (Exception e) {
            log.error("[Kafka Server is not available] message = {}", e.getMessage());
            return false;
        }
728x90
반응형

'Kafka' 카테고리의 다른 글

Kafka | SASL(PLAINTEXT) | 세팅하기 with Docker Compose  (0) 2024.02.23
728x90
반응형

회사에서 API 게이트웨이 서버를 만들게 되었다.

검색을 해도 중구난방 잘 안되어있어서 경험하면서 기본적인 틀을 기록하고자 한다.

 

1. build.gradle에 추가해준다.

ext {
    set('snippetsDir', file("build/generated-snippets"))
    set('springCloudVersion', "2022.0.4")
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}

dependencies {

	...
    
    implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'org.postgresql:postgresql'
    runtimeOnly 'org.postgresql:r2dbc-postgresql'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.projectreactor:reactor-test'
}

 

2. application-properties.yml 파일 수정

server:
  port: 8070

spring:
  jackson:
    timezone: Asia/Seoul
  data:
    r2dbc:
      repositories:
        enabled: true
  datasource:
    url: r2dbc:postgresql://localhost:5433/postgres
    username: postgres
    password: postgres
    driver-class-name: org.postgresql.Driver
  r2dbc:
    url: r2dbc:postgresql://localhost:5433/postgres
    username: postgres
    password: postgres
  cloud:
    gateway:
      default-filters: # Gateway 공통 필터
        - name: GlobalFilter
          args:
            baseMessage: hello world
      routes:
        - id: router-1
          uri: http://localhost:3000
          predicates:
            - Path=/**

 

비동기 서버를 위한 r2dbc를 사용해야 한다.

 

3. GlobalFilter 생성

@Slf4j
@Component
public class GlobalFilter extends AbstractGatewayFilterFactory<FilterDto> {

    public GlobalFilter() {
        super(FilterDto.class);
    }

    @Override
    public GatewayFilter apply(FilterDto dto) {
        return (exchange, chain) -> {
            log.info("GlobalFilter baseMessage: {}", dto.getMessage());
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                log.info("GlobalFilter End: {}", exchange.getResponse());
            }));
        };
    }
}

 

4. FilterDto 생성

@Getter
public class FilterDto {

    private String message;
}

 

5. ApiRoute Entity 생성

@Entity
@Getter
@Table(name = "api_route")
@TableGenerator(name = "api_route", allocationSize = 1)
public class ApiRoute {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO, generator = "api_route_seq")
    @SequenceGenerator(name = "api_route_seq", sequenceName = "api_route_seq", allocationSize = 1)

    @Column(name = "id")
    private String id;

    @Column
    private String routeIdentifier;
    @Column
    private String uri;
    @Column
    private String method;
    @Column
    private String path;
}

 

6. ApiRouteRepository 생성

@Repository
public interface ApiRouteRepository extends R2dbcRepository<ApiRoute, String> {

}

 

7. ApiRouteService 생성

Service Interface는 아래의 Override 한 메소드만 추가해주면 된다.

@Service
@RequiredArgsConstructor
public class ApiRouteServiceImpl implements ApiRouteService {

    private final ApiRouteRepository apiRouteRepository;

    @Override
    public Flux<ApiRoute> getAll() {
        return this.apiRouteRepository.findAll();
    }

    public Mono<ApiRoute> create(ApiRoute apiRoute) {
        return this.apiRouteRepository.save(apiRoute);
    }

    public Mono<ApiRoute> getById(String id) {
        return this.apiRouteRepository.findById(id);
    }
}

 

8. ApiPathRouteLocatorImpl 생성

@AllArgsConstructor
public class ApiPathRouteLocatorImpl implements RouteLocator {

    private final ApiRouteService apiRouteService;
    private final RouteLocatorBuilder routeLocatorBuilder;

    @Override
    public Flux<Route> getRoutes() {
        RouteLocatorBuilder.Builder routesBuilder = routeLocatorBuilder.routes();
        return apiRouteService.getAll()
            .map(apiRoute -> routesBuilder.route(String.valueOf(apiRoute.getRouteIdentifier()),
                predicateSpec -> setPredicateSpec(apiRoute, predicateSpec)))
            .collectList()
            .flatMapMany(builders -> routesBuilder.build()
                .getRoutes());
    }

    private Buildable<Route> setPredicateSpec(ApiRoute apiRoute, PredicateSpec predicateSpec) {
        BooleanSpec booleanSpec = predicateSpec.path(apiRoute.getPath());
        if (!StringUtils.isEmpty(apiRoute.getMethod())) {
            booleanSpec.and()
                .method(apiRoute.getMethod());
        }
        return booleanSpec.uri(apiRoute.getUri());
    }

    @Override
    public Flux<Route> getRoutesByMetadata(Map<String, Object> metadata) {
        return RouteLocator.super.getRoutesByMetadata(metadata);
    }
}

 

9. GatewayConfig 생성

@Configuration
@Slf4j
public class GatewayConfig {

    @Bean
    public RouteLocator routeLocator(ApiRouteService routeService,
        RouteLocatorBuilder routeLocationBuilder) {
        return new ApiPathRouteLocatorImpl(routeService, routeLocationBuilder);
    }

}

 

여기까지가 기본적인 프록시를 위한 라우터이다.

다음 부터는 API로 route를 CRUD 하기 위한 작업이다.

 

10. ApiRouteRouter Configuration 생성

@Configuration
public class ApiRouteRouter {

    @Bean
    public RouterFunction<ServerResponse> route(ApiRouteHandler apiRouteHandler) {
        return RouterFunctions.route(POST("/routes")
                .and(accept(MediaType.APPLICATION_JSON)), apiRouteHandler::create)
            .andRoute(GET("/routes/{routeId}")
                .and(accept(MediaType.APPLICATION_JSON)), apiRouteHandler::getById)
            .andRoute(GET("/routes/refresh-routes")
                .and(accept(MediaType.APPLICATION_JSON)), apiRouteHandler::refreshRoutes);
    }
}

 

11. ApiROuteHandler 생성

@RequiredArgsConstructor
@Component
@Slf4j
public class ApiRouteHandler {

    private final ApiRouteService routeService;

    private final GatewayRoutesRefresher gatewayRoutesRefresher;

    public Mono<ServerResponse> create(ServerRequest serverRequest) {
        Mono<ApiRoute> apiRoute = serverRequest.bodyToMono(ApiRoute.class);
        return apiRoute.flatMap(route ->
            ServerResponse.status(HttpStatus.OK)
                .contentType(MediaType.APPLICATION_JSON)
                .body(routeService.create(route), ApiRoute.class));
    }

    public Mono<ServerResponse> getById(ServerRequest serverRequest) {
        log.info("serverRequest.pathVariable(\"routeId\") = {}",
            serverRequest.pathVariable("routeId"));
        final String apiId = serverRequest.pathVariable("routeId");
        Mono<ApiRoute> apiRoute = routeService.getById(apiId);
        return apiRoute.flatMap(route -> ServerResponse.ok()
                .body(fromValue(route)))
            .switchIfEmpty(ServerResponse.notFound()
                .build());
    }

    public Mono<ServerResponse> refreshRoutes(ServerRequest serverRequest) {
        gatewayRoutesRefresher.refreshRoutes();
        return ServerResponse.ok().body(BodyInserters.fromObject("Routes reloaded successfully"));
    }
}

 

12. GatewayRoutesRefresher 생성

@Component
public class GatewayRoutesRefresher implements ApplicationEventPublisherAware {

    private ApplicationEventPublisher applicationEventPublisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }

    /**
     * Refresh the routes to load from data store
     */
    public void refreshRoutes() {
        applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
    }
}

 

 


만약, domain Hostname에 따라 Proxy되는 서버의 주소를 변경하고자 하면 다음과 같은 방법을 쓸 수 있다.

@Override
    public GatewayFilter apply(HostNameFilterDto hostNameFilterdto) {

        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();

            request.getHeaders();

            String[] split = request.getURI().getHost().split("\\.");
            String siteId = split[0];

            Mono<Site> siteMono = siteService.findById(siteId);
            Site site = siteMono.share().block();

            String uri = Objects.requireNonNull(site).getConnectHost();
            int port = site.getConnectPort();
            if (port != 80) {
                uri += ":" + port;
            }

            Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
            Route newRoute = Route.async()
                .id(site.getId())
                .uri(uri)
                .predicate(serverWebExchange -> false)
                .order(Objects.requireNonNull(route).getOrder())
                .filters(route.getFilters())
                .build();
            exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, newRoute);

            return chain.filter(exchange);
        };
    }
728x90
반응형

+ Recent posts