• terraform
  • jenkins
  • ci-cd
  • aws

Terraform CI/CD 구축 (1): GitHub PR과 Jenkins 연동

PR이 열리면 Jenkins가 머지 결과 기준으로 Terraform Plan을 실행하도록 Webhook과 Pipeline 트리거를 설정한 과정.

시리즈 · Infra2 / 3
  1. 접속 불가 상태의 LightSail 인스턴스에서 데이터 복구하기
  2. Terraform CI/CD 구축 (1): GitHub PR과 Jenkins 연동
  3. Terraform CI/CD 구축 (2): Jenkinsfile로 Plan 결과를 PR에 남기기

사내에 Terraform을 도입하면서 CI/CD를 구축했다. 마침 Jenkins도 함께 도입되었기 때문에 GitHub Actions 대신 Jenkins를 선택했다.

목표

만들고자 한 Flow는 다음과 같다.

GitHub PR, Jenkins, Terraform, S3·DynamoDB 상태 저장소로 이어지는 CI/CD 흐름과 단계 번호

  1. 개발자가 Terraform 코드를 수정해 GitHub에 Pull Request를 올린다.
  2. GitHub가 PR 이벤트를 Webhook으로 Jenkins에 전송한다.
  3. Jenkins는 main 브랜치에 머지될 결과를 기준으로 Terraform Plan을 실행한다.
  4. Terraform Plan 결과를 해당 PR에 댓글로 남긴다.
  5. 코드 리뷰 완료 후 main 브랜치로 Merge한다.
  6. 관리자가 Jenkins에서 수동으로 CD(Terraform Apply)를 진행한다.

State 관리: S3와 DynamoDB

terraform init을 하면 terraform.tfstate 파일이 생성된다. Terraform은 이 파일을 기준으로 코드가 변경되었을 때 클라우드 자원에 어떤 수정이 생길지 결정한다. 이 State를 S3와 DynamoDB 기반의 원격 백엔드로 관리했다.

  • S3: terraform.tfstate를 안전하게 보관하고 여러 사용자가 동일한 상태 파일을 공유한다. tfstate에는 sensitive로 선언한 변수의 값까지 모두 포함되기 때문에 Git으로 형상관리해서는 절대 안 된다.
  • DynamoDB: Lock을 통해 여러 사용자가 동시에 Terraform Apply하는 것을 방지한다.
# backend.tf

terraform {
  backend "s3" {
    bucket         = "mh-tfstate"
    key            = "security_group/mh_d_apnortheast2/terraform.tfstate"
    region         = "ap-northeast-2"
    encrypt        = true
    dynamodb_table = "terraform-lock-mh"
  }
}

GitHub Webhook 설정

먼저 Jenkins가 PR 이벤트를 수신할 수 있도록 Webhook을 설정한다. 저장소의 Settings → Webhooks → Add webhook에서 다음과 같이 입력한다.

항목 값
Payload URL https://<jenkins-domain>/github-webhook/
Content type application/json
Events Let me select individual events → Pull requests

GitHub Add webhook 화면에 Payload URL과 Content type을 입력한 모습

이렇게 하면 Pull Request가 열리거나 닫힐 때마다 Webhook이 실행된다. 실행 내역은 Webhook을 클릭한 뒤 상단의 Recent Deliveries에서 확인할 수 있다.

Webhook을 수신하려면 Jenkins 쪽에서 다음 GitHub IP 대역의 443 포트 인바운드를 허용해야 한다.

192.30.252.0/22
185.199.108.0/22
140.82.112.0/20
143.55.64.0/20

Jenkins Pipeline 설정

플러그인 설치

  • GitHub Integration Plugin: PR 감지
  • Pipeline GitHub Notify Step Plugin: PR의 checks 상태 변경

Pipeline 생성

New Item → Pipeline을 선택하고 Configure에서 다음을 설정한다.

General

  • GitHub project 체크, Project url: https://github.com/Test/Terraform/

Triggers

  • GitHub Pull Requests 선택 (GitHub Integration 플러그인 필요)
    • Trigger Mode: Hooks with Persisted Data
    • Crontab line: 비워 둔다. 경고가 떠도 Webhook으로 수신할 것이므로 상관없다.
    • Set status before build: 빌드 시작 전에 PR 상태를 Pending으로 만들어 준다.
    • Trigger Events: Pull Request Opened

Pending 상태가 된 PR은 다음과 같이 보인다.

PR의 Terraform/CI 체크가 Jenkins 대기 중인 pending 상태로 표시된 화면

Pipeline

  • Definition: Pipeline script from SCM (저장소 안에서 Jenkinsfile을 찾는다)
  • SCM: Git
  • Repository URL: https://github.com/Test/Terraform.git
  • Credentials: 해당 저장소에 권한이 있는 Credentials 선택
  • Script Path: Jenkinsfile의 경로. 나는 scripts/jenkinsfile에 두었다.

어느 브랜치의 코드로 Plan을 실행할 것인가

test → main으로 PR을 올렸다고 가정해 보자. 어느 브랜치의 코드로 Terraform Plan을 보여줘야 할까? test일까, main일까?

정답은 main에 test가 머지된 결과다. GitHub는 PR이 생성되면 머지 결과를 가리키는 가상의 ref(refs/pull/<PR-Num>/merge)를 만들어 준다. 이 ref를 체크아웃하도록 Repositories의 고급 설정에서 Refspec과 Branch Specifier를 지정한다.

Jenkins Git 설정에서 Refspec과 Branch Specifier를 PR 머지 ref로 지정한 화면

refs/pull/<PR-Num>/merge를 로컬 저장소의 origin/pr/<PR-Num>이라는 이름으로 가져오고, 그 브랜치를 빌드 대상으로 삼는 설정이다.

설정 요약

General
- GitHub project (체크)
  - Project url : https://github.com/Test/Terraform/

Triggers
- GitHub Pull Requests
  - Trigger Mode : Hooks with Persisted Data
  - Set status before build (체크)
  - Trigger Events : Pull Request Opened

Pipeline
- Definition : Pipeline script from SCM
  - SCM : Git
    - Repositories
      - Repository URL : https://github.com/Test/Terraform.git
      - Credentials : (Select)
      - 고급
        - Refspec : +refs/pull/${GITHUB_PR_NUMBER}/merge:refs/remotes/origin/pr/${GITHUB_PR_NUMBER}
    - Branches to build : origin/pr/${GITHUB_PR_NUMBER}
  - Script Path : scripts/jenkinsfile

여기까지가 Jenkins UI에서 하는 설정이다. 다음 글에서는 Jenkinsfile을 작성해 실제 Pipeline을 만든다.