1탄에 이어 2탄이다.
Jenkins Pipeline을 만들어보자.
위의 Flow로 진행될 예정이다.
Jenkins UI에서 설정한 디렉토리에 jenkinsfile을 만들어야 한다.
필자는 scripts/jenkinsFile로 지정했다.
노출되어서는 안될 것들이다.
Jenkins credentails로 관리할 값이기도 하다.
ASSUME_ROLE_ARN은 디렉토리 마다 권한을 다르게 만들어, 승격 시키면서 써야한다. ( 이 부분은 아직 TODO 이다. )
environment {
AWS_ACCESS_KEY_ID = credentials('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = credentials('AWS_SECRET_ACCESS_KEY')
ASSUME_ROLE_ARN = credentials('ASSUME_ROLE_ARN')
GITHUB_TOKEN = credentials('GITHUB_TOKEN')
}
PR을 Pending으로 만들어 주는 Step이다.
Jenkins UI에서 Pending으로 만드는 체크를 했지만,
이걸 사용하다 보니 check가 2개 생기는 오류가 발생했다.
수동으로 Pending으로 만들어주자.
stage('Set PR to Pending') {
steps {
script {
def commitSha = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
githubNotify(
account: 'account',
repo: 'repo',
context: 'Terraform/CI',
description: 'Terraform Plan Queued',
status: 'PENDING',
sha: commitSha,
credentialsId: 'Github-Common'
)
}
}
}
이 부분이 중요하다.
나의 디렉토리는 이런 식으로 구성되어 있다.
Project/
├─ .idea/
├─ scripts/
│ └─ jenkinsFile
├─ terraform/
│ ├─ ec2/
│ │ ├─ _module/
│ │ ├─ app_d_apnortheast2/
│ │ ├─ app_p_apnortheast2/
│ │ ├─ app_s_apnortheast2/
│ ├─ rds/
│ │ ├─ app_p_apnortheast2/
│ │ └─ app_s_apnortheast2/
│ ├─ security_group/
│ └─ vpc/
├─ .gitignore
└─ README.md
-> 즉 terraform plan을 해야 하는 디렉토리가 다르다.
-> 수정된 사항의 위치를 파악해서 그 곳에서 plan을 해야 한다.
stage('Detect Changed Terraform Directories') {
steps {
script {
// 현재 브랜치에서 바뀐 파일 추출
def diffFiles = sh(
script: '''git diff --name-only origin/main...HEAD | grep -E '\\.tf$|\\.tfvars$' || true''',
returnStdout: true
).trim().split('\n').findAll { it?.trim() }
// 바뀐 파일이 없다면, SUCCESS로 종료.
if (diffFiles.isEmpty()) {
echo "No changed .tf or .tfvars files detected."
postCommentToGitHubPR('.', 'No Terraform changes detected.')
currentBuild.result = 'SUCCESS'
return
}
// 각 디렉토리마다 provider.tf는 무조건 존재한다. -> 여기서 Plan을 해야 한다.
def tfDirs = diffFiles
.collect {
def path = it.contains('/') ? it.substring(0, it.lastIndexOf('/')) : '.'
return path
}
.unique()
.findAll { dir ->
fileExists("${dir}/provider.tf")
}
if (tfDirs.isEmpty()) {
echo "No relevant Terraform directories found."
postCommentToGitHubPR('.', 'No Terraform changes detected.')
currentBuild.result = 'SUCCESS'
return
}
env.CHANGED_DIRS = tfDirs.join(',')
echo "Detected changed Terraform directories: ${env.CHANGED_DIRS}"
}
}
}
이제 실제 디렉토리로 이동해서 Terraform Plan을 수행한다.
그러면서, GitHub에는 올릴 수 없는 값들을 Jenkins Credential로 관리해서 tfvars에 채워준다.
변수 관리 방식도 바꿔야 하지만, 일단은 이렇게 진행했다.
stage('Terraform Plan for Each Changed Directory') {
when {
allOf {
expression { return env.CHANGED_DIRS }
expression { return params.GITHUB_PR_NUMBER != null }
}
}
steps {
script {
def secrets = [
'assume_role_arn',
]
def creds = secrets.collect {
string(credentialsId: "TF_VAR_${it}", variable: "TF_VAR_${it}")
}
def dirs = env.CHANGED_DIRS.tokenize(',')
for (dir in dirs) {
echo "Running plan for ${dir}"
withCredentials(creds) {
def sedScript = secrets.collect {
"sed -i 's|\\\${${it}}|'\"\$TF_VAR_${it}\"'|' terraform.tfvars || true"
}.join('\n')
sh """
cd ${dir}
${sedScript}
terraform init -input=false
terraform validate
terraform plan -input=false -no-color > plan_output.txt
"""
def output = readFile("${dir}/plan_output.txt")
def escaped = output.take(60000).replaceAll(/[`\\]/, '\\\\$0')
postCommentToGitHubPR(dir, escaped)
}
}
}
}
}
postCommentToGitHubPR는 github PR에 댓글을 다는 함수이다.
-> comment의 위아래 \는 제거해야 한다.
def postCommentToGitHubPR(String dir, String comment) {
if (!params.GITHUB_PR_NUMBER?.trim()) return
def header = (dir == '.' || dir.trim() == '')
? "### ✅ Terraform File No Changed"
: "### ✅ Terraform Plan Result for `${dir}`"
def body = """${header}
\`\`\`
${comment}
\`\`\`
"""
writeFile file: 'payload.json', text: groovy.json.JsonOutput.toJson([body: body])
withCredentials([string(credentialsId: 'GITHUB_TOKEN', variable: 'GITHUB_TOKEN')]) {
sh """
curl -s -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Content-Type: application/json" \
--data @payload.json \
https://api.github.com/repos/${params.GITHUB_PR_SOURCE_REPO_OWNER}/Terraform/issues/${params.GITHUB_PR_NUMBER}/comments
"""
}
}
이제 모든 Step이 끝났으므로, PR의 상태를 SUCCESS or FAILE로 변경해준다.
post {
success {
script {
def commitSha = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
githubNotify(
account: 'account',
repo: 'Terraform',
context: 'Terraform/CI',
description: 'Terraform Plan SUCCESS',
status: 'SUCCESS',
targetUrl: "${env.BUILD_URL}",
sha: commitSha,
credentialsId: 'Github-Common'
)
}
}
failure {
script {
def commitSha = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
githubNotify(
account: 'account',
repo: 'Terraform',
context: 'Terraform/CI',
description: 'Terraform Plan FAILURE',
status: 'FAILURE',
targetUrl: "${env.BUILD_URL}",
sha: commitSha,
credentialsId: 'Github-Common'
)
}
}
}
pipeline {
agent any
environment {
AWS_ACCESS_KEY_ID = credentials('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = credentials('AWS_SECRET_ACCESS_KEY')
ASSUME_ROLE_ARN = credentials('ASSUME_ROLE_ARN')
GITHUB_TOKEN = credentials('GITHUB_TOKEN')
}
stages {
stage('Set PR to Pending') {
steps {
script {
def commitSha = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
githubNotify(
account: 'account',
repo: 'Terraform',
context: 'Terraform/CI',
description: 'Terraform Plan Queued',
status: 'PENDING',
sha: commitSha,
credentialsId: 'Github-Common'
)
}
}
}
stage('Detect Changed Terraform Directories') {
steps {
script {
def diffFiles = sh(
script: '''git diff --name-only origin/main...HEAD | grep -E '\\.tf$|\\.tfvars$' || true''',
returnStdout: true
).trim().split('\n').findAll { it?.trim() }
if (diffFiles.isEmpty()) {
echo "No changed .tf or .tfvars files detected."
postCommentToGitHubPR('.', 'No Terraform changes detected.')
currentBuild.result = 'SUCCESS'
return
}
def tfDirs = diffFiles
.collect {
def path = it.contains('/') ? it.substring(0, it.lastIndexOf('/')) : '.'
return path
}
.unique()
.findAll { dir ->
fileExists("${dir}/provider.tf")
}
if (tfDirs.isEmpty()) {
echo "No relevant Terraform directories found."
postCommentToGitHubPR('.', 'No Terraform changes detected.')
currentBuild.result = 'SUCCESS'
return
}
env.CHANGED_DIRS = tfDirs.join(',')
echo "Detected changed Terraform directories: ${env.CHANGED_DIRS}"
}
}
}
stage('Terraform Plan for Each Changed Directory') {
when {
allOf {
expression { return env.CHANGED_DIRS }
expression { return params.GITHUB_PR_NUMBER != null }
}
}
steps {
script {
def secrets = [
'assume_role_arn',
]
def creds = secrets.collect {
string(credentialsId: "TF_VAR_${it}", variable: "TF_VAR_${it}")
}
def dirs = env.CHANGED_DIRS.tokenize(',')
for (dir in dirs) {
echo "Running plan for ${dir}"
withCredentials(creds) {
def sedScript = secrets.collect {
"sed -i 's|\\\${${it}}|'\"\$TF_VAR_${it}\"'|' terraform.tfvars || true"
}.join('\n')
sh """
cd ${dir}
${sedScript}
terraform init -input=false
terraform validate
terraform plan -input=false -no-color > plan_output.txt
"""
def output = readFile("${dir}/plan_output.txt")
def escaped = output.take(60000).replaceAll(/[`\\]/, '\\\\$0')
postCommentToGitHubPR(dir, escaped)
}
}
}
}
}
}
post {
success {
script {
def commitSha = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
githubNotify(
account: 'account',
repo: 'Terraform',
context: 'Terraform/CI',
description: 'Terraform Plan SUCCESS',
status: 'SUCCESS',
targetUrl: "${env.BUILD_URL}",
sha: commitSha,
credentialsId: 'Github-Common'
)
}
}
failure {
script {
def commitSha = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
githubNotify(
account: 'account',
repo: 'Terraform',
context: 'Terraform/CI',
description: 'Terraform Plan FAILURE',
status: 'FAILURE',
targetUrl: "${env.BUILD_URL}",
sha: commitSha,
credentialsId: 'Github-Common'
)
}
}
}
}
def postCommentToGitHubPR(String dir, String comment) {
if (!params.GITHUB_PR_NUMBER?.trim()) return
def header = (dir == '.' || dir.trim() == '')
? "### ✅ Terraform File No Changed"
: "### ✅ Terraform Plan Result for `${dir}`"
def body = """${header}
\`\`\`
${comment}
\`\`\`
"""
writeFile file: 'payload.json', text: groovy.json.JsonOutput.toJson([body: body])
withCredentials([string(credentialsId: 'GITHUB_TOKEN', variable: 'GITHUB_TOKEN')]) {
sh """
curl -s -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Content-Type: application/json" \
--data @payload.json \
https://api.github.com/repos/${params.GITHUB_PR_SOURCE_REPO_OWNER}/Terraform/issues/${params.GITHUB_PR_NUMBER}/comments
"""
}
}
그럼 이제 PR을 올릴 때 마다 댓글로 다음과 같이 작성된다.
