Skip to content

Commit aff66b6

Browse files
authored
Feat/terraform_workflow_target (#213)
1 parent e0108cf commit aff66b6

File tree

1 file changed

+330
-0
lines changed

1 file changed

+330
-0
lines changed
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
---
2+
run-name: 'Terraform workflow --target'
3+
on:
4+
workflow_call:
5+
inputs:
6+
working_directory:
7+
required: true
8+
type: string
9+
description: 'Root directory of the terraform where all resources exist.'
10+
provider:
11+
required: true
12+
type: string
13+
description: 'Cloud provider to run the workflow. e.g. azurerm, aws, gcp or digitalocean'
14+
aws_region:
15+
required: false
16+
type: string
17+
default: us-east-2
18+
description: 'AWS region of terraform deployment.'
19+
gcp_region:
20+
required: false
21+
type: string
22+
description: 'GCP region of terraform deployment.'
23+
var_file:
24+
required: false
25+
type: string
26+
description: 'Terraform var file directory. e.g. vars/dev.tfvars'
27+
destroy:
28+
required: false
29+
type: boolean
30+
default: false
31+
description: 'Set true to destroy terraform infrastructure.'
32+
approvers:
33+
required: false
34+
type: string
35+
description: 'Approvals list to approve apply or destroy.'
36+
terraform_version:
37+
type: string
38+
default: 1.3.6
39+
description: 'Terraform version to use.'
40+
timeout:
41+
required: false
42+
type: number
43+
default: 10
44+
description: 'Timeout for approval step.'
45+
minimum-approvals:
46+
required: false
47+
type: string
48+
default: 1
49+
description: 'Minimum approvals required to accept the plan.'
50+
token_format:
51+
required: false
52+
type: string
53+
default: access_token
54+
description: 'Token format for GCP authentication.'
55+
access_token_lifetime:
56+
required: false
57+
type: string
58+
default: 300s
59+
description: 'Lifetime of access token for GCP.'
60+
project_id:
61+
required: false
62+
type: string
63+
description: 'GCP project ID.'
64+
create_credentials_file:
65+
required: false
66+
type: string
67+
default: true
68+
description: 'Whether to create credentials file for GCP.'
69+
git_ssh_key_setup:
70+
required: false
71+
type: string
72+
default: false
73+
description: 'Whether to setup SSH keys for Git access.'
74+
target_environment:
75+
required: false
76+
type: string
77+
default: ""
78+
description: "Deployment environment (e.g., dev, prod)."
79+
target:
80+
required: false
81+
type: string
82+
description: 'Target specific Terraform resource (e.g., module.vpc_ec2). If not set, value will be read from target.txt.'
83+
target_file:
84+
required: false
85+
type: string
86+
description: 'Path to file with target resource (e.g., vars/target.txt)'
87+
88+
secrets:
89+
AZURE_CREDENTIALS:
90+
required: false
91+
description: 'Azure Credentials to install Azure in github runner.'
92+
AWS_ACCESS_KEY_ID:
93+
required: false
94+
description: 'AWS Access Key ID to install AWS CLI.'
95+
BUILD_ROLE:
96+
required: false
97+
description: 'AWS OIDC role for aws authentication.'
98+
AWS_SECRET_ACCESS_KEY:
99+
required: false
100+
description: 'AWS Secret access key to install AWS CLI'
101+
AWS_SESSION_TOKEN:
102+
required: false
103+
description: 'AWS Session Token to install AWS CLI'
104+
GCP_CREDENTIALS:
105+
required: false
106+
description: 'The Google Cloud JSON service account key to use for authentication'
107+
DIGITALOCEAN_ACCESS_TOKEN:
108+
required: false
109+
description: 'The DigitalOcean Personal Access Token for Application & API'
110+
env-vars:
111+
required: false
112+
description: 'Pass required environment variables'
113+
WORKLOAD_IDENTITY_PROVIDER:
114+
required: false
115+
description: 'The full identifier of the Workload Identity Provider'
116+
SERVICE_ACCOUNT:
117+
required: false
118+
description: 'The service account to be used'
119+
SSH_PRIVATE_KEY:
120+
required: false
121+
description: 'Private SSH key to register in the SSH agent'
122+
123+
jobs:
124+
terraform-workflow:
125+
runs-on: ubuntu-latest
126+
environment: ${{ inputs.target_environment }}
127+
128+
outputs:
129+
tfplanExitCode: ${{ steps.tf-plan.outputs.exitcode }}
130+
131+
steps:
132+
- name: Checkout
133+
uses: actions/checkout@v4
134+
135+
- uses: webfactory/[email protected]
136+
if: ${{ inputs.git_ssh_key_setup == 'true' }}
137+
with:
138+
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
139+
140+
- name: Set environment variables
141+
run: |
142+
(
143+
cat <<'_EOT'
144+
${{ secrets.env-vars }}
145+
_EOT
146+
) >> "$GITHUB_ENV"
147+
148+
- name: Install AWS CLI
149+
if: ${{ inputs.provider == 'aws' }}
150+
uses: aws-actions/configure-aws-credentials@v4
151+
with:
152+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
153+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
154+
aws-session-token: ${{ secrets.AWS_SESSION_TOKEN }}
155+
role-to-assume: ${{ secrets.BUILD_ROLE }}
156+
aws-region: ${{ inputs.aws_region }}
157+
role-duration-seconds: 900
158+
role-skip-session-tagging: true
159+
160+
- name: Install Azure CLI
161+
if: ${{ inputs.provider == 'azurerm' }}
162+
uses: azure/login@v2
163+
with:
164+
creds: ${{ secrets.AZURE_CREDENTIALS }}
165+
166+
- name: Authenticate to Google Cloud
167+
if: ${{ inputs.provider == 'gcp' }}
168+
uses: google-github-actions/auth@v2
169+
with:
170+
credentials_json: '${{ secrets.GCP_CREDENTIALS }}'
171+
create_credentials_file: ${{ inputs.create_credentials_file }}
172+
token_format: ${{ inputs.token_format }}
173+
workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }}
174+
service_account: ${{ secrets.SERVICE_ACCOUNT }}
175+
access_token_lifetime: ${{ inputs.access_token_lifetime }}
176+
project_id: ${{ inputs.project_id }}
177+
178+
- name: Install doctl
179+
if: ${{ inputs.provider == 'digitalocean' }}
180+
uses: digitalocean/action-doctl@v2
181+
with:
182+
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
183+
184+
- name: Set up Terraform
185+
uses: hashicorp/setup-terraform@v3
186+
with:
187+
terraform_version: ${{ inputs.terraform_version }}
188+
189+
- name: Terraform Format
190+
if: ${{ inputs.destroy != true }}
191+
id: fmt
192+
uses: dflook/terraform-fmt-check@v2
193+
with:
194+
actions_subcommand: fmt
195+
path: ${{ inputs.working_directory }}
196+
197+
- name: Terraform Init
198+
run: |
199+
cd ${{ inputs.working_directory }}
200+
terraform init
201+
202+
- name: Terraform Validate
203+
if: ${{ inputs.destroy != true }}
204+
id: validate
205+
uses: dflook/terraform-validate@v2
206+
with:
207+
path: ${{ inputs.working_directory }}
208+
209+
- name: Terraform Plan
210+
id: tf-plan
211+
run: |
212+
cd ${{ inputs.working_directory }}
213+
214+
TARGET=""
215+
if [ -n "${{ inputs.target }}" ]; then
216+
echo "Using direct input target"
217+
TARGET="${{ inputs.target }}"
218+
elif [ -n "${{ inputs.target_file }}" ] && [ -f "${{ inputs.target_file }}" ]; then
219+
echo "Using absolute/relative path as-is: ${{ inputs.target_file }}"
220+
TARGET=$(cat "${{ inputs.target_file }}" | tr -d '\n')
221+
elif [ -f "target.txt" ]; then
222+
echo "Using fallback target.txt"
223+
TARGET=$(cat target.txt | tr -d '\n')
224+
fi
225+
226+
PLAN_CMD="terraform plan -out=tfplan"
227+
if [ "${{ inputs.destroy }}" = "true" ]; then
228+
PLAN_CMD="terraform plan -destroy -out=tfplan"
229+
fi
230+
231+
if [ -n "${{ inputs.var_file }}" ]; then
232+
PLAN_CMD="$PLAN_CMD --var-file=${{ inputs.var_file }}"
233+
fi
234+
235+
if [ -n "$TARGET" ]; then
236+
echo "Target detected: $TARGET"
237+
PLAN_CMD="$PLAN_CMD --target=$TARGET"
238+
else
239+
echo "No target specified. Running full plan."
240+
fi
241+
242+
echo "Running: $PLAN_CMD"
243+
eval "$PLAN_CMD"
244+
245+
- name: Upload Terraform Plan
246+
uses: actions/upload-artifact@v4
247+
with:
248+
name: tfplan
249+
path: ${{ inputs.working_directory }}/tfplan
250+
251+
- name: Create String Output
252+
id: tf-plan-string
253+
run: |
254+
cd ${{ inputs.working_directory }}
255+
TERRAFORM_PLAN=$(terraform show -no-color tfplan)
256+
delimiter="$(openssl rand -hex 8)"
257+
echo "summary<<${delimiter}" >> $GITHUB_OUTPUT
258+
echo "## Terraform Plan Output" >> $GITHUB_OUTPUT
259+
echo "<details><summary>Click to expand</summary>" >> $GITHUB_OUTPUT
260+
echo "" >> $GITHUB_OUTPUT
261+
echo '```terraform' >> $GITHUB_OUTPUT
262+
echo "$TERRAFORM_PLAN" >> $GITHUB_OUTPUT
263+
echo '```' >> $GITHUB_OUTPUT
264+
echo "</details>" >> $GITHUB_OUTPUT
265+
echo "${delimiter}" >> $GITHUB_OUTPUT
266+
267+
- name: Manual Approval
268+
uses: trstringer/manual-approval@v1
269+
timeout-minutes: ${{ inputs.timeout }}
270+
with:
271+
secret: ${{ github.TOKEN }}
272+
approvers: ${{ inputs.approvers }}
273+
minimum-approvals: ${{ inputs.minimum-approvals }}
274+
issue-title: "Terraform Plan for Infrastructure Update"
275+
276+
- name: Terraform Apply
277+
if: ${{ inputs.destroy != true }}
278+
run: |
279+
cd ${{ inputs.working_directory }}
280+
281+
TARGET=""
282+
if [ -n "${{ inputs.target }}" ]; then
283+
echo "Using direct input target"
284+
TARGET="${{ inputs.target }}"
285+
elif [ -n "${{ inputs.target_file }}" ] && [ -f "${{ inputs.target_file }}" ]; then
286+
echo "Using absolute/relative path as-is: ${{ inputs.target_file }}"
287+
TARGET=$(cat "${{ inputs.target_file }}" | tr -d '\n')
288+
elif [ -f "target.txt" ]; then
289+
echo "Using fallback target.txt"
290+
TARGET=$(cat target.txt | tr -d '\n')
291+
fi
292+
293+
if [ -n "$TARGET" ]; then
294+
echo "Target specified: $TARGET"
295+
else
296+
echo "No target specified. Applying full plan."
297+
fi
298+
299+
if [ -n "${{ inputs.var_file }}" ]; then
300+
terraform apply -var-file="${{ inputs.var_file }}" -auto-approve tfplan
301+
else
302+
terraform apply -auto-approve tfplan
303+
fi
304+
305+
- name: Find Errored Terraform State
306+
if: ${{ always() }}
307+
run: |
308+
cd ${{ inputs.working_directory }}
309+
if [ -f "errored.tfstate" ]; then
310+
echo "Errored state found."
311+
fi
312+
313+
- name: Upload Errored Terraform State Artifact
314+
if: ${{ always() }} && success() && steps.find_errored_tfstate.outputs['errored_found'] == 'true'
315+
uses: actions/upload-artifact@v4
316+
with:
317+
name: errored_tfstate
318+
path: ${{ inputs.working_directory }}/errored.tfstate
319+
320+
- name: Terraform Destroy
321+
if: ${{ inputs.destroy == true }}
322+
id: destroy
323+
run: |
324+
cd ${{ inputs.working_directory }}
325+
if [ -n "${{ inputs.var_file }}" ]; then
326+
terraform destroy -var-file="${{ inputs.var_file }}" -auto-approve
327+
else
328+
terraform destroy -auto-approve
329+
fi
330+
...

0 commit comments

Comments
 (0)