최근 sw를 서비스 형태로 제공하는 게 일반화되어 웹앱 or SaaS(sw as a service)라 부르게 되었다.
TF app은 아래 특징을 가진 SaaS 앱을 만들기 위한 방법론이다.
•
설정 자동화를 위한 절차(declarative)를 체계화하여 새로운 개발자가 프로젝트에 참여하는 데 드는 시간과 비용을 최소화한다.
•
OS 별 다른 부분을 명확히 하고, 실행 환경 사이의 이식성을 극대화한다.
•
클라우드 플랫폼 배포에 적합하고, 서버와 시스템의 관리가 필요없게 된다.
•
개발 환경과 운영 환경의 차이를 최소화하고 민첩성을 극대화하기 위해 지속적인 배포가 가능하다.
•
툴, 아키텍처, 개발 방식을 크게 바꾸지 않고 확장(Scale up)할 수 있다.
배경
이 문서 기여자들은 Heroku 플랫폼을 통해 방대한 앱 개발, 운영, 확장을 간접적으로 관찰했다.
시간이 지나면서 앱이 유기적으로 성장하는 부분, 앱 코드베이스에서 작업하는 개발자들 간의 협업, 시간이 지나면서 망가지는 소프트웨어 유지비용을 줄이는 법에 집중했다.
버전 관리되는 하나의 코드베이스와 다양한 배포
앱은 항상 Git 같은 버전 컨트롤 시스템으로 변화를 추적하며, 버전 추적 DB의 사본을 저장소라 부른다.
코드베이스는 단일 저장소(Subversion 같은 중앙집중식)이나 루트 커밋을 공유하는 여러 저장소(Git 같은 분산 버전 관리 시스템)일 수도 있다.
코드 베이스는 앱과 1대 1이다. 여러 개 앱이 공유한다면 TF를 위반하는 것으로 해당 코드를 라이브러리화시켜서 종속성 매니저로 관리해야 한다.
앱의 코드베이스는 한개여야 하지만 배포는 여러개일 수 있다.
배포는 실행중인 앱의 인스턴스를 가리킨다.
로컬 개발 환경에 실행되는 앱도 하나의 배포다.
명시적으로 선언되고 분리된 종속성
대부분 언어는 라이브러리 배포를 위한 패키징 시스템을 제공하고 있다.
TF App은 전체 시스템에 특정 패키지가 암묵적으로 존재하는 것에 절대 의존하지 않는다. 종속성 선언 mainifest?를 이용해 모든 종속성을 완전하고 엄격하게 선언한다. 종속성 분리 툴을 사용해 유출되지 않게 한다.
어떤 툴체인을 사용하든 종속성 선언과 분리는 항상 같이 선언되어야 한다.
명시적 종속성 선언의 장점 중 하나는 앱 개발에 참가하는 개발자가 설치를 간단하게 할 수 있다는 것이다.
언어의 런타임과 종속성 매니저만 설치하면 된다. 정해진 빌드 명령어만 입력하면 모두 설치할 수 있다.
TF App은 어떤 시스템 도구에 암시적으로 의존하지 않는다. 어떤 앱이 시스템 도구가 필요하다면 그걸 앱에 통합해야 한다.
환경(environment)에 저장된 설정
앱의 설정(config)은 배포(스테이징, 프로덕션, 개발 환경 등)마다 달라질 수 있는 모든 것이다.
•
데이터베이스, memcached 등 백엔드 서비스들의 리소스 핸들
•
Amazon S3이나 트위터 등의 외부 서비스 인증 정보
•
배포된 호스트의 정규화된 호스트 이름(canonical hostname)처럼 각 배포마다 달라지는 값
앱은 종종 config을 상수로 저장한다. 이는 TF를 위반한다. TF는 설정을 코드에서 엄격히 분리하는 것을 요구한다.
앱의 모든 설정이 분리된 것은 어떠한 인증 정보도 유출시키지 않고 코드베이스가 지금 당장 OSS가 될 수 있는지 확인하는 것이다.
“설정"의 의미는 앱의 내부 설정을 포함하지 않는다.
또 다른 접근 방식은 버전 관리 시스템에 등록되지 않은 설정 파일을 이용하는 것이다. 이는 등록된 상수보다는 매우 큰 발전이지만 모든 설정을 한 곳에서 확인하고 관리하기는 어렵다.
TF App은 설정을 환경변수(env)에 저장한다.
코드 저장소에 올라갈 가능성이 낮다. 언어나 OS에 의존하지 않는 표준이다
설정 관리의 다른 측면은 그룹핑이다. 응용 프로그램의 배포가 증가함에 따라 ‘staging’ 같은 이름이 필요하게 된다. 이는 배포가 불안정해진다.
TF App에서 환경 변수는 매우 정교한 관리이고 서로 직교한다. “environments”로 절대 그룹으로 묶이지 않지만 각 배포마다 독립적으로 관리된다.
백엔드 서비스를 연결된 리소스 취급
백엔드 서비스는 애플리케이션 정상 동작 중 네트워크를 통해 이용하는 모든 서비스다. 데이터 저장소(MySQL), 메시지 큐잉 시스템(RabbitMQ), SMTP 서비스(Postfix), 캐시 시스템(Memcached) 등이 있다.
앱은 로컬에서 관리하는 서비스 대신 서드파티에 의해 제공되고 관리되는 서비스를 이용할 수 있다. 예를 들어 SMTP 서비스, 지표 수집 서비스, 스토리지 서비스, API로 접근 가능한 소비자 서비스(Twitter,,,) 등이 있다.
TF App의 코드는 로컬 서비스와 서드파티 서비스를 구별하지 않는다. 코드 수정 없이 다른 서비스로 전환이 가능해야 한다.
각각의 다른 백엔드 서비스는 리소스다. 서로 느슨하게 결합된다.
리소스는 자유롭게 배포에 연결되거나 분리될 수 있다.
철저히 분리된 빌드와 실행 단계
코드베이스는 3단계를 거쳐 (개발용이 아닌)배포로 변환된다.
•
빌드 단계는 코드 저장소를 빌드라는 실행 가능한 번들로 변환시키는 단계다. 지정된 버전을 사용하며, 종속성을 가져와 바이너리와 에셋들을 컴파일한다.
•
릴리즈 단계에서 빌드 단계에서 만들어진 빌드와 배포의 현재 설정을 결합한다. 완성된 릴리즈는 빌드와 설정을 모두 포함하여 실행환경에서 바로 실행되도록 준비한다.
•
실행 단계(런타임)에서는 선택된 릴리즈에 대한 애플리케이션 프로세스의 집합을 시작하며, 앱을 실행 환경에서 돌아가도록 한다.
TF App은 빌드, 릴리즈, 실행 단계를 엄격히 서로 분리한다.
실행 단계에서 코드를 변경할 수 없다.
배포 도구는 일반적으로 릴리즈 관리 도구를 제공한다.
특히 주목할만한 점은 이전 릴리즈로 되돌릴 수 있는 롤백 기능이다.
모든 릴리즈는 항상 유니크한 릴리즈 아이디를 지녀야 한다. 타임스탬프나 증가하는 번호가 있다. 모든 변경은 새로운 릴리즈를 만들어야 한다.
빌드는 새로운 코드가 배포될 때 개발자에 의해 시작된다. 실행 단계는 서버가 재부팅되거나 충돌이 발생한 프로세스가 프로세스 매니저에 의해 재시작되었을 때 자동으로 실행할 수 있다.
따라서 실행 단계는 최대한 변화가 적어야 한다. 빌드 단계는 좀더 복잡해도 된다. 배포 진행중인 개발자 앞에서 에러가 발생하기 때문이다.
애플리케이션을 하나 혹은 여러 개의 stateless 프로세스로 실행
실행 환경에서 앱은 하나 이상의 프로세스로 실행된다.
가장 간단한 케이스는 stand-alone 스크립트다. 실행 환경은 개발자의 언어 런타임이 설치된 로컬 노트북이며, 프로세스는 커맨드 라인 명령어에 의해 실행된다.
복잡한 케이스는 많은 프로세스 타입별로 여러개의 프로세스가 사용되는 복잡한 앱이 있다.
TF 프로세스는 stateless이며, 아무것도 공유하지 않는다. 유지될 필요 있는 모든 데이터는 DB 같은 안정된 백엔드 서비스에 저장되어야 한다.
짧은 단일 트랜잭션 내에서 캐시로 프로세스의 메모리 공간이나 파일시스템을 사용해도 된다.
TF 앱에서 절대로 메모리나 디스크에 캐시된 내용이 미래의 요청/작업에 유효할 거라 가정해서는 안된다.
프로세스가 여러개 동작한다면 미래의 요청은 다른 프로세스가 처리할 가능성이 높다. 하나만 동작하는 경우에도 여러 요인에 의한 재실행은 보통 로컬 상태(메모리와 파일 시스템 등)를 없애버린다.
에셋 패키징 도구는 컴파일된 애셋을 저장할 캐시로 파일 시스템을 사용한다. TF App은 이러한 컴파일을 런타임에 진행하기보단 빌드 단계에 수행한다.
웹 시스템 중에서는 Sticky Session 에 의존하는 것도 있다. 이는 유저의 세션 데이터를 앱의 프로세스 메모리에 캐싱하고, 같은 유저의 이후 요청도 같은 프로세스로 전달될 것을 가정하는 것이다. 이는 TF에 위반되며 절대 사용하거나 의존하면 안된다.
세션 상태 데이터는 Memcached나 Redix처럼 유효기간을 제공하는 데이터 저장소에 저장하는 것이 적합하다.
포트 바인딩을 사용해서 서비스를 공개함
웹앱은 웹 서버 컨테이너 내부에서 실행되기도 한다.
TF App은 완전히 독립적이며 웹서버가 웹 서비스를 만들기 위해 처리하는 실행환경에 대한 런타임 인젝션에 의존하지 않는다. TF App은 포트를 바인딩하여 HTTP 서비스로 공개되며 그 포트로 들어오는 요청을 기다린다.
로컬 개발 환경에서는 http://localhost:5000 과 같은 주소를 통해 개발자가 애플리케이션 서비스에 접근할 수 있다. 배포에서는 라우팅 레이어가 외부에 공개된 호스트명으로 들어온 요청을 포트에 바인딩된 웹 프로세스에 전달한다.
이는 종속성 선언에 웹 서버 라이브러리를 추가함으로써 구현된다. 예를 들어 자바의 Jetty가 있다. 유저 스페이스, 즉 앱의 코드 내에서 처리된다. 실행환경과의 규약은 요청을 처리하기 위해 포트를 바인딩하는 것이다.
포트 바인딩에 의해 공개되는 서비스는 HTTP 뿐만이 아니다. 거의 모든 종류의 SW는 포트를 바인딩하고 요청이 들어오길 기다리는 프로세스를 통해 실행될 수 있다.
포트 바인딩을 사용하는 것은 하나의 앱이 다른 앱을 위한 백엔드 서비스가 될 수 있다는 것을 의미한다. 백엔드 앱의 URL을 사용할 앱의 설정의 리소스 핸들로 추가하는 방식으로 앱이 다른 앱을 백엔드 서비스로 이용할 수 있다.
프로세스 모델을 사용한 확장
모든 컴퓨터 프로그램은 실행되면 하나 이상의 프로세스로 표현된다. 웹 앱은 다양한 형태다. 예를 들면, PHP 프로세스는 아파치의 자식 프로세스로 실행된다. 자바 프로세스는 반대 방향이다. JVM은 시작할 때 큰 시스템 리소스(CPU와 메모리) 블록을 예약하는 하나의 거대한 부모 프로세스를 제공하고, 내부 쓰레드를 통해 동시성을 관리한다. 두 경우 모두 실행되는 프로세스는 앱 개발자에게 최소한으로 노출된다.
TF App에서 프로세스들은 일급 시민이다. 서비스 데몬들을 실행하기 위한 유닉스 프로세스 모델에서 큰 힌트를 얻었다. 이 모델을 사용하면 개발자는 앱의 작업을 적절한 프로세스 타입에 할당함으로서 다양한 작업 부하를 처리할 수 있도록 설게할 수 있다. 예를 들어 HTTP 요청은 웹 프로세스가 처리하고 오래 걸리는 백그라운드 작업은 worker 프로세스가 처리하게 한다.
이는 런타임 VM 내부의 쓰레드나 EventMachine, Node.js 에서 구성된 것처럼 async/evented 모델처럼 개별 프로세스가 내부적으로 동시에 처리하는 것을 금지하는 것은 아니다. 하지만 개별 VM이 너무 커질 수 있다.(수직 확장) 앱은 여러 물리 머신에서 돌아가는 여러 프로세스로 넓게 퍼질 수 있어야 한다.
프로세스 모델이 진정 빛나는 것은 수평 확장이다. 아무것도 공유하지 않고, 수평으로 분할하는 TF App 프로세스의 성질은 동시성을 높이는 것이 간단하고 안정적임을 의미한다. 프로세스 타입과 배치를 프로세스 포메이션이라 한다.
TF App 프로세스는 절대 데몬화해서는 안되며 PID 파일을 작성해서는 안된다. 대신 OS 프로세스 관리자나 클라우드 플랫폼의 분산 프로세스 매니저, Foreman 같은 툴에 의존하며 아웃풋 스트림을 관리하고, 충돌이 발생한 프로세스에 대응하고 재시작과 종료를 처리해야 한다.
빠른 시작과 그레이스풀 셧다운(graceful shutdown)을 통한 안정성 극대화
TF App의 프로세스는 간단히 폐기 가능하다. 즉, 프로세스는 바로 시작하거나 종료할 수 있다. 이 속성은 신축성 있는 확장과 코드나 설정의 변화를 빠르게 배포하는 것을 쉽게 하며, production 배포를 안정성 있게 해준다.
프로세스는 시작 시간을 최소화해야 한다. 짧은 실행 시간은 릴리즈 작업과 확장이 민첩하게 이뤄질 수 있게 한다. 프로세스 매니저가 더 쉽게 프로세스를 옮길 수 있어 안정성도 높아진다.
프로세스 매니저로부터 SIGTERM 신호를 받으면 그레이스풀 셧다운을 한다. 서비스 포트의 수신을 중지하고, 현재 처리 중인 요청이 끝나길 기다린 뒤에 프로세스가 종료되게 한다. 이 모델은 암묵적으로 HTTP 요청이 짧다(몇초)는 가정을 깔고 있다. long polling의 경우에는 클라가 연결이 끊긴 시점에 바로 다시 연결을 시도해야 한다.
worker 프로세스의 경우 그레이스풀 셧다운은 현재 처리중인 작업을 작업 큐로 되돌리는 방법으로 구현된다. 이 모델은 암묵적으로 모든 작업은 재입력 가능(reentrant)하다고 가정한다. 이는 보통, 결과를 트랜잭션으로 감싸거나 요청을 멱등(idempotent)하게 함으로써 구현될 수 있다.
프로세스는 하드웨어 에러에 의한 갑작스런 죽음에도 견고해야 한다. 이 대책으로 Beanstalkd 같은 큐잉 백엔드를 사용하는 것을 권장한다. 클라이언트가 접속이 끊기거나, 타임아웃이 발생했을 때, 작업을 큐로 되돌린다. TF App은 예기치 않은 종료도 처리할 수 있도록 설계된다.
개발, 스테이징, 프로덕션 환경을 최대한 비슷하게 유지
역사적으로 개발환경은 프로덕션 환경과 큰 차이가 있었다.
•
시간의 차이 : 개발자가 작업한 코드가 프로덕션에 반영되기에 몇개월이 걸릴 수도 있다.
•
담당자의 차이 : 개발자가 작성한 코드를 시스템 엔지니어가 배포한다.
•
툴의 차이 : 프로덕션 배포는 아파치, MySQL, 리눅스를 사용하는데, 개발자는 Nginx, SQLite, OS X를 사용할 수 있다.
TF App은 개발 환경과 프로덕션 환경의 차이를 작게 유지하면서 지속적인 배포가 가능하도록 디자인되었다.
•
시간 차이 최소화
•
담당자 차이 최소화 : 코드 작성자가 배포와 프로덕션 모니터링에 깊게 관여한다.
•
툴의 차이 최소화
DB, 큐잉 시스템, 캐시와 같은 백엔드 서비스는 dev/prod 일치가 중요한 영역 중 하나다.
프로덕션 환경에서는 더 강력한 백엔드가 사용됨에도 개발자는 로컬에서 가벼운 것을 쓰는 데 매력을 느낄 수 있다.
TF 개발자는 dev와 prod에서 다른 백엔드를 쓰고 싶은 충동에 저항한다. 어댑터가 차이를 추상화해준다고 해도 약간의 불일치가 prod에서 오류를 일으킬 수 있기 때문이다. 이 오류는 지속적인 배포를 방해한다. 앱의 생명 주기 전체를 보았을 때 이러한 방해와 지속적인 배포의 둔화가 발생시키는 손해는 엄청나게 크다.
가벼운 로컬 서비스는 필수가 아니다.
어댑터는 여전히 유용하다. 하지만 모든 앱의 배포들은 같은 종류와 버전의 백엔드를 사용해야 한다.
로그를 이벤트 스트림으로 취급
로그는 실행중인 App의 동작을 확인할 수 있는 수단이다. 서버 기반 환경에서는 보통 디스크에 파일로 저장된다.
모든 실행중인 프로세스와 백그라운드 서비스와 아웃풋 스트림으로부터 수집된 이벤트가 시간 순서로 정렬된 스트림이다.
로그는 고정된 시작과 끝이 있는 게 아니라 app 실행 동안 계속 흐르는 흐름이다.
TF App은 아웃풋 스트림의 전달이나 저장에 절대 관여하지 않는다. 로그 파일 작성/관리하려 하지 않는다.
각 프로세스는 이벤트 스트림을 버퍼링 없이 stdout 에 출력한다.
스테이징이나 prod 배포에서 각 프로세스의 스트림은 실행 환경에 의해 수집된다. 앱에서 접근 불가지만 대신 완벽히 관리된다. 이를 위해 OSS 로그 라우터를 사용할 수 있다.(Logplex)
앱의 이벤트 스트림은 파일로 보내지거나 터미널에서 실시간으로 보여질 수 있다. 가장 중요한 점은 스트림은 Splunk 같은 로그 분석 시스템과 Hadoop/Hive 같은 범용 데이터 보관소에 보낼 수 있다는 점이다. 이러한 시스템은 장기간의 앱 동작을 조사하는 강력함과 유연성을 가지게 된다.
•
과거 특정 이벤트 찾기
•
트렌드에 의한 거대한 규모의 그래프(분당 요청수)
•
유저가 정의한 휴리스틱에 따른 알림(분당 오류수가 임계값을 넘을 때 알림)
admin/maintenance 작업을 일회성 프로세스로 실행
프로세스 포메이션은 앱의 일반적인 기능(Web req 처리)을 처리하기 위한 프로세스들의 집합이다. 이와 별도로 종종 일회성 관리나 유지 보수 작업이 필요하다.
•
데이터베이스 마이그레이션을 실행한다.
•
임의의 코드를 실행하거나 라이브 디비에서 앱의 모델을 조사하기 위해 콘솔을 실행한다. 대부분은 인터프리터를 실행하거나 REPL로 할 수 있다.
•
앱 저장소에 커밋된 일회성 스크립트의 실행
일회성 admin 프로세스는 앱의 일반적인 오래 실행되는 프로세스들과 동일한 환경에서 실행되어야 한다.
릴리즈 기반으로 실행되며, 해당 릴리즈를 기반으로 돌아가는 모든 프로세스처럼 같은 코드베이스와 설정을 사용해야 한다. admin 코드는 동기화 문제를 피하기 위해 앱 코드와 함께 배포되어야 한다.
TF는 별도의 설치나 구성없이 REPL shell을 제공하는 언어를 강하게 선호한다. 이는 일회성 스크립트를 실행하기 쉽게 만들어주기 때문이다.
로컬배포에서 개발자는 앱을 체크아웃한 디렉터리에서 일회성 admin 프로세스를 shell 명령어로 바로 실행시킨다. prod 배포에서 개발자는 ssh나 배포의 실행 환경에서 제공하는 다른 원격 명령어 실행 메커니즘을 사용하여 admin 프로세스를 실행할 수 있다.