Skip to content

Instantly share code, notes, and snippets.

@HarshadRanganathan
Last active July 28, 2022 11:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save HarshadRanganathan/fbe47e2986f92c14c081ec2c4520fa4c to your computer and use it in GitHub Desktop.
Save HarshadRanganathan/fbe47e2986f92c14c081ec2c4520fa4c to your computer and use it in GitHub Desktop.
Terraform Planner script
import glob
import json
import subprocess
import sys
import json
from pathlib import Path
env = 'alpha'
keyword = ''
def find_backend_file_paths(keyword, env):
"""
Returns backend file paths that match the given keyword and env
e.g.
keyword -> os
env -> alpha
will return all backend file paths that belong to 'alpha' dir and contain 'os' keyword
./environments/alpha/os.backend.tfvars -> will match
./environments/alpha/test.backend.tfvars -> will not match
./environments/prod/os.backend.tfvars -> will not match
If the keyword is empty then it matches all the files with the env dir
./environments/alpha/os.backend.tfvars -> will match
./environments/alpha/test.backend.tfvars -> will match
./environments/prod/os.backend.tfvars -> will not match
"""
all_backend_files = glob.glob('./**/*.backend.tfvars', recursive=True)
if not keyword:
matched_backend_files = all_backend_files
else:
matched_backend_files = list(filter(lambda path: (keyword in path), all_backend_files))
return filter_file_paths_by_env(matched_backend_files, env)
def find_shared_tfvars_file_path():
"""
Returns shared.tfvars file path
"""
return ' '.join(glob.glob('./**/shared.tfvars', recursive=True))
def filter_file_paths_by_env(file_paths, env):
"""
Returns the input file paths for the given env
"""
return list(filter(lambda f: (env in f), file_paths))
def map_backend_tfvars(backend_file_paths):
"""
Returns a map of backend file paths and their corresponding tfvars file path
e.g. ./environments/alpha/os.backend.tfvars -> ./environments/alpha/os.tfvars
"""
backend_tfvars_map = {}
for backend_file_path in backend_file_paths:
backend_tfvars_map[backend_file_path] = backend_file_path.replace(".backend", "")
return backend_tfvars_map
def clear_local_tf_state():
"""
Removes terraform.tfstate since we are using different remote backend configurations for each app
"""
print('Clearing terraform.tfstate file')
command_call("rm .terraform/terraform.tfstate")
def clear_local_tf_folder():
"""
Removes .terraform folder for re-initializing in case the region has changed
"""
print('Clearing .terraform folder')
command_call("rm -rf .terraform")
def determine_backend_region(backend_file_path):
"""
Determine backend aws region
"""
if 'us-west-2' in backend_file_path:
return 'us-west-2'
else:
return 'us-east-1'
def clear_local_tf_cache(backend_file_path):
"""
Determines whether to clear local tf folder or the state file
We want to avoid downloading plugins for each init operation and instead aim to re-use the donwloaded modules/plugins if the execution is for a matching region
e.g.
If we have previously downloaded plugins/modules for us-east-1 region and the current execution is also for same region then clear only tfstate file
If we have previously downloaded plugins/modules for us-west-2 region and the current execution is also for a different region then clear tf folder for re-initialization
"""
tf_state_backend_region = 'us-east-1'
try:
with open('./.terraform/terraform.tfstate', 'r') as json_file:
data = json.load(json_file)
tf_state_backend_region = data['backend']['config']['region']
except OSError as e:
print(e)
clear_local_tf_folder()
return None
backend_region = determine_backend_region(backend_file_path)
if tf_state_backend_region != backend_region:
clear_local_tf_folder()
else:
clear_local_tf_state()
def execute_tf_init(backend_file_path):
"""
Performs tf init for the given backend
"""
clear_local_tf_cache(backend_file_path)
print("Executing TF init for backend: " + backend_file_path)
command_call("terraform init -backend-config={path}".format(path = backend_file_path.lstrip("./")))
def execute_tf_plan(shared_tfvars_path, tfvars_path):
"""
Performs tf plan for the given shared file and tfvars file
If the region is 'us-west-2' we then switch the shared file path to the region specific one e.g. shared-us-west-2.tfvars instead of shared.tfvars
"""
formatted_shared_tfvars_path = shared_tfvars_path.lstrip("./")
formatted_tfvars_path = tfvars_path.lstrip("./")
file_name = Path(tfvars_path).stem + ".plan"
if 'us-west-2' in tfvars_path:
formatted_shared_tfvars_path = formatted_shared_tfvars_path.replace(".tfvars", "-us-west-2.tfvars")
print("Executing TF plan for [{}, {}]: ".format(formatted_shared_tfvars_path, formatted_tfvars_path))
tf_plan_cmd = "terraform plan -var-file={shared_tfvars_path} -var-file={tfvars_path} -no-color > {file_name}".format(shared_tfvars_path = formatted_shared_tfvars_path, \
tfvars_path = formatted_tfvars_path, file_name = file_name)
command_call(tf_plan_cmd)
def command_call(command):
"""
Executes command in shell mode
"""
try:
subprocess.check_call(command, shell=True, stdout=sys.stdout, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
print(e.stderr)
if __name__ == '__main__':
backend_file_paths = find_backend_file_paths(keyword, env)
shared_tfvars_path = find_shared_tfvars_file_path()
backend_tfvars_dict = map_backend_tfvars(backend_file_paths)
for backend_file_path in backend_file_paths:
execute_tf_init(backend_file_path)
execute_tf_plan(shared_tfvars_path, backend_tfvars_dict[backend_file_path])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment