12 Factor App이란?
최근 소프트웨어를 서비스 형태로 제공하는 게 일반화되면서, 웹앱 혹은 SaaS(Software As A Service)라고 부르게 되었다. Twelve-Factor app은 아래 특징을 가진 SaaS 앱을 만들기 위한 방법론이다.
- 설정 자동화를 위한 절차(declarative) 를 체계화 하여 새로운 개발자가 프로젝트에 참여하는데 드는 시간과 비용을 최소화한다.
- OS에 따라 달라지는 부분을 명확히하고, 실행 환경 사이의 이식성을 극대화 한다.
- 최근 등장한 클라우드 플랫폼 배포에 적합하고, 서버와 시스템의 관리가 필요없게 된다.
- 개발 환경과 운영 환경의 차이를 최소화하고 민첩성을 극대화하기 위해 지속적인 배포가 가능하다.
- 툴, 아키텍처, 개발 방식을 크게 바꾸지 않고 확장(scale up) 할 수 있다.
Twelve-Factor 방법론은 어떤 프로그래밍 언어로 작성된 앱에도 적용할 수 있고 백엔드 서비스(데이터베이스, 큐, 메모리 캐시 등)와 다양한 조합으로 사용할 수 있다.
12 Factor App은 말 그대로 12가지 요소입니다. 12 Factor는 백엔드 영역에서 필수로 지켜야 합니다.
그 이유는 애플리케이션을 독립적으로 만들 때 충분히 검증됐고 확실한 방법이기 때문입니다. 예를들면 12 Factor 원칙대로 애플리케이션을 구성하면 자연스럽게 환경(IDC, OS, 등)으로부터 독립됩니다. Cloud Native 한 애플리케이션이 되니 쿠버네티스나 AWS 어디든 올릴 수 있게 됩니다.
아래 글은 다음 사이트를 참고하여 작성하였습니다!
https://fe-developers.kakaoent.com/2021/211125-create-12factor-app-with-nextjs/
1. 코드베이스
버전 관리되는 하나의 코드 베이스와 다양한 배포
버전 컨트롤을 통해 코드 베이스와 앱 사이에는 항상 1대 1 관계가 성립시키는 것이 중요합니다.
- 코드 베이스가 여러 개 있는 경우, 앱이 아니라 분산 시스템으로 봐야합니다. 분산 시스템의 개별 구성요소가 앱이 되며, 개별 앱이 Twelve-Factor를 따릅니다.
- 여러개 앱이 동일한 코드를 공유한다면 Twelve-Factor를 위반하는 것입니다. 이를 해결하려면 공유하는 코드를 라이브러리화 시키고, 해당 라이브러리를 종속성 매니저로 관리해야 합니다.
첫 번째 원칙인 코드 베이스에서 가장 중요한 것은 서비스 간 의존성을 낮추고 독립된 커뮤니케이션 구조를 유지하는 것입니다. 이를 만족하면 자연스럽게 독립된 배포 환경에 도달하게 됩니다. 이는 곧 팀의 작업 속도 향상을 가져오고 서비스의 성장과 속도에도 영향을 끼치게 됩니다.
2. 종속성
명시적으로 선언되고 분리된 종속성
Twelve-Factor App은 전체 시스템에 특정 패키지가 암묵적으로 존재하는 것에 절대 의존하지 않습니다!
종속성에서 중요한 것은 바로 외부 시스템으로 독립되는 것입니다. 예를 들어 app 실행에 관련된 의존성을 package.json에 선언을 한다고 하면 npm이나 yarn을 통해서 쉽게 설치 및 실행이 가능해집니다.
암묵적으로 존재하는 것에 절대 의존하지 않는다고 했습니다. 예를 들어 node.js 버전이 있습니다.
package.json 이나 .nvmrc 에 명시하여 종속성을 관리합니다.
그리고 OS, 실행환경에서 암묵적인 종속성에 탈피하기 위하여 좋은 방법은 현재 배우고 있는 도커 컨테이너화 하는 것이 있겠습니다.
3. 설정
환경(environment)에 저장된 설정
애플리케이션의 설정은 배포 (스테이징, 프로덕션, 개발 환경 등) 마다 달라질 수 있는 모든 것들입니다. 설정에는 다음이 포함됩니다.
- 데이터베이스, memcached 등 백엔드 서비스들의 리소스 핸들
- API 정보
- 배포된 호스트의 정규화된 호스트 이름(canonical hostname)처럼 각 배포마다 달라지는 값
세 번째 원칙 설정은 환경 변수를 코드 내부에 저장하지 않고 코드에서 엄격하게 분리하는 것입니다!
Twelve-Factor App은 설정을 환경 변수(env)에 저장합니다.
환경 변수는 코드의 변경 없이 자유롭게 배포 및 변경이 가능해집니다.
또한 설정의 다른 측면은 그룹핑이 있습니다.
종종 애플리케이션은 명명된 그룹으로 구성하기도 합니다. 해당 그룹은 Rails의 ‘development’, ‘test’, ‘production’ environments처럼, 배포의 이름을 따서 명명됩니다. 이 방법은 깔끔하게 확장하기 어렵습니다. 응용 프로그램의 배포가 증가함에 따라, ‘staging’이라던가 ‘qa’ 같은 새로운 그룹의 이름이 필요하게 됩니다. 프로젝트가 성장함에 따라, 개발자는 자기 자신의 그룹을 추가하게 됩니다. 결과적으로 설정이 각 그룹의 조합으로 폭발하게 되고, 애플리케이션의 배포를 불안정하게 만듭니다.
4. 백엔드 서비스
백엔드 서비스를 연결된 리소스로 취급
백엔드 서비스는 애플리케이션 정상 동작 중 네트워크를 통해 이용하는 모든 서비스입니다. 예를 들어, 데이터 저장소(예: MySQL, CouchDB), 메시지 큐잉 시스템(예: RabbitMQ, Beanstalkd), 메일을 보내기 위한 SMTP 서비스 (예: Postfix), 캐시 시스템(예: Memcached) 등이 있습니다.
Twelve-Factor App의 코드는 로컬 서비스와 서드파티 서비스를 구별하지 않습니다.
말 그대로 백엔드 서비스로부터 독립하는 것입니다. 각각의 다른 백엔드 서비스는 리소스입니다.
위와 같이 MySQL 데이터베이스는 하나의 리소스입니다. 애플리케이션 레이어에서 샤딩을 하는 두 개의 MySQL 데이터베이스는 두 개의 서로 다른 리소스라고 볼 수 있습니다. Twelve-Factor App은 이러한 데이터베이스들을 첨부된(Attached) 리소스로 다룹니다. 이는 서로 느슨하게 결합된다는 점을 암시합니다.
5. 빌드, 릴리즈, 실행
철저하게 분리된 빌드와 실행 단계
코드 베이스는 3 단계를 거쳐 (개발용이 아닌) 배포로 변환됩니다.
- 빌드 단계는 코드 저장소를 빌드라는 실행 가능한 번들로 변환시키는 단계입니다. 빌드 단계에서는 커밋된 코드 중 배포 프로세스에서 지정된 버전을 사용하며, 종속성을 가져와 바이너리와 에셋들을 컴파일합니다.
- 릴리즈 단계에서는 빌드 단계에서 만들어진 빌드와 배포의 현재 설정을 결합합니다. 완성된 릴리즈는 빌드와 설정을 모두 포함하며 실행 환경에서 바로 실행될 수 있도록 준비됩니다. (도커를 이용하여 배포에 필요한 설정 결합)
- 실행 단계(런타임이라고도 하는)에서는 선택된 릴리즈에 대한 애플리케이션 프로세스의 집합을 시작하여, 애플리케이션을 실행 환경에서 돌아가도록 합니다.
Twelve-Factor App은 빌드, 릴리즈, 실행 단계를 엄격하게 서로 분리합니다.
다섯 번째 원칙을 지키게 된다면 개발 - 인프라, 개발 - 운영에 관계에서 의존성이 낮아져 결합도를 느슨하게 유지할 수 있다는 원칙입니다!
6. 프로세스
애플리케이션을 하나 혹은 여러 개의 무상태(stateless) 프로세스로 실행
Twelve-Factor 프로세스는 무상태(stateless)이며, 아무것도 공유하지 않습니다.
Twelve-Factor 앱에서 절대로 메모리나 디스크에 캐시 된 내용이 미래의 요청이나 작업에서도 유효할 것이라고 가정해서는 안됩니다. 각 프로세스 타입의 프로세스가 여러 개 돌아가고 있는 경우, 미래의 요청은 다른 프로세스에 의해서 처리될 가능성이 높습니다. 하나의 프로세스만 돌고 있는 경우에도 여러 요인(코드 배포, 설정 변경, 프로세스를 다른 물리적 장소에 재배치 등)에 의해서 발생하는 재실행은 보통 모든 로컬의 상태(메모리와 파일 시스템 등)를 없애버립니다.
웹 시스템 중에 Sticky Session에 의존하는 경우 위를 위반하게 됩니다. Sticky Session은 사용자의 정보를 캐싱하고 이후 같은 유저의 요청도 같은 프로세서에 전달될 것을 가정하게 되는데 세션 상태 데이터는 Memcached나 Redis처럼 유효기간을 제공하는 데이터 저장소에 저장하는 것이 적합합니다.
7. 포트 바인딩
포트 바인딩을 사용해서 서비스를 공개함
웹앱은 웹서버 컨테이너 내부에서 실행되기도 합니다. 예를 들어, PHP 앱은 Apache HTTPD의 모듈로 실행될 수도 있고, Java 앱은 Tomcat 내부에서 실행될 수도 있습니다.
Twelve-Factor 앱은 완전히 독립적이며 웹서버가 웹 서비스를 만들기 위해 처리하는 실행환경에 대한 런타임 인젝션에 의존하지 않습니다. Twelve-Factor 웹 앱은 포트를 바인딩하여 HTTP 서비스로 공개되며 그 포트로 들어오는 요청을 기다립니다.
보편적으로 프론트엔드 부분에서는 브라우저를 통해 포트 바인딩된 서비스에 접근하기 때문에 대부분 지켜진다고 생각됩니다!
꼭 프론트엔드뿐만 아니라 포트 바인딩을 사용한다는 것은 하나의 앱이 다른 앱을 위한 백엔드 서비스가 될 수 있다는 것을 의미할 수 있습니다. 백엔드 앱의 URL을 사용할 앱의 설정의 리소스 핸들로 추가하는 방식으로 앱이 다른 앱을 백엔드 서비스로 사용할 수 있습니다.
8. 동시성
프로세스 모델을 통한 확장
모든 컴퓨터 프로그램은 실행되면 하나 이상의 프로세스로 표현되는데요. 웹 애플리케이션은 다양한 프로세스 실행 형태를 취합니다. 예를 들어 자바 가상 머신(JVM)은 시작될 때 큰 시스템 리소스(CPU, 메모리)를 예약하는 하나의 거대 부모 프로세스를 제공하고 내부 스레드를 통해 동시성을 관리합니다.
프로세스 모델이 가장 빛나는 방법은 수평적으로 확장될 때입니다. 이는 위에서 언급한 프로세스 원칙을 지키면서 프로세스의 동시성을 높일 수 있을 수 있습니다.
Node.js의 경우 싱글 쓰레드 기반입니다. node 명령어로 서버를 실행할 때 단 하나의 스레드만 돌아가는데 CPU 중 core 하나만 사용하게 됩니다. 이 방법은 비효율적이라서 프로세스를 수평으로 확장하는 방법으로 프로세스 매니저인 pm2를 사용합니다!
9. 폐기 가능
빠른 시작과 그레이스풀 셧다운(graceful shutdown)을 통한 안정성 극대화
Twelve-Factor App의 프로세스는 간단하게 폐기 가능합니다. 즉, 프로세스는 바로 시작하거나 종료될 수 있습니다. 이러한 속성은 신축성 있는 확장과 코드나 설정의 변화를 빠르게 배포하는 것을 쉽게 하며, production 배포를 안정성 있게 해 줍니다.
그레이스풀 셧다운은 애플리케이션을 안전하게 종료할 수 있는 것을 뜻합니다.
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 환경을 최대한 비슷하게 유지
제일 중요한 것은 개발, 스테이징, 프로덕션에서 각 환경 차이가 없도록 하자는 것입니다. 대표적으로 시간, 담당자, 툴 세 가지가 있습니다.
전통적 애플리케이션 | Twelve-Factor App | |
배포간의 간격 | 몇 주 | 몇 시간 |
코드 작성자와 코드 배포자 | 다른 사람 | 같은 사람 |
개발 환경과 production 환경 | 불일치 | 최대한 유사함 |
시간 부분은 프로젝트가 길어질수록 기억을 잘 못할 수도 있으니 간격이 짧을수록 좋습니다. 또한 이슈에 상대적으로 더 잘 대응할 수 있게 되어 집니다.
개발 환경과 production 환경이 유사해야 하는 이유는 잠재적 위험 요소를 빠르게 파악할 수 있게 되기 때문입니다.
환경이 다르다면 배포 전까지 이슈를 파악하지 못하는 불상사가 있을 수 있습니다.
- https 적용 여부, 인증서 종류 -> 인증서 신뢰도
- API 정책 -> 이중화, timeout
- 데이터 -> format, 글자 수 제한
위와 같이 다양한 문제들이 발생할 수 있는데 개발과 production 환경이 유사하다면 이를 빨리 발견할 수 있게 됩니다.
11. 로그
로그를 이벤트 스트림으로 취급
로그는 실행 중인 app의 동작을 확인할 수 있는 수단입니다. 서버 기반 환경에서 로그는 보통 디스크에 파일(로그 파일)로 저장됩니다. 하지만, 이것은 출력 포맷 중 하나에 불과합니다.
Twelve-Factor App은 아웃풋 스트림의 전달이나 저장에 절대 관여하지 않습니다.
app은 로그 파일을 작성하거나, 관리하려고 해서는 안됩니다. 대신, 각 프로세스는 이벤트 스트림을 버퍼링 없이 stdout에 출력합니다. 로컬 개발환경에서 작업 중인 개발자는 app의 동작을 관찰하기 원하면 각자의 터미널에 출력되는 이 스트림을 볼 수 있습니다.
앱의 이벤트 스트림은 파일로 보내지거나 터미널에서 실시간으로 보여질 수 있습니다. 가장 중요한 점은 스트림은 Splunk같은 로그 분석 시스템과 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을 사용해야 합니다.
Twelve-Factor는 별도의 설치나 구성없이 REPL shell을 제공하는 언어를 강하게 선호합니다. 이러한 점은 일회성 스크립트를 실행하기 쉽게 만들어주기 때문입니다.
아마존 aws를 사용하여 Twelve Factor App을 구현할 수 있는 방법들입니다.
https://aws.amazon.com/ko/blogs/korea/twelve-factor-app-on-cloud-native/