AWX를 이용한 CI/CD Pipeline: Pylon

AWX를 이용한 CI/CD Pipeline: Pylon

시작하며

안녕하세요 카카오페이 SRE팀 RE파트 데이빗입니다. RE파트의 주 업무는 “for the 배포, of the 배포, by the 배포”입니다. 즉 배포가 주 업무이고 배포 프로세스 개선 및 배포 효율화를 하고 있습니다. 배포 업무를 하다 보면 안전(safe)과 속도(rapid)의 두 마리 토끼를 잡기 위해 고민을 하고 개선을 하게 되는데요. 이 글을 통해 저희 팀에서 안전과 속도 개선을 어떻게 했고, 어떤 문제들을 겪었는지 공유하여 비슷한 환경에서 겪고 계실 이슈에 도움이 되었으면 좋겠습니다.

배경

배포 방식

카카오페이에서는 개발자, 배포자를 분리하여 운영(production) 배포를 하고 있습니다.

자세한 내용은 핀테크 산업과 전자금융감독규정을 참고해 주세요.

배포 방식은 온프레미스 환경에서 크게 2가지 타입으로, 전통적인 배포 방식과 컨테이너 기반의 방식으로 구성되어 있습니다.

레거시가 되어버린 물리 서버 환경의 배포 방식
레거시가 되어버린 물리 서버 환경의 배포 방식

레거시 배포 방식의 리스크

컨테이너 기반의 배포 방식은 컨테이너 환경에 맞게 구현한 파이프라인으로 배포를 통제, 관리하게 되었습니다. 반면 물리 환경은 조직별로 구성한 Jenkins의 배포 Job으로 진행하고 있었고, 레거시 시스템이더라도 시스템이 유지될 때까지는 배포 업무에 있어서 리스크가 있는 부분(아래)은 개선할 필요가 있었습니다.

  1. 배포 시 필요한 매개변수 입력에 휴먼 에러 발생
  2. 롤백 시 불필요한 빌드 단계
  3. 변경사항 전개가 잘 안되는 점
  4. 장애 추적을 위한 배포 이력 및 버전 관리 부재

Problems → Solutions

  1. 불필요한 단계

    Problem 레거시 시스템인 물리 배포 방식의 Jenkins 배포 Job은 “빌드 -> 배포”로 구성되어 있습니다. 이런 구조는 빌드가 필요 없는 롤백을 진행할 때도 빌드 후 배포를 진행합니다.

    Solution 빌드와 배포 사이에 이미지 리포지터리를 두어 빌드 시 리포지터리에 이미 빌드한 결과물이 있는지 확인 후 있다면 바로 배포를 진행하게 하면 될 것 같습니다.

  2. 휴먼 에러

    Problem 빌드, 배포에 필요한 매개변수를 Jenkins 배포 Job 실행 시 입력하여 배포를 합니다. 서두에 말씀드린 것과 같이 배포 담당자가 직접 배포를 하고 있는데요. 아무래도 사람이 하는 일이고, 동시에 많은 모듈들을 신경 쓰다 보니 어쩔 수 없이 브랜치명 오기입 혹은 오타 등 휴먼 에러가 발생하기도 합니다.

    Solution Jenkins에서 입력하는 부분을 자동화하면 휴먼 에러를 줄일 수 있기 때문에 업무 효율화에도 도움이 될 것 같습니다.

  3. 수많은 배포 Job들

    Problem 레거시 시스템인 물리 배포 방식의 Jenkins 배포 Job의 구성은 config 파일로 구성되어 있습니다. Job 수만큼 config 파일이 존재하는데요. 많은 Job의 로직 수정이 필요할 경우 모든 config를 수정해야 합니다.

    Solution 일괄 적용 및 수정, 삭제를 위해선 자동화 시스템이 필요한데요. 공통 파이프라인을 만들어 모든 배포 Job이 일관된 루틴으로 진행할 수 있도록 하고, 프로젝트 관리를 Jenkins의 config 방식에서 코드 베이스로 변경하여 파이프라인에서 프로젝트별 코드에 따른 추가 루틴을 진행하도록 하면 될 것 같습니다.

  4. 배포 정보 관리 부재

    Problem Jenkins 배포 Job의 구성은 빌드, 배포가 하나로 구성되어 있어서 실제 빌드, 배포에 소요된 시간은 확인할 수 없습니다. 또한 배포 이력은 Jenkins의 job history로만 관리되고 있어 쉽게 원하는 데이터를 얻을 수 없습니다.

    Solution 빌드와 배포를 분리하고 각 단계에 필요한 데이터를 수집하여 의미있는 대시보드가 있으면 될 것 같습니다.

파일런 소개

문제 인식을 하고 해결책을 찾아본 결과 레거시 시스템인 물리 배포 방식에도 통제 파이프라인 적용이 필요하다고 생각했습니다. 그래서 “파일런”이라는 통제 파이프라인을 만들었습니다. 이름에서 다들 느끼셨겠지만 스타크래프트 프로토스의 그 “파일런”에서 이름을 가져왔습니다. (파일런을 통해 건물, 유닛들을 안전하게 소환, 배달을 하기 때문…)

파일런 파이프라인은 CI (jenkins)와 CD (AWX)로 구성되어 있습니다. AWX는 Ansible Tower의 OSS (Open Source Software) 버전으로 ansible을 시각적으로 관리 및 수행할 수 있게 해주는 미들웨어라고 보면 될것 같습니다. AWX를 CD로 구성한 이유는 배포될 모듈을 시각적으로 보여줌으로써 직관적이고 맡은 프로젝트 한에서만 배포가 가능하게 역할 기반 접근제어1가 가능하다는 것입니다. 이를 통해 보안을 강화하여 감사 추적에 용이하여 “금감원” 감사 대응에도 좋죠. 또한 물리 배포 방식에서 배포 툴로 ansible을 사용하고 있었기 때문에 AWX를 CD로 적용하는 것은 잘 맞아떨어지는 그림이었습니다.

그럼 물리 통제 파이프라인 파일런의 특장점을 살펴보겠습니다.

특징

  1. CI/CD 분리
    파일런 파이프라인은 Jenkins (CI) - AWX (CD)의 구조로 되어있습니다. Jenkins에는 CI만을 위한 Job들이 존재하고, AWX에는 CD만을 위한 파이프라인이 존재하게 됩니다. 이러한 구조는 CI, CD가 독립적으로 수행될 수 있으며 각 권한 정책을 다르게 가져갈 수 있습니다.

    위 구조에서 볼 수 있듯이 빌드한 결과물은 리포지터리에 보관이 되기 때문에 동일한 코드의 빌드는 미리 확인하여 Test, Build, Upload image 단계를 skip하게 됩니다. 때문에 롤백, 카나리 배포 시 리드타임을 단축시킬 수 있게 됩니다.

  2. 커스텀 기능
    통제 파이프라인의 기본 구조는 빌드, 배포이지만 여기에 각 개발팀에서 필요한 기능들을 추가할 수 있도록 커스텀 기능을 추가하였습니다. 파일런 파이프라인은 groovy script로 구현된 Jenkins Shared Library이기도 하고, 각 개발팀에서 자체 파이프라인을 만들어 쓰는 팀도 있었기 때문에 커스텀이 가능한 부분을 groovy script로 구성하도록 하였습니다. 예를 들어 Build 전, Deploy 후에 추가 action이 필요하다면 아래와 같이 작성할 수 있겠습니다.

def preBuild(){
	scp localhost:8080/hello.img targetNode:8080/welcome.img
}

def postDeploy(){
	ping targetNode
}
return this
  1. 코드 기반 빌드/배포
    기존 물리 배포 방식에서는 parameter의 추가/수정/삭제를 할 경우 전체 Job을 수정하였고, 이 과정에서 휴먼 에러도 발생하기도 했습니다. 이제 빌드, 배포에 필요한 정보들을 YAML 파일(코드)로 관리하여 휴먼 에러를 최소화하고 수정사항의 전개가 쉬워졌습니다.
build:
  test: false
  tool_name: jdk11
  script: './gradlew clean build -x test'
sonarqube: true
deploy:
  ansible: true
  1. 정적 분석 툴 연동
    카카오페이에서는 정적 분석 툴로 ‘소나큐브’를 사용하고 있습니다. 소나큐브를 사용하는 프로젝트는 추가적으로 소나큐브용 Job을 구성하여 사용하고 있습니다. 소나큐브용 Job을 따로 구성하는 이유는 기존 배포 Job이 Build, Deploy까지 통합 구성되어 있기 때문입니다. Github webhook 연동으로 소나큐브용 Job이 트리거되어 정적 분석을 진행합니다. 이와 같이 YAML 파일에 “소나큐브 사용”만 작성해 주면 파이프라인에서 알아서 배포 전 정적 분석까지 진행하게 되며, 매번 새로운 job을 만드는 번거로움에서 벗어날 수 있습니다.

AWX 전략

파일런에서 CD를 담당하는 AWX는 위에서 언급한 것과 같이 권한 제어 및 배포 파이프라인 시각화 및 구성에 용이합니다. AWX REST API를 이용하여 파일런에서 어떻게 사용하는지 간략히 공유드리겠습니다.

AWX 파이프라인 구성

배포를 나가는 모듈의 타입(배치, api, was 등)에 따라 배포에 사용하는 ansible playbook이 다르기 때문에 모듈마다 AWX 파이프라인이 다르게 구성되어야 합니다. AWX에서는 playbook을 수행하는 작은 단위인 Job Template (JT)이 있으며, Job Template을 n개로 구성하게 되면 Workflow Job Template이 됩니다. 이것을 AWX 파이프라인이라 부르겠습니다. AWX 파이프라인을 동적으로 구성하기 전에 사용할 Playbook들은 미리 Job Template으로 만들어 주는 걸 추천합니다 :)

Jenkins에서 CI를 거치고 CD AWX로 넘어오게 됩니다. 이때 해당 모듈의 이름으로 AWX 파이프라인(Workflow Job Template) 존재하는지 확인을 합니다. 있으면 해당 AWX 파이프라인을 call 하면 되지만 없을 경우 새로 만들어줍니다.

def createWorkflowTemplate (phase, workflow_template_name) {
  def url = "${awxURL.toString()}/api/v2/workflow_job_templates/"
  def inventory = getInventory(phase, "${phase}-inventory")
  def organization = getOrganization(phase, "Kakaopay Org")

  def body = ["name": "${workflow_template_name}",
              "description": "${workflow_template_name} pipeline",
              "extra-vars": "",
              "organization": "${organization.id}",
              "survey_enabled": true,
              "allow_simultaneous": false,
              "ask_variables_on_launch": true,
              "inventory": "${inventory.id}",
              "limit": "",
              "scm_branch": "",
              "ask_inventory_on_launch": false,
              "ask_scm_branch_on_launch": false,
              "ask_limit_on_launch": false
						 ]
  def json = JsonOutput.toJson(body)
  def response = sendRequest('POST', url, json)
  return response
}

이와 같이 POST call을 수행하여 빈 껍데기의 AWX 파이프라인을 만듭니다. 이제 빈 껍데기에 Job Template (playbook)들을 추가하여 원하는 AWX 파이프라인을 만들 겁니다.

def createAWXPipeline(phase) {
  def workflow_template
  def job_template
  def node_id
  def deploy_type = "{deploy_type}"

  // 준비단계. 빈껍데기 Work Flow Template 만들기
  workflow_template = createWorkflowTemplate(phase, "${phase}-${module_name}")
  // 생성한 Work Flow Template에 권한 설정하기
  setRole(phase, workflow_template.id)

  // 첫번째 Job template. 빌드 이미지 다운로드 및 환경변수 세팅
  job_template = getJobTemplate(phase, 'INTRO')
  // Job Template추가 후 return 되는 id로 꼬리에 꼬리를 물어서 쭉 연결된 파이프라인이 된다
  node_id = addJobTemplate(phase, workflow_template.id, job_template.id)

  if("${deploy_type}".toString() == "WAS"){
    // 두번째 Job template. WAS일 경우 WAS Job template 추가
    job_template = getJobTemplate(phase, 'WAS')
    node_id = addJobTemplate(phase, node_id, job_template.id, 'success_nodes')
  } else if("${deploy_type}".toString() == "BATCH"){
    // 두번째 Job template. BATCH일 경우 BATCH Job template 추가
    job_template = getJobTemplate(phase, 'BATCH')
    node_id = addJobTemplate(phase, node_id, job_template.id, 'success_nodes')
  }
  // 세번째 Job template. 배포 완료 알림
  job_template = getJobTemplate(phase, 'NOTIFY')
  node_id = addJobTemplate(phase, node_id, job_template.id, 'success_nodes')

  return true
}

def addJobTemplate(phase, node_id, id, condition) {
  def url = "${awxURL.toString()}/api/v2/workflow_job_template_nodes/${node_id}/${condition}/"
  def inventory = getInventory(phase, "${phase}-inventory")
  def body = ["unified_job_template": "${id}", "inventory":"${inventory.id}"]

  def json = JsonOutput.toJson(body)
  def response = sendRequest('POST', url, json)
  return response.id
}

AWX에서 제공하는 REST API를 이용하여 구성한 결과를 보겠습니다.

권한 제어

개발 단계에서의 배포는 개발자가 배포할 수 있습니다. 하지만 운영단계의 배포는 배포 담당자인 저희 RE 파트에서만 진행할 수 있죠. 이를 위해 개발, 운영단계별 파이프라인별로 권한이 달라야 합니다. 바로 위에서 AWX 파이프라인 구성 시 setRole() 함수를 통해 파이프라인 별로 권한을 다르게 할당해야 합니다. Workflow Job Template 또는 Job Template은 “관리자”, “읽기”, “실행” 권한을 가질 수 있는데 이를 적절히 할당하는 게 중요합니다.

def setRole(phase, workflowJobTemplateId) {
  //AWX 팀별 roles 관리
  def assignRoleUrl = "${awxURL.toString()}/api/v2/teams/${team.id}/roles/"
  //AWX에 등록되어 있는 전체 roles
  def getRoleUrl = "${awxURL.toString()}/api/v2/roles"

  //AWX에 생성된 Job, Workflow template의 role 중 Read 권한을 기본으로 할당합니다.
  response = sendRequest('GET', getRoleUrl, null)
  result = response.results.find { it.summary_fields.resource_id == "${workflowJobTemplateId}".toInteger() && it.name == "Read"}
  body = ["id": "${result.id}".toInteger()]
  json = JsonOutput.toJson(body)
  sendRequestNocontent('POST', assignRoleUrl, json)
}

기본적으로 생성된 Job, Workflow Template은 “읽기” 권한을 부여하며 배포 권한이 있는 사용자 또는 팀과 모듈이 일치하는 Workflow Template일 경우 “실행” 권한을 부여합니다.

동일한 사용자 경험 제공

새로운 환경과 배포 경험을 제공하는 것도 중요하지만 동일 사용자 경험을 제공하여 선택지를 주는 것도 좋은 방법이라 생각합니다 :) 기존 배포 방식에서는 CI, CD가 통합되어 있어 Jenkins에서 빌드, 배포 로그를 확인 가능했지만 파일런 파이프라인이 적용되어 CI, CD가 분리되어 CI의 로그는 Jenkins에서 CD의 로그는 AWX에서 확인할 수 있습니다.
여기에 개발자들의 기존과 동일한 사용자 경험을 그대로 제공하기 위해 Jenkins의 Ansible Tower 플러그인을 사용했습니다.

기존 Jenkins console log
기존 Jenkins console log

AWX에서 가져오는 Jenkins console log
AWX에서 가져오는 Jenkins console log

개선 효과

Rapid

CI, CD가 분리된 파이프라인 적용 후 카나리 배포 및 롤백 시 아래와 같은 개선 효과를 볼 수 있었습니다. 빌드 시간이 길수록 개선 효과가 잘 보이는데요. 기존 B 모듈의 카나리 소요시간 30분이 무려 40%나 줄어든 18분으로 배포 소요시간이 단축되었습니다. :)

파일런 적용 전/후 배포 소요 시간 비교
파일런 적용 전/후 배포 소요 시간 비교

종류적용 전적용 후
A 모듈 카나리6분 41초4분 50초
A 모듈 롤백3분 20초2분
B 모듈 카나리30분18분 9초
B 모듈 롤백14분8분 32초

Safe

Safe 측면으로는 기대효과를 다음과 같이 볼 수 있겠습니다.

  • 매개변수, 빌드, 배포 정보를 프로젝트의 코드로 관리하여 휴먼 에러 방지
  • CI, CD 단계에서 사용자 접근 제어
  • 통제 파이프라인에서 수집하는 배포 데이터를 통해 이상 배포 시 Alert

마치며

CLI 환경에서 진행하던 ansible 작업들은 파일런 파이프라인 적용을 하면서 알게 된 AWX로 대체될 것으로 보입니다. 프로젝트별로 playbook 관리와 접근제어 및 직관적인 UI 제공으로 정형화된 작업을 할 수 있게 해주는 AWX를 안 쓸 이유가 없죠 :) 카카오페이에서 레거시 시스템이 되어버린 물리 배포 방식에서 파이프라인 적용을 통한 개선 건을 보여드렸는데요. 현재 파일런은 정적 분석 툴 연동과 같이 취약 분석 툴(fortify scan) 연동도 적용 준비하고 있습니다. 또한 api testing 지원도 파이프라인 안에서 진행할 예정으로 이렇게 배포를 위한 플랫폼으로 자리를 잡아가는 것을 기대하고 있습니다. 긴 글 읽어 주셔서 감사합니다 ( __)

Footnotes

  1. 역할 기반 액세스 제어(Role Based Acces Control)는 사용자가 수행할 수 있는 작업을 제어하는 방법으로 각 사용자에게 “역할”을 할당하고 각 역할에 서로 다른 권한을 부여하여 이를 수행합니다.

david.guetta
david.guetta

안녕하세요 카카오페이 SRE팀 RE파트 데이빗입니다. 저는 제너럴리스트보다 스페셜리스트가 되고 싶은 한 우물만 파는 엔지니어입니다.