Programming/IaC

[IaC] 형상관리도구 & 워크플로

Hayley Shim 2023. 10. 28. 18:03

안녕하세요. 최근 도서 “테라폼으로 시작하는 IaC” 의 내용을 기준으로 스터디한 내용을 정리했습니다. 지난 [IaC] State & 모듈 글에 이어 해당 글에서는 형상관리도구 & 워크플로에 대해 알아보겠습니다.

 

형상 관리 도구

형상 관리 도구는 버전 관리 시스템

  • SVN : 중앙 저장소에서 코드와 히스토리를 관리하는 방식 — [참고: 아파치 서브버전]
  •  Git : 분산형 관리 시스템으로 작업 환경에서도 별도로 코드 히스토리를 관리하고 중앙 저장소와 동기화 — [참고: Git(깃), 리누스 토발즈 TED 영상]

Git

중앙 저장소와 코드를 동기화하지 않아도 같은 파일을 여러명 작업 가능

깃은 코드 형상관리를 작업 환경인 로컬 저장소 Local Repository와 리모트 저장소 Remote Repository에 저장가능

  • 로컬 저장소 : 작업자 로컬 환경에 저장되는 개인 전용 저장소
  • 리모트 저장소 : 코드 형상관리 이력과 코드가 원격지에서 관리되고 여러 사람이 공유하는 저장소

[참고] Git / GitHub 안내서

코드 관리

깃허브 포크

  • 깃허브는 포크 fork 기능을 제공해 기존 리모트 저장소 본인 소유의 저장소로 복사 가능
  • 포크한 저장소는 원본 저장소와 연결이 되어 있으므로 이후 변경 사항을 원본 저장소에 적용하는 요청(Pull Request) 가능

공유 제외 대상

  • 코드 파일 공유 시 깃 관리 대상 제외 → .gitignore 정의
  • .terraform 디렉터리 : init 실행 시 작성되므로 제외 → 사이즈가 커질 수 있으므로 init시 각 로컬에 다운로드 받는 것 권장
  • .tfstate 파일 : 프로비저닝 결과 데이터 소스 정보, 민감 데이터가 포함, 다른 사용자가 같은 State 파일을 사용하는 경우 인프라 불합치 유발
  • tfvars 파일 : 프로비저닝 시 적용할 변수 값을 보관하는 파일로, 작업자 마다 별도 변수 사용 → TFC/TFE는 변수관리 기능 제공
  • 시크릿 파일 : 인프라 구성에 필요한 시크릿 정보 파일 → Credentials 는 안전하게 별도로 관리 필요
  • terraformrc 파일 : 작업자의 CLI 설정 파일
# .gitignore
# Local .terraform directories
**/.terraform/*

# .tfstate files
*.tfstate
*.tfstate.*

# Crash log files
crash.log
crash.*.log

# Exclude all .tfvars files, which are likely to contain sensitive data, such as
# password, private keys, and other secrets. These should not be part of version 
# control as they are data points which are potentially sensitive and subject 
# to change depending on the environment.
*.tfvars
*.tfvars.json

# no creds
*.pem

# Ignore override files as they are usually used to override resources locally and so
# are not checked in
override.tf
override.tf.json
*_override.tf
*_override.tf.json

# Include override files you do wish to add to version control using negated pattern
# !example_override.tf

# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
# example: *tfplan*

# Ignore CLI configuration files
.terraformrc
terraform.rc

로컬 저장소에 복제

  • 로컬 작업 환경을 위해 리모트 저장소의 내용을 복제

MyGit=<각자 자신의 깃허브 계정>
MyGit=hayleyshim
git clone https://github.com/$MyGit/test-terraform

# 확인
tree test-terraform
cd test-terraform
git remote get-url origin
# 예시 : https://github.com/hayleyshim/test-terraform
  • Push 테스트 : test.txt / 자신의 깃헙 사이트에서 추가된 파일 확인
#
echo "T101 Study" >> test.txt

#
git add test.txt
git commit -m "first commit"
git push

# Git 자격 증명 설정
git config --global user.name $MyGit
git config --global user.email <깃허브 가입 이메일주소>
git config --global user.email yhshim17@gmail.com
cat ~/.gitconfig

# push 실행을 위해서 로그인 정보 입력
git push
Username for 'https://github.com': <깃허브 계정 정보>
Password for 'https://yhshim17@gmail.com': <토큰 정보>
...

# 로그인 정보 입력 방안2
export GITHUB_USER=<깃허브 계정 정보>
export GITHUB_TOKEN=<토큰 정보>

코드 협업을 위한 1인 2역을 준비 : 복제한 디렉터리 변경 및 추가 복사

  • terraform-aws-collaboration-tom : tom 작업 디렉터리
  • terraform-aws-collaboration-jerry : jerry 작업 디렉터리
cd ..
mv terraform-aws-collaboration terraform-aws-collaboration-tom
cp -r terraform-aws-collaboration-tom terraform-aws-collaboration-jerry

Push & Pull

  • 커밋된 코드는 로컬 저장소에 기록되며 푸시를 하기 전까지는 리모트 저장소에 공유되지 않음 → 또한 작업 중 풀을 통해 리모트 저장소의 변경 사항을 로컬 저장소에 반영
  • tom 디렉터리의 main.tf 파일 코드 내용 수정
...
provider "aws" {
  region = var.region
  default_tags {
    tags = {
      Project = "T101-Study-6week"
    }
  }
}
...
  • 수정된 코드를 커밋하고 리모트 저장소에 푸시
#
cd terraform-aws-collaboration-tom
git remote get-url origin
git add main.tf
git commit -m "add default tags & project name"
git push
  • 자신의 깃헙 사이트에서 main.tf 파일 코드 내용 수정 확인
  • jerry 디렉터리에서 작업
#
cd ..
cd terraform-aws-collaboration-jerry
git remote get-url origin
git pull
cat main.tf |grep Project

tom과 jerry 양쪽에서 default_tags에 Owner 항목을 추가하고 저장 시도

  • jerry 디렉터리의 main.tf 파일 코드 내용 수정
...
provider "aws" {
  region = var.region
  default_tags {
    tags = {
      Project = "T101-Study-6week"
      Owner = "jerry"
    }
  }
}
...
  • 수정된 코드를 커밋하고 리모트 저장소에 푸시
#
cd terraform-aws-collaboration-jerry
git add main.tf
git commit -m "add default tags & Owner is jerry"
git push
  • 자신의 깃헙 사이트에서 main.tf 파일 코드 내용 수정 확인

tom 디렉터리의 main.tf 파일 코드 내용 수정

...
provider "aws" {
  region = var.region
  default_tags {
    tags = {
      Project = "T101-Study-6week"
      Owner = "tom"
    }
  }
}
...
  • 수정된 코드를 커밋하고 리모트 저장소에 Push
#
cd ..
cd terraform-aws-collaboration-tom
git add main.tf
git commit -m "add default tags & Owner is tom"

# tom의 루트 모듈 변경 코드를 리모트 저장소에 보낼 때 실패
git push
  • Push 전 Pull을 먼저 수행해 리모트의 변경 사항을 로컬과 비교한 후 필요할 때 수정해 푸시하는 습관 필요
  • 변경 사항에 충돌이 없는 경우 자동으로 커밋 지점이 병합됨
  • Pull 동작에 충돌이 있을 것으로 가정해 옵션을 붙여 수행 : git rebase
git pull --no-rebase
#
terraform fmt
git add main.tf
git commit -m "change default tags Owner"
git push
cat main.tf | grep Owner

Pull Request

  • Push와 Pull로도 코드 협업이 가능하지만 다른 사람의 커밋을 확인하기가 쉽지 않고, 리모트 저장소에 Push할 때가 되어야 충돌 상황을 확인하게 된다는 단점
  • 작업된 테라폼 코드에 대한 자연스러운 리뷰와 메인스트림 main branch의 병합을 관리하기 위해 Pull Request 방식을 사용한다
  • 깃으로 코드를 관리할 때 주로 사용되는 브랜치 branch를 이용하는 방안으로, 작업자가 코드 수정을 위해 메인과 별개의 브랜치를 생성하고 작업 후 본인의 브랜치를 푸시하고 코드 관리자에게 검토 후 병합을 요청하는 방식

코드 작성자 1단계

  • 코드 충돌을 유발하기 위해 앞 실습의 결과로 tom이 메인 브랜치에 푸시한 상황에서 jerry가 Owner 항목을 jerry & tom 으로 변경하고 싶어할경우
  • 메인 브랜치에 바로 Push하지 않고 작업을 위한 브랜치를 생성하고 작업을 수행
#
cd ..
cd terraform-aws-collaboration-jerry

# 최신 코드 pull 해두기
git pull

# 현재 브렌치 확인
git branch
<q로 빠져나옴>

# 새로 만든 jerry-owner-task 브랜치로 전환
git checkout -b jerry-owner-task
git branch
  • jerry 디렉터리의 main.tf 파일 코드 내용 수정
...
provider "aws" {
  region = var.region
  default_tags {
    tags = {
      Project = "T101-Study-6week"
      Owner = "jerry & tom"
    }
  }
}
...
  • jerry에서 작업중인 브랜치로 코드를 푸시
#
$ git add main.tf
$ git commit -m "add default tags & Owner is jerrh & tom"
$ git push origin jerry-owner-task

Enumerating objects: 38, done.
Counting objects: 100% (38/38), done.
Delta compression using up to 8 threads
Compressing objects: 100% (24/24), done.
Writing objects: 100% (38/38), 11.07 KiB | 11.07 MiB/s, done.
Total 38 (delta 15), reused 28 (delta 10), pack-reused 0
remote: Resolving deltas: 100% (15/15), done.
remote: 
remote: Create a pull request for 'jerry-owner-task' on GitHub by visiting:
remote:      https://github.com/gasida/terraform-aws-collaboration/pull/new/jerry-owner-task
remote: 
To https://github.com/gasida/terraform-aws-collaboration
 * [new branch]      jerry-owner-task -> jerry-owner-task

위 링크 방문 → 자신의 저장소 선택 : Change Owner Tag 입력 후 Create pull request

  • Pull Request는 코드를 Merge하는 관리자에게 자신이 수정한 내용을 메인 코드 병합해달라는 요청을 하는 것이다.
  • Push로 코드를 Merge하는 것과는 다르게 Pull Request는 코드 리뷰를 강제하기 때문에 코드 변경 사항을 검토할 수 있다.

코드 관리자(Admin)

  • 리모트 저장소의 쓰기 권한이 있는 관리자는 풀 리퀘스트가 발생하면 코드를 기존 코드에 Merge할 권한을 갖는다.
  • Pull Request에는 요청자의 변경 사항이 표기된다. Pull Request 수행 방식은 세 가지를 지원하나 여기서는 기본 설정대로 진행한다
  1. Create a merge commit : 브랜치의 모든 커밋이 병합을 통해 기본 브랜치에 추가
  2. Squash and Merge : 브랜치의 변경 이력을 모두 합쳐 새로운 커밋을 생성하고 메인에 추가
  3. Rebase and Merge : 브랜치 변경 이력이 각각 메인에 추가

코드 작성자 2단계

  • Merge가 완료되면 작업자는 메인 브랜치로 이동 후 Pull을 수행해 코드를 동기화하고 기존 브랜치를 삭제
# terraform-aws-collaboration-jerry 디렉터리에서 아래 작업 수행
git checkout main
git pull
git branch -d jerry-owner-task
git branch

코드 협업자

  • Merge 이후 코드를 작업하는 작업자는 개별 로컬 저장소에 새로운 코드를 가져올 수 있다
# terraform-aws-collaboration-tom 디렉터리에서 아래 작업 수행
cd ..
cd terraform-aws-collaboration-tom
git checkout main
git pull

State 백엔드

구성 목적

  • 관리 : 지속적인 State 백업을 위해서 local 이외의 저장소가 필요
  • 공유 : 다수의 작업자가 동일한 State로 접근해 프로비저닝하기 위한 공유 스토리지 필요
  • 격리 : 민감한 데이터가 State 파일에 저장될 가능성을 고려하여, 각각의 환경에 따라 접근 권한 제어 필요

구성 가능한 백엔드 : [참고]

Terraform Cloud (TFC) 백엔드

  • 하시코프에서 프로비저닝 대상과 별개로 State를 관리할 수 있도록 SaaS 환경인 TFC를 제공하며 State 관리 기능은 무상 제공
  • 제공 기능 : 기본 기능 무료, State 히스토리 관리, State lock 기본 제공, State 변경에 대한 비교 기능
  • Free Plan 업데이트 : 사용자 5명 → 리소스 500개, 보안 기능(SSO, Sentinel/OPA로 Policy 사용) — 링크

백엔드 구성

# Clone 받은 저장소를 tom & jerry 이름으로 복제
mv terraform-aws-collaboration terraform-aws-collaboration-tom
cp -r terraform-aws-collaboration-tom terraform-aws-collaboration-jerry

# main.tf 파일에 프로바이더 블럭 수정
...
provider "aws" {
  region = var.region
  default_tags {
    tags = {
      Project = "T101-Study-6week"
    }
  }
}
...
  • tom 루트 모듈과 jerry 루트 모듈을 사용해 공통 백엔드 구성과 동작을 확인 → 두 작업자가 동일한 AWS 인프라를 프로비저닝하기를 원하는 상황
  • tom 루트 모듈에서 실행
```bash
#
cd ..
cd terraform-aws-collaboration-tom

#
terraform init
terraform plan -var=**prefix=dev**
terraform apply -auto-approve -var=**prefix=dev**

# 확인
terraform workspace list
terraform state list
ls terraform.tfstate*
terraform output
```
  • jerry 루트 모듈에서 실행 : plan 결과
#
cd ..
cd terraform-aws-collaboration-jerry


# plan 결과 : 동일한 코드이지만 State를 공유하지 않으므로 또 다른 리소스를 만들기 위한 실행계획 수행
terraform init && terraform plan -var=prefix=dev

TFC를 통하여 작업 결과 공유 설정

  • TFC의 workspaces는 CLI에서의 workspace처럼 테라폼 구성과는 별개로 State를 관리하는 단위
  • tom 루트 모듈에 main.tf 파일 수정
terraform {
  cloud {
    organization = "<MY_ORG_NAME>"         # 생성한 ORG 이름 지정
    hostname     = "app.terraform.io"      # default

    workspaces {
      name = "terraform-aws-collaboration"  # 없으면 생성됨
    }
  }
...
#
cd ..
cd terraform-aws-collaboration-tom

#
terraform init
yes

# 파일 내용
ls terraform.tfstate*
cat terraform.tfstate
# 아무런 State 내용이 없음

백엔드 활용

TFC State 잠금 기능 : 프로비저닝 수행 시 동일한 State 접근 못하게 잠금 → State 마지막 상태에 대한 무결성 확보

  • Jerry가 프로비저닝 실행 시 변경 사항이 발생하도록 코드를 수정한다고 가정하여, null_resource에 정의한 내용이 항상 수행되도록 trigger를 추가함
  • trigger에 timestamp() 함수를 지정하면 테라폼 프로비저닝 실행마다 다른 값이 저장되므로 항상 변경이 유발함
  • jerry 루트 모듈에서 main.tf 파일 코드 변경 및 실행
resource "null_resource" "configure-cat-app" {
  depends_on = [aws_eip_association.hashicat]

  triggers = {
    build_number = timestamp()
}
# 
terraform apply -var=prefix=dev
...
Enter a value: <대기>
  • 신규 터미널에서 tom 루트 모듈에서 실행
# 
terraform apply -var=prefix=dev
╷
│ Error: Error acquiring the state lock
│ 
│ Error message: workspace already locked (lock ID: "gasida-org/terraform-aws-collaboration")
│ Lock Info:
│   ID:        4869b6f7-8c57-6f50-857c-9730999b8b2a
│   Path:      
│   Operation: OperationTypeApply
│   Who:       gasida@seojonghoui-MacBookPro.local
│   Version:   1.5.1
│   Created:   2023-08-06 09:01:03.131157 +0000 UTC
│   Info:      
│ 
│ 
│ Terraform acquires a state lock to protect the state from being written
│ by multiple users at the same time. Please resolve the issue above and try
│ again. For most commands, you can disable locking with the "-lock=false"
│ flag, but this is not recommended.
  • TFC Workspace 에 State 확인 → Jerry 실습에서 완료되기 전 확인
  • jerry 루트 모듈에서 실행
# 
terraform apply -var=prefix=dev
...
Enter a value: yes
  • TFC Workspace 에 State → [Changes in this version] 에서 변경 사항 확인
  • 실습 완료 후 리소스 삭제
terraform destroy -auto-approve -var=prefix=dev

워크플로

규모에 따른 워크플로

  • Write → Plan → Apply, 워크스페이스 별로 접근 권한을 관리하고 중앙에서 관리되는 실행 환경을 설계하여 규모에 맞는 워크플로 설계가 필요

[참고: The Core Terraform Workflow]

개인 워크플로 : 개인이 테라폼으로 일하는 방식의 예

Write : 프로비저닝하려는 목적에 따라 테라폼 코드를 작성

  • 개인 작업이더라도 반복적인 사용성 고려
  • 인수에 할당되는 값을 입력 변수화하고 반복적인 구조가 발생하는 경우 리소스 단위별로 반복문을 사용할지 다수의 리소스를 모듈화할지 결정

Plan : 적용하기 위한 실행 계획을 통해 리뷰

  • 테라폼의 Plan뿐 아니라, terraform fmt를 통해 코드 형태를 포멧팅하고 변경되는 리소스를 리뷰
  • 또한 테라폼과 함께 동작하는 tfsec이나 terrascan 같은 보안 취약성 점검 툴 등을 활용하는 것도 좋은 방안

Apply : 코드로 실제 인프라를 프로비저닝

  • 실행 계획상으로는 정상이지만 실제 프로비저닝하는 단계에서 인수 값, 생성 순서, 종속성에 따라 오류가 발생할 수 있음
  • 성공적인 완료를 위해 Write > Plan > Apply 단계를 반복하고 성공하는 경우 코드 관리를 위해 VCS에 코드를 병합함

다중 작업자 워크플로

Write

  • 여러 작업자의 테라폼 코드가 충돌하지 않도록 VCS와 같은 형상관리 도구에 익숙해져야 한다.
  • 작업자는 작업 전에 미리 원격 저장소 코드를 받고 깃에서는 브랜치를 활용해 개별적으로 작업한다.
  • 개인의 워크플로에서 고려한 변수화와 더불어 패스워드와 인증서 같은 민감 데이터가 포함되지 않도록 코드를 설계한다.
  • 또한 개인 작업 환경에서만 사용되는 변수는 공유하지 않는다.
  • 깃을 사용한다면 작업자 개인의 변수 terraform.tfvars 에 선언하고 .gitignore에 추가해 개별적으로 테스트할 수 있는 환경을 구성할 수 있다
  • 이 단계에서 개별 작업자는 **작은 단위의 개별 워크플로(**Write > Plan > Apply)를 반복해야 한다.
  • 개별 작업 환경과 별개로 병합되는 코드가 실제 운영 중인 인프라에 즉시 반영되면 실행 후 발생할 오류 예측이 어려워 부담이 될 수 있다.
  • 이를 보완하기 위해 프로비저닝 대상의 환경을 검증 운영, 또는 그 이상의 환경으로 구성 가능하도록 구조화한다.
  • 이때 사용하는 방식은 디렉터리 기반 격리 깃 기반의 브랜치 격리다.

Plan

  • 둘 이상의 작업자는 프로비저닝 이전에 팀원 간 리뷰를 거쳐 변경된 내역을 확인하고 공통 저장소에 병합해야 한다.
  • 리뷰 단계에서는 추가, 삭제, 수정된 내역을 관련 작업자가 검증, 질의, 배움의 단계를 거쳐 복기함으로써 코드 상태를 개선 유지하고 작업자 간에 의도를 공유한다.
  • 코드 자체 외에도 테라폼의 Plan 결과를 풀 리퀘스트 단계에 같이 제공하면 영향을 받는 리소스와 서비스 중단에 대한 예측이 더 쉬워진다.
  • CI 툴과 연계하거나 Terraform Cloud/Enterprise의 VCS 통합 기능으로 자동화할 수 있다.

Apply

  • 코드가 최종 병합되면 인프라 변경이 수행됨을 알리고 변경되는 대상 환경의 중요도에 따라 승인이 필요할 수 있다.
  • 또한 변경하는 코드가 특정 기능, 버그 픽스, 최종 릴리즈를 위한 병합인가에 따라 이 단계에 추가로 코드 병합이 발생할 수 있다.
  • 관리하는 단위를 나누는 기준은 조직 R&R, 서비스, 인프라 종류 등으로 구분된다.

다수 팀의 워크플로 : R&R이 분리된 다수 팀 또는 조직의 경우

  • R&R이 분리된 다수 팀 또는 조직의 경우 테라폼의 프로비저닝 대상은 하나이지만 관리하는 리소스가 분리된다.
  • 단일 팀의 워크플로가 유지되고 그 결과에 대해 공유해야 하는 핵심 워크플로가 필요하다.

Write

  • 대상 리소스가 하나의 모듈에서 관리되지 않고 R&R에 의해 워크스페이스가 분리된다.
  • 서로 다른 워크스페이스에서 구성된 리소스 데이터를 권한이 다른 팀에게 공유하기 위해, 저장된 State 접근 권한을 제공하고 output을 통해 공유 대상 데이터를 노출한다.
  • 테라폼 코드 작성 시 다른 워크스페이스에서의 변경 사항을 데이터 소스로 받아 오는 terraform_remote_state 또는 별도 KV-store를 활용하는 코드 구성이 요구된다.
  • 또한 관리 주체가 다른 곳에서 생긴 변경 사항의 영향을 최소화하도록 리모트 데이터 소스의 기본값을 정의하거나 코드적인 보상 로직을 구현하는 작업이 필요하다.

Plan

  • 코드 기반으로 진행되는 리뷰는 반영되는 다른 팀의 인프라를 VCS상의 코드 리뷰만으로도 공유받고 영향도를 검토할 수 있다.
  • 병합을 승인하는 단계에 영향을 받는 다른 팀의 작업자도 참여해야 한다.

Apply

  • 프로비저닝 실행과 결과에 대한 안내가 관련 팀에 알려져야 하므로 파이프라인 구조에서 자동화하는 것을 추천한다.
  • 실행 후의 영향도가 여러 팀이 관리하는 리소스에 전파될 수 있으므로 코드 롤백 훈련이 필요하다.
  • 생성된 결과에 다른 워크스페이스에서 참조되는 output 값의 업데이트된 내용을 다른 팀이 확인하는 권한 관리가 필요하다

격리 구조

테라폼 수준의 격리 목표 : State를 분리

  • 테라폼은 파일이나 하위 모듈로 구분하더라고 동작 기준은 실행하는 루트 모듈에서 코드를 통합하고 하나의 State로 관리한다.
  • 애플리케이션 구조가 모놀리식(+아키텍처)에서 MSA로 변화하는 과정은 테라폼의 IaC 특성과도 결부된다.
  • 테라폼 또한 사용하는 리소스가 적고 구조가 단순하면 모놀리식 방식으로 구성하는 것이 인프라 프로비저닝 구축 속도는 빠를 수 있다.
  • 하지만 유지 보수, 인수인계, 운영의 관점에서는 프로비저닝 단위별로 분류하는, 마치 MSA와도 같은 분산된 설계가 매몰 비용과 기술 부채를 줄이는 데 효과적이다.
  • 규모가 큰 워크플로를 만들기 위해서는 간단하고 조합 가능한 부분들이 모여 집합을 이루어야 한다.
  • 이러한 집합에서 발생하는 정보는 다른 집합과 교환할 수 있지만, 각 집합은 독립적으로 실행되며 다른 집합에 영향을 받지 않는 격리된 구조가 필요하다.
  • 초기 테라폼 적용 단계에서 단일 또는 소수의 작업자는 단일 대상에 대해 IaC를 적용하고 하나의 루트 모듈에 많은 기능을 포함시킬 가능성이 높다.

루트 모듈 격리(파일/디렉터리)

  • 단일 작업자가 테라폼으로 프로비저닝을 하는 많은 경우에 관리 편의성 및 배포 단순화를 위해 하나의 루트 디렉터리에 파일로 리소스들을 구분하거나, 디렉터리를 생성하고 하위에 구성 파일 묶음을 위치시켜 루트 모듈에서 하위 디렉터리를 모듈로 읽는 구조를 사용한다.
  • 작업자가 관리하는 영역 또는 프로비저닝되는 리소스 묶음의 독립적인 실행을 위해 단일 루트 모듈 내의 리소스를 다수의 루트 모듈로 분리하고 각 모듈의 State를 참조하도록 격리한다.
  • 관리적인 측면으로는 작업자들의 관리 영역을 분리시키고 깃 기준의 리모트 저장소도 접근 권한을 관리할 수 있다.
  • 협업과 관련해 작업자별로 특정 루트 모듈을 선정해 구성 작업을 진행해 코드 충돌을 최소화하는 환경을 구성하고 인수인계 과정에서 리뷰하는 영역을 최소화할 수 있다.

환경 격리 — 깃 브랜치

  • 서비스의 테스트, 검증, 운영 배포를 위해 테라폼으로 관리하는 리소스가 환경별로 격리되어야 한다면 디렉터리 구조로 분리하는 방안을 고려할 수 있다.
  • 디렉터리별로 각 환경을 나누는 것은 개인의 관리 편의성은 높지만, 환경의 아키텍처를 고정시키고 코드 수준의 승인 체계를 만들기 위해서는 최종 형상에 대한 환경별 브랜치를 구성하기를 권장한다.
  • 디렉터리 구조 만으로는 환경에 따라 사용자를 격리할 수 없다. 이때 깃의 브랜치 기능을 활용하면 환경 별로 구별된 작업과 협업이 가능하다.
  • 관리의 편의성을 고려해 Hot-fix와 Release 브랜치를 추가할 수도 있지만 인프라의 특성상 개발, 검증, 운영으로 나눈다.
  • 환경 간에 프로비저닝이 되는 리소스를 갖추고 있다면 운영을 위한 프로비저닝 환경을 안정적으로 유지할 수 있다는 장점이 있다.
  • 디렉터리 구조로 관리하는 환경별 디렉터리 구성 방식에서는 개발할 때 작성한 구성을 다시 복사해 검증 또는 운영에 반영하므로 환경별로 구성이 다른 상황이 발생할 여지가 높고, 모든 디렉터리에 접근 가능할 경우 검증과 운영을 위한 구성을 직접 수정하는 일이 발생할 가능성이 높다.
  • 따라서 작업자가 다수의 환경을 동시에 관리한다면 디렉터리로 구분하더라도 각 디렉터리마다 동일한 깃 저장소의 브랜치별 리모트 구성을 하는 것이 바람직하다.

프로비저닝 파이프라인 설계 — 깃허브

프로비저닝 파이프라인 + Github Action 준비

  • 실제 서비스가 실행되는 대상을 프로비저닝하면 테라폼의 Plan과 Apply 과정 상에 추가로 코드 검증, 실행 계획 검증, 실행 후 결과 확인과 같은 추가 동작을 자동화해 연계할 필요성이 생긴다.
  • 도구 : 젠킨스, Github Action, TFC/TFE
  • Github Action : 깃허브 환경에서 제공하는 CI/CD 자동화 도구 — 워크플로를 설계하고 다양한 라이브러리들을 이용해 다양한 작업 구성이 가능
  • 저장소 포크 : https://github.com/terraform101/terraform-aws-github-action
  • Github Action은 별도의 State 저장소를 제공하지 않기 때문에 테라폼 실행으로 생성되는 State가 항상 초기화되어 프로비저닝 결과를 유지할 수 없다.
  • 따라서 아래의 과정으로 백엔드를 활성화
  1. 리모트 저장소를 로컬 환경에 복제
#
MyGit=<각자 자신의 깃허브 계정>
MyGit=hayleyshim
git clone https://github.com/$MyGit/terraform-aws-github-action

# 확인
tree terraform-aws-github-action
cd terraform-aws-github-action
git remote get-url origin

2. main.tf의 terraform 블록에서 사용자의 TFC 설정 organization으로 변경


# terraform login 설정 토큰 확인
cat ~/.terraform.d/credentials.tfrc.json | jq
  • main.tf 내용 수정
terraform {
  cloud {
    organization = "<MY_ORG_NAME>"         # 생성한 ORG 이름 지정
    hostname     = "app.terraform.io"      # default

    workspaces {
      name = "terraform-aws-github-action"
    }
  }
...
  • .github/workflow/action.yml 내용 수정

env:
  MY_PREFIX: DEV
  TF_VERSION: **1.5.6** # 1.2.5에서 변경
  • push
git add main.tf
git add .github/workflows/action.yml
git commit -m "init"
git push

3. 지정된 Terraform Cloud 백엔드 활성화를 위해 terraform init을 수행

# terraform login이 되어있다고 가정
# cat ~/.terraform.d/credentials.tfrc.json | jq
terraform init
tree .terraform

4. 생성된 TFC 워크스페이스의 실행 모드 Execution mode를 Local로 수정 → 별도 설정하지 않았을 경우, Organization Default 설정인 Remote 로 기본 설정

  • terraform-aws-github-action 워크스페이스(State 백엔드 역할만 수행) 확인 → 선택 후 좌측에 Settings 클릭 → General
  • 실행 모드 변경 : 위 사진에서 Custom 클릭 → Local 선택 ****→ 하단의 Save settings 클릭하여 변경 적용

실습에서 사용되는 Github Action에 정의된 동작의 설명

  • Feature Branch — main Branch 나눠서 작업
  • Feature Branch에서 개발자는 기능 테스트 후 PR(Pull Request)
  • PR에 대한 SCAN 후 코드 검증 및 결과 공지
  • Terraform Plan 수행 후 승인권자는 main Branch 병합
  • main Branch에 병합 후 SCAN 후 코드 검증 및 결과 공지
  • Terraform Apply 수행 후 State Backend에 결과반영
  • Job ‘Scan’ : 테라폼 코드 검증
  • Job ‘Terraform’ : 테라폼 실행 ← Job ‘Scan’ 이후 실행

예시의 동작 외에도 프로비저닝 이후의 테스트를 위한 terratest 도구와 비용 예측을 위한 terracost, infracost 도구들도 추가 가능

  • action.yml : Github Action의 구성은 .github/workflows의 yml 파일 형태로 작성
name: Terraform DEV

on:
  push:
    branches:
      - main
  pull_request:

env:
  MY_PREFIX: DEV
  TF_VERSION: 1.2.5

jobs:
  SCAN:
    name: SCAN
    runs-on: ubuntu-latest
    # env:
    #   working-directory: terraform
    #   TF_WORKSPACE: my-workspace
    steps:
      # - name: Configure AWS credentials
      #   uses: aws-actions/configure-aws-credentials@v1
      #   with:
      #     aws-region: eu-west-1

      - name: Check out code
        uses: actions/checkout@v3
        
      - name: Run Terrascan
        id: terrascan
        uses: tenable/terrascan-action@main
        with:
          iac_type: 'terraform'
          iac_version: 'v14'
          policy_type: 'aws'
          only_warn: true
          sarif_upload: true

      - name: Upload SARIF file
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: terrascan.sarif  
  Terraform:
    needs: SCAN
    name: Terraform
    runs-on: ubuntu-latest
    steps:
      - name: Check out code
        uses: actions/checkout@v3

      - uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: $TF_VERSION
          cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}

      - name: Terraform Fmt
        id: fmt
        run: terraform fmt -recursive -check
        continue-on-error: true

      - name: Terraform init
        id: init
        run: terraform init -upgrade
        # working-directory: ${{ env.working-directory }}

      - name: Terraform validate
        id: validate
        run: terraform validate -no-color

      - name: Terraform plan
        id: plan
        run: terraform plan -no-color -var=prefix="$MY_PREFIX"
        # working-directory: ${{ env.working-directory }}
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          TF_LOG: info

      - name: Plan output
        id: output
        uses: actions/github-script@v3
        if: github.event_name == 'pull_request'
        env:
          PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
            #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`
            #### Terraform Plan 📖\`${{ steps.plan.outcome }}\`
            <details><summary>Show Plan</summary>
            \`\`\`hcl
            ${process.env.PLAN}
            \`\`\`
            </details>
            **Pusher**: @${{ github.actor }}
            **Action**: ${{ github.event_name }}
            `;
            github.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            })

      - name: Terraform apply
        id: apply
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: terraform apply -auto-approve -var=prefix="$MY_PREFIX" -input=false
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
  • main.tf : 프로비저닝의 실행은 작업자나 Github Action에서 발생하더라도 동일한 State유지를 위해 백엔드 구성 추가
terraform {
  cloud {
    organization = "<MY_ORG_NAME>"         # 생성한 ORG 이름 지정
    hostname     = "app.terraform.io" # default

    workspaces {
      name = "terraform-aws-github-action"
    }
  }
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"
    }
  }
}

provider "aws" {
  region = var.region
  default_tags {
    tags = {
      Project = "Coffee-Mug-Cake"
      Owner   = "jerry & tom"
    }
  }
}

resource "aws_vpc" "hashicat" {
  cidr_block           = var.address_space
  enable_dns_hostnames = true

  tags = {
    name        = "${var.prefix}-vpc-${var.region}"
    environment = "Production"
  }
}

resource "aws_subnet" "hashicat" {
  vpc_id     = aws_vpc.hashicat.id
  cidr_block = var.subnet_prefix

  tags = {
    name = "${var.prefix}-subnet"
  }
}

resource "aws_security_group" "hashicat" {
  name = "${var.prefix}-security-group"

  vpc_id = aws_vpc.hashicat.id

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port       = 0
    to_port         = 0
    protocol        = "-1"
    cidr_blocks     = ["0.0.0.0/0"]
    prefix_list_ids = []
  }

  tags = {
    Name = "${var.prefix}-security-group"
  }
}

resource "aws_internet_gateway" "hashicat" {
  vpc_id = aws_vpc.hashicat.id

  tags = {
    Name = "${var.prefix}-internet-gateway"
  }
}

resource "aws_route_table" "hashicat" {
  vpc_id = aws_vpc.hashicat.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.hashicat.id
  }
}

resource "aws_route_table_association" "hashicat" {
  subnet_id      = aws_subnet.hashicat.id
  route_table_id = aws_route_table.hashicat.id
}

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"] # Canonical

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

resource "aws_eip" "hashicat" {
  instance = aws_instance.hashicat.id
  vpc      = true
}

resource "aws_eip_association" "hashicat" {
  instance_id   = aws_instance.hashicat.id
  allocation_id = aws_eip.hashicat.id
}

resource "aws_instance" "hashicat" {
  ami                         = data.aws_ami.ubuntu.id
  instance_type               = var.instance_type
  key_name                    = aws_key_pair.hashicat.key_name
  associate_public_ip_address = true
  subnet_id                   = aws_subnet.hashicat.id
  vpc_security_group_ids      = [aws_security_group.hashicat.id]

  tags = {
    Name = "${var.prefix}-hashicat-instance"
  }
}

# We're using a little trick here so we can run the provisioner without
# destroying the VM. Do not do this in production.

# If you need ongoing management (Day N) of your virtual machines a tool such
# as Chef or Puppet is a better choice. These tools track the state of
# individual files and can keep them in the correct configuration.

# Here we do the following steps:
# Sync everything in files/ to the remote VM.
# Set up some environment variables for our script.
# Add execute permissions to our scripts.
# Run the deploy_app.sh script.
resource "null_resource" "configure-cat-app" {
  depends_on = [aws_eip_association.hashicat]

  // triggers = {
  //   build_number = timestamp()
  // }

  provisioner "file" {
    source      = "files/"
    destination = "/home/ubuntu/"

    connection {
      type        = "ssh"
      user        = "ubuntu"
      private_key = tls_private_key.hashicat.private_key_pem
      host        = aws_eip.hashicat.public_ip
    }
  }

  provisioner "remote-exec" {
    inline = [
      "sudo apt -y update",
      "sleep 15",
      "sudo apt -y update",
      "sudo apt -y install apache2",
      "sudo systemctl start apache2",
      "sudo chown -R ubuntu:ubuntu /var/www/html",
      "chmod +x *.sh",
      "PLACEHOLDER=${var.placeholder} WIDTH=${var.width} HEIGHT=${var.height} PREFIX=${var.prefix} ./deploy_app.sh",
      "sudo apt -y install cowsay",
      "cowsay Mooooooooooo!",
    ]

    connection {
      type        = "ssh"
      user        = "ubuntu"
      private_key = tls_private_key.hashicat.private_key_pem
      host        = aws_eip.hashicat.public_ip
    }
  }
}

resource "tls_private_key" "hashicat" {
  algorithm = "RSA"
}

locals {
  private_key_filename = "${var.prefix}-ssh-key.pem"
}

resource "aws_key_pair" "hashicat" {
  key_name   = local.private_key_filename
  public_key = tls_private_key.hashicat.public_key_openssh
}

Github Action 과정에서 필요로 하는 State 공유를 위한 Terraform Cloud의 토큰, AWS 프로비저닝을 위한 AWS Credential과 같은 민감 데이터를 저장소에서 민감 변수로 처리할 수 있다.

# AWS IAM 계정 생성 및 자격증명 획득

aws iam create-user --user-name testuser
aws iam create-access-key --user-name testuser
aws iam attach-user-policy --policy-arn arn:aws:iam::aws:policy/AdministratorAccess --user-name testuser
# 설정된 민감 변수는 Github Action 정의 파일에서 ${{ secrets. 변수이름 }} 으로 호출
grep secrets .github/workflows/action.yml
cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
github-token: ${{ secrets.GITHUB_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

위와 같은 방법으로 아래 3가지 Secrets을 추가

  • TF_API_TOKEN : TFC의 기존 사용 토큰(credentials.tfrc.json 에 저장됨) 또는 신규 토큰을 생성해 입력
  • AWS_ACCESS_KEY_ID : AWS Access Key 입력
  • AWS_SECRET_ACCESS_KEY : AWS Secret Access Key 입력

Terraform Cloud (TFC)

  • 워크플로 구성 환경 제공, Github Action 보다 자유도는 낮지만 VCS연동, 변수 구성, RBAC, 원격 실행 환경 등의 기능 활용

TFC의 워크플로와 역할 기반 접근 및 정책 워크플로를 확인하기 위한 조건

  1. TFC의 워크플로를 수행하기 위해 Standard 활성화 필요
  2. Standard Plandms 500개까지 Resource 초과 후 과금
  3. HCP 최초 가입 시 $50 지급
MyGit=<>
MyGit=hayleyshim
git clone https://github.com/$MyGit/terraform-aws-tfc-workflow
cd terraform-aws-tfc-workflow

MyTfcOrg=<각자 자신의 TFC 조직명>
MyTfcOrg=hayley
sed -i -e "s/<MY-ORG>/$MyTfcOrg/g" main.tf

#
git add main.tf
git commit -m "init"
git push

#
terraform init
  • TFC 생성 워크스페이스에 Execution Mode를 Remote로 유지
  • plan : 실행 후 출력은 스트림으로 처리 → [CTRL+C] 입력 시 원격 환경에서 실행 안내 받음, 실행 확인 url 출력
  • TFC 리모트 실행 환경에서의 테라폼 입력 변수와 시스템 환경 변수 관리 : 워크스페이스 → Variables

 

 

blog migration project

written in 2023.10.8

https://medium.com/techblog-hayleyshim/iac-%ED%98%95%EC%83%81%EA%B4%80%EB%A6%AC%EB%8F%84%EA%B5%AC-%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C-38ca7a7d9f25