Building a Terraform Provider-Part-IV — Import and Build

Allan John
4 min readJun 6, 2022

--

Image from Unsplash

Introduction

This is a continuation of Part-IV. We have all the stuff for doing CRUD operations. But what about an already existing resource? It would be nice to have the import function working as well for a Terraform provider to be deemed complete. So lets get on it.

Import

First we need to add the Import function. The purpose is to

  • Get the ID of the resource provided in the terraform import command
  • Read the Record with the ID
  • Set the fqdn, ipaddress, and ID of the resource to be imported and pass it to Import Context

We have a slight issue with Import. Because when we create a resource using the Custom RestAPI, we are passing Owners list, to be made as owners when the record is being created. So this will be stored in the state. But when we are importing, Azure API does not return any info on Owners. So when we import, and try to run a terraform plan, there will be changes on updating the Owners list. So when importing the resource, we always need to run a plan and apply to add the Owners to the state.

So lets get on with the function to Import the state and then add the context. Modify the file records/resource_dnsARecord.go, and add the function:

//resourceRestAPIImport - Function to import a resource
func resourceRestAPIImport(d *schema.ResourceData, m interface{}) (imported []*schema.ResourceData, err error) {
//Calling the client
c := m.(*client.Client)

// Get the resource ID
recordID := d.Id()

// Read the Record information
res, err := c.GetRecord(recordID)
if err != nil {
return nil, err
}

// Setting the values from the response into a way terraform understands
d.Set("fqdn", res.Properties.Fqdn)
for _, ip := range res.Properties.ARecords {
d.Set("ipaddress", ip.Ipv4Address)
}

// Set the ID with the resource ID
d.SetId(res.ID)

// Appending data to the imported data list
imported = append(imported, d)

// Return imported data
return imported, nil
}

Now modify the context and add the following code in the context section:

...
// resourceARecord - Function that implements all reequests for the records
func resourceDnsARecord() *schema.Resource {
return &schema.Resource{
...
DeleteContext: resourceARecordDelete,
Importer: &schema.ResourceImporter{
State: resourceRestAPIImport,
},

And now we have an import section!

Build

Now we can build our provider for testing. I have a nice Makefile here which makes life easier

TEST?=$$(go list -buildvcs=false ./... | grep -v 'vendor')
HOSTNAME=terraform.test.com
NAMESPACE=alpha
NAME=rest
BINARY=terraform-provider-${NAME}
VERSION=0.0.2
OS_ARCH=linux_amd64
default: installbuild:
go build -buildvcs=false -o ${BINARY}
release:
goreleaser release --rm-dist --snapshot --skip-publish --skip-sign
install: build
mkdir -p ~/.terraform.d/plugins/${HOSTNAME}/${NAMESPACE}/${NAME}/${VERSION}/${OS_ARCH}
mv ${BINARY} ~/.terraform.d/plugins/${HOSTNAME}/${NAMESPACE}/${NAME}/${VERSION}/${OS_ARCH}
test:
go test -i $(TEST) || exit 1
echo $(TEST) | xargs -t -n4 go test $(TESTARGS) -timeout=30s -parallel=4
testacc:
TF_ACC=1 go test $(TEST) -v $(TESTARGS) -timeout 120m

All of this is kind of self-explanatory. Something to note is:

  • Added parameter -buildvcs=false for the build process. So when you are building multiple packages in Go, you have the possibility to add the pacakges to be used as a git repo, or in the same repo, like I did. So when we build like that, if we do not enable this option, then Go will cry a lot and will not build the binary
  • Created HOSTNAME and NAMESPACE for Terraform build. This is something to understand, because Terraform downloads plugins from internet when terraform init is run. So if the binary built is supposed to be non-public, then the only possible solutions are to setup a private terraform registry, or copy the binaries manually to proper location where Terraform is running. If the HOSTNAME is a domain name, then Terraform always downloads, but if there is a subdomain, Terraform first checks locally and if the plugin is there, it will not download.

So to build the package, simply run

make install

This will build and place the provider in proper directory

Test

Time to test the package. In examples, modify the main.tf and the code:

terraform {
required_providers {
pe = {
version = "~> 0.0.2"
source = "terraform.test.com/alpha/records"
}
}
}
provider "records" {
username = "tester"
password = "tester@123"
custom_client_url = "https://localhost:23543"
}
resource "dns_a_record" "test_record" {
ipaddress = "192.168.8.9"
fqdn = "tester.private.hosted.zone.net"
owners {
owner_type = "User"
resource_owner = "allanjohn909@gmail.com"
}
}

As you can see, I am adding the new built provider in required_providers section and adding the provider{} with the credentials and inputs. I add this variables just for testing, I usually keep the provider empty and pass all the variables through environment variables.

And then I am creating a resource, as configured in the Provider with the values.

If all looks good, the provider can create, update, delete and import resource now!.

Deployment

The best option for deploying the Terraform provider is using the Hashicorp Terraform Registry. But if the provider is something to be non-public, then the options are:

  • Create a Private Registry with the instance running Terraform has access to the registry and push the provider there
  • Copy the binary manually to the instance where Terraform is running. The plugin can be stored in a different location, but then it should be installed using the provider installation method provided by Terraform

Conclusion

So now we have a simple Terraform Provider which is build, and I hope you understood. I wrote this doc, because it was really hard for me to look a lot of places to build one, and everywhere there was a lack of documentation. So I thought the next guy who wants to build can make use of this doc. Hope you enjoyed !

--

--