최근 소프트웨어를 서비스 형태로 제공하는 게 일반화되면서, 웹앱 혹은 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 어디든 올릴 수 있게 됩니다.
코드 베이스는 VCS(Version Control System)을 사용해 변화를 추적하고 코드를 저장하는 저장소를 의미합니다.(ex Git,SVN)
이 방법론에서는 코드 베이스 - App 이 항상 1대1 관계를 맺어야합니다. 코드는 한 곳에서 개발/배포가 되어야한다는 뜻입니다.
코드 베이스가 여러 개 있는 경우
앱이 아니라 분산 시스템으로 봐야합니다. 분산 시스템의 개별 구성요소가 앱이 되며, 개별 앱이 12 Factor를 따릅니다.
여러개 앱이 동일한 코드를 공유하는 경우
12 Factor를 위반하는 것입니다. 이를 해결하려면 공유하는 코드를 라이브러리화 시키고, 해당 라이브러리를 종속성 매니저로 관리해야 합니다.
앱 배포가 여러개인 경우(개발/알파/릴리즈 등)
코드 베이스 자체는 동일하게 유지하되 Git Branch 등으로 동일한 앱을 여러개로 배포할 수 있습니다.
첫 번째 원칙인 코드 베이스에서 가장 중요한 것은 서비스 간 의존성을 낮추고 독립된 커뮤니케이션 구조를 유지하는 것입니다. 이를 만족하면 자연스럽게 독립된 배포 환경에 도달하게 됩니다. 이는 곧 팀의 작업 속도 향상을 가져오고 서비스의 성장과 속도에도 영향을 끼치게 됩니다.
명시적으로 선언되고 분리된 종속성
Twelve-Factor App은 전체 시스템에 특정 패키지가 암묵적으로 존재하는 것에 절대 의존하지 않습니다! 즉 명시적으로 선언해 사용해야한다는 것이죠. 대부분 Java 프로젝트에서는 Gradle , Maven 을 이용해 의존성을 관리할 수 있습니다. Spring boot의 경우 내장 톰캣, jetty 를 임베딩해 배포까지도 가능합니다.
종속성에서 중요한 것은 바로 외부 시스템으로 독립되는 것입니다. 예를 들어 app 실행에 관련된 의존성을 package.json에 선언을 한다고 하면 npm이나 yarn을 통해서 쉽게 설치 및 실행이 가능해집니다.
예를 들어 node.js 버전의 경우 package.json 이나 .nvmrc 에 명시하여 종속성을 관리합니다.
그리고 OS, 실행환경에서 암묵적인 종속성에 탈피하기 위하여 좋은 방법은 도커 컨테이너화 등이 있겠습니다.
환경(environment)에 저장된 설정
애플리케이션의 설정은 배포 (스테이징, 프로덕션, 개발 환경 등) 마다 달라질 수 있는 모든 것들입니다. 설정에는 다음이 포함됩니다.
이 정보들은 코드 상에 상수 형태라도 저장되면 안되고, 코드 베이스와 WAS 설정파일(ex JNDI 설정) 에도 저장하지 않는 것을 권장합니다. 따라서 Twelve-Factor App은 설정을 환경 변수(env)에 저장합니다. Spring Cloud 를 이용하는 경우에는 Spring Cloud Config 를 사용하는 것도 방법이 될 수 있습니다.
또한 설정의 다른 측면은 그룹핑이 있습니다.
종종 애플리케이션은 명명된 그룹으로 구성하기도 합니다. 해당 그룹은 Rails의 ‘development’, ‘test’, ‘production’ environments처럼, 배포의 이름을 따서 명명됩니다. 이 방법은 깔끔하게 확장하기 어렵습니다. 응용 프로그램의 배포가 증가함에 따라, ‘staging’이라던가 ‘qa’ 같은 새로운 그룹의 이름이 필요하게 됩니다. 프로젝트가 성장함에 따라, 개발자는 자기 자신의 그룹을 추가하게 됩니다. 결과적으로 설정이 다양해지며 애플리케이션의 배포를 불안정하게 만듭니다.
백엔드 서비스는 네트워크를 통해 이용하는 모든 서비스를 의미하며, 서드파티 서비스 또한 포함됩니다. (Twelve-Factor App의 코드는 로컬 서비스와 서드파티 서비스를 구별하지 않습니다.)
말 그대로 백엔드 서비스로부터 독립하는 것입니다. 각각의 다른 백엔드 서비스는 리소스입니다.
위와 같이 MySQL 데이터베이스는 하나의 리소스입니다. 애플리케이션 레이어에서 샤딩을 하는 두 개의 MySQL 데이터베이스는 두 개의 서로 다른 리소스라고 볼 수 있습니다. 12 Factor App은 이러한 백엔드 서비스를 연결된 리소스로 취급하고, 자유롭게 연결 및 분리가 가능해야하고 코드 수정 없이 전환이 가능해야합니다.
이 규약의 경우 리소스의 연결 정보를 Config로 지정해 준수가능합니다.
예를 들면 개발 단계와 배포 단계의 데이터베이스 연결 정보를 Config로 지정해 코드 수정 없이 배포 단계마다 적절한 데이터베이스에 연결하도록 할 수 있습니다.
12 Factor App에서는 코드 베이스를 빌드, 릴리즈, 실행 단계로 엄격히 분리합니다.
코드 변경은 빌드 단계에서만 이루어져야하며, 모든 릴리즈는 항상 유니크한 릴리즈 아이디를 가져야합니다. 또 릴리즈는 추가만 가능하며 한 번 만들어진 릴리즈는 변경될 수 없고, 이전 버전으로 롤백이 가능해야합니다.
다섯 번째 원칙을 지키게 된다면 개발 - 인프라, 개발 - 운영에 관계에서 의존성이 낮아져 결합도를 느슨하게 유지할 수 있다는 원칙입니다!
애플리케이션을 하나 혹은 여러 개의 무상태(stateless) 프로세스로 실행
12 Factor 프로세스는 무상태(stateless)이며, 아무것도 공유하지 않습니다.
유지될 필요가 있는 모든 데이터는 백엔드 서비스(ex RDB, Redis 등)에 저장되어야합니다.
12 Factor 앱에서 절대로 메모리나 디스크에 캐시 된 내용이 미래의 요청이나 작업에서도 유효할 것이라고 가정해서는 안됩니다. 각 프로세스 타입의 프로세스가 여러 개 돌아가고 있는 경우, 미래의 요청은 다른 프로세스에 의해서 처리될 가능성이 높습니다. 하나의 프로세스만 돌고 있는 경우에도 여러 요인(코드 배포, 설정 변경, 프로세스를 다른 물리적 장소에 재배치 등)에 의해서 발생하는 재실행은 보통 모든 로컬의 상태(메모리와 파일 시스템 등)를 없애버립니다.
웹 시스템 중에 Sticky Session에 의존하는 경우 위를 위반하게 됩니다. Sticky Session은 사용자의 정보를 캐싱하고 이후 같은 유저의 요청도 같은 프로세서에 전달될 것을 가정하게 되는데 세션 상태 데이터는 Memcached나 Redis처럼 유효기간을 제공하는 데이터 저장소에 저장하는 것이 적합합니다.
SaaS 어플리케이션 자체가 Scale-out 이 가능하기 때문에 각 인스턴스가 메모리와 파일을 공유할 수 없고, 공유하더라도 메모리에 저장된 내용이 다른 프로세스에 의해 처리될 수 있으므로 로컬의 상태를 없애야합니다.
포트 바인딩을 사용해서 서비스를 공개함
웹앱은 웹서버 컨테이너 내부에서 실행되기도 합니다. 예를 들어, PHP 앱은 Apache HTTPD의 모듈로 실행될 수도 있고, Java 앱은 Tomcat 내부에서 실행될 수도 있습니다.
12 Factor 앱은 완전히 독립적이며 웹서버가 웹 서비스를 만들기 위해 처리하는 실행환경에 대한 런타임 인젝션에 의존하지 않습니다.
12 Factor 웹 앱은 포트를 바인딩하여 HTTP 서비스로 공개되며 그 포트로 들어오는 요청을 기다립니다.
예를 들면 마이크로서비스 A,B가 있을때, A가 B의 서비스를 사용해야하는 경우 B의 데이터베이스 접속 정보와 계정 권한을 A에게 부여해 B의 데이터베이스에 접속하는 것은 규약을 위반하는 것입니다.
이 규약을 준수하기 위해서는 A에서 B의 서비스로 HTTP 요청을 통해 원하는 데이터를 CRUD 해야합니다.
보편적으로 프론트엔드 부분에서는 브라우저를 통해 포트 바인딩된 서비스에 접근하기 때문에 대부분 지켜진다고 생각됩니다!
꼭 프론트엔드뿐만 아니라 포트 바인딩을 사용한다는 것은 하나의 앱이 다른 앱을 위한 백엔드 서비스가 될 수 있다는 것을 의미할 수 있습니다. 백엔드 앱의 URL을 사용할 앱의 설정의 리소스 핸들로 추가하는 방식으로 앱이 다른 앱을 백엔드 서비스로 사용할 수 있습니다.
프로세스 모델을 통한 확장
모든 컴퓨터 프로그램은 실행되면 하나 이상의 프로세스로 표현되는데요. 웹 애플리케이션은 다양한 프로세스 실행 형태를 취합니다. 예를 들어 자바 가상 머신(JVM)은 시작될 때 큰 시스템 리소스(CPU, 메모리)를 예약하는 하나의 거대 부모 프로세스를 제공하고 내부 스레드를 통해 동시성을 관리합니다.
프로세스 모델이 가장 빛나는 방법은 수평적으로 확장될 때입니다. 이는 위에서 언급한 프로세스 원칙을 지키면서 프로세스의 동시성을 높일 수 있을 수 있습니다.
Node.js의 경우 싱글 쓰레드 기반입니다. node 명령어로 서버를 실행할 때 단 하나의 스레드만 돌아가는데 CPU 중 core 하나만 사용하게 됩니다. 이 방법은 비효율적이라서 프로세스를 수평으로 확장하는 방법으로 프로세스 매니저인 pm2를 사용합니다!
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을 보내면 개발자가 구현한 종료 함수가 호출되지 않고 즉시 프로세스가 종료되어 버리므로 데이터가 유실되거나 리소스가 제대로 안 닫히는 큰 문제가 발생할 수 있습니다.
따라서 아래와 같은 방법은 사용하지 않는 것을 권합니다!
development, staging, production 환경을 최대한 비슷하게 유지
제일 중요한 것은 개발, 스테이징, 프로덕션에서 각 환경 차이가 없도록 하자는 것입니다. 대표적으로 시간, 담당자, 툴 세 가지가 있습니다.
특히 데이터베이스, 큐잉 시스템, 캐시와 같은 백엔드 서비스는 두 환경의 일치가 가장 중요한 영역입니다.
하지만 동시에 개발자 입장에서는 프로덕션에서 사용하는 서비스보다 가벼운 환경을 구축해 개발하는 것을 원하는 경우가 많습니다. (예를 들어 prod DB는 MYSQL인데 개발자 로컬은 H2 를 사용하는 경우)
12 Factor App에서는 이것 또한 프로덕션 단계에서의 오류 가능성이 존재하므로 강력히 제한합니다.
로그를 이벤트 스트림으로 취급
로그는 실행 중인 app의 동작을 확인할 수 있는 수단입니다. 서버 기반 환경에서 로그는 보통 디스크에 파일(로그 파일)로 저장됩니다. 하지만, 이것은 출력 포맷 중 하나에 불과합니다.
12 Factor App은 아웃풋 스트림의 전달이나 저장에 절대 관여하지 않습니다.
app은 로그 파일을 작성하거나, 관리하려고 해서는 안됩니다. 즉 어플리케이션 로직과 로깅이 분리가 되어야하는 것이죠. 어플리케이션 자체가 실행되지 않는 상태에서도 로깅은 정상적으로 작동해야하는겁니다. 대신, 각 프로세스는 이벤트 스트림을 버퍼링 없이 stdout에 출력합니다. 로컬 개발환경에서 작업 중인 개발자는 app의 동작을 관찰하기 원하면 각자의 터미널에 출력되는 이 스트림을 볼 수 있습니다.
앱의 이벤트 스트림은 파일로 보내지거나 터미널에서 실시간으로 보여질 수 있습니다. 가장 중요한 점은 스트림은 Azure Cloud Monitoring ,Splunk Log Observer, ELK 같은 로그 분석 시스템과 Hadoop/Hive같은 범용 데이터 보관소에 보내질 수 있다는 점입니다. 이러한 시스템은 장기간에 걸쳐 앱의 동작을 조사할 수 있는 강력함과 유연성을 가지게 됩니다.
과거의 특정 이벤트를 찾기
트렌드에 대한 거대한 규모의 그래프 (예: 분당 요청 수)
유저가 정의한 휴리스틱에 따른 알림 (예: 분당 오류 수가 임계 값을 넘는 경우 알림을 발생시킴)
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을 제공하는 언어를 강하게 선호합니다. 이러한 점은 일회성 스크립트를 실행하기 쉽게 만들어주기 때문입니다.