The Shell provider (https://github.com/scottwinkler/terraform-provider-shell) is a third-party provider proudly developed and maintained by yours truly. This provider is available on the Terraform Registry and allows you to create custom resources by invoking shell scripts, alleviating the need to create one-off Terraform providers for specific tasks. Many people find it useful for patching gaps in existing providers or creating specific utility functions. This appendix covers how to install the provider and goes through some examples of what can be done with it.
To install a custom Terraform provider, you first have to declare that you want to use a custom Terraform provider. Each Terraform module must declare which providers it requires, and I usually put this information in versions.tf, as provider requirements are declared in the required_providers
block of Terraform settings. This is used to source the provider from the Terraform Registry:
terraform { required_providers { shell = { source = "scottwinkler/shell" version = "~> 1.0" } } }
Note Terraform first checks the local directory and ~/.terraform.d/plugins before it checks the Terraform Registry.
You can now install the third-party provider by running a normal terraform
init
:
$ terraform init Initializing the backend... Initializing provider plugins... - Finding scottwinkler/shell versions matching "~> 1.0"... - Installing scottwinkler/shell v1.7.3... - Installed scottwinkler/shell v1.7.3 (self-signed, key ID 2CAB13AD54B7DF3D) Partner and community providers are signed by their developers. If you'd like to know more about provider signing, you can read about it here: https://www.terraform.io/docs/plugins/signing.html Terraform has been successfully initialized! You may now begin working with Terraform. Try running "terraform plan" to see any changes that are required for your infrastructure. All Terraform commands should now work. If you ever set or change modules or backend configuration for Terraform, rerun this command to reinitialize your working directory. If you forget, other commands will detect it and remind you to do so if necessary.
Once you have the Shell provider installed, you can access two new resources: a shell_script
resource and a shell_script
data source. These two resources allow you to create custom resources in Terraform by specifying commands that will be run during Terraform CRUD operations. You can also set computed attributes and reference them from Terraform. For example, the following listing shows a simple data source that can read the current logged in user with whoami
:
Listing D.1 Shell script data source
terraform { required_providers { shell = { source = "scottwinkler/shell" version = "~> 1.0" } } } data "shell_script" "user" { lifecycle_commands { read = <<-EOF echo "{"user": "$(whoami)"}" ❶ EOF } } output "user" { value = data.shell_script.user.output["user"] ❷ }
❶ Sets the output of the custom data source
If you ran this, you would get the following:
$ terraform apply -auto-approve data.shell_script.user: Refreshing state... Apply complete! Resources: 0 added, 0 changed, 0 destroyed. Outputs: user = swinkler
TIP This pattern could also be used to read generic environment variables into Terraform variables.
I know what you might be thinking: a data source that calls external scripts is not particularly useful or interesting; the same thing could also be done with external data sources or the Null provider. What makes the Shell provider unique is its support for managed resources that implement the full lifecycle of Terraform resources. This example implementation gets the current weather in London and saves it to a local file.
Listing D.2 Shell script resource
terraform { required_providers { shell = { source = "scottwinkler/shell" version = "~> 1.0" } } } resource "shell_script" "weather" { lifecycle_commands { create = <<-EOF echo "{"London": "$(curl wttr.in/London?format="%l:+%c+%t")"}" > state.json cat state.json EOF delete = "rm state.json" } } output "weather" { value = shell_script.weather.output["London"] }
Applying this queries the weather from wttr.in and saves it into a local file called state.json:
$ terraform apply -auto-approve shell_script.weather: Creating... shell_script.weather: Creation complete after 0s [id=bpcrf2dgrkri1bd7rgsg] Apply complete! Resources: 1 added, 0 changed, 0 destroyed. Outputs: weather = London: ⛅️ +14°C
Since it’s a normal Terraform resource, it participates in the resource lifecycle by saving state to the state file:
$ terraform state show shell_script.weather
# shell_script.weather:
resource "shell_script" "weather" {
dirty = false
id = "btdk3gdgrkru9f4634h0"
output = {
"London" = "London: ⛅️ +14°C"
}
working_directory = "."
lifecycle_commands {
create = <<~EOT
echo "{"London": "$(curl wttr.in/London?format="%l:+%c+%t")"}" > state.json
cat state.json
EOT
delete = "rm state.json"
}
}
Additionally, you can see that a new file has been created, state.json. This file stores the output of the command and represents a managed resource:
$ cat state.json
{"London": "London: ☁ +14°C"}
Calling terraform
destroy
ensures that the state.json file is deleted:
$ terraform destroy -force shell_script.weather: Refreshing state... [id=bpcrg45grkri1sm1kf00] shell_script.weather: Destroying... [id=bpcrg45grkri1sm1kf00] shell_script.weather: Destruction complete after 0s Destroy complete! Resources: 1 destroyed. You can verify that it has been deleted by cat-ing it out once more: $ cat state.json cat: state.json: No such file or directory
The Shell provider can be used for much more than what we have seen. It supports full CRUD resource lifecycle management and allows you to do almost anything that would normally only be possible by writing a custom Terraform provider. Because it stores stateful information like any other Terraform resource and supports read and update capabilities, it’s more versatile than Null resources with attached local-exec
provisioners. To give you an idea of what is possible, here is an example of using the Shell provider to create a GitHub repository:
variable "oauth_token" { type = string } provider "shell" { sensitive_environment = { OAUTH_TOKEN = var.oauth_token } } resource "shell_script" "github_repository" { lifecycle_commands { create = file("${path.module}/scripts/create.sh") read = file("${path.module}/scripts/read.sh") update = file("${path.module}/scripts/update.sh") delete = file("${path.module}/scripts/delete.sh") } environment = { NAME = "My-Github-Repo-Name" DESCRIPTION = "some description" } }
Note For the complete example, refer to the Shell provider documentation: http://mng.bz/VG8P.
3.235.60.197