애저 DevOps에서 CI/CD 파이프라인을 구성하다보면 보통 반복적인 작업들이 많습니다. 이게 태스크 Tasks 수준일 수도 있고, 작업 Jobs 수준일 수도 있고, 스테이지 Stages 수준일 수도 있는데, 코딩을 할 때는 반복적인 부분을 리팩토링 한다지만, 파이프라인에서 반복적인 부분을 리팩토링할 수는 없을까요? 물론 있습니다. 그것도 파이프라인을 리팩토링할 수 있는 포인트가 최소 여섯 군데 정도 있습니다. 이 포스트에서는 애저 파이프라인의 YAML 형식 템플릿을 이용해서 반복적으로 나타나는 부분을 리팩토링하는 방법에 대해 알아보겠습니다.

이 포스트에 쓰인 예제 파이프라인 코드를 이 리포지토리에서 확인해 보세요!

빌드 파이프라인

우선 일반적인 빌드 파이프라인을 한 번 만들어 보겠습니다. 아래는 그냥 빌드 Stage를 작성한 것입니다. Stages/Stage 아래 Jobs/Job 아래 Steps/Task가 들어가 있습니다. Greeting 이라는 변수값을 출력시키는 파이프라인입니다 (line #18, 25)

# pipeline.yaml
...
stages:
...
- stage: BuildWithoutTemplate
displayName: 'Build without Template'
jobs:
- job: HostedVs2017
displayName: 'Hosted VS2017'
pool:
name: 'Hosted VS2017'
workspace:
clean: all
variables:
- name: Greeting
value: 'Hello World'
steps:
- task: PowerShell@2
displayName: 'Echo Greeting in PowerShell'
inputs:
targetType: Inline
script: 'Write-Host "$(Greeting)"'
...

이 파이프라인을 실행시키면 아래와 같은 결과가 나옵니다. Hello World가 보이죠?

리팩토링 전 애저 빌드 파이프라인 실행 결과

이제 이 빌드 파이프라인을 리팩토링할 차례입니다. 리팩토링은 크게 세 곳에서 가능한데요, 하나는 Steps 수준, 다른 하나는 Jobs 수준, 그리고 마지막 하나는 Stages 수준입니다.

빌드 파이프라인을 Steps 수준에서 리팩토링하기

예를 들어 node.js 기반의 애플리케이션을 하나 만든다고 가정해 보죠. 이 경우 보통 순서가

  1. node.js 런타임 설치하기
  2. npm 패키지 복원하기
  3. 애플리케이션 빌드하기
  4. 애플리케이션 테스트하기
  5. 아티팩트 생성하기

정도가 될 것입니다. 이 때 마지막 5번 항목을 제외하고는 거의 대부분의 경우 같은 순서로, 그리고 저 1-4번 작업을 한 세트로 해서 진행을 하게 되죠. 그렇다면 이 1-4번 작업 흐름을 그냥 하나로 묶어서 템플릿 형태로 빼 놓을 수도 있지 않을까요? 이럴 때 바로 Steps 수준의 리팩토링을 진행하게 됩니다. 만약 다른 작업에서는 이후 추가 작업을 더 필요로 한다고 하면 템플릿을 돌리고 난 후 추가 태스크를 정의하면 되므로 별 문제는 없습니다.

이제 위에 정의한 빌드 파이프라인의 Steps 부분을 별도의 템플릿으로 분리합니다. 그렇다면 원래 파이프라인과 템플릿은 아래와 같이 바뀔 것입니다. 원래 파이프라인(pipeline.yaml)의 steps 항목 아래에 template 라는 항목이 생기고 (line #21), parameters를 통해 템플릿으로 값을 전달하는 것이 보일 것입니다 (line #22-23).

# pipeline.yaml
...
stages:
...
- stage: BuildWithStepsTemplate
displayName: 'Build with Steps Template'
jobs:
- job: HostedVs2017
displayName: 'Hosted VS2017'
pool:
name: 'Hosted VS2017'
workspace:
clean: all
variables:
- name: Greeting
value: 'Hello World'
steps:
- template: 'template-steps-build.yaml'
parameters:
message: 'This is from the steps template'
...

그리고 Steps 수준 리팩토링 결과 템플릿인 template-steps-build.yaml을 보면, 아래와 같이 parameterssteps를 정의했습니다 (line #2, 5). 이 parameters 항목을 통해 부모 파이프라인과 템플릿 사이 값을 교환할 수 있게 해 줍니다.

# template-steps-build.yaml
parameters:
message: ''
steps:
- task: PowerShell@2
displayName: 'Echo Greeting in PowerShell'
inputs:
targetType: Inline
script: |
Write-Host "$(Greeting)"
Write-Host "${{ parameters.message }}"

이렇게 리팩토링을 한 후 파이프라인을 돌려보면 아래와 같은 결과 화면을 보게 됩니다. 부모 파이프라인에서 템플릿으로 넘겨준 파라미터 값이 잘 표현되는 것이 보이죠?

Steps 수준 리팩토링 후 애저 빌드 파이프라인 실행 결과

빌드 파이프라인을 Jobs 수준에서 리팩토링하기

이번에는 Jobs 수준에서 리팩토링을 한 번 해보겠습니다. 앞서 연습해 봤던 Steps 수준 리팩토링은 공통의 태스크들을 묶어주는 정도였다면, Jobs 수준의 리팩토링은 그보다 큰 덩어리를 다룹니다. 이 덩어리에는 빌드 에이전트의 종류까지 결정할 수 있고, 템플릿 안의 모든 태스크를 동일하게 가져갈 수 있습니다.

물론 조건 표현식과 같은 고급 기능을 사용하면 좀 더 다양한 시나리오에서 다양한 태스크들을 활용할 수 있습니다.

아래와 같이 부모 파이프라인을 수정해 보죠 (line #13-16).

# pipeline.yaml
...
stages:
...
- stage: BuildWithJobsTemplate
displayName: 'Build with Jobs Template'
variables:
- name: Greeting
value: 'Hello World'
jobs:
- template: 'template-jobs-build.yaml'
parameters:
vmImage: 'vs2017-win2016'
message: 'This is from the jobs template'
...

그리고 난 후, 아래와 같이 template-jobs-build.yaml 파일을 작성합니다. 파라미터로 vmImagemessage를 넘겨 템플릿에서 어떻게 사용하는지 살펴보죠 (line #2-4).

# template-jobs-build.yaml
parameters:
vmImage: ''
message: ''
jobs:
- job: TemplatedJob
displayName: 'Templated Job'
pool:
vmImage: '${{ parameters.vmImage }}'
workspace:
clean: all
steps:
- task: PowerShell@2
displayName: 'Echo Greeting in PowerShell'
inputs:
targetType: Inline
script: |
Write-Host "$(Greeting)"
Write-Host "${{ parameters.message }}"

Jobs 수준에서 사용하는 빌드 에이전트의 종류까지도 변수화시켜 사용할 수 있는 것이 보이나요? 부모 템플릿에서 에이전트를 Windows Server 2016 버전으로 설정했으므로 실제 이를 파이프라인으로 돌려보면 아래와 같은 결과가 나타납니다.

Jobs 수준 리팩토링후 애저 빌드 파이프라인 실행 결과

빌드 파이프라인을 Stages 수준에서 리팩토링하기

이번에는 Stages 수준에서 파이프라인 리팩토링을 시도해 보겠습니다. 하나의 스테이지에는 여러개의 Job을 동시에 돌리거나 순차적으로 돌릴 수 있습니다. Job 수준에서 돌아가는 공통의 작업들이 있다면 이를 Job 수준에서 묶어 리팩토링 할 수 있겠지만, 아예 공통의 Job들 까지 묶어서 하나의 Stage를 만들고 이를 별도의 템플릿으로 빼낼 수 있는데, 이것이 이 연습의 핵심입니다. 아래 부모 파이프라인 코드를 보세요. stages 아래에 곧바로 템플릿을 지정하고 변수를 보냅니다 (line #9-12).

# pipeline.yaml
...
variables:
- name: Greeting
value: "G'day, mate"
...
stages:
...
- template: 'template-stages-build.yaml'
parameters:
vmImage: 'ubuntu-16.04'
message: 'This is from the stages template'
...

위에서 언급한 template-stage-build.yaml 파일은 아래와 같이 작성할 수 있습니다. 부모에서 받아온 파라미터를 통해 빌드 에이전트에 쓰일 OS와 다른 값들을 설정할 수 있는게 보이죠 (line #2-4)?

# template-stages-build.yaml
parameters:
vmImage: ''
message: ''
stages:
- stage: BuildWithStagesTemplate
displayName: 'Build with Stages Template'
jobs:
- job: TemplatedStage
displayName: 'Templated Stage'
pool:
vmImage: '${{ parameters.vmImage }}'
workspace:
clean: all
steps:
- task: PowerShell@2
displayName: 'Echo Greeting in PowerShell'
inputs:
targetType: Inline
script: |
Write-Host "$(Greeting)"
Write-Host "${{ parameters.message }}"

이렇게 해서 파이프라인을 실행해 본 결과는 대략 아래와 같습니다. 변수를 통해 전달한 값에 따라 빌드 에이전트가 Ubuntu 16.04 버전으로 설정이 되었고, 글로벌 변수 값을 별도로 재정의하지 않았으므로 아래 그림과 같이 G'day, mate라는 글로벌 변수 값을 볼 수 있습니다.

Stages 수준 리팩토링 후 애저 빌드 파이프라인 실행 결과

빌드 파이프라인을 다단계 템플릿으로 리팩토링하기

이렇게 Steps 수준, Jobs 수준, Stages 수준에서 모두 리팩토링을 해 봤습니다. 그렇다면 리팩토링의 결과물인 템플릿을 다단계로 걸쳐서 사용할 수는 없을까요? 물론 당연히 되죠. 아래와 같이 부모 파이프라인을 수정해 보겠습니다. 이번에는 맥OS를 에이전트로 선택해 볼까요 (line #9-12)?

# pipeline.yaml
...
variables:
- name: Greeting
value: "G'day, mate"
...
stages:
...
- template: 'template-stages-nested-build.yaml'
parameters:
vmImage: 'macOS-10.13'
message: 'This is from the nested stages template'
...

Stage 수준에서 다단계 템플릿을 만들어서 붙여봤습니다. 이 템플릿 안에서 또다시 Jobs 수준의 다단계 템플릿을 호출합니다 (line #11-14).

# template-stages-nested-build.yaml
parameters:
vmImage: ''
message: ''
stages:
- stage: BuildWithNestedStagesTemplate
displayName: 'Build with Nested Stages Template'
jobs:
- template: 'template-jobs-nested-build.yaml'
parameters:
vmImage: '${{ parameters.vmImage }}'
message: '${{ parameters.message }}'

Jobs 수준의 다단계 템플릿은 대략 아래와 같습니다. 그리고, 이 안에서 또다시 앞서 만들어 둔 Steps 수준의 템플릿을 호출합니다 (line #17-19).

# template-jobs-nested-build.yaml
parameters:
vmImage: ''
message: ''
jobs:
- job: NestedBuildJob
displayName: 'Nested Build Job on ${{ parameters.vmImage }}'
pool:
vmImage: '${{ parameters.vmImage }}'
workspace:
clean: all
steps:
- template: 'template-steps-build.yaml'
parameters:
message: '${{ parameters.message }}'

이렇게 다단계로 템플릿을 만들어 붙여놓은 후 파이프라인을 돌려보면 아래와 같습니다.

다단계 리팩토링 후 애저 빌드 파이프라인 실행 결과

아주 문제 없이 다단계 템플릿이 잘 돌아가는게 보이죠?

지금까지 빌드 파이프라인을 리팩토링해 봤습니다. 이제 릴리즈 파이프라인으로 들어가 보겠습니다.

릴리즈 파이프라인

릴리즈 파이프라인은 빌드 파이프라인과 크게 다르지 않습니다. 다만 job 대신 deployment job을 사용한다는 차이가 있을 뿐입니다. 이 둘의 차이에 대해 얘기하는 것은 이 포스트의 범위를 벗어나니 여기까지만 하기로 하고, 실제 릴리즈 파이프라인의 구성을 보겠습니다. 템플릿 리팩토링 없는 전형적인 릴리즈 스테이지는 아래와 같습니다.

# pipeline.yaml
...
stages:
...
- stage: ReleaseWithoutTemplate
displayName: 'Release without Template'
jobs:
- deployment: HostedVs2017
displayName: 'Hosted VS2017'
pool:
name: 'Hosted VS2017'
environment: release-without-template
variables:
- name: Greeting
value: 'Hello World'
strategy:
runOnce:
deploy:
steps:
- task: PowerShell@2
displayName: 'Echo Greeting in PowerShell'
inputs:
targetType: Inline
script: 'Write-Host "$(Greeting)"'
...

위 코드를 보면 Jobs 수준에 deployment를 사용해서 작업 단위를 정의한 것을 볼 수 있죠 (line #9)? 이를 실행시킨 결과는 대략 아래와 같습니다.

리팩토링 전 애저 릴리즈 파이프라인 실행 결과

이제 이 릴리즈 파이프라인을 동일하게 세 곳, Steps, Jobs, Stages 수준에서 리팩토링을 할 수 있습니다. 각각의 리팩토링 방식은 크게 다르지 않으므로 아래 리팩토링 결과만을 적어놓도록 하겠습니다.

릴리즈 파이프라인을 Steps 수준에서 리팩토링하기

우선 Steps 수준에서 릴리즈 템플릿을 만들어 보도록 하죠. 부모 템플릿은 아래와 같습니다 (line #24-26).

# pipeline.yaml
...
stages:
...
- stage: ReleaseWithStepsTemplate
displayName: 'Release with Steps Template'
jobs:
- deployment: HostedVs2017
displayName: 'Hosted VS2017'
pool:
name: 'Hosted VS2017'
environment: release-with-steps-template
variables:
- name: Greeting
value: 'Hello World'
strategy:
runOnce:
deploy:
steps:
- template: 'template-steps-release.yaml'
parameters:
message: 'This is from the steps template'
...

그리고 템플릿으로 빼낸 Steps는 아래와 같습니다. 앞서 빌드 파이프라인에서 사용한 템플릿과 구조가 다르지 않죠 (line #2-3)?

# template-steps-release.yaml
parameters:
message: ''
steps:
- task: PowerShell@2
displayName: 'Echo Greeting in PowerShell'
inputs:
targetType: Inline
script: |
Write-Host "$(Greeting)"
Write-Host "${{ parameters.message }}"

그리고 그 결과를 보면 아래와 같습니다.

Steps 수준 리팩토링 후 애저 릴리즈 파이프라인 실행 결과

릴리즈 파이프라인을 Jobs 수준에서 리팩토링하기

이번에는 릴리즈 파이프라인을 Jobs 수준에서 리팩토링해 보겠습니다 (line #13-17).

# pipeline.yaml
...
stages:
...
- stage: ReleaseWithJobsTemplate
displayName: 'Release with Jobs Template'
variables:
- name: Greeting
value: 'Hello World'
jobs:
- template: 'template-jobs-release.yaml'
parameters:
templateLevel: jobs
vmImage: 'vs2017-win2016'
message: 'This is from the jobs template'
...

그리고 리팩토링한 템플릿은 아래와 같습니다. 여기서 눈여겨 봐야 할 부분은 바로 environment 이름도 파라미터로 처리가 가능하다는 점입니다 (line #14). 즉, 거의 대부분의 설정을 부모 파이프라인에서 파라미터로 내려주면 템플릿에서 받아 처리가 가능합니다 (line #2-5).

# template-jobs-release.yaml
parameters:
templateLevel: ''
vmImage: ''
message: ''
jobs:
- deployment: TemplatedRelease
displayName: 'Templated Release'
pool:
vmImage: '${{ parameters.vmImage }}'
environment: release-with-${{ parameters.templateLevel }}-template
strategy:
runOnce:
deploy:
steps:
- task: PowerShell@2
displayName: 'Echo Greeting in PowerShell'
inputs:
targetType: Inline
script: |
Write-Host "$(Greeting)"
Write-Host "${{ parameters.message }}"

Jobs 수준 리팩토링 후 애저 릴리즈 파이프라인 실행 결과

릴리즈 파이프라인을 Stages 수준에서 리팩토링하기

더 이상의 자세한 설명은 생략합니다 (line #5-9). 😉

# pipeline.yaml
...
stages:
...
- template: 'template-stages-release.yaml'
parameters:
templateLevel: stages
vmImage: 'ubuntu-16.04'
message: 'This is from the stages template'
...
# tmplate-stages-release.yaml
parameters:
templateLevel: ''
vmImage: ''
message: ''
stages:
- stage: ReleaseWithStagesTemplate
displayName: 'Release with Stages Template'
jobs:
- deployment: TemplatedStage
displayName: 'Templated Stage'
pool:
vmImage: '${{ parameters.vmImage }}'
environment: release-with-${{ parameters.templateLevel }}-template
strategy:
runOnce:
deploy:
steps:
- task: PowerShell@2
displayName: 'Echo Greeting in PowerShell'
inputs:
targetType: Inline
script: |
Write-Host "$(Greeting)"
Write-Host "${{ parameters.message }}"

Stages 수준 리팩토링 후 애저 릴리즈 파이프라인 실행 결과

릴리즈 파이프라인을 다단계 템플릿으로 리팩토링하기

릴리즈 파이프라인 역시 다단계 템플릿으로 구성이 가능합니다.

# pipeline.yaml
...
stages:
...
- template: 'template-stages-nested-release.yaml'
parameters:
templateLevel: nested-stages
vmImage: 'macOS-10.13'
message: 'This is from the nested stages template'
...
# template-stages-nested-release.yaml
parameters:
templateLevel: ''
vmImage: ''
message: ''
stages:
- stage: ReleaseWithNestedStagesTemplate
displayName: 'Release with Nested Stages Template'
jobs:
- template: 'template-jobs-nested-release.yaml'
parameters:
templateLevel: '${{ parameters.templateLevel }}'
vmImage: '${{ parameters.vmImage }}'
message: '${{ parameters.message }}'
# template-jobs-nested-release.yaml
parameters:
templateLevel: ''
vmImage: ''
message: ''
jobs:
- deployment: NestedDeploymentJob
displayName: 'Nested Deployment Job on ${{ parameters.vmImage }}'
pool:
vmImage: '${{ parameters.vmImage }}'
environment: release-with-${{ parameters.templateLevel }}-template
strategy:
runOnce:
deploy:
steps:
- template: 'template-steps-release.yaml'
parameters:
message: '${{ parameters.message }}'

다단계 리팩토링 후 애저 릴리즈 파이프라인 실행 결과


이렇게 빌드 및 릴리즈 파이프라인을 모든 Stages, Jobs, Steps 수준에서 템플릿을 이용해 리팩토링을 해 보았습니다. 파이프라인 작업을 하다 보면 분명히 리팩토링이 필요한 순간이 생깁니다. 그리고 어느 수준에서 템플릿을 만들어 써야 할 지는 전적으로 상황마다 다르다고 할 수 있습니다.

다만 한 가지 고려해야 할 것은, 템플릿은 가급적이면 단순한 작업을 할 수 있게끔 만드는 것이 좋습니다. 템플릿 표현식을 보면 조건문도 있고 반복문도 있고 굉장히 고급 기능을 사용할 수 있긴 하지만, 우선은 단순하게 시작해서 템플릿을 다듬어 나가는 것이 좋을 것입니다. 아무쪼록 애저 데브옵스 파이프라인다중 스테이지 파이프라인 기법을 통해 다양한 템플릿 활용 테크닉을 도입해 보고 그 강력함을 느낄 수 있기를 바랍니다.

더 궁금하다면...