PyTerraBackTYL is a generic HTTP/REST backend that uses plugins for managing your Terraform state and locking in whatever way you see fit. By default, PyTerraBackTYL comes with plugins to manage locking and state using Git, or using simple 'shelve' objects to persist to disk. Additionally, non-persistent plugins can be used to automate tasks when a user has issued a terraform
command, such as automatically adding the new hosts into monitoring, or pushing notifications to Slack like in the provided example.
For detailed setup instructions, see the documentation on readthedocs.io.
Visit PyTerraBackTYL Docker Hub page to get up and running quickly. By default, the container starts up using the PyShelveBackend module, but contains all package requirements for the GitBackend and the AESBackend.
- PyTerraBackTYL was developed using Python 3.6 and has since been updated to require Python 3.10. Please open a GitHub Isssue for any Python changes that have broken functionality.
- flask
- pyyaml
- jsonpath
- SSH to the host you intend to run the PyTerraBackTYL service on.
- e.g.
ssh user@linuxhost
- e.g.
- Check if
python3
is installed.- e.g.
whereis python3
- e.g.
- Install Python 3.x if needed
- RedHat/CentOS (detailed instructions):
- Install yum utils:
yum install yum-utils
- Install libs/dependencies:
yum groupinstall development
- Install the Yum Repo with Python 3.x:
yum -y install https://centos7.iuscommunity.org/ius-release.rpm
- Install Python 3.x:
yum -y install python36u python36u-pip
- Install yum utils:
- Debian/Ubuntu:
- Note: This currently installs Python 3.5, PyTerraBackTYL was developed in Python 3.6.
- Update your local apt cache data:
apt-get update
- Install Python 3.x:
apt-get install python3 python3-pip
- RedHat/CentOS (detailed instructions):
- Create the directories and service account for PyTerraBackTYL:
- Create the non-privileged user that will run the PyTerraBackTYL service:
adduser tfbackendsvc
(this also creates a directory for the user in/home
which you will need.) - Create the directory the PyTerrBackTYL service will run from:
mkdir /opt/pyterrabacktyl
- Create the directory the service will store Terraform states in:
mkdir /opt/pyterrabacktyl/data
- Set directory ownership:
chown -R tfbackendsvc /opt/pyterrabacktyl
- Create the non-privileged user that will run the PyTerraBackTYL service:
- Install PyTerraBackTYL and dependencies:
- Clone the PyTerraBackTYL repository:
cd /opt/pyterrabacktyl
su tfbackendsvc
git clone https://github.com/dev-dull/PyTerraBackTyl.git
- Install the required Python Libraries:
- Note: Depending on your OS and Python installation method, the
pip3
command may be something like,pip3.6
. - Note: Omit the
--user
flag and run as root if you want these libraries to be accessible to all users on the system. pip3 install setuptools --user
requried to install Flask.pip3 install flask pyyaml jsonpath --user
- Note: Depending on your OS and Python installation method, the
- Clone the PyTerraBackTYL repository:
The contents of the config.yaml
configuration file will largely depend on which backend module you choose to
to use with PyTerraBackTYL. Below are the configuration items for core PyTerraBackTYL service:
Modify config.yaml
and set the following items to the desired values.
BACKEND_SERVICE_IP: '127.0.0.1'
- The IP the service should listen for requests on. If you are unsure what to use here, set
0.0.0.0
- The IP the service should listen for requests on. If you are unsure what to use here, set
BACKEND_SERVICE_PORT: 2442
- The port number the service should listen on.
BACKEND_PLUGINS_PATH: 'backends'
- The directory where PyTerraBackTYL can find the Backend plugins.
- The value shown here means the
backends
subdirectory where PyTerraBackTYL is installed.
BACKEND_CLASS: 'pyshelve_backend.PyShelveBackend'
- The file and class name of the PyTerraBackTYL plugin to use; Python will look in a file called
pyshelve_backend.py
for the classPyShelveBackend
- The file and class name of the PyTerraBackTYL plugin to use; Python will look in a file called
POST_PROCESS_CLASSES: ['slack_notify_post_processor.SlackNotifyPostProcessor']
- A list where each item contains a file and class name of a PyTerraBackTYL nonpersistent plugin to use; Python will look in a file called
slack_notify_post_processor.py
for the classSlackNotifyPostProcessor
. Set this to an empty list (i.e.[]
) if you do not wish to use any post-processors.
- A list where each item contains a file and class name of a PyTerraBackTYL nonpersistent plugin to use; Python will look in a file called
LOG_LEVEL: 'INFO'
- The amount of information to log. Valid values are: INFO, DEBUG, WARNING, ERROR
- If an invalid value is specified, PyTerraBackTYL will default to INFO.
USE_SSL: false
- Disabled by default, this specifies if the service should use SSL (HTTPS) or not (HTTP).
- A Bash script can be found in the
ssl
subdirectory which will generate these keys.
SSL_PUBLIC_KEY: 'ssl/public.key'
- The path and filename where the SSL public key can be found.
- The value shown here means the
ssl/
subdirectory where PyTerraBackTYL is installed.
SSL_PRIVATE_KEY: 'ssl/private.key'
- The path and filename where the SSL private key can be found.
- The value shown here means the
ssl/
subdirectory where PyTerraBackTYL is installed.
HELPER_HOSTNAME_QUERY_MAP:
- Generally, this should not need to be changed.
- Contains key:value pairs where the key is the name of a Terraform provider (an exact match for what is found in a Terraform state file) and the value is a JSONPath that will return the hostnames found in a Terraform state file for that provider type.
- This configuration is used by the
TYLHelpers.get_hostnames_from_tfstate
function (from abc_tylstore import TYLHelpers
)
## IP and ports to listen on. Defaults shown.
BACKEND_SERVICE_IP: '0.0.0.0'
BACKEND_SERVICE_PORT: 2442
BACKEND_PLUGINS_PATH: 'backends'
BACKEND_CLASS: 'pyshelve_backend.PyShelveBackend'
POST_PROCESS_CLASSES:
- 'slack_notify_post_processor.SlackNotifyPostProcessor'
LOG_LEVEL: 'DEBUG' # INFO, DEBUG, WARNING, ERROR
# openssl req -x509 -nodes -days 3650 -newkey rsa:4096 -keyout private.key -out public.key -passout "pass:"
USE_SSL: false # Disabled by default -- generate SSL key pair then set this to 'true'
SSL_PUBLIC_KEY: 'ssl/public.key' # The path and filename of the public SSL key -- 'ssl/' is a subdirectory where PyTerraBackTYL is installed.
SSL_PRIVATE_KEY: 'ssl/private.key' # The path and file name of the private SSL key.
# Key:Value pairs where the key matches a Terraform provider, and key is a jsonPath to fetch hostnames from the terraform state
HELPER_HOSTNAME_QUERY_MAP:
digitalocean_droplet: 'modules[*].resources.*.primary.attributes.name'
vsphere_virtual_machine: '$.modules[*].resources.[?(@.type == "vsphere_virtual_machine")].primary[?(@.memory != 0)].name'
If this is your first time configuring PyTerraBackTYL, it is recommended that you start with this class to validate service configuration. Once you've validated PyTerraBackTYL is working as expected, you can change your BACKEND_CLASS
setting to the module you plan on using for handling Terraform state.
Modify config.yaml
and set the following items to the desired values.
PYSHELVE_DATA_PATH: '/opt/pyterrabacktyl/data'
- The directory to save Python 'shelve' files that contain state.
- The user running the PyTerraBackTYL service must have read/write access to this directory.
- Do NOT set this value to a temporary director (such as /tmp)
PYSHELVE_DB_FILE_NAME: '_terraform_state'
- The file name to use for Python 'shelve' oblects.
- Note: The name of the environment will be prepended to this file name (e.g. if 'QA' is set as the environment name, the file 'QA_terraform_state' will be created).
##
## pyshelve_backend.PyShelveBackend configuration
##
# The path to where the python shelf objects should be saved (no trailing slash).
PYSHELVE_DATA_PATH: '/opt/pyterrabacktyl/data'
# The filename for the tfstate shelf objects -- the environment name will be prepended
# e.g. for environment TEST, file name will be TEST_terraform_state
PYSHELVE_DB_FILE_NAME: '_terraform_state'
- SSH to the host running the PyTerraBackTYL service and become the user running the service (e.g.
su tfbackendsvc
) - Install GitPython:
pip3 install GitPython --user
- Create SSH keypair:
- Check if a keypair already exists:
ls -lah ~/.ssh
- Generate a keypair if needed:
ssh-keygen -t rsa
leave all prompts empty by pressing enter until the command completes.
- Check if a keypair already exists:
- Set the git global config for the tfbackendsvc user:
git config --global user.email "you@example.com"
git config --global user.name "Your Name"
- Create a new Git repository.
- WARNING: Make this repository private as
terraform.tfstate
files can contain sensitive information about your infrastructure. - This process will vary between various Git services (GitHub, GitLab, et. al.)
- WARNING: Make this repository private as
- Add the public key to grant permissions to the Git repository
- Copy and paste the contents of
~/.ssh/id_rsa.pub
to grant permissions for the tfbackendsvc user to make changes to the newly created repository. This process will vary between various Git services. - Do an initial clone, add, commit, and push to the repository as the tfbackendsvc user to verify everything is working as expected.
- Copy and paste the contents of
- Modify the
config.yaml
configuration file.- Optional settings:
GIT_WORKING_PATH: null
- When omitted or
null
, GitBackend will use the default temporary directory of the operating system. - If you set a value here, make sure the directory exists and that you have set the correct ownership/permissions.
- When omitted or
- Required settings:
GIT_REPOSITORY: 'git@github.com:dev-dull/backend_test.git'
- The newly created Git repository.
- Note: HTTP/HTTPS repositories are not currently supported.
GIT_DEFAULT_CLONE_BRANCH: 'origin/master'
- If you're unsure, just use the default here.
GIT_COMMIT_MESSAGE_FORMAT: '{Who}, {Operation} - {ID}'
- The string format to use when making commits to the repository
- valid values are:
- ID: An ID created by Terraform.
- Operation: The Terraform operation being carried out (plan, apply, etc.)
- Info: (TODO: check terraform documentation)
- Who: The username and hostname who initiated the terraform command.
- Version: The version of Terraform that was used.
- Created: Timestamp of when the terraform command ws run.
- Path: (TODO: check terraform documentation)
GIT_STATE_CHANGE_LOG_SCROLLBACK: 300
- The maximum number of rows to keep in log file commited alongside terraform.tfstate.
GIT_STATE_CHANGE_LOG_FILENAME: 'state_change.log'
- The name of the log file to be commited alongside terraform.tfstate
GIT_STATE_CHANGE_LOG_FORMAT: '{Created} - {Operation}: {Who} {ID}'
- The details to record in the log file commited alongside terraform.tfstate
- See the list of values for the commit message for valid values here.
- Optional settings:
##
## git_backend.GitBackend configuration
##
## --- Optional settings: ---
GIT_WORKING_PATH: null # Let the app create one in the temp directory of the OS.
## --- REQUIRED settings! ---
# The SSH repo to use to store tfstate, lock status, and logs.
GIT_REPOSITORY: 'git@github.com:dev-dull/backend_test.git' # No HTTP/S yet.
# The branch to clone when creating a new environment with Terraform.
GIT_DEFAULT_CLONE_BRANCH: 'origin/master'
# The format to use for commit messages. Options are case sensitive.
# Valid values: ID, Operation, Info, Who, Version, Created, Path
GIT_COMMIT_MESSAGE_FORMAT: '{Who}, {Operation} - {ID}'
# Maximum number of log messages to keep in the GIT_STATE_CHANGE_LOG_FILENAME file.
GIT_STATE_CHANGE_LOG_SCROLLBACK: 300
# Name of the log file to commit to record locks/unlocks
GIT_STATE_CHANGE_LOG_FILENAME: 'state_change.log'
# The format to use for logging messages. Options are case sensitive.
# Valid values: ID, Operation, Info, Who, Version, Created, Path
GIT_STATE_CHANGE_LOG_FORMAT: '{Created} - {Operation}: {Who} {ID}'
PyTerraBackTYL was developed with the expectation that you're looking at this project because you would like to manage your Terraform state file in a method not already handled by one of the standard backends. For this reason, PyTerraBackTYL allows you to implement your own backend handler. The following is a brief outline on how to implement a custom backend module. Refer to the PyShelveBacked class for an example.
Note: Except in very rare cases, you should not do any exception handling. Any failures should be allowed to be raised so the exception (HTTP status code 500) is sent to Terraform. This prevents Terraform from continuing and lets the user know the action did not work as expected.)
Import TYLPersistent
and define your subclass:
from abc_tylstore import TYLPersistent
class MyPersistentBackend(TYLPersistent):...
Implement the following functions in your class:
__init__(self, environment, constants, **kwargs):
environment
every environment will get a separate instance of your class and as a result, this value should be treated as a constant. Use this value to keep environment states isolated from each other. For example, if you are saving your Terraform states into plain text files, you'd likely want your file name to be something like<environment>_terraform.tfstate
constants
a python class containing constant values. These values are partly populated byconfig.yaml
as key:value pairs. For example, if you setMY_BACKEND_FOO: 'Hello, World!'
inconfig.yaml
, you can access the string value withconstants.MY_BACKEND_FOO
. This allows you to configure your backend module without the need of a separate configuration file.**kwargs
It is recommended to add this to help ensure forward compatibility of your module with future versions of PyTerraBackTYL.
def set_locked(self, state_obj, **kwargs):
state_obj
Unpacked JSON (dict
) with the specifics on who is putting a lock on the environment.**kwargs
includesraw
which contains the original JSON string (str
) value.- RETURNS: value that evaluates to
True
on a successful lock; value that evaluates toFalse
when someone else is already holding the lock.
def set_unlocked(self, state_obj, **kwargs):
state_obj
Unpacked JSON (dict
) with the specifics on who is unlocking the environment. TheID
value should match the one provided when lock was created. However, when a user issues aterraform force-unlock <ID value>
command, the ID is not currently being passed to the backend. Validating the ID in this function will make forcing an unlock impossible until HashiCorp addresses this deficiency.**kwargs
includesraw
which contains the original JSON string (str
) value.- RETURNS: value that evaluates to
True
on a successful unlock; value that evaluates toFalse
when the environment is NOT still locked (logs warning message).
def get_lock_state(self):
- RETURNS: The string (
str
) or JSON compatible object of the lock (the value received inset_locked()
). Return and empty string when no lock is held.
- RETURNS: The string (
def store_tfstate(self, tfstate_obj, **kwargs):
tfstate_obj
Unpacked JSON (dict
) which specifies the current terraform state (terraform.tfstate
)**kwargs
includesraw
which contains the original JSON string (str
) value.- No return value. Return
None
if needed.
def get_tfstate(self):
- RETURNS: The string (
str
) or JSON compatible object of the Terraform state (the value received instore_tfstate()
).
- RETURNS: The string (
def backend_status(self):
OPTIONAL- RETURNS: A JSON compatible object containing health and status information about the backend. This is a good place to report where data is being stored and lock state for the environment.
su tfbackendsvc
cd /opt/pyterrabacktyl/PyTerraBackTyl
./start.sh
Modify this script to change the location of log file if needed.curl -s http://localhost:2442/state
Check the current state of the backend. When first started, output will appear similar to the below.
{
"backend_module": "pyshelve_backend.PyShelveBackend",
"post_processor_modules": [
"zenoss_post_processor.ZenossPostProcessor",
"slack_notify_post_processor.SlackNotifyPostProcessor"
],
"environments": []
}
In your project, configure the HTTP backend service:
terraform {
backend "http" {
address = "http://localhost:2442/?env=DEVTEST"
lock_address = "http://localhost:2442/lock?env=DEVTEST"
unlock_address = "http://localhost:2442/unlock?env=DEVTEST"
skip_cert_verification = "true"
}
}
Remember to change 'localhost' to the hostname or IP of where the PyTerraBackTYL service is running, and to change http
to https
if you are using SSL.
WARNING: The ?env=YOURVALUE
is how PyTerraBackTYL tracks states across multiple environments (e.g. production, test, QA, etc.). Be sure that you are always setting the value of this parameter to accurately to reflect the environment you are making changes to.
Similarly to how custom backend modules are managed, you can also create a TYLNonpersistent
backend. These are optional backends that don't store any locking or state data, but rather allow you the opportunity to parse the Terraform lock and state files to perform other actions. Examples of tasks you may want to perform with a non-persistent backed include adding new hosts to monitoring, generating a chat notification that a user has performed an action, generating a report email containing a list of new hostnames, etc. These non-persistent backends should be kept in the same directory as the persistent backends (e.g. the backends
directory). You can reference the SlackNotifyPostProcessor as an example.
Note: All exceptions raised by non-persistent backends are ignored as they cannot be allowed to interfere with the functionality of the persistent backend which is handling state and locking.
Example config.yaml
configuration for multiple non-persistent backends:
POST_PROCESS_CLASSES:
- 'zenoss_post_processor.ZenossPostProcessor'
- 'slack_notify_post_processor.SlackNotifyPostProcessor'
Import TYLNonpersistent
and define your subclass:
from abc_tylstore import TYLNonpersistent
class MyNonpersistentBackend(TYLNonpersistent):...
Implement the following functions in your class:
__init__(self, environment, constants, **kwargs):
environment
every environment will get a separate instance of your class and as a result, this value should be treated as a constant.constants
a python class containing constant values. These values are partly populated byconfig.yaml
as key:value pairs. For example, if you setMY_POST_PROCESSOR_FOO: 'Hello, World!'
inconfig.yaml
, you can access the string value withconstants.MY_POST_PROCESSOR_FOO
. This allows you to configure your backend module without the need of a separate configuration file.**kwargs
It is recommended to add this to help ensure forward compatibility of your module with future versions of PyTerraBackTYL.
def on_locked(self, state_obj, **kwargs):
state_obj
Unpacked JSON (dict
) with the specifics on who is putting a lock on the environment.**kwargs
includesraw
which contains the original JSON string (str
) value.- No return value. Return
None
if needed.
def on_unlocked(self, state_obj, **kwargs):
state_obj
Unpacked JSON (dict
) with the specifics on who is unlocking the environment.**kwargs
includesraw
which contains the original JSON string (str
) value.- No return value. Return
None
if needed.
def process_tfstate(self, tfstate_obj, **kwargs):
tfstate_obj
Unpacked JSON (dict
) which specifies the current terraform state (terraform.tfstate
)**kwargs
includesraw
which contains the original JSON string (str
) value.- No return value. Return
None
if needed.
def post_processor_status(self):
OPTIONAL- RETURNS: A JSON compatible object containing health and status information about the backend. This is a good place to report where data is being stored.
- As always, check the log. If you haven't modified the default start script, a new subdirectory called
logs
will be created in the same location as the script. - Check what information is found in the
/state
endpoint. You can monitor state changes with the commandwatch -d "curl -s http://localhost:2442/state"
.
PyTerraBackTYL art and logos are based on the creative works of Matthew Green. Show your support and visit his website TetraVariations, or purchase something rad from his Etsy store!