CodeBuild Buildspec for posting Terraform output to GitHub PRs
GitHub PRs’ comments can be modified using GitHub’s REST API, hence we can make use of PAT tokens and write back Terraform commands’ outputs back as a comment for better infra changes review.
By storing the PAT token into AWS SecretsManager we can reference it to make comments to PR as a bot user.
Terraform Plan:
version: 0.2
env:
secrets-manager:
PAT_TOKEN: "AWS_ARN_OF_SECRETS_MANAGER_SECRET"
variables:
TF_DIR: "envs/test"
phases:
install:
runtime-versions:
nodejs: 18
commands:
- curl -s -qL -o terraform_install.zip https://releases.hashicorp.com/terraform/1.8.0/terraform_1.8.0_linux_amd64.zip
- unzip terraform_install.zip -d /usr/bin/
- chmod +x /usr/bin/terraform
finally:
- terraform --version
- aws sts get-caller-identity --o text
build:
commands: |
cd $TF_DIR
terraform init -no-color > /tmp/plan.txt 2>&1
terraform plan -var-file="dev.tfvars" -no-color >> /tmp/plan.txt 2>&1
# Save TF Plan Command output
cat /tmp/plan.txt
finally: |
export TF_PLAN_OUTPUT="$(cat '/tmp/plan.txt')"
export JSON_ENCODED_TF=$(echo "$TF_PLAN_OUTPUT" | jq -Rs .)
export CODE_BLOCK_START='Terraform Plan Output: \n```\n'
export CODE_BLOCK_END='\n```'
# Insert code block wrapper before and after
export FORMATTED_TF="${JSON_ENCODED_TF:0:1}$TF_DIR \n$CODE_BLOCK_START${JSON_ENCODED_TF:1}"
export FORMATTED_TF="${FORMATTED_TF::-1}$CODE_BLOCK_END${FORMATTED_TF: -1}"
export PR_NUMBER="${CODEBUILD_SOURCE_VERSION##*/}"
# Check if there is existing PR comment
comment_response=$(curl -s -H "X-GitHub-Api-Version: 2022-11-28" -H "Authorization: token ${PAT_TOKEN}" -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/lloydslistintelligence/${CODEBUILD_SRC_DIR##*/}/issues/${PR_NUMBER}/comments?q=user:svc-seasearcher-sas)
# echo "$comment_response" | jq -c '.[]'
match_found=0
while IFS= read -r comment; do
# Extract the id and body attributes from the current JSON object
id=$(jq -r '.id' <<< "$comment")
body=$(jq -r '.body' <<< "$comment")
echo $id
# Extract the first line of the body attribute
first_line=$(echo "$body" | head -n 1)
echo "$first_line"
# Check if the first line contains the specified substring
if [[ "$first_line" == *"$TF_DIR"* ]]; then
echo "Match found with id: $id - Modify existing comment for $TF_DIR"
curl -X PATCH -H "Authorization: Bearer ${PAT_TOKEN}" -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" "https://api.github.com/repos/lloydslistintelligence/${CODEBUILD_SRC_DIR##*/}/issues/comments/$id" -d "{\"body\": $FORMATTED_TF}"
match_found=1
fi
done < <(echo "$comment_response" | jq -c '.[]')
# Check if any matches were found based on the flag
if [[ $match_found -eq 0 ]]; then
echo "No matches found. Creating new comment for $TF_DIR"
curl -s -H "X-GitHub-Api-Version: 2022-11-28" -H "Authorization: token ${PAT_TOKEN}" -X POST -d "{\"body\": ${FORMATTED_TF}}" "https://api.github.com/repos/lloydslistintelligence/${CODEBUILD_SRC_DIR##*/}/issues/${PR_NUMBER}/comments"
fi
With the above logic, we can make sure the bot does not create new comment but update the existing comment for repeated workflows.
As the trigger to the CodeBuild Workflow provides the PR number as the key for the API to update, each new PR can be properly updated by its related webhook.
For further setup you may refer to the following for the CodeBuild infrastructure setup. Make sure the buildspec file reference to set to the sample above!
For an improved version of the logic to allow multiple CodeBuilds to run within the same repo, you may refer to the one below:
version: 0.2
env:
secrets-manager:
PAT_TOKEN: "ARN_OF_SECRETS_MANAGER_SC"
variables:
TF_DIR: "PATH_TO_TF_FILES"
ACCOUNT_ID: <AWS_ACCOUNT_ID>
phases:
install:
runtime-versions:
nodejs: 18
commands:
- curl -s -qL -o terraform_install.zip https://releases.hashicorp.com/terraform/1.7.5/terraform_1.7.5_linux_amd64.zip
- unzip terraform_install.zip -d /usr/bin/
- chmod +x /usr/bin/terraform
- ASSUME_ROLE_ARN="arn:aws:iam::${ACCOUNT_ID}:role/security_groups_manager_role"
- TF_ROLE=$(aws sts assume-role --role-arn $ASSUME_ROLE_ARN --role-session-name tfplan)
- export TF_ROLE
- export AWS_ACCESS_KEY_ID=$(echo "${TF_ROLE}" | jq -r '.Credentials.AccessKeyId')
- export AWS_SECRET_ACCESS_KEY=$(echo "${TF_ROLE}" | jq -r '.Credentials.SecretAccessKey')
- export AWS_SESSION_TOKEN=$(echo "${TF_ROLE}" | jq -r '.Credentials.SessionToken')
finally:
- terraform --version
- aws sts get-caller-identity --o text
build:
commands: |
cd $TF_DIR
terraform init
terraform plan -no-color -var-file=uat.tfvars &> /tmp/plan.txt
# Save TF Plan Command output
cat /tmp/plan.txt
finally: |
export TF_PLAN_OUTPUT="$(cat '/tmp/plan.txt')"
export JSON_ENCODED_TF=$(echo "$TF_PLAN_OUTPUT" | jq -Rs .)
export CODE_BLOCK_START='Terraform Plan Output: \n```\n'
export CODE_BLOCK_END='\n```'
# Insert code block wrapper before and after
export FORMATTED_TF="${JSON_ENCODED_TF:0:1}$TF_DIR \n$CODE_BLOCK_START${JSON_ENCODED_TF:1}"
export FORMATTED_TF="${FORMATTED_TF::-1}$CODE_BLOCK_END${FORMATTED_TF: -1}"
export PR_NUMBER="${CODEBUILD_SOURCE_VERSION##*/}"
# Check if there is existing PR comment
comment_response=$(curl -s -H "X-GitHub-Api-Version: 2022-11-28" -H "Authorization: token ${PAT_TOKEN}" -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/lloydslistintelligence/${CODEBUILD_SRC_DIR##*/}/issues/${PR_NUMBER}/comments?q=user:svc-seasearcher-sas)
# echo "$comment_response" | jq -c '.[]'
match_found=0
while IFS= read -r comment; do
# Extract the id and body attributes from the current JSON object
id=$(jq -r '.id' <<< "$comment")
body=$(jq -r '.body' <<< "$comment")
echo $id
# Extract the first line of the body attribute
first_line=$(echo "$body" | head -n 1)
echo "$first_line"
# Check if the first line contains the specified substring
if [[ "$first_line" == *"$TF_DIR"* ]]; then
echo "Match found with id: $id - Modify existing comment for $TF_DIR"
curl -X PATCH -H "Authorization: Bearer ${PAT_TOKEN}" -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" "https://api.github.com/repos/lloydslistintelligence/${CODEBUILD_SRC_DIR##*/}/issues/comments/$id" -d "{\"body\": $FORMATTED_TF}"
match_found=1
fi
done < <(echo "$comment_response" | jq -c '.[]')
# Check if any matches were found based on the flag
if [[ $match_found -eq 0 ]]; then
echo "No matches found. Creating new comment for $TF_DIR"
curl -s -H "X-GitHub-Api-Version: 2022-11-28" -H "Authorization: token ${PAT_TOKEN}" -X POST -d "{\"body\": ${FORMATTED_TF}}" "https://api.github.com/repos/lloydslistintelligence/${CODEBUILD_SRC_DIR##*/}/issues/${PR_NUMBER}/comments"
fi
The above checks the comment’s first line as the comment key, it makes sure that only when the key matches before updating existing comment or create a new comment when the comment key does not exist! In this case, the PATH where the terraform files are located serves as its key.