https://www.flickr.com/photos/imholzer/14910801170/in/photolist-oHBKGN-Kmx7VN-Ke4MTs-6N9dtW-pD7T6y-ooH5L6-g6bxNK-fdw31o-fk5g8G-mp2hZe-fdB8GJ-fErBaj-eB4WKX-eACRCq-iiTBZZ-eQpLFG-eWVD1S-jepzUP-FeBhSs-8DPgZ1-4dhyna-B7QcG8-8DjuR6-8DjuS4-aCgaPg-gT1MSv-h9QRWY-9aAgeE-fFB3Ph-fFB3Pf-oB5vLz-fFB3Mf-dgUM9c-dgUK6x-ooeEx2-axuGi9-fFB3fN-67vR3h-9aAgif-9aAgGL-BWXnxg-9A9qNf-2gM2qx-8DjuQi-ozYwW4-EShFV5-2gRtxm-2Mg8GA-9aAgQW-fFjrUp
<The 12-factor app 에 관련하여 가장 잘 설명한 infographic>
https://imgur.com/gallery/V9nAWbd
 

앞의 글에서 언급 했던것 처럼, 클라우드 컴퓨팅(Cloud Computing, 이하 클라우드) 환경에서 효과적인 웹 서비스 운영을 하기 위해서는 클라우드 네이티브 어플리케이션(Cloud-native application) 형태의 구성이 필수적일 수 밖에 없다.

기존 온프레미스(On-premise)에 적합하게 설계되어 동작하는 어플리케이션 만으로는 클라우드라는 환경을 효율적으로 사용하기 어렵기 때문이다.

흔히 리-호스팅(re-hosting) 이라는 형태로, 리프트 엔 시프트(lift and shift) 전략을 반영하여 기존에 온프레미스 환경에서 동작하는 시스템 구성을 그대로 떠서(lift) 교체(shift)하는 형태로 IDC등에 위치한 인프라 구성을 클라우드 환경으로 마이그레이션(Cloud Migration)하는 방식을 많이들 시도하고 있다.

이 방식은 기존에 사용하던 어플리케이션(Application) 구조와 코드를 최소한으로 변경하여 진행하고, 기존에 알 수 없었던 시스템간의 관계를 굳이 정리하지 않고도 진행 할 수 있어서, 클라우드 마이그레이션 성공률을 높일 수 있다는 이점이 있다.

예를 들어, 기존에 WWW-WAS-DB 형태의 기본적인 3 tier 서버 설계를 가지는 서비스라면, 클라우드 환경에 세 종류의 서버 인스턴스를 생성하여, Apache-Tomcat-MySQL 과 같은 어플리케이션 서버를 설치한 후, 기존에 있던 산출물(Artifact)를 그대로 클라우드 환경에 적용하면 된다. IT 인프라 운영 관점에서는 기존 온프레미스에 있던 데이터를 그대로 클라우드 환경으로 옮기기만 하고, 시스템 간의 설정을 거기에 맞춰서 적용만 하면 되니 서비스 가동을 위해 그나마 덜 복잡한 단계를 거친다고 할 수 있다.

하지만 우리가 기존에 운영하던 서비스는 앞에서 설명한 것 보다 훨씬 복잡하며 다양한 요소가 더 많이 작용하고 있다.

온프레미스에 있는 모든 데이터를 한번에 클라우드로 옮기고 한번에 가동을 시작하는 경우라면 매우 행복한 상황이 되겠지만, 현실은 Legacy 시스템에 담겨져 있는 이력을 알 수도 없는 것들로 인해 발목이 잡히는 경우를 종종 보게 된다.

만약 그렇지 않고, Legacy 시스템에 대한 이력이 모두 남겨져 있어서 리프트 엔 시프트 형태로 기존의 모든 인프라 자원을 클라우드 환경으로 마이그레이션을 성공한다고 하더라도 결국 우리가 원하는 ‘답’ 과는 거리가 있다는 것을 실감하게 될 것이다.

우리가 원하는 답은 바로, ‘저비용의 인프라’, ‘쉽게 확장 가능하고, 트래픽 유입량에 따라 능동적으로 대처 해주는 인프라’ 이기 때문이다.

기존 온프레미스 환경에 위치해 있던 시스템 구성을 그대로 클라우드 인프라 환경으로 옮기게 되면 비용이 오히려 더 많이 발생하는 현실을 당면하게 될 수도 있다.

예를 들어, WWW 서비스로 유입되는 트래픽을 처리하기 위해 사전에 생성한 WWW 서버 10대, WAS 20대, DB 2대 구성을 24시간 유지하게 된다면 운영 비용은 기존 IDC 구성해서 사용하는 비용보다 훨씬 높을 수 밖에 없게 된다(보통 자기소유 차량과 렌터카 간의 차이 개념으로 설명하곤 한다).

그렇게 사용 할 것이 아니라, 클라우드 환경에서의 인프라 자원은 즉각적으로 시스템의 확장이 가능하므로, WWW 서버 1대, WAS 1대, DB 1대 만으로 서비스를 기본적으로만 구성하고, 트래픽 유입 상황에 따라 서버 인스턴스를 추가적으로 늘려주거나, 크기를 크게(Scale-up) 조정하는 방법으로 효율적으로 상황에 맞게 사용하기를 권장한다.

하지만 이 마저도 이미 개발된 어플리케이션의 구성 자체가 다른 시스템과 연관성이 높고, 고정되어 있는 자원을 바라보고 있다고 한다면, 확장 및 대응에 관련해서는 클라우드 환경의 이점들을 모두 사용할 수 없는 상황이 될 수 밖에 없게 된다.

그래서 나온 개념과 가이드가 The Twelve-Factor App이다.

The Twelve-Factor App 개념은 쉽게 말해, 기존의 웹서비스가 기반이 되던 환경이 바뀌었으니, 바뀐 환경에 적합하게 적용 되어야 할 부분과 지켜져야 할 부분들을 알려주는 가이드 문서라 생각하면 되겠다.

이번 글을 통하여 12-Factors App이 알려주고자 하는 부분이 무엇인지를 짚어볼까 한다.

1. Codebase

아주 당연하고 기초적인 부분이 아닐 수 없다. 모든 소스는 Git 또는 SVN 등 Source code를 관리(A.K.A. SCM: Source Code Management)하는 리파지토리(Repository) 에 기록되고 관리되어져야 하며, 동일한 소스 코드가 개발(Development)/검증(Staging)/실운영(Production) 환경에 구분되어 배포 되어져야 한다. 이때 개발되는 소스 코드의 범위는 버전 별 관리를 통해 진행되게 된다.

여기에서 주의 할 점은 하나의 어플리케이션이 여러개의 codebase를 가지지 않아야 하고, 배포 단위로 개발 프로젝트를 생성하여 개발 및 배포를 하나의 연장선 상에서 생각하고 진행해야 한다는 것이다.

다른 어플리케이션 간의 연관관계를 느슨하게(loose coupling) 하고, API를 통해 서로를 Backing services 형태로 사용하게 되면, 개별 어플리케이션을 대상으로 배포를 진행함에 있어서 상하위 버전 호환 관계를 맞춰서 개발을 진행할 수 밖에 없게 되어, 타 서비스간 의존성 없이 언제든지 배포가 가능한 상태로 유지 할 수 있게 된다.

2. Dependencies

개발을 진행함에 있어서 필요한 라이브러리나 패키지들의 정확한 버전을 명시하여, 개발 환경과 테스트 환경간 오차를 줄여주는 역할을 한다. auto-update라던지, latest version을 사용하게 될 시에는, 개발 및 빌드되는 시점에서 각기 라이브러리 버전 다운로드에 의해 각각 다른 산출물들이 생성 될 수도 있음으로, 정확한 Test가 이뤄지지 않아 장애를 유발하고, 장애탐색을 어렵게 만드는 요소가 될 수도 있다.

3. Config

서비스를 운영해 본 사람이라면 많이들 공감 할 수 있는 부분이 해당 부분 일 것이다. 특정 어플리케이션의 설정이 도대체 어디에 있는지, 어떤 설정들이 존재하는지 다 알지 못해서 서비스 운영에 있어서 차질을 겪는 경우를 종종 접하게 된다.

12-Factor App에서는 이러한 부분을 투명하게 관리 할 수 있도록, 각종 설정에 대한 부분은 특정 파일 및 상수 형태로 기록하지 않고, 시스템의 환경변수에 기록하고 조회 할 수 있도록 각별히 권고하고 있다.

여기에서, ‘특정 시스템에 접근하기 위한 ID/PW가 환경변수로 노출되는 부분에 대해서는 어떻게 처리 하느냐’ 라는 질문이 있을 수 있다. 해당 부분은 시스템에 관련한 접근 권한 관리, 격리(isolation) 등으로 1차적으로 처리가 가능하며, 좀 더 전문적인 외부도구나 서비스(Credentials store)등을 사용하여 안전하게 암호화된 크리덴셜(Credential) 정보를 저장소로부터 읽어들여 사용 할 수 있게 된다.

4. Backing services

해당 부분에 관련해서는 그나마 국내 IT산업의 이해도와 성숙도가 높아져 있다고 볼 수도 있으나, 상황에 따라 다르게 받아들여질 수도 있는 부분으로써 여기에서 짚고 넘어 갈까 한다.

예를 들어, 웹 어플리케이션 서버가 위치한 동일한 장비(로컬호스트 — Localhost)에 MySQL DB를 설치하고, 웹 어플리케이션이 로컬호스트 장비의 DB에 (DB 접근 host 설정 없이)바로 접근하여 둘 간의 의존 관계를 만들지 않도록 가이드 하는 부분이다.

이는, 각 어플리케이션에 발생하는 문제들이 동일 장비에 여파를 미치지 않게 분리하는 개념인 것과 동시에, 서비스의 확장을 위해서 라도 다른 원격지의 어플리케이션이 네트워크를 통해 해당 DB로 접근 할 수 있도록 만들어져야 한다는 것이다.

위와 같이 분리된 각각의 백엔드 서비스(Backend service)를 하나의 연결된 자원 개념으로 인식하게 되고, 각각의 백엔드 서비스는 특정 서비스 담당자에 의해 관리가 이루어지며, 서비스간의 호환관계를 고려하여 운영/유지 되도록 한다.

여기에서 중요한 부분은, 내가 현재 개발중인 어플리케이션도 백엔드 서비스가 될 수 있으며, 해당 서비스의 개발 진행에 따라 변경되는 기능의 상/하위 호환성을 고려하여 개발을 진행해야 한다는것이다. (예. API v1, v2 방식의 여러 버전의 공존 고려)

5. Build, release, run

개발이 완료된 소스 코드를 지정된 버전으로 빌드를 진행하고, 빌드가 완료된 산출물에 환경별 설정을 적용(ready to release)하여 바로 가동(run)할 수 있도록 관리하는 방식을 말한다. 이렇게 되면 동일한 산출물을 각기 다른 여러 환경별로 적합하게 설정하여 사용 할 수 있게 된다.

예를 들어, 동일한 aaa 컨테이너가 개발/검증/실운영 환경에 각각 배포 될 때, 환경별 각기 다른 설정을 포함하여, 개발 환경에서는 db01.api.dev.nexclipper.io 라는 DB를 바라보고, 검증 환경 에서는 db01.api.stg.nexclipper.io, 실운영환경 에서는 db.api.nexclipper.io 라는 DB를 바라보게 지정하여 사용할 수 있게 된다.

이때 사용 할 수 있는 방법으로 예를 들면, dev-docker-compose.yml 파일을 생성하고, 파일에 다음과 같은 설정을 입력한 후,

was:
   image: example/aaa:0.2
   environment:
      - MYSQL_DB="db01.api.dev.nexclipper.io"

docker-compose를 이용하여 설정이 미리 정의된 해당 세트(set)를 통해 컨테이너를 가동 시킬 수 있게 된다.

docker-compose -f dev-docker-compose.yml up -d

이때, 빌드가 완료된 산출물은 각 단계에서 추가적인 수정이 불가하며, 문제가 있을 시 운영자 또는 시스템에 의해 롤백(Rollback) 처리하여 이전 버전으로 돌아가게 한다. 소스코드에 대한 추가 수정이 필요 할 경우, 개발 단계로 돌아가서 개발자에 의해 수정을 진행하고 다시 빌드 과정을 통해 각 단계별로 산출물이 전달 될 수 있도록 단방향 업무 프로세스와 정책을 확립하는 부분도 필요하다.

이렇게 관리가 이루어질 경우, 검증(Staging)/실운영(Production) 환경에는 개발자의 접근이 불필요해짐으로, 계정에 대한 관리와 보안적인 부분, 변경관리에 대한 모든 기록을 제어/관리 할 수 있게 된다.

위와 같은 업무 프로세스에 관련해서는 차후 CI/CD pipeline 구축에 대한 내용으 조금 더 자세하고 전문적으로 다루도록 하겠다.

이상으로 The 12-factor app 항목에 관련하여 절반 정도를 다뤄봤으며, 나머지 6~12에 관련해서는 다음번 글을 통해 계속 이어 나갈 예정이다.