Github Actions와 함께 하는 Terraform CI/CD
Github Actions와 함께 하는 Terraform CI/CD
시작 하며
안녕하세요. 파이썬과 함께 데이터를 주무르고 있는 주니어 개발자 입니다.
개요
사이드 프로젝트를 진행 하게 되면 인프라를 관리하는 역할은 대개 서버 파트에서 많이 하는 것 같습니다.
매번 혼자 작업 해와서 개인 환경에서 프로비저닝 하고 코드만 깃허브 프라이빗 레포지토리로 관리 해왔었는데요.
이번 프로젝트에서는 인프라를 다루는 동료와 협업을 하게 되어 코드 리뷰와 리소스 프로비저닝을 같이할 수 있는 GitOps 방식을 활용했습니다.
대상 독자
이 글을 읽는 대상은 Github Actions로 테라폼 워크플로우를 실행 하는데 관심이 있는 개발자 입니다.

테라폼은 위 이미지와 같은 워크플로우를 띄고 있습니다.
이 과정은 혼자 작업할 때 불편함은 전혀 없습니다.
온전히 제가 작성한 코드가 정상적으로 프로비저닝 되면 되기 때문이죠.
하지만, 동료와 같이 작업할 때는 이러한 문제를 마주할 수 있습니다.
- 동시성 문제
- A 개발자와 B 개발자가 동시에 프로비저닝 할 때 발생할 수 있습니다. 흔히 “S3”와 “DynamoDB” 를 통해 동시성 락을 걸어서 해결할 수 있습니다.
2. 환경변수 파일 관리 문제
- 환경변수 파일은 깃허브 프라이빗 레포지토리에서 관리하게 되면 그대로 파일을 올려도 문제가 없겠죠. 그럼에도 보안 상 문제가 된다면 “SOPS” 암호화 툴이나 “Base64” 인코딩 후 “Github Secret”에 등록하는 방법도 사용할 수 있겠죠.
3. 코드 품질 비평준화
- A 개발자는 아키텍처가 잘 설계 되어 고려해야 할 부분을 미리 모듈화 하여 분리 하여 작업 합니다.
- B 개발자는 당장 프로비저닝 해야 할 리소스만 구성합니다.
- A와 B가 같은 결과를 나타낼 수 있지만 추후 유지보수 단계에서 서로 다른 컨벤션으로 인해 수정할 때 개발자가 소모 해야 할 시간적 비용이 증가할 수 있습니다.
깃허브 액션 구성

terraform 공식문서
깃허브 액션을 구성하기 전 아래 목록이 필요합니다.
1. AWS credential keys
2. Slack channel (Optional)

github action secret variables
- 테라폼 환경 변수 등록하기
env:
TF_VERSION: 1.9.5
working-directory: {project-directory} // change me
AWS_REGION: "ap-northeast-2"
- TF_VERSION: providers.tf 에서 명시한 테라폼 버전
- working-directory: 테라폼 워크플로우를 실행 시킬 디렉터리 위치
- AWS_REGION: 프로비저닝 할 리소스의 리전
“테라폼을 실행 시킬 때 리소스 별 파일을 어떻게 관리할까?”
테라폼을 실행 시키기 위한 전제 조건 중 하나가, *.tf 파일이 존재 해야 한다는 것인데요.
만약, 프로젝트 내부에 이런 디렉터리 구조를 갖고 있다고 가정 해보고 테라폼을 실행 시키면 어떻게 실행 될지 확인 해보겠습니다.
terraform
- aws
- project
- modules
- vpc
- main.tf
- variables.tf
- outputs.tf
- ec2
- main.tf
- variables.tf
- outputs.tf
- vpc
- main.tf
- backend.tf
- providers.tf
- outputs.tf
- config.yaml
- ec2
- main.tf
- backend.tf
- providers.tf
- outputs.tf
- config.yaml
- working-directory: ./terraform/aws/project
이렇게 구성 되어 있다면 테라폼은 루트 디렉터리에서 아무런 .tf 파일도 찾지 못하게 되어 “No configuration files found”와 같은 메시지를 출력하며 초기화 작업이 진행되지 않습니다.
“그렇다면 github-actions 에서 어느 디렉터리에서 init — plan — apply 를 해야할지에 대한 고민을 하게 됩니다.”
위 문제에 대해 아래 2가지 방식으로 접근할 수 있을 것 같습니다.
프로젝트 루트 디렉터리에 실행 가능한 .tf 파일을 작성하고 모든 리소스를 main.tf 안에서 참조한다
terraform
- aws
- project
- modules
- vpc
- main.tf
- variables.tf
- outputs.tf
- ec2
- main.tf
- variables.tf
- outputs.tf
- vpc
- main.tf
- backend.tf
- providers.tf
- outputs.tf
- config.yaml
- ec2
- main.tf
- backend.tf
- providers.tf
- outputs.tf
- config.yaml
- main.tf
- providers.tf
- backend.tf
프로젝트 루트 디렉터리에 리소스를 개별 .tf 파일로 분리한다
terraform
- aws
- modules
- vpc
- main.tf
- variables.tf
- outputs.tf
- ec2
- main.tf
- variables.tf
- outputs.tf
- project
- init
- init.tf
- config
- vpc
- config.yaml
- ec2
- config.yaml
- sources
- etc files
- ec2.tf
- vpc.tf
- providers.tf
- backend.tf
2가지 방법 중 현재는 “리소스를 개별 .tf 파일로 분리한다” 방법을 적용 해서 진행 중입니다.
어차피 모듈에서 대부분 관리가 되기 때문에 개별 리소스에서 작성 되는 코드의 양이 많지 않을 것이라고 생각이 들기 때문에 위 방법을 선택하게 되었습니다.
2. 테라폼 환경 구성 하기
jobs:
Terraform:
name: Terraform
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
defaults:
run:
working-directory: ${{ env.working-directory }}
steps:
- name: Check out code
uses: actions/checkout@v3
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
3. 테라폼 코드에 대한 검증하기
- name: Terraform init
id: init
run: terraform init -upgrade
- name: Terraform validate
id: validate
run: terraform validate -no-color
- name: Terraform plan
id: plan
run: terraform plan -no-color -parallelism=30
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
TF_LOG: info
4. 테라폼 실행 계획을 PR 코멘트에 남기기
- name: Plan output
id: output
uses: actions/github-script@v5
if: github.event_name == 'pull_request'
env:
PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const output = `
## 🪧 Terraform CI/CD 결과
- **Terraform Format and Style 🖌**: \`${{ steps.fmt.outcome }}\`
- **Terraform Initialization ⚙️**: \`${{ steps.init.outcome }}\`
- **Terraform Plan 📖**: \`${{ steps.plan.outcome }}\`
- **Terraform Validate 📖**: \`${{ steps.validate.outcome }}\`
<details><summary>Terraform Plan 결과 보기</summary>
\`\`\`hcl
${process.env.PLAN}
\`\`\`
</details>
**🚀 Pusher**: @${{ github.actor }}
**📌 Action Type**: ${{ github.event_name }}
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
코멘트에 남길 문구는 자유롭게 수정 해서 보기 편한 방식을 이용하시면 됩니다.
“각 워크플로 실행이 시작될 때 GitHub은(는) 워크플로에서 사용할 고유한
GITHUB_TOKEN비밀을 자동으로 만듭니다.GITHUB_TOKEN은 워크플로 실행에서 인증하는 데 사용할 수 있습니다.” — Github 공식문서
- secrets.GITHUB_TOKEN
별도의 GITHUB_TOKEN 을 작성하지 않더라도 해당 워크플로우에서 자동 생성 됩니다.
4–1. 테라폼 실행 계획 슬랙 알람 발송하기 (Optional)
- name: Slack alert create pull request
id: slack-alert-pr-create
if: github.event_name == 'pull_request'
uses: slackapi/slack-github-action@v2.0.0
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
{
"channel": "${{ secrets.SLACK_CHANNEL_ID }}",
"username": "terraform-alert",
"attachments": [
{
"color": "#36a64f",
"text": "🚀 *Terraform Plan 실행 완료!*",
"fields": [
{
"title": "Result",
"value": "✅ Success",
"short": true
},
{
"title": "📌 Repository",
"value": "${{ github.repository }}",
"short": false
},
{
"title": "🔗 PR Link",
"value": "<${{ github.event.pull_request.html_url }}|PR 보기>",
"short": false
}
],
"footer": "👤 Pusher: @${{ github.actor }}"
}
]
}
5. 테라폼 코드 반영하기
- name: Terraform apply
id: apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve -input=false
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
PR을 등록만 했는데도 코드가 반영 되는 불상사가 일어나면 안되겠죠. 그렇기 때문에 “메인” 브랜치에 “Push” 이벤트가 동작하는 경우 코드를 반영 하도록 작성합니다.
5–1. 테라폼 코드 반영 내용 슬랙 알람 발송하기 (Optional)
- name: Slack alert pull request merge
id: slack-alert-pr-merge
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: slackapi/slack-github-action@v2.0.0
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
{
"channel": "${{ secrets.SLACK_CHANNEL_ID }}",
"username": "terraform-alert",
"attachments": [
{
"color": "${{ steps.apply.outcome == 'success' && '#36a64f' || '#ff0000' }}",
"text": "🚀 *Terraform Apply 실행 결과*",
"fields": [
{
"title": "Result",
"value": "${{ steps.apply.outcome == 'success' && '✅ Success' || '❌ Failure' }}",
"short": true
},
{
"title": "📌 Repository",
"value": ${{ github.repository }},
"short": false
},
{
"title": "🔗 GitHub Actions",
"value": "<https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|Workflow 실행 링크>",
"short": false
}
],
"footer": "👤 Pusher: @${{ github.actor }}"
}
]
}
Github Actions 전체 코드 보기
name: Terraform CI/CD
on:
push:
branches:
- main
paths:
- terraform/aws/**
pull_request:
paths:
- terraform/aws/**
env:
TF_VERSION: 1.9.5
working-directory: ./terraform/aws/project
AWS_REGION: "ap-northeast-2"
jobs:
Terraform:
name: Terraform
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
defaults:
run:
working-directory: ${{ env.working-directory }}
steps:
- name: Check out code
uses: actions/checkout@v3
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Terraform Fmt
id: fmt
run: terraform fmt -recursive -check
continue-on-error: true
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Terraform init
id: init
run: terraform init -upgrade
- name: Terraform validate
id: validate
run: terraform validate -no-color
- name: Terraform plan
id: plan
run: terraform plan -no-color -parallelism=30
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@v5
if: github.event_name == 'pull_request'
env:
PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const output = `
## 🪧 Terraform CI/CD 결과
- **Terraform Format and Style 🖌**: \`${{ steps.fmt.outcome }}\`
- **Terraform Initialization ⚙️**: \`${{ steps.init.outcome }}\`
- **Terraform Plan 📖**: \`${{ steps.plan.outcome }}\`
- **Terraform Validate 📖**: \`${{ steps.validate.outcome }}\`
<details><summary>Terraform Plan 결과 보기</summary>
\`\`\`hcl
${process.env.PLAN}
\`\`\`
</details>
**🚀 Pusher**: @${{ github.actor }}
**📌 Action Type**: ${{ github.event_name }}
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
- name: Slack alert create pull request
id: slack-alert-pr-create
if: github.event_name == 'pull_request'
uses: slackapi/slack-github-action@v2.0.0
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
{
"channel": "${{ secrets.SLACK_CHANNEL_ID }}",
"username": "terraform-alert",
"attachments": [
{
"color": "#36a64f",
"text": "🚀 *Terraform Plan 실행 완료!*",
"fields": [
{
"title": "Result",
"value": "✅ Success",
"short": true
},
{
"title": "📌 Repository",
"value": "${{ github.repository }}",
"short": false
},
{
"title": "🔗 PR Link",
"value": "<${{ github.event.pull_request.html_url }}|PR 보기>",
"short": false
}
],
"footer": "👤 Pusher: @${{ github.actor }}"
}
]
}
- name: Terraform apply
id: apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve -input=false
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Slack alert pull request merge
id: slack-alert-pr-merge
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: slackapi/slack-github-action@v2.0.0
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
{
"channel": "${{ secrets.SLACK_CHANNEL_ID }}",
"username": "terraform-alert",
"attachments": [
{
"color": "${{ steps.apply.outcome == 'success' && '#36a64f' || '#ff0000' }}",
"text": "🚀 *Terraform Apply 실행 결과*",
"fields": [
{
"title": "Result",
"value": "${{ steps.apply.outcome == 'success' && '✅ Success' || '❌ Failure' }}",
"short": true
},
{
"title": "📌 Repository",
"value": ${{ github.repository }},
"short": false
},
{
"title": "🔗 GitHub Actions",
"value": "<https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|Workflow 실행 링크>",
"short": false
}
],
"footer": "👤 Pusher: @${{ github.actor }}"
}
]
}
인프라 리소스를 정리하는 액션 구성 작성하기
name: Terraform Destroy
on:
workflow_dispatch:
env:
TF_VERSION: 1.9.5
working-directory: ./terraform/aws/project
jobs:
Terraform:
name: Terraform
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ env.working-directory }}
steps:
- name: Check out code
uses: actions/checkout@v3
- uses: hashicorp/setup-terraform@v2
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Terraform init
id: init
run: terraform init -upgrade
- name: Terraform validate
id: validate
run: terraform validate -no-color
- name: Terraform destroy
id: Destroy
run: terraform destroy -auto-approve
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
위 Github Action 을 구성하시고 나면 깃허브 홈페이지의 Actions 탭에서 수동으로 트리거를 발동 시켜 리소스를 모두 정리할 수 있습니다.

실제 적용 사례
깃허브 Pull Request 코멘트

슬랙 알람

고민 해볼만한 내용
아무래도 config.yaml 에 담겨 있는 변수 파일을 어떻게 관리할지 고민이 많이 되는데요.
실제로는 Private Repository 에 있으니 스스럼 없이 파일을 모두 올려버리고는 합니다.
이 부분에 대한 해결책은 다양하게 접근할 수 있으나 한 번 고민 해볼만한 내용 중 하나인 것 같습니다.
요약
- Github Actions 를 구성하게 되면 동료와 협업 하여 테라폼 코드를 리뷰하고 프로비저닝 할 수 있습니다.
- Github Actions 구성 내에서 테라폼 워크플로우를 실행 시킬 디렉터리를 특정 짓게 되는 특성 때문에 리소스 구성 파일들을 관리하는 방법에 대한 고민이 필요합니다.
- Private Repository 에서 환경 변수를 관리할 때 파일에 대한 보안 사항 없이 업로드 할 수 있지만 암호화 툴이나 깃허브에서 제공하는 시크릿 변수를 이용하여 관리할 수 있습니다.
참고 자료