[Spring Cloud] 12 Factors

HenryHong·2023년 4월 7일
0
post-thumbnail

12 Factor App이란?

최근 소프트웨어를 서비스 형태로 제공하는 게 일반화되면서, 웹앱 혹은 SaaS(Software As A Service)라고 부르게 되었다. Twelve-Factor app은 아래 특징을 가진 SaaS 앱을 만들기 위한 방법론이다. 사내 서비스들을 클라우드화 하고자 하기에 학습이 필요했다.
업로드중..

12 Factors 의 목적은
1. 설정 자동화를 위한 절차(declarative) 를 체계화 하여 새로운 개발자가 프로젝트에 참여하는데 드는 시간과 비용 최소화
2. OS에 따라 달라지는 부분을 명확히하고, 실행 환경 사이의 이식성을 극대화함
3. 최근 등장한 클라우드 플랫폼 배포에 적합하고, 서버와 시스템의 관리가 필요없게 됨
4. 개발 환경운영 환경의 차이를 최소화하고 민첩성을 극대화하기 위해 지속적인 배포가 가능하다.
5. 과거 scale-up/out 을 위해 직접 물리PC를 변경, 확장하는 데에 드는 비용을 절감하고, 툴, 아키텍처, 개발 방식을 크게 바꾸지 않고 확장(scale up) 할 수 있다.

12 Factor는 백엔드 영역에서 필수로 지켜야 합니다.

그 이유는 애플리케이션을 독립적으로 만들 때 충분히 검증됐고 확실한 방법이기 때문입니다. 예를들면 12 Factor 원칙대로 애플리케이션을 구성하면 자연스럽게 환경(IDC, OS 등)으로부터 독립됩니다. Cloud Native 한 애플리케이션이 되니 쿠버네티스나 AWS 어디든 올릴 수 있게 됩니다.

1. 코드베이스

코드 베이스는 VCS(Version Control System)을 사용해 변화를 추적하고 코드를 저장하는 저장소를 의미합니다.(ex Git,SVN)

이 방법론에서는 코드 베이스 - App 이 항상 1대1 관계를 맺어야합니다. 코드는 한 곳에서 개발/배포가 되어야한다는 뜻입니다.

  • 코드 베이스가 여러 개 있는 경우
    앱이 아니라 분산 시스템으로 봐야합니다. 분산 시스템의 개별 구성요소가 앱이 되며, 개별 앱이 12 Factor를 따릅니다.

  • 여러개 앱이 동일한 코드를 공유하는 경우
    12 Factor를 위반하는 것입니다. 이를 해결하려면 공유하는 코드를 라이브러리화 시키고, 해당 라이브러리를 종속성 매니저로 관리해야 합니다.

  • 앱 배포가 여러개인 경우(개발/알파/릴리즈 등)
    코드 베이스 자체는 동일하게 유지하되 Git Branch 등으로 동일한 앱을 여러개로 배포할 수 있습니다.

첫 번째 원칙인 코드 베이스에서 가장 중요한 것은 서비스 간 의존성을 낮추고 독립된 커뮤니케이션 구조를 유지하는 것입니다. 이를 만족하면 자연스럽게 독립된 배포 환경에 도달하게 됩니다. 이는 곧 팀의 작업 속도 향상을 가져오고 서비스의 성장과 속도에도 영향을 끼치게 됩니다.

2. 종속성

명시적으로 선언되고 분리된 종속성
Twelve-Factor App은 전체 시스템에 특정 패키지가 암묵적으로 존재하는 것에 절대 의존하지 않습니다! 즉 명시적으로 선언해 사용해야한다는 것이죠. 대부분 Java 프로젝트에서는 Gradle , Maven 을 이용해 의존성을 관리할 수 있습니다. Spring boot의 경우 내장 톰캣, jetty 를 임베딩해 배포까지도 가능합니다.

종속성에서 중요한 것은 바로 외부 시스템으로 독립되는 것입니다. 예를 들어 app 실행에 관련된 의존성을 package.json에 선언을 한다고 하면 npm이나 yarn을 통해서 쉽게 설치 및 실행이 가능해집니다.

예를 들어 node.js 버전의 경우 package.json 이나 .nvmrc 에 명시하여 종속성을 관리합니다.

그리고 OS, 실행환경에서 암묵적인 종속성에 탈피하기 위하여 좋은 방법은 도커 컨테이너화 등이 있겠습니다.

3. 설정

환경(environment)에 저장된 설정
애플리케이션의 설정은 배포 (스테이징, 프로덕션, 개발 환경 등) 마다 달라질 수 있는 모든 것들입니다. 설정에는 다음이 포함됩니다.

  • 데이터베이스, 메모리캐시 등 백엔드 서비스들의 리소스 핸들
  • OAuth 또는 외부 서비스 인증 정보
  • 서비스 포트 정보
  • 배포된 호스트의 정규화된 호스트 이름(canonical hostname)처럼 각 배포마다 달라지는 값

이 정보들은 코드 상에 상수 형태라도 저장되면 안되고, 코드 베이스와 WAS 설정파일(ex JNDI 설정) 에도 저장하지 않는 것을 권장합니다. 따라서 Twelve-Factor App은 설정을 환경 변수(env)에 저장합니다. Spring Cloud 를 이용하는 경우에는 Spring Cloud Config 를 사용하는 것도 방법이 될 수 있습니다.

또한 설정의 다른 측면은 그룹핑이 있습니다.
종종 애플리케이션은 명명된 그룹으로 구성하기도 합니다. 해당 그룹은 Rails의 ‘development’, ‘test’, ‘production’ environments처럼, 배포의 이름을 따서 명명됩니다. 이 방법은 깔끔하게 확장하기 어렵습니다. 응용 프로그램의 배포가 증가함에 따라, ‘staging’이라던가 ‘qa’ 같은 새로운 그룹의 이름이 필요하게 됩니다. 프로젝트가 성장함에 따라, 개발자는 자기 자신의 그룹을 추가하게 됩니다. 결과적으로 설정이 다양해지며 애플리케이션의 배포를 불안정하게 만듭니다.

4. 백엔드 서비스

백엔드 서비스는 네트워크를 통해 이용하는 모든 서비스를 의미하며, 서드파티 서비스 또한 포함됩니다. (Twelve-Factor App의 코드는 로컬 서비스와 서드파티 서비스를 구별하지 않습니다.)

  • 데이터 베이스
  • MQ(메세지 큐)
  • SMTP 서비스
  • 캐시 시스템
  • Push 서비스
  • Amazon S3
  • GoogleMaps

말 그대로 백엔드 서비스로부터 독립하는 것입니다. 각각의 다른 백엔드 서비스는 리소스입니다.

위와 같이 MySQL 데이터베이스는 하나의 리소스입니다. 애플리케이션 레이어에서 샤딩을 하는 두 개의 MySQL 데이터베이스는 두 개의 서로 다른 리소스라고 볼 수 있습니다. 12 Factor App은 이러한 백엔드 서비스를 연결된 리소스로 취급하고, 자유롭게 연결 및 분리가 가능해야하고 코드 수정 없이 전환이 가능해야합니다.

이 규약의 경우 리소스의 연결 정보를 Config로 지정해 준수가능합니다.
예를 들면 개발 단계와 배포 단계의 데이터베이스 연결 정보를 Config로 지정해 코드 수정 없이 배포 단계마다 적절한 데이터베이스에 연결하도록 할 수 있습니다.

5. 빌드, 릴리즈, 실행

12 Factor App에서는 코드 베이스를 빌드, 릴리즈, 실행 단계로 엄격히 분리합니다.
코드 변경은 빌드 단계에서만 이루어져야하며, 모든 릴리즈는 항상 유니크한 릴리즈 아이디를 가져야합니다. 또 릴리즈는 추가만 가능하며 한 번 만들어진 릴리즈는 변경될 수 없고, 이전 버전으로 롤백이 가능해야합니다.

  • 빌드
    • 빌드 단계는 코드 저장소를 빌드라는 실행 가능한 번들로 변환시키는 단계입니다. 빌드 단계에서는 커밋된 코드 중 배포 프로세스에서 지정된 버전을 사용하며, 종속성을 가져와 바이너리와 에셋들을 컴파일합니다.
    • 새로운 코드가 배포될 때 마다 개발자에 의해 시작됩니다.
  • 릴리즈
    • 릴리즈 단계에서는 빌드 단계에서 만들어진 빌드와 배포의 현재 설정을 결합합니다. 완성된 릴리즈는 빌드와 설정을 모두 포함하며 실행 환경에서 바로 실행될 수 있도록 준비됩니다. (도커를 이용하여 배포에 필요한 설정 결합)
  • 실행(런타임)
    • 실행 단계에서는 선택된 릴리즈에 대한 애플리케이션 프로세스의 집합을 시작하여, 애플리케이션을 실행 환경에서 돌아가도록 합니다.

다섯 번째 원칙을 지키게 된다면 개발 - 인프라, 개발 - 운영에 관계에서 의존성이 낮아져 결합도를 느슨하게 유지할 수 있다는 원칙입니다!

6. 프로세스

애플리케이션을 하나 혹은 여러 개의 무상태(stateless) 프로세스로 실행
12 Factor 프로세스는 무상태(stateless)이며, 아무것도 공유하지 않습니다.
유지될 필요가 있는 모든 데이터는 백엔드 서비스(ex RDB, Redis 등)에 저장되어야합니다.

12 Factor 앱에서 절대로 메모리나 디스크에 캐시 된 내용이 미래의 요청이나 작업에서도 유효할 것이라고 가정해서는 안됩니다. 각 프로세스 타입의 프로세스가 여러 개 돌아가고 있는 경우, 미래의 요청은 다른 프로세스에 의해서 처리될 가능성이 높습니다. 하나의 프로세스만 돌고 있는 경우에도 여러 요인(코드 배포, 설정 변경, 프로세스를 다른 물리적 장소에 재배치 등)에 의해서 발생하는 재실행은 보통 모든 로컬의 상태(메모리와 파일 시스템 등)를 없애버립니다.

웹 시스템 중에 Sticky Session에 의존하는 경우 위를 위반하게 됩니다. Sticky Session은 사용자의 정보를 캐싱하고 이후 같은 유저의 요청도 같은 프로세서에 전달될 것을 가정하게 되는데 세션 상태 데이터는 Memcached나 Redis처럼 유효기간을 제공하는 데이터 저장소에 저장하는 것이 적합합니다.

SaaS 어플리케이션 자체가 Scale-out 이 가능하기 때문에 각 인스턴스가 메모리와 파일을 공유할 수 없고, 공유하더라도 메모리에 저장된 내용이 다른 프로세스에 의해 처리될 수 있으므로 로컬의 상태를 없애야합니다.

7. 포트 바인딩

포트 바인딩을 사용해서 서비스를 공개함
웹앱은 웹서버 컨테이너 내부에서 실행되기도 합니다. 예를 들어, PHP 앱은 Apache HTTPD의 모듈로 실행될 수도 있고, Java 앱은 Tomcat 내부에서 실행될 수도 있습니다.

12 Factor 앱은 완전히 독립적이며 웹서버가 웹 서비스를 만들기 위해 처리하는 실행환경에 대한 런타임 인젝션에 의존하지 않습니다.
12 Factor 웹 앱은 포트를 바인딩하여 HTTP 서비스로 공개되며 그 포트로 들어오는 요청을 기다립니다.

예를 들면 마이크로서비스 A,B가 있을때, A가 B의 서비스를 사용해야하는 경우 B의 데이터베이스 접속 정보와 계정 권한을 A에게 부여해 B의 데이터베이스에 접속하는 것은 규약을 위반하는 것입니다.
이 규약을 준수하기 위해서는 A에서 B의 서비스로 HTTP 요청을 통해 원하는 데이터를 CRUD 해야합니다.

보편적으로 프론트엔드 부분에서는 브라우저를 통해 포트 바인딩된 서비스에 접근하기 때문에 대부분 지켜진다고 생각됩니다!
꼭 프론트엔드뿐만 아니라 포트 바인딩을 사용한다는 것은 하나의 앱이 다른 앱을 위한 백엔드 서비스가 될 수 있다는 것을 의미할 수 있습니다. 백엔드 앱의 URL을 사용할 앱의 설정의 리소스 핸들로 추가하는 방식으로 앱이 다른 앱을 백엔드 서비스로 사용할 수 있습니다.

8. 동시성

프로세스 모델을 통한 확장
모든 컴퓨터 프로그램은 실행되면 하나 이상의 프로세스로 표현되는데요. 웹 애플리케이션은 다양한 프로세스 실행 형태를 취합니다. 예를 들어 자바 가상 머신(JVM)은 시작될 때 큰 시스템 리소스(CPU, 메모리)를 예약하는 하나의 거대 부모 프로세스를 제공하고 내부 스레드를 통해 동시성을 관리합니다.

프로세스 모델이 가장 빛나는 방법은 수평적으로 확장될 때입니다. 이는 위에서 언급한 프로세스 원칙을 지키면서 프로세스의 동시성을 높일 수 있을 수 있습니다.

Node.js의 경우 싱글 쓰레드 기반입니다. node 명령어로 서버를 실행할 때 단 하나의 스레드만 돌아가는데 CPU 중 core 하나만 사용하게 됩니다. 이 방법은 비효율적이라서 프로세스를 수평으로 확장하는 방법으로 프로세스 매니저인 pm2를 사용합니다!

9. 폐기 가능

12 Factor App 에서는 프로세스의 시작과 종료, 배포가 빈번하기 때문에 어플리케이션의 시작/종료 시간 최소화가 중요합니다.
또한 종료에 있어서는 종료 시그널을 받고 나서 어플리케이션은 신규 요청은 받지 않고 기존 요청은 최대한 빠르게 처리한 후 종료되어야합니다. 이를 그레이스풀 셧다운(graceful shutdown)이라고 합니다.
따라서 12 Factor App의 프로세스는 간단하게 폐기 가능합니다. 즉, 프로세스는 바로 시작하거나 종료될 수 있습니다. 이러한 속성은 신축성 있는 확장과 코드나 설정의 변화를 빠르게 배포하는 것을 쉽게 하며, production 배포를 안정성 있게 해 줍니다.

long polling의 경우 연결이 끊긴 시점에서 즉시 연결을 재시도해야하고, worker 프로세스의 경우에는 현재 처리중인 작어블 작업 큐로 되돌리고 종료해야합니다.

하드웨어 에러에 의한 갑작스러운 다운에도 견고해야하며, Spring Cloud를 사용하는 경우에는 Spring Cloud Circuit Breaker 를 통해 장애에 대비할 수 있습니다.

안전한 kill 명령 참고자료
https://www.lesstif.com/system-admin/unix-linux-kill-12943674.html

kill 시그널 목록은 아래와 같습니다.

$ kill -l

1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
$ kill 123

이는 프로세스 ID가 123인 것을 안전하게 종료하라는 시그널입니다.

kill 명령어 뒤에 시그널 이름이나 숫자를 제외하고 실행하면 TERM(15번) signal 이 전송됩니다.

$ kill -9 123

유닉스의 표준상 handler를 등록할 수 없는 2개의 시그널이 있는데 바로 SIGKILL(9)과 SIGSTOP(19)이며 kill -9 명령어는 KILL signal을 보내겠다는 의미입니다.

즉 kill -9로 signal을 보내면 개발자가 구현한 종료 함수가 호출되지 않고 즉시 프로세스가 종료되어 버리므로 데이터가 유실되거나 리소스가 제대로 안 닫히는 큰 문제가 발생할 수 있습니다.

따라서 아래와 같은 방법은 사용하지 않는 것을 권합니다!

10. 개발/프로덕션 환경 일치

development, staging, production 환경을 최대한 비슷하게 유지

제일 중요한 것은 개발, 스테이징, 프로덕션에서 각 환경 차이가 없도록 하자는 것입니다. 대표적으로 시간, 담당자, 툴 세 가지가 있습니다.

  • 시간 부분은 프로젝트가 길어질수록 기억을 잘 못할 수도 있으니 간격이 짧을수록 좋습니다. 또한 이슈에 상대적으로 더 잘 대응할 수 있게 되어 집니다.
  • 개발 환경과 production 환경이 유사해야 하는 이유는 잠재적 위험 요소를 빠르게 파악할 수 있게 되기 때문입니다. 환경이 다르다면 배포 전까지 이슈를 파악하지 못하는 불상사가 있을 수 있습니다.

특히 데이터베이스, 큐잉 시스템, 캐시와 같은 백엔드 서비스는 두 환경의 일치가 가장 중요한 영역입니다.
하지만 동시에 개발자 입장에서는 프로덕션에서 사용하는 서비스보다 가벼운 환경을 구축해 개발하는 것을 원하는 경우가 많습니다. (예를 들어 prod DB는 MYSQL인데 개발자 로컬은 H2 를 사용하는 경우)
12 Factor App에서는 이것 또한 프로덕션 단계에서의 오류 가능성이 존재하므로 강력히 제한합니다.

  • https 적용 여부, 인증서 종류 -> 인증서 신뢰도
  • API 정책 -> 이중화, timeout
  • 데이터 -> format, 글자 수 제한
    위와 같이 다양한 문제들이 발생할 수 있는데 개발과 production 환경이 유사하다면 이를 빨리 발견할 수 있게 됩니다.

11. 로그

로그를 이벤트 스트림으로 취급

로그는 실행 중인 app의 동작을 확인할 수 있는 수단입니다. 서버 기반 환경에서 로그는 보통 디스크에 파일(로그 파일)로 저장됩니다. 하지만, 이것은 출력 포맷 중 하나에 불과합니다.

12 Factor App은 아웃풋 스트림의 전달이나 저장에 절대 관여하지 않습니다.

app은 로그 파일을 작성하거나, 관리하려고 해서는 안됩니다. 즉 어플리케이션 로직과 로깅이 분리가 되어야하는 것이죠. 어플리케이션 자체가 실행되지 않는 상태에서도 로깅은 정상적으로 작동해야하는겁니다. 대신, 각 프로세스는 이벤트 스트림을 버퍼링 없이 stdout에 출력합니다. 로컬 개발환경에서 작업 중인 개발자는 app의 동작을 관찰하기 원하면 각자의 터미널에 출력되는 이 스트림을 볼 수 있습니다.

앱의 이벤트 스트림은 파일로 보내지거나 터미널에서 실시간으로 보여질 수 있습니다. 가장 중요한 점은 스트림은 Azure Cloud Monitoring ,Splunk Log Observer, ELK 같은 로그 분석 시스템과 Hadoop/Hive같은 범용 데이터 보관소에 보내질 수 있다는 점입니다. 이러한 시스템은 장기간에 걸쳐 앱의 동작을 조사할 수 있는 강력함과 유연성을 가지게 됩니다.

과거의 특정 이벤트를 찾기
트렌드에 대한 거대한 규모의 그래프 (예: 분당 요청 수)
유저가 정의한 휴리스틱에 따른 알림 (예: 분당 오류 수가 임계 값을 넘는 경우 알림을 발생시킴)

12. Admin 프로세스

admin/maintenance 작업을 일회성 프로세스로 실행

프로세스 포메이션은 애플리케이션의 일반적인 기능들(예: Web request의 처리)을 처리하기 위한 프로세스들의 집합입니다. 이와는 별도로, 개발자들은 종종 일회성 관리나 유지 보수 작업이 필요합니다. 그 예는 아래와 같습니다.

데이터베이스 마이그레이션을 실행합니다. (예: Django에서 manage.py migrate, Rail에서 rake db:migrate)
임의의 코드를 실행하거나 라이브 데이터베이스에서 앱의 모델을 조사하기 위해 콘솔(REPL Shell로도 알려져 있는)을 실행합니다. 대부분의 언어에서는 인터프리터를 아무런 인자 없이 실행하거나(예: python, perl) 별도의 명령어로 실행(예: ruby의 irb, rails의 rails console)할 수 있는 REPL를 제공합니다.
애플리케이션 저장소에 커밋된 일회성 스크립트의 실행 (예: php scripts/fix_bad_records.php)

모든 프로세스 타입들에는 동일한 종속성 분리 기술이 사용되어야 합니다. 예를 들어, 루비 웹 프로세스가 bundle exec thin start 명령어를 사용한다면, 데이터베이스 마이그레이션은 bundle exec rake db:migrate를 사용해야 합니다.

마찬가지로, virtualenv를 사용하는 파이썬 프로그램은 tornado 웹 서버와

모든 manage.py admin 프로세스가 같은 virtualenv에서의 bin/python을 사용해야 합니다.

12 Factor는 별도의 설치나 구성없이 REPL shell을 제공하는 언어를 강하게 선호합니다. 이러한 점은 일회성 스크립트를 실행하기 쉽게 만들어주기 때문입니다.

profile
주니어 백엔드 개발자

0개의 댓글