Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
284 changes: 137 additions & 147 deletions .github/workflows/atlantis.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
name: Terraform with Atlantis
name: Atlantis

on:
pull_request:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, closed]
pull_request_review:
types: [submitted]
pull_request_review_comment:
types: [created]
issue_comment:
types: [created]
push:
branches:
- main
- master

permissions:
contents: read
Expand All @@ -18,178 +21,165 @@ permissions:
statuses: write

jobs:
terraform:
atlantis:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3

- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.0.0

- name: Setup Google Cloud SDK
uses: google-github-actions/setup-gcloud@v0.6.0
with:
project_id: polished-tube-312806
service_account_key: ${{ secrets.GCP_SA_KEY }}
export_default_credentials: true
fetch-depth: 0

- name: Download Atlantis
# Determine if this is a PR comment and if it contains an Atlantis command
- name: Check for Atlantis command
id: check_comment
if: github.event_name == 'issue_comment' && github.event.issue.pull_request
run: |
wget https://github.com/runatlantis/atlantis/releases/download/v0.19.8/atlantis_linux_amd64.zip
unzip atlantis_linux_amd64.zip
sudo mv atlantis /usr/local/bin/
COMMENT="${{ github.event.comment.body }}"
if [[ "$COMMENT" == "atlantis "* ]]; then
echo "command=$(echo $COMMENT | cut -d' ' -f2-)" >> $GITHUB_OUTPUT
echo "Found Atlantis command: $(echo $COMMENT | cut -d' ' -f2-)"
else
echo "No Atlantis command found"
fi

- name: Create Atlantis config
run: |
cat > atlantis-config.yaml <<EOF
repos:
- id: ${{ github.repository }}
workflow: custom
workflows:
custom:
plan:
steps:
- run: terraform init -input=false
- run: terraform plan -input=false -out=\$PLANFILE
apply:
steps:
- run: terraform apply -input=false \$PLANFILE
EOF

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

- name: Terraform Init
id: init
run: terraform init

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

- name: Terraform Plan
id: plan
# If this is a PR, get the PR number
- name: Get PR number
id: get_pr
if: github.event_name == 'pull_request'
run: |
# Set SSH key for CI/CD - use secret if available, otherwise use dummy key
if [ -n "${{ secrets.SSH_PUBLIC_KEY }}" ]; then
echo 'ssh_pub_key = "${{ secrets.SSH_PUBLIC_KEY }}"' >> terraform.auto.tfvars
else
echo 'ssh_pub_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDummy"' >> terraform.auto.tfvars
fi
terraform plan -no-color
continue-on-error: true
echo "pr_number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT

- name: Update Pull Request
uses: actions/github-script@v6
# If this is a comment, get the PR number from the issue
- name: Get PR number from comment
id: get_pr_from_comment
if: github.event_name == 'issue_comment' && github.event.issue.pull_request
run: |
echo "pr_number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT

# Checkout the PR branch if this is a comment on a PR
- name: Checkout PR branch
if: github.event_name == 'issue_comment' && github.event.issue.pull_request && steps.check_comment.outputs.command != ''
run: |
PR_NUMBER=${{ steps.get_pr_from_comment.outputs.pr_number }}
git fetch origin pull/$PR_NUMBER/head:pr-$PR_NUMBER
git checkout pr-$PR_NUMBER

# Run Atlantis in Docker for autoplan on PR
- name: Run Atlantis Autoplan
if: github.event_name == 'pull_request'
env:
PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }}
run: |
# Create a directory for Atlantis data
mkdir -p ~/.atlantis

# Set default project ID if not provided
PROJECT_ID="${GCP_PROJECT_ID:-default-project-id}"

# Run Atlantis server in Docker with autoplan
docker run --rm \
-e ATLANTIS_GH_TOKEN="${GITHUB_TOKEN}" \
-e ATLANTIS_GH_USER="${{ github.repository_owner }}" \
-e ATLANTIS_GH_WEBHOOK_SECRET="dummy-secret" \
-e ATLANTIS_REPO_ALLOWLIST="${{ github.repository }}" \
-e ATLANTIS_ATLANTIS_URL="https://github.com/${{ github.repository }}/pull/${{ steps.get_pr.outputs.pr_number }}" \
-e GOOGLE_CREDENTIALS="${GCP_SA_KEY}" \
-e TF_VAR_project_id="${PROJECT_ID}" \
-v "$(pwd):/atlantis/repos" \
-v ~/.atlantis:/atlantis/data \
ghcr.io/runatlantis/atlantis:v0.22.3 \
plan \
--dir=. \
--workspace=default \
--project=terraform-gcp-vm-instance > /tmp/atlantis_output.txt 2>&1 || true

# Display output for debugging
cat /tmp/atlantis_output.txt

# Run Atlantis in Docker for comment commands
- name: Run Atlantis Command
if: github.event_name == 'issue_comment' && github.event.issue.pull_request && steps.check_comment.outputs.command != ''
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }}
run: |
# Create a directory for Atlantis data
mkdir -p ~/.atlantis

# Set default project ID if not provided
PROJECT_ID="${GCP_PROJECT_ID:-default-project-id}"

# Extract the Atlantis command
ATLANTIS_CMD=$(echo "${{ steps.check_comment.outputs.command }}" | cut -d' ' -f1)
ATLANTIS_ARGS=$(echo "${{ steps.check_comment.outputs.command }}" | cut -d' ' -f2- || echo "")

# Run Atlantis server in Docker with the specified command
docker run --rm \
-e ATLANTIS_GH_TOKEN="${GITHUB_TOKEN}" \
-e ATLANTIS_GH_USER="${{ github.repository_owner }}" \
-e ATLANTIS_GH_WEBHOOK_SECRET="dummy-secret" \
-e ATLANTIS_REPO_ALLOWLIST="${{ github.repository }}" \
-e ATLANTIS_ATLANTIS_URL="https://github.com/${{ github.repository }}/pull/${{ steps.get_pr_from_comment.outputs.pr_number }}" \
-e GOOGLE_CREDENTIALS="${GCP_SA_KEY}" \
-e TF_VAR_project_id="${PROJECT_ID}" \
-v "$(pwd):/atlantis/repos" \
-v ~/.atlantis:/atlantis/data \
ghcr.io/runatlantis/atlantis:v0.22.3 \
${ATLANTIS_CMD} ${ATLANTIS_ARGS} \
--dir=. \
--workspace=default \
--project=terraform-gcp-vm-instance > /tmp/atlantis_output.txt 2>&1 || true

# Display output for debugging
cat /tmp/atlantis_output.txt

# Comment on PR with autoplan output
- name: Comment on PR with autoplan output
if: github.event_name == 'pull_request'
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
#### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`
#### Terraform Validation 🤖\`${{ steps.validate.outcome }}\`
#### Terraform Plan 📖\`${{ steps.plan.outcome }}\`

<details><summary>Show Plan</summary>

\`\`\`
${process.env.PLAN}
\`\`\`

</details>
const fs = require('fs');
let output;

*Pushed by: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*
try {
output = fs.readFileSync('/tmp/atlantis_output.txt', 'utf8');
} catch (error) {
output = 'Atlantis autoplan completed. No output captured.';
}

*Note: Cost estimates will be provided by the Infracost GitHub app integration.*`;

github.rest.issues.createComment({
issue_number: context.issue.number,
issue_number: ${{ github.event.pull_request.number }},
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})

- name: Process Atlantis Commands
id: atlantis_command
if: github.event_name == 'issue_comment' && contains(github.event.comment.body, 'atlantis')
run: |
# Set SSH key for CI/CD - use secret if available, otherwise use dummy key
if [ -n "${{ secrets.SSH_PUBLIC_KEY }}" ]; then
echo 'ssh_pub_key = "${{ secrets.SSH_PUBLIC_KEY }}"' >> terraform.auto.tfvars
else
echo 'ssh_pub_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDummy"' >> terraform.auto.tfvars
fi

if [[ "${{ github.event.comment.body }}" == *"atlantis apply"* ]]; then
echo "command=apply" >> $GITHUB_OUTPUT
terraform apply -auto-approve | tee apply_output.txt
echo "Applied changes via Atlantis command"
elif [[ "${{ github.event.comment.body }}" == *"atlantis plan"* ]]; then
echo "command=plan" >> $GITHUB_OUTPUT
terraform plan | tee plan_output.txt
echo "Generated plan via Atlantis command"
fi

- name: Comment on PR with Atlantis Command Result
body: `## Atlantis Autoplan Output\n\n\`\`\`\n${output}\n\`\`\``
});

# Comment on PR with command output
- name: Comment on PR with command output
if: github.event_name == 'issue_comment' && github.event.issue.pull_request && steps.check_comment.outputs.command != ''
uses: actions/github-script@v6
if: github.event_name == 'issue_comment' && contains(github.event.comment.body, 'atlantis')
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
let commandOutput = '';
let commandType = '${{ steps.atlantis_command.outputs.command }}';
let output;

try {
if (commandType === 'apply') {
commandOutput = fs.readFileSync('apply_output.txt', 'utf8');
title = '🚀 Terraform Apply Completed';
} else if (commandType === 'plan') {
commandOutput = fs.readFileSync('plan_output.txt', 'utf8');
title = '📋 Terraform Plan Generated';
}

const output = `### ${title}

<details><summary>Show Output</summary>

\`\`\`
${commandOutput}
\`\`\`

</details>

*Command executed by: @${{ github.event.comment.user.login }} via \`${{ github.event.comment.body }}\`*`;

github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
});
output = fs.readFileSync('/tmp/atlantis_output.txt', 'utf8');
} catch (error) {
console.log(`Error: ${error.message}`);

github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `### ⚠️ Error Processing Atlantis Command

There was an error processing the \`${{ github.event.comment.body }}\` command:

\`\`\`
${error.message}
\`\`\`

Please check the GitHub Actions logs for more details.`
});
output = 'Atlantis command completed. No output captured.';
}

const command = "${{ steps.check_comment.outputs.command }}".split(' ')[0];

github.rest.issues.createComment({
issue_number: ${{ github.event.issue.number }},
owner: context.repo.owner,
repo: context.repo.repo,
body: `## Atlantis ${command.charAt(0).toUpperCase() + command.slice(1)} Output\n\n\`\`\`\n${output}\n\`\`\``
});
36 changes: 25 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,23 +115,35 @@ When prompted, type `yes` to confirm the deletion of resources.

## CI/CD with Terraform and Atlantis

This repository is configured with a GitHub Actions workflow that integrates Terraform with Atlantis-style commands. This setup automates Terraform plan and apply operations in response to pull requests and comments.
This repository is configured with a GitHub Actions workflow that integrates Terraform with Atlantis for automated infrastructure management. The workflow uses the official Atlantis Docker image to run Atlantis commands directly in response to pull requests and comments.

### How it works

1. When you create a pull request that modifies Terraform files (*.tf, *.tfvars), the workflow automatically runs `terraform plan` and posts the results as a comment on the PR.
2. To apply the changes, comment on the PR with:
1. When you create a pull request that modifies Terraform files (*.tf, *.tfvars), the workflow automatically runs Atlantis autoplan and posts the results as a comment on the PR.
2. You can use the following Atlantis commands in PR comments:
```
atlantis apply
atlantis plan # Generate a new plan
atlantis apply # Apply the current plan
atlantis unlock <id> # Unlock the Terraform state with the specified lock ID
atlantis approve_policies # Approve any policy checks
atlantis version # Show the Atlantis version
atlantis help # Show help information
```
3. To generate a new plan, comment on the PR with:
3. Example usage:
```
# To run a plan
atlantis plan

# To apply changes
atlantis apply

# To unlock a state
atlantis unlock 1234abcd-ef56-7890
```
4. The workflow will process these commands and execute the corresponding Terraform operations.
5. After processing the command, the workflow will post a comment with the results, including the full output of the Terraform command.
4. The workflow runs the Atlantis Docker container with your specified command and captures the output.
5. After processing the command, the workflow posts a comment on the PR with the results, including the full output from Atlantis.

The GitHub Actions workflow is configured to respond to comments containing "atlantis" commands, providing a similar experience to the actual Atlantis server but without requiring a separate server deployment.
The GitHub Actions workflow is configured to respond to comments containing Atlantis commands, providing the same experience as a dedicated Atlantis server but without requiring a separate server deployment. It uses the official Atlantis Docker image to ensure compatibility and consistent behavior with standard Atlantis deployments.

### Infracost Integration

Expand All @@ -158,9 +170,11 @@ resource "google_compute_network" "vpc_network" {

### GitHub Secrets Required

For the GitHub Actions workflow to function properly, you need to set up the following secrets in your GitHub repository:
For the Atlantis GitHub Actions workflow to function properly, you need to set up the following secrets in your GitHub repository:

- `GCP_SA_KEY`: The JSON key of a GCP service account with appropriate permissions for the resources in your Terraform configuration.
- `GCP_SA_KEY`: The JSON key of a GCP service account with appropriate permissions for the resources in your Terraform configuration. This is passed to the Atlantis Docker container as the `GOOGLE_CREDENTIALS` environment variable.
- `GCP_PROJECT_ID`: Your Google Cloud project ID. This is passed to the Atlantis Docker container as the `TF_VAR_project_id` environment variable.
- `GITHUB_TOKEN`: This is automatically provided by GitHub Actions and is used by Atlantis to comment on PRs and interact with the GitHub API.
- `SSH_PUBLIC_KEY` (optional): Your SSH public key for VM access. If not provided, a dummy key will be used in CI/CD environments.

You can use the provided `setup-github-secrets.sh` script to help you create a GCP service account and set up the required GitHub secrets:
Expand Down Expand Up @@ -202,7 +216,7 @@ If you want to run Atlantis locally for testing:
- `outputs.tf`: Output definitions
- `terraform.tfvars`: Variable values
- `atlantis.yaml`: Atlantis configuration file
- `.github/workflows/atlantis.yml`: GitHub Actions workflow for Terraform with Atlantis-style commands
- `.github/workflows/atlantis.yml`: GitHub Actions workflow for Terraform with Atlantis comment-based operations
- `setup-github-secrets.sh`: Helper script to set up GitHub secrets for CI/CD

## Notes
Expand Down
Loading