728x90
반응형

[오류 상황]

기존에 서비스를 EC2에서 사용하다가 자원에 따른 비용이 너무 비싸져서 NCloud로 옮기면서 Kafka의 브로커를 3개에서 1개로 축소하였습니다. 해당 변경사항을 반영하기 위해 각 topic에 대한 replicationFactor도 모두 1로 바꾸었는데 계속해서 아래와 같은 replicationFactor가 3으로 설정되어있다는 오류가 발생하였습니다.

[2024-07-18 06:00:55,185] INFO [Admin Manager on Broker 1]: Error processing create topic request CreatableTopic(name='__transaction_state', numPartitions=50, replicationFactor=3, assignments=[], configs=[CreateableTopicConfig(name='compression.type', value='uncompressed'), CreateableTopicConfig(name='cleanup.policy', value='compact'), CreateableTopicConfig(name='min.insync.replicas', value='2'), CreateableTopicConfig(name='segment.bytes', value='104857600'), CreateableTopicConfig(name='unclean.leader.election.enable', value='false')]) (kafka.server.ZkAdminManager) org.apache.kafka.common.errors.InvalidReplicationFactorException: Replication factor: 3 larger than available brokers: 1.

[원인 및 해결]

Kafka가 원래 JVM 기반 자바 애플리케이션이라 자바와 호환성이 좋은데, Spring @Transactional 을 사용해서 카프카 메시지 produce를 하게 되면 이 과정 자체를 트랜잭션 처리를 하게 됩니다. (저도 유저 생성 시 Kafka 메시지를 보내기 위해 DB 저장 로직과 Kafka 전송 로직을 트랜잭션 처리하였습니다)

이때 카프카 내부적으로 사용하는 토픽이 _transaction_state 이고, 이 토픽의 기본 replication_factor가 3입니다.

Kafka의 _transaction_state 토픽은 트랜잭션 기반 메시징 시스템에서 중요한 역할을 합니다. 이 토픽은 Kafka 브로커가 트랜잭션의 상태를 관리하고 유지하는 데 사용됩니다.

(default로 3으로 설정되어있기 때문에 계속해서 오류가 생겼던 것입니다)

_transaction_state 가 하는 역할

Kafka의 트랜잭션 기능을 이해하려면 _transaction_state 토픽의 역할과 작동 방식을 이해하는 것이 중요합니다.

Kafka 트랜잭션 개요

Kafka 트랜잭션은 메시지의 일관성과 원자성을 보장합니다. 이를 통해 메시지 생산자와 소비자가 특정 작업을 완전히 완료하거나 전혀 완료하지 않는 트랜잭션을 수행할 수 있습니다.

Kafka의 _transaction_state 토픽은 트랜잭션 상태를 관리하고 유지하는 핵심 구성 요소입니다. 이 토픽은 트랜잭션의 상태를 저장하고, 복구 과정을 지원하며, 트랜잭션의 일관성을 보장합니다. 이를 통해 Kafka는 높은 신뢰성과 일관성을 요구하는 애플리케이션에서도 안전하게 사용할 수 있습니다.

_transaction_state 토픽의 역할

  1. 트랜잭션 상태 저장: _transaction_state 토픽은 각 트랜잭션의 상태 정보를 저장합니다. 여기서 의미하는 상태정보는 트랜잭션의 시작, 커밋, 중단 등의 상태가 포함됩니다. 이 정보는 Kafka 브로커가 트랜잭션을 추적하고 관리할 수 있도록 합니다.
  2. 복구: 브로커가 실패하거나 재시작될 때, _transaction_state 토픽의 데이터를 사용하여 트랜잭션 상태를 복구합니다. 이를 통해 시스템은 일관된 상태를 유지할 수 있습니다.
  3. 코디네이터: Kafka는 트랜잭션 코디네이터를 사용하여 트랜잭션을 관리합니다. 코디네이터는 _transaction_state 토픽을 사용하여 트랜잭션의 현재 상태를 기록하고, 각 트랜잭션의 상태를 유지합니다. 이는 트랜잭션의 커밋 또는 중단을 처리할 때 중요합니다.

트랜잭션 상태 전이

트랜잭션은 여러 상태를 거치며 전이합니다.

  1. Empty: 초기 상태로, 트랜잭션이 아직 시작되지 않은 상태.
  2. Ongoing: 트랜잭션이 시작되고, 메시지가 생산되고 있는 상태.
  3. PrepareCommit: 트랜잭션이 커밋 준비 중인 상태.
  4. PrepareAbort: 트랜잭션이 중단 준비 중인 상태.
  5. CompleteCommit: 트랜잭션이 성공적으로 커밋된 상태.
  6. CompleteAbort: 트랜잭션이 성공적으로 중단된 상태.

[시나리오]

  1. 트랜잭션 시작:
    • 프로듀서가 트랜잭션을 시작합니다.
    • 트랜잭션 코디네이터가 _transaction_state 토픽에 트랜잭션 ID와 함께 상태를 기록합니다.
  2. 메시지 생산:
    • 프로듀서는 여러 메시지를 생성합니다.
    • 각 메시지는 트랜잭션 ID와 함께 기록됩니다.
  3. 트랜잭션 커밋:
    • 프로듀서가 커밋을 요청합니다.
    • 트랜잭션 코디네이터가 _transaction_state 토픽에 상태를 PrepareCommit으로 업데이트합니다.
    • 모든 메시지가 성공적으로 기록되면 상태를 CompleteCommit으로 변경합니다.
  4. 트랜잭션 중단:
    • 프로듀서가 중단을 요청하거나 오류가 발생합니다.
    • 트랜잭션 코디네이터가 상태를 PrepareAbort로 업데이트합니다.
    • 모든 메시지가 무효화되면 상태를 CompleteAbort으로 변경합니다.
728x90
반응형
728x90
반응형

[목표]

현재 팀원들이 개발을 진행중인 여러 개의 Application들을 Main branch에 push를 하게 되면 자동으로 쿠버네티스 서버에 배포가 되게 하도록 해야합니다.

[Tool]

  • Gitea : Git은 Gitlab보다 가벼운 Gitea(깃티)라는 소프트웨어 패키지를 사용하고 있습니다.
  • Jenkins : 빌드된 파일들을 Dockerfile을 이용해서 이미지로 만드는 등의 Pipeline CI/CD 작업 툴로 Jenkins를 사용합니다.
  • ArgoCD : ArgoCD는 쿠버네티스 배포를 도와주는 도구입니다.
  • Kubernetes : Microservice를 구현할 수 있게 컨테이너화 된 애플리케이션을 관리합니다.
  • Helm : Kubernetes 패키지 관리를 위하여 사용합니다.

[전체 과정]

Gitea와 Jenkins 연동

1. Jenkins에 Gitea관련 설정 추가

Jenkins에 Gitea를 사용할 수 있는 Plugin을 설치해야 합니다.
https://plugins.jenkins.io/gitea-checks/
https://plugins.jenkins.io/gitea/

2. Gitea 연결 (Jenkins 관리 > 시스템 설정)

  • Name : 사용할 이름을 정해주면 됩니다. gitea로 설정하였습니다.
  • Server URL : Gitea의 URL을 입력해야 합니다. (Gitea 설정이 정상적으로 되었다면 아래에 조그만 글씨로 Gitea Version 정보가 나타나게 됩니다.)
    • Credentials 설정 (Jenkins관리 > Credentials > System > Global credentials)에서 Add Credentials를 클릭합니다.
    • Username with password를 통해 설정합니다. (root / cloud1234)
      • (이미 설정하였기에 This ID is already in use 가 생기고, Unique한 값으로 설정해줍니다)
      • Credentials는 구축된 Gitea에 접근하기 위한 인증입니다. Jenkins는 기본적으로 Credentials Plugin을 제공하며, 이를 통해 인증 정보를 관리하고 사용합니다.Manage hooks : Credentials를 지정해주어야 합니다.

3. Jenkins Webhook 설정

  • Multibranch Pipeline Project로 생성해야 합니다.

(Gitea 연결은 다른 Freestyle Project 또는 Pipeline으로는 연결이 불가능합니다.)

 

    • Multibranch Pipeline을 통해 아래와 같이 설정해줍니다.
      • Gitea Server는 gitea 서버를 설정해줍니다.
      • gitea 서버를 설정하면 아래 Repository 목록이 나올 것입니다. 원하는 Repository를 선택하면 됩니다.

  • Build Configuration
    • Mode는 Jenkinsfile로 설정합니다. 이후에 Webhook은 설정한 Repository에 있는 Jenkinsfile을 실행하게 됩니다
  • Scan Multibranch Pipeline Triggers
    • Scan by webhook에서 Trigger token은 이후에 Gitea에서 Webhook 설정을 할 때 header에 담을 token과 동일하게 설정해야 하기 때문에 기억한 뒤 Gitea Webhook 설정에 사용해야합니다.
    • 4insure-msa-app으로 설정해두었습니다.

4. Gitea Webhook 설정

    • Gitea에서 Repository의 설정 > Webhooks 에서 Webhook을 추가하면 됩니다.
      (Setting이 보이지 않는다면 해당 Repository에 권한이 없기 때문입니다)
    • Target URL
      • http://{Jenkins URL}/multibranch-webhook-trigger/invoke?token={토큰 값}
      • 토큰 값은 위에 Jenkins Webhook 설정 때 만들었던 token입니다.

5. Jenkinsfile 작성

  • Jenkinsfile을 해당 Repository의 main branch에 commit & push해주면 Webhook이 동작하고 Jenkins Pipeline이 동작합니다.

 

Jenkins Pipeline 작성

1. Maven 등록 (Jenkins 관리 > Tools)

  • Jenkins가 실행되면 Jenkins에게 Build Tool를 주어야 Build 할 수 있습니다.
  • Maven를 빌드 툴로 설정하였기 때문에 아래와 같이 설치합니다.

2. Jenkins WorkFlow

import java.text.SimpleDateFormat
def dateFormat = new SimpleDateFormat("yyMMdd_HHmmss")
def TAG = dateFormat.format(new Date())
pipeline {
    agent any
    tools {
        maven "M3"
    }
    environment {
        REGISTRY_HOST = 'msa.harbor.com'
        REGISTRY_PROJECT = 'conn-test'
        GIT_OPS_REPOSITORY = 'msa-gitops'
        TAG = 'latest'
        APP = '4insure-msa-api-user'
        APP_PATH = 'UserApi'
        NAMESPACE = 'portal'
        PROFILE = 'msa' //'prod'
        ENV = 'msa'
    }
    stages {
        stage('echo test') {
            steps {
                echo "4insure-msa-api-user app"
            }
        }
        stage('Maven Build') {
            steps {
                 sh 'ls -al' // Run Maven build
                 sh 'mvn -f pom.xml clean install' // Run Maven build
            }
        }
        stage('Docker Build & Push') {
            steps {
                sh "docker login ${REGISTRY_HOST} -u admin -p cloud1234"
                sh "docker build -t ${REGISTRY_HOST}/${REGISTRY_PROJECT}/${APP}:${TAG} ."
                sh "docker push ${REGISTRY_HOST}/${REGISTRY_PROJECT}/${APP}:${TAG}"
            }
        }
        stage('Clean dummy images') {
            steps {
                sh """docker image prune --all --force --filter label=app=${APP} --filter label=io.kubernetes.pod.namespace=jenkins"""
            }
        }
        stage('Remote api updating gitops job') {
            steps {
                sh """curl -X GET -u \"admin:cloud1234\" 'http://172.10.40.243:30070/job/msa-gitops/buildWithParameters?token=MSA_GITOPS_TOKEN&TARGET=${APP}&TAG=${TAG}&ENV=${ENV}'"""
            }
        }
    }   // End of stages
} // End of pipeline

Source Build

  • Source Repository에서 Maven을 이용해 해당 애플리케이션을 Build 합니다

Docker Build & Push

  • Build된 것을 해당 Repository의 Dockerfile을 이용해서 Image를 만듭니다.
  • 만들어진 이미지를 Harbor에 Push합니다.
FROM neduekwunife/openjdk8-jre-alpine-with-fontconfig
LABEL app="4insure-msa-api-user"
ADD ./target/UserApi-3.0.jar /
WORKDIR /
ENV SPRING_PROFILES_ACTIVE=msa
ENTRYPOINT ["java", "-Dspring.profiles.active=${SPRING_PROFILES_ACTIVE}", "-Dfile.encoding=UTF8", "-Duser.timezone=Asia/Seoul", "-jar", "UserApi-3.0.jar"]

 

Clean Docker Image

  • 필요없는 docker image를 삭제합니다.
  • namespace를 제대로 설정해야 정확한 image를 타겟팅하여 삭제할 수 있습니다.

Update GitOps

  • 새롭게 만들어진 Image를 배포하기 위해 Jenkins의 GitOps Job를 호출해서 실행해야 합니다.
  • Jenkins의 원격 빌드를 이용하여 빌드를 유발합니다.

3. Update GitOps

    • 새로운 Item 생성을 통해 Job을 생성합니다. 이번에는 Pipeline을 선택합니다.
    • 매개변수가 있기 때문에 매개변수를 등록하여 사용할 수 있도록 합니다.
      • Jenkins의 매개변수 설정 중 Trim the string 을 해주어야 합니다.
    • 소스코드 관리
      • Gitea에 GitOps를 위한 새로운 Repository를 생성합니다. 이름은 msa-gitops로 하였습니다
      • 소스코드 관리 탭에서 Repository URL과 Credentials를 설정해줍니다

  • 빌드 유발
    • 빌드를 원격으로 유발을 클릭하고 Authentication Token을 등록해줍니다.
    • 해당 Token값은 위에서 마지막 Stage인 Update GitOps를 작성할 때 token={TOKEN_NAME}의 TOKEN_NAME과 동일하여야 합니다.

Shell Script 내용

  • 위에서 설정한 파라미터들이 shell에서 사용됩니다.
  • Shell은 Helm을 사용하여 Kubernetes에 배포하는 과정을 자동화하는 것을 목적으로 합니다.
  • Image tag는 현재 시간값으로 재설정됩니다. 해당 내용은 위에서 작성한 Jenkins Workflow에서 확인할 수 있습니다.
    • def TAG = dateFormat.format(new Date())
  • Image tag를 바꾼 뒤, GitOps를 하기위한 Repository에 commit, push를 진행합니다.
#!/bin/bash
MODULES=$TARGET
APP_LIST="4insure-msa-api-portal 4insure-msa-api-user 4insure-msa-app"
echo "Deploy target application: "$TARGET
echo "TAG: "$TAG
chmod 777 manifests/$TARGET.yaml
helm template -n test $TARGET charts/$TARGET -f charts/$TARGET/values.yaml \
     --set image.tag=${TAG} > manifests/$TARGET.yaml
echo "TAG changed: "$TAG
# git add/commit/push
git add .
git commit -m "updating manifest file completed."
git status
git push origin HEAD:main

4. ArgoCD

ArgoCD는 GitOps Repository를 바라보고 있습니다.

해당 GitOps Job에 의해 변화된 이미지가 Repository에 push 되었으므로 ArgoCD가 이를 감지하여 Sync를 맞추어 쿠버네티스에 새로운 이미지를 사용하여 자동으로 배포합니다.

 

Helm 설정

Helm Chart

Helm은 쿠버네티스 애플리케이션을 관리하기 위한 패키지 매니저입니다.
npm 패키지 매니저의 package.json이 있듯이 Helm에는 Chart를 통해서 쿠버네티스의 리소스를 설치하는데 필요한 모든 정보를 포함하고 있습니다.
helm chart에는 templates폴더, values.yaml파일이 포함되어 있는데, 차트의 설정 값이 있는 values.yaml파일을 통하여 templates파일들을 렌더링하여 쿠버네티스의 리소스 파일을 생성합니다.
values.yaml파일의 설정값을 사용자가 재정의하여 커스터마이징을 할 수 있습니다.

Gitea Helm Chart Install

# gitea 공식 Helm Repo Add 
helm repo add gitea-charts <https://dl.gitea.com/charts/> 
# Offline Mode 
helm fetch gitea-charts/gitea 
# install 
helm install -n gitea gitea . -f values.yaml
helm repo add {사용자 임의 정의 Repo 이름} {추가할 Helm repository URL}
helm fetch {다운로드 받을 Chart} # 다운로드 받은 파일은 .tgz확장자를 가진 압축파일

Gitea의 공식 Repository를 Helm에 추가하여 Helm을 통해 Gitea 차트를 설치하거나 업데이트할 수 있도록 합니다.

Directory 구성

  • charts
    • templates
      • 쿠버네티스 리소스 yaml 파일들이 위치합니다.
        (Deployment.yaml, Service.yaml을 Values.yaml의 변수를 통해서 작성합니다.)
  • manifests
    • 변경된 yaml 파일이 저장됩니다.
    • ArgoCD는 manifests에 변환된 yaml파일을 감지하여 Sync를 맞추어 자동으로 배포합니다.
  • scripts
    • GitOps Job에서 ArgoCD를 동작시키기 위하여 Helm의 Image를 변경하기 위해 실행되는 script입니다
  • Chart.yaml
    • chart에 대한 메타데이터가 저장되어있습니다.
728x90
반응형
728x90
반응형

Worker Node를 잠시 내루거나, 업데이트가 필요할 때

Node 내부의 Software를 업그레이드 해야하거나, 혹은 문제가 생겼을 경우에 해당 Node를 안전하게 내리고 다시 올려야 합니다.

그럼 문제가 되는 부분은 해당 Node에서 실행되고 있던 Pod들입니다. 만약 아무도 모르는 어떠한 오류로 인하여 Worker Node-1이 내려간다고 가정하였을 때, 사용자 입장에서 해당 Pod에 접속하다가 오류를 만나게 될 것입니다.

위의 그림에서처럼 만약 사용자가 파란색 Pod에 접속하여 서비스를 이용하고 있었을때, Worker Node-1번이 이유모를 오류로 내려갔다고 가정해봅시다.

그럼 사용자는 LoadBalancer와 같은 경로를 통해 Worker Node-2번에 배포되어 있는 파란색 Pod로 접속하면 다시 서비스가 가능합니다.

이렇게 ReplicaSet이 여러 개이고, 여러 Node에 흩어져 있는 경우는 문제가 되지 않습니다.

문제가 되는 경우는 아래와 같습니다.

  1. 사용자가 초록색 Pod에 접속하다가 Worker Node-1번이 내려간 경우
  2. 사용자가 빨간색 Pod에 접속하다가 Worker Node-3번이 내려간 경우

이런 경우에는 사용자는 기존에 이용하던 서비스를 이용할 수 없게 됩니다.

쿠버네티스는 어떻게 하고 있을까?

쿠버네티스는 Worker-Node가 다시 올라오길 기다립니다.

내려갔었던 Node가 바로 다시 돌아오면 사실 문제가 없습니다. 근데 이 '바로' 는 과연 몇 초, 몇 분일까요?

이건 kube-controller-manager 가 정할 수 있으며 default는 5분입니다.

해당 내용은 아래와 같은 명령어로 수정할 수 있습니다.

즉, node가 다운될때마다 master node는 5분동안 기다립니다. 해당 시간을 Pod Eviction Timeout이라고 합니다.

만약 Pod Eviction Timeout이 지난 후에 node가 돌아오면 node는 공백이 된 상태로 돌아오게 됩니다. (안에 아무 Pod가 없는 상태)

이렇게 되면 문제가 될 수 있기 때문에 명령어를 통해 미리 방지할 수 있게 도와줍니다.

drain , uncordon , cordon

drain - worker node에 있는 Pod들을 안전하게 다른 Node들로 이동

drain 명령어는 worker node에 있는 Pod들을 안전하게 다른 Node들로 이동시켜줍니다.

만약 node-1이 문제가 생길 것 같다던가 모든 Pod들을 옮기고 update를 진행해야 하는 경우에 아래와 같은 명령어를 사용합니다

그래서 모든 작업이 다른 Node로 이동할 수 있게 합니다.

즉, drain 명령어를 실행하면 해당 node-1에서 pod 들이 정상적으로 종료되고, 다른 Node에서 Node-1에서 실행되던 Pod들이 재현되게 됩니다ㅏ.

restriction을 끝내지 않는 이상 위의 명령어를 입력하게 되면 node-1에서는 scheduling에서 제외되어서 pod 일정을 잡을 수 없게 됩니다.

그럼 다시 node-1이 정상적으로 돌아오더라도 node-1은 scheduling에서 제외됩니다.

다시 node-1이 일을 하게 하려면 uncordon 이라는 명령어를 사용하여야 합니다.

uncordon - Worker Node Restriction 해제

위의 명령어를 사용하면 node-1이 다시 Pod Schedule이 가능하게 됩니다.

물론 그렇다고 해서 이동한 Pod들이 다시 돌아오는 것은 아닙니다. 다른 Node에서 Pod는 그대로 실행되고, 이후에 Pod가 삭제되거나 새로 생성될 때 Schedule 될 수 있습니다.

그럼 만약 Node-1에 있던 Pod들을 이동시키지 않고, 해당 Node-1이 더이상 Schedule하지 않도록 하려면 cordon 명령어를 사용합니다.

cordon - 기존 node에서 pod를 종료하거나 이동시키지 않고 싶을 때

cordon은 drain과 달리 기존 node에서 pod를 종료하거나 이동시키지 않습니다.

단순히 해당 node에 새 pod가 scheduling되지 않도록 하는 명령어 입니다.

728x90
반응형
728x90
반응형

[Spring Security를 사용하기 위해 알아야하는 흐름]

 

스프링 Security는 Spring의 어떤 Layer에서 동작할까요?

해당 질문에 답하기 위해서는 필터와 인터셉터를 구분하고, 필터가 어떤 layer의 어떤 순서로 동작하는지 알고 있어야 합니다.

먼저, 스프링부트가 스프링에 비해 가지는 강점은 Tomcat(WAS 서버)가 내장되어 있기 때문에 WAS 서버를 별도로 설치할 필요가 없다는 것입니다.
(빌드의 결과물인 ~.jar 는 WAS 서버를 포함한 것이고, war 를 사용한다면 Tomcat서버를 따로 설치하여 실행해야 합니다.)

스프링부트가 WAS 서버까지 포함하고 있기 때문에 클라이언트의 Request가 왔을 때 어떤 순서로 서버가 해당 요청을 처리해주는지 흐름을 이해해야 합니다.

간략히 말하면, 클라이언트의 Request가 오면 서블릿 컨테이너를 거치고 스프링 컨테이너를 거치게 됩니다.

여기에 필터와 인터셉터를 덧붙여보면 클라이언트의 Request가 오면 서블릿 컨테이너를 거치고, 서블릿 필터를 거치고 스프링 컨테이너를 거치고 인터셉터를 거치게 됩니다.

여기에 Spring Security까지 더해보겠습니다.

이제는 클라이언트 Request가 오면 서블릿 컨테이너를 거치고 서블릿 필터를 거치고 Spring Security를 거치고 스프링 컨테이너를 거치고 그 다음 인터셉터가 동작하게 됩니다.

그럼 Spring Security는 서블릿 필터를 거친 뒤, DispatcherServlet에 가기전에 동작한다라고 볼 수 있습니다.

그런데 Spring Security는 아예 별개의 layer라기 보다는 Servlet Filter의 맨 끝에 붙어서 동작합니다.

즉, Servlet Filter가 실행된 뒤 Spring Security도 Servlet Filter처럼 동작합니다.

그럼 여기서 의문이 생기는 점은 Spring Security는 어떻게 서블릿 필터를 동작하게 할 수 있을까? 하는 점입니다.

왜냐하면 스프링 컨테이너는 스프링 빈만을 관리하고, 서블릿 필터는 서블릿 컨테이너가 관리합니다.
즉, 서블릿 필터는 스프링에서 정의된 Bean을 주입해서 사용할 수 없습니다.

그래서 스프링에서는 DelegatingFilterProxy 라는 클래스가 존재하고, 이 클래스가 서블릿 필터와 Spring Security 동작의 핵심입니다.

DelegatingFilterProxy

DelegatingFilterProxy는 스프링은 자체적으로 서블릿의 Bean을 관리할 수 없기 때문에 서블릿 필터를 호출할 수 있는 DelegatingFilterProxy 를 호출한다고 이해하면 됩니다.

서블릿 컨테이너는 서블릿과 필터를 관리하며, 서블릿과 필터의 라이프사이클을 관리합니다. 따라서 스프링이 서블릿 필터를 호출하려면 서블릿 컨테이너를 통해 이를 수행해야 한다. DelegatingFilterProxy는 이를 가능하게 해주는 역할을 합니다.

이름그대로 해석하면 Filter Proxy를 위임한다 즉 가짜 서블릿 필터를 스프링에게 위임한다(제가 이해한..) 는 뜻인 것 같습니다.

헷갈릴 수 있는 부분

그런데 만약 스프링에서 Servlet Filter의 기능을 동작하고 싶으면 Filter를 생성하여서 WebConfig에 FilterRegistrationBean 와 @WebFilter 를 사용하는 방법도 있는데, 이것과는 다른 것입니다.
이 방법은 스프링과 직접적인 관련이 없고, 서블릿 API를 사용하는 방식입니다.
(그리고 보통 스프링을 사용하면 서블릿 필터보다는 스프링 인터셉터를 사용하여 처리하는 것이 더 많은 옵션을 제공하여 장점을 가집니다.)

FilterRegistrationBean 을 사용하는 방법은 서블릿 필터 API를 사용하여 필터를 등록하는 방법이고 DelegatingFilterProxy 는 스프링 컨테이너의 관리를 받고, Spring Security와 같이 사용됩니다.

FilterChainProxy

FilterChainProxy는 서블릿 필터로 등록되어 있고, DelegatingFilterProxy의 핵심 클래스입니다.
DelegatingFilterProxy 로 부터 요청 위임을 받고 실제 보안 요소들을 처리하는 역할입니다.

FilterChainProxy는 스프링에 빈으로 등록되어 있지만, Spring Security에서 서블릿 필터 마지막에 붙어서 서블릿 필터처럼 동작할 수 있습니다.

그럼 FilterChainProxydoFilter() 를 실행하게 됩니다.

FilterChainProxy는 해당 요청을 처리할 SecurityFilterChain 을 찾습니다.

일치하는 SecurityFilterChain 의 Security Filter를 순서대로 거쳐서 인증 및 인가처리를 하게 됩니다.

여기서 Security Filter에 해당하는 다양한 Filter들이 있습니다.
필요한 Filter만 적용할 수 있으며, Filter들의 순서를 조정할 수 있습니다. 여기서 로그인, 회원가입, JWT 토큰 생성 등을 할 때 보았던 UsernamePasswordAuthenticationFilter 가 있는 것입니다.

Filter에는 다음과 같은 종류가 있습니다.

SecurityContextPersistenceFilter

SecurityContextRepository에서 SecurityContext를 가져와 유저의 Authentication에 접근할 수 있도록 하는 Filter

LogoutFilter

로그아웃 요청을 처리하는 Filter

UsernamePasswordAuthenticationFilter

ID와 PW를 사용하는 Form기반 유저 인증을 처리하는 Filter

DefaultLoginPageGeneratingFilter

커스텀 로그인 페이지를 지정하지 않았을 경우, 기본 로그인 페이지를 반환하는 Filter

AnonymousAuthenticationFilter

이 Filter가 호출되는 시점까지 사용자가 인증되지 않았다면, 익명 사용자 토큰을 반환하는 Filter

ExceptionTranslationFilter

Filter Chain 내에서 발생된 모든 예외를 처리하는 Filter

FilterSecurityInterceptor

권한 부여와 관련된 결정을 AccessDecisionManager에게 위임해 권한 부여 및 접근 제어를 처리하는 Filter

RequestCacheAwareFilter

로그인 성공 후, 이전 요청 정보를 재구성 하기위해 사용하는 Filter

SessionManagementFilter

로그인 이후 인증된 사용자인지 확인하거나, 설정된 세션의 메커니즘에 따라 작업을 수행하는 Filter

BasicAuthenticationFilter

HTTP 요청의 인증 헤더를 처리하여 결과를 SecurityContextHolder에 저장하는 Filter

RememberMeAuthenticationFilter

세션이 사라지거나 만료 되더라도 쿠키 또는 DB를 사용하여 저장된 토큰 기반으로 인증을 처리하는 Filter

용어 정리

  • Authentication : 해당 사용자가 본인인지 확인하는 절차 => 인증
  • Authorization : 특정 페이지나 자원에 접근 가능한지 결정하는 요소 => 인가
  • Principal : 인증 대상 => 아이디
  • Credential : 인증하기 위해 필요한 정보 => 비밀번호
  • details : 인증 부가 정보

정리

그러니까 원래 서블릿 컨테이너 -> 서블릿 필터 -> Spring Security -> 스프링 컨테이너 -> 인터셉터 .. 이런식으로 동작하게 되는데, Spring Bean 중에 FilterChainProxy는 Spring Security의 필터들을 서블릿 필터 끝단에 붙게 해서 서블릿 필터처럼 동작하게 한다는 것입니다.

FilterChainProxy도 서블릿 필터로 등록되어 있으며, 해당 필터의 doFilter() 메서드가 호출되면 스프링 시큐리티 필터 체인이 실행되는 것입니다.

따라서, 스프링 시큐리티는 서블릿 필터가 동작한 다음에 적용되며, 이를 가능하게 한 것이 FilterChainProxy라고 생각할 수 있습니다.

클라이언트가 Request를 보내게 되면 서블릿의 필터를 거친 뒤, DelegatingFilterProxy를 호출하게 됩니다.

DelegatingFilterProxy는 핵심 클래스인 FilterChainProxy를 호출하게 된다. FilterChainProxy는 스프링에 빈으로 등록되어 있지만, Spring Security에서 서블릿 필터 마지막에 붙어서 서블릿 필터처럼 동작할 수 있습니다.

그럼 FilterChainProxy는 doFilter() 를 실행하게 됩니다.

FilterChainProxy는 해당 요청을 처리할 SecurityFilterChain 을 찾습니다.

일치하는 SecurityFilterChain 의 Security Filter를 순서대로 거쳐서 인증 및 인가처리를 하게 됩니다.

728x90
반응형
728x90
반응형

현재 상황

  1. spring boot의 각 서비스들을 jar 빌드, Docker 빌드, 서버 배포 하는데 시간이 너무 오래걸림
  2. 마이크로서비스를 쿠버네티스 클러스터 서버에 배포해야하는데 각각의 서비스 수정하고 배포하는데 너무 오래 걸림
  3. git action으로 자동화를 하려고 했지만 디버깅하기가 어려워서 로컬에 동일한 환경을 만들어서 테스트하려고 함.

해결 방법

진행하려는 프로젝트에서 springboot 서비스들 중, 로컬로 실행할 수 있는 것들을 로컬에서 테스트하려고 함

→ 그러기 위해서 springboot의 설정 파일들을 분리해야 함.

예) application.yml , application-local.yml , bootstrap.yml , bootstrap-local.yml

  1. 로컬에서 실행할 파일 설정은 application-local.yml , bootstrap-local.yml 을 적용
  2. 서버에 배포할 파일 설정은 application.yml , bootstrap.yml 로 둠

Intellij에서 설정하기

Intellij에서 spring boot를 run하면 console 제일 상단에 찍히는 로그가 설정 파일 등을 담은 명령어이다.

기본으로 설정하면 default를 잡기 때문에 application.yml 설정 파일을 가지고 실행한다.

해당 사항을 바꾸려면 2가지 방법이 있다.(물론 결국에 같다)

  1. edit configuration에 들어가서 active profiles에 적용할 yml 입력하기
    1. 예를 들어, application-local.yml을 적용하고 싶으면 local만 입력하면 된다.
  2. edit configuration에 들어가서 modify option > Add VM options에서

-Dspring.profiles.active=local 이린식으로 적어주기

시행착오

설정을 local 설정으로 바꿔서 실행했는데, 계속 application.yml에 있는 내용이 적용되는 경우

이 부분에 대해서 오늘 꼬박 고생을 했는데.. 계속해서 application.yml에 있는 내용이 적용되어서 config server의 문제인지.. api-gateway의 문제인지 고민했는데 결국은 스프링 기본을 몰랐던 것이였다.

스프링은 application.yml과 application-local.yml이 실행되고, profile을 application-local.yml로 설정해주더라도, 공통된 내용은 application-local.yml 이 적용되지만 application-local.yml에 없는 내용은 application.yml 내용이 적용된다.

예를 들어서, application.yml과 application-local.yml이 아래와 같을 때

# application.yml
greeting:
  hi
  
  
---
# application-local.yml
greeting:
  hi2
@RestController
@RequiredArgsConstructor
public class controller {

    private final Environment env;

    @GetMapping("/greeting")
    public String get() {
        return env.getProperty("greeting");
    }
}

다음 코드를 active-profile을 local로 지정하면 hi2가 나온다.

즉, 둘 다 같은 키값이 있을 때는 local이 적용된다.

만약 두 파일이 다음과 같으면 어떻게 될까?

# application.yml
greeting:
  hi
  
  
---
# application-local.yml
greeting2:
  hi2

위의 코드를 active-profile을 local로 지정하면 hi가 나온다.

즉, application-local에 없는 greeting 이라는 키의 경우 application.yml 파일의 내용이 적용된 것을 확인할 수 있다.

따라서 **run 모드**가 **local**로 설정되면 Spring Boot는 application.yml 및 **application-local.yml**을 모두 읽어들여서, 중복되는 설정이 있다면 **application-local.yml**의 설정이 우선시된다.

일반적으로 **application.yml**은 모든 환경에서 사용되는 설정을 정의하고, **application-local.yml**은 로컬 환경에서만 필요한 설정을 정의한다. 이러한 설정은 **application-local.yml**에만 포함될 것입니다.

(이 내용을 몰라서 application.yml에 적용한게 실행될 때 같이 적용되어서 문제가 무엇인지 한참 고민했다.)

bootstrap.yml을 적용하고 싶을 때

다음과 같이 사용하면 bootstrap.yml을 application.yml보다 먼저 적용할 수 있다.

spring:
  config:
    import: classpath:/bootstrap.yml

이렇게 하면 bootstrap.yml을 먼저 적용한다. 그럼 위와 같이 만약 profiles를 local로 실행한다면 bootstrap.yml에 있는 내용과 application.yml이 겹치면 bootstrap.yml에 있는 내용이 적용되고, 없는 내용은 application.yml에 있는 키 값의 value를 찾을 수 있다.

728x90
반응형
728x90
반응형

작성한 docker-compose 파일은 다음과 같다

version: '3.1'
services:
  zookeeper:
    container_name: zookeeper
    image: wurstmeister/zookeeper
    ports:
      - "2181:2181"
  kafka:
    image: wurstmeister/kafka
    container_name: kafka
    ports:
      - "9092:9092"
    environment:
      KAFKA_ADVERTISED_HOST_NAME: kafka
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
    depends_on:
      - zookeeper

zookeeper → 2181:2181

kafka → 9092:9092

docker-compose 실행

docker-compose -f docker-compose-single-broker.yml up -d

docker-compose 종료

docker-compose -f docker-compose-single-broker.yml down

kafka container에 들어가서 /opt/kafka_2.13-2.8.1/bin 으로 들어가면 사용할 수 있는 명령어들을 확인할 수 있다.

kafka-topics.sh --create --zookeeper zookeeper:2181 --replication-factor 1 --partitions 1 --topic quickstart

해당 명령어로 토픽 만들어서 실행해봄

kafka UI Tool 사용

Kafka-UI Tool 을 이용하여 Kafka 관리하기

UI for Apache Kafka 사용

version: '2'
services:
  kafka-ui:
    image: provectuslabs/kafka-ui
    container_name: kafka-ui
    ports:
      - "8989:8080"
    restart: always
    environment:
      - KAFKA_CLUSTERS_0_NAME=localhost
      - KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS=kafka:9092
      - KAFKA_CLUSTERS_0_ZOOKEEPER=zookeeper:2181
 

GitHub - provectus/kafka-ui: Open-Source Web UI for Apache Kafka Management

Open-Source Web UI for Apache Kafka Management. Contribute to provectus/kafka-ui development by creating an account on GitHub.

github.com

현재 접속 가능한 URL

http://172.10.40.152:8989/

(이 주소는 현재 회사 VPN으로 연결된 서버라서 본인 우분투 서버의 public ip에 8989 포트를 이용해서 열면된다.)

[Brokers 화면]

[Topics 화면]

UI로도 만들 수 있는데, 실제 컨테이너에 들어가서 명령어를 이용해서 topic을 생성할 수도 있다.

[topic 생성해보기]

docker exec -it kafka /bin/bash

cd /opt/kafka_2.13-2.8.1/bin

kafka-topics.sh --create --zookeeper zookeeper:2181 --replication-factor 1 --partitions 1 --topic test

Topic 생성된 것 확인 가능

[Topic으로 신호 주고받기]

test topic 듣고 있는 producer를 하나 만듬

kafka-console-producer.sh --topic test --bootstrap-server kafka:9092

그리고 hi, hello, yo-man을 차례대로 보냈다.

UI에서 해당 메시지가 온것을 확인할 수 있다. (아니면 직접 consumer를 명령어로 실행시켜서 확인할 수도 있다.)

kafka-console-consumer.sh —topic tet —from-beginning —bootstrap-server kafka:9092

카프카를 사용하는 서비스(Store Service) 배포

docker build --platform linux/amd64 -t kimtaeheon/storeservice:1.0 .

docker save -o store-service.tar kimtaeheon/storeservice:1.0

scp store-service.tar root@{harbor주소}:/root/kth

docker load -i /kth/store-service.tar

docker tag kimtaeheon/storeservice:1.0 msa.harbor.com/library/storeservice:1.0

docker push msa.harbor.com/library/storeservice:1.0
apiVersion: apps/v1
kind: Deployment
metadata:
  name: store-deployment
  labels:
    app: store
spec:
  replicas: 1
  selector:
    matchLabels:
      app: store
  template:
    metadata:
      labels:
        app: store
    spec:
      containers:
      - name: store
        image: msa.harbor.com/library/storeservice:1.0
        imagePullPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
  name: store-service
spec:
  clusterIP: 10.104.193.24
  selector:
    app: store
  ports:
    - port: 8080
      protocol: TCP
      targetPort: 8080
 

오류 1 : kafka 연결이 안되는듯..

java.net.UnknownHostException: kafka

해결 : kafka docker-compose에서 KAFKA_ADVERTISED_HOST_NAME 속성 수정

version: '3.1'
services:
  zookeeper:
    container_name: zookeeper
    image: wurstmeister/zookeeper
    ports:
      - "2181:2181"
  kafka:
    image: wurstmeister/kafka
    container_name: kafka
    ports:
      - "9092:9092"
    environment:
      KAFKA_ADVERTISED_HOST_NAME: 172.10.40.152
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
    depends_on:
      - zookeeper
728x90
반응형
728x90
반응형

이전 상태

이전까지 상태

  • Deployment
    • API Gateway
    • Discovery
    • Config
  • Service
    • NodePort : API Gateway
    • NodePort : Discovery

[이전까지 완료한 상태]

API Gateway는 30010으로 열려있고, Discovery Service는 30007번, Config Service는 30008번으로 열려있다.
마이크로서비스인 User-service API를 요청하려면 API Gateway를 통해서 Discovery Service에 등록된 User Service API를 요청해야 한다.
즉, API Gateway의 IP로 /user-service/** 요청이 들어오면 User-Service API로 요청을 보내라는 의미이므로 API Gateway가 라우팅을 통해 User-service를 요청하는지 확인해야 된다.

(그러기 위해서 User-service에 간단한 API 인 /health_check 를 만들어둔 상태)

해결 과정

1. [Discovery Service를 제외하고 API Gateway와 User-Service 사이에는 문제가 없는지 확인]

먼저, Discovery Service에 등록해주지 않고 API Gateway가 User-Service API로 라우팅을 해주는지부터 확인을 해주었다.
그래서 API Gateway에 라우팅되는 주소에 서비스 이름인 USER-SERVICE 대신에 user-service의 ip주소를 넣어주었다.

실패 : User-Service의 NodePort를 만들어서 해당 URI를 API-Gateway에 등록

User-Service의 IP를 등록하기 위해서 NodePort를 만들고, NodePort의 해당 IP를 API Gateway에 등록하였다. 예를 들어, 쿠버네티스의 IP가 1.1.1.1 이고 NodePort를 30011 번 port를 열었다면 아래와 같이 API Gateway가 라우팅 하게 해주었다. 하지만 {API-Gateway 주소}/user-service/health_check 로 보낸 요청은 실패했다.

  cloud:
    gateway:
      routes:
        - id: user-service
          uri: <http://1.1.1.1:30011>
          predicates:
            - Path=/user-service/**

[실패 이유]
NodePort의 개념을 정확히 몰라서 그랬던 것이다. NodePort는 ClusterIP를 뚫고, 해당 포트를 외부로 연결해주는 포트일 뿐이다. 따라서 외부에서 NodePort로 접속하면 ClusterIP를 거쳐 Pod로 통신이 가능하도록 해주는 것이다.
그러나 API Gateway는 외부 NodePort를 등록하면 안된다. 쿠버네티스의 통신은 내부에서 일어나므로 API Gateway에는 ClusterIP를 등록해주어야 한다.

해결 : User-Service에 Cluster IP를 만들어서 해당 URI를 API-Gateway에 등록

Service 종류를 ClusterIP로 만들어서 해당 ClusterIP의 URI를 등록해준다. 작성한 user의 Deployment와 Service는 다음과 같다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: users-deployment
  labels:
    app: users
spec:
  replicas: 1
  selector:
    matchLabels:
      app: users
  template:
    metadata:
      labels:
        app: users
    spec:
      containers:
      - name: users
        image: msa.harbor.com/library/usersservice:1.0
        imagePullPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
  name: user-service
spec:
  selector:
    app: users
  ports:
    - port: 46279
      protocol: TCP
      targetPort: 46279

이렇게 한 뒤 API Gateway IP로 User Service API에 요청한다면 요청이 성공한다.
그럼 API Gateway와 User Service 사이에는 문제가 없다는 의미이다.

2. API Gateway와 User-Service를 Discovery Service에 등록해서 확인

API Gateway가 정상적으로 마이크로서비스를 라우팅한다는 사실을 알았으니, Discovery Service에 API Gateway와 User-service를 등록한 뒤, 요청을 해보았다.
Eureka 페이지는 NodePort로 30007번으로 열었기 때문에 해당 화면에서 연결된 서비스들을 확인할 수 있었다.

USER-SERVICE의 URL에 마우스 커서를 올리면 연결된 곳이 나온다.
나는 USER-SERVICE의 애플리케이션(jar)은 0번 포트로 열었기 때문에 랜덤 포트로 연결되어 있다. 그래서 해당 User-service는 Spring Cloud Gateway에 등록해두었다.
http://{API Gateway IP 주소}/user-service/health_check
문제는 Discovery Service가 User Service가 열린 포트로 연결해서 호출해줘야하는데, 계속해서 user-service에 등록한 {spring.application.instance_id} 으로 연결되는게 문제였다.
로그를 확인해 본 결과 Discovery Service가 이상한 값인 spring.application.instance_id 이 값을 보고 있는데 이런 값은 등록한 적이 없으니 요청이 되지 않고 500 에러가 계속해서 발생했다.
예를 들어, users-deployment-65db46d67b-gtn8l 로 계속해서 연결되었다. 이 값은 쿠버네티스에서 Deployment를 만들고 뒤에 랜덤 해시값을 붙여서 배포하기 때문에 저런 이상한 값이 나오게 되는 것이다. 당연히 이 주소로 들어가면 연결이 되지 않는다.
내가 원하는 것은 http://{API Gateway}/user-service/health_check 로 호출하면 http://{user-service}/health_check 로 요청되는 것을 원한다.

3. User-Service 앞에 ClusterIP를 만들어줘서 ClusterIP → Pod

Discovery Service가 User-Service의 Pod를 등록하고 있어서 문제가 생기고 있다고 생각하여, ClusterIP를 User-Service 앞에 두어서 API Gateway가 Pod가 아닌, ClusterIP를 등록하도록 해서 이어줄려고 하였다.
(그럼 API Gateway는 User의 ClusterIP를 통해서 Pod이름이 뭐든 간에 User service의 API를 요청할 수 있을것이니까)
그런데 ClusterIP를 User-Service로 등록해두어도 ClusterIP가 등록되지 않고 계속해서 Discovery Service는 User Service의 Pod를 등록하고 있었다. 즉, ClusterIP가 사용되지 않고 있었다.

방안 생각

‘Discovery Service에 ClusterIP의 주소를 직접 등록하면 되지 않을까’ 라고 알려주셔서 해당 방법으로 해결하였다.

즉, 직접 Pod를 등록하지 않고 User-SVC(쿠버네티스 객체인 서비스를 의미, ClusterIP)를 Discovery Service에 등록해준다.

이유

해당 방법이 된다고 생각한 이유는 쿠버네티스의 마스터에서 User의 ClusterIP를 통해서 API를 Discovery Server로 curl 요청을 했을 때 성공하였다.
curl [http://{쿠버네티스 주소}:30007/eureka](<http://172.10.40.174:30007/eureka>) → 성공

해결 방법 1 : eureka.instance.homePageUrl 사용 (실패)

11. Service Discovery: Eureka Clients

 

11. Service Discovery: Eureka Clients

Service Discovery is one of the key tenets of a microservice-based architecture. Trying to hand-configure each client or some form of convention can be difficult to do and can be brittle. Eureka is the Netflix Service Discovery Server and Client. The serve

cloud.spring.io

User-Service에 eurkea.instance.homePageUrl 에 ClusterIP를 등록하면 해결 가능할 것이라 생각해여기에 등록했는데 결과적으로 잘 되지 않았다.

해결 방법 2 : prefer-ip-address=true , ip-address={ClusterIP 주소}

Eureka InstanceConfigBean에서 IpAddress 지정하는 부분만 찾아야한다.
그리고 해당 내용을 설정 파일에 등록해주면 된다.

eureka:
  instance:
  # home-page-url: <http://10.100.225.34>
    instance-id: ${spring.cloud.client.hostname}:${spring.application.instance_id:${random.value}}
    prefer-ip-address: true
    ip-address: {User-service의 Cluster IP 주소}

이렇게 변경해서 배포하면 성공한다.
http://{API gateway:NodePort}/user-service/health_check 를 하면 잘 요청이 되는 것을 확인할 수 있다.

 

ClusterIP에서 IP 고정

지금은 동적 할당이기 때문에 ClusterIP의 spec에서 ClusterIP를 고정할 수 있다.

apiVersion: v1
kind: Service
metadata:
  name: user-service
spec:
  clusterIP: 10.104.193.23

바꾸고 kubectl replace -f users-service.yml --force 적용

728x90
반응형
728x90
반응형

이런 에러가 계속해서 발생했다. 원래 tab을 누르면 자동으로 커맨드를 추천하거나 찾아야 되는데 일일이 입력하기가 너무 불편했다.

bash를 사용하고 있다면 bash-completion이 없어서 생기는 문제이다.

mac이라면 brew install bash-completion
linux라면 brew install bash-completion

그런데 이렇게 하면 리눅스 bash 자동완성이 다 해결되지는 않는다. kubectl 명령의 경우 해결되지 않는다.

리눅스에서 bash 자동 완성으을 사용하고 있는지 확인하려면 다음 커맨드를 입력해본다.

type _init_completion

이렇게 입력했을때 bash 내용이 주르륵 나오면 사용하고 있는 것이고 type 명령어를 찾을 수 없다 등의 내용이 나오면 사용하고 있지 않고 위의 오류가 계속해서 생길 수 있다.

다음 순서대로 진행하면 실행할 수 있다.
apt-get install bash-completion
exec bash
이렇게 하고 type _init_completion 입력

-> 만약 실패한다면
vi ~/.bashrc 로 들어가서 마지막 줄에 다음 내용 입력 source /usr/share/bash-completion/bash_completion

그다음 다시 type _init_completion 입력 -> 아래와 같이 나온다면 성공

728x90
반응형

+ Recent posts