|
| 1 | ++++ |
| 2 | +date = '2026-02-05T19:50:51+05:00' |
| 3 | +draft = true |
| 4 | +title = 'Terraform Provider Development Experience' |
| 5 | +categories = ["Terraform", "Go"] |
| 6 | ++++ |
| 7 | + |
| 8 | +Developing your own terraform provider might be required in case |
| 9 | +you have some internal services. And that service and your terraform |
| 10 | +modules are used by many teams. Such cases having your own provider |
| 11 | +and distributing makes terraform code cleaner, allows to avoid glue |
| 12 | +shell scripts. |
| 13 | + |
| 14 | +I did develop few providers for our internal services, below want to |
| 15 | +list points which was interesting for me, or points which I missed |
| 16 | +initially. |
| 17 | + |
| 18 | +## SDK version |
| 19 | + |
| 20 | +There is two SDK versions are available to develop provider: [SDKv2](https://developer.hashicorp.com/terraform/plugin/sdkv2) |
| 21 | +and [plugin framework](https://developer.hashicorp.com/terraform/plugin/framework). |
| 22 | +Later is newer and recommended by terraform, but that |
| 23 | +doesn't mean SDKv2 is deprecated, it is still widely used. Probably |
| 24 | +it will take years before all will be migrated to new one. In my case |
| 25 | +I used only plugin framework as recommended. Also there was need to |
| 26 | +read other providers which uses SDKv2, and there is no problems to do |
| 27 | +that. There is differences between them but probably because of same |
| 28 | +protocol it is easy to understand what is going on even in case of SDKv2. |
| 29 | + |
| 30 | +## Pay attention to how import works |
| 31 | + |
| 32 | +If you start provider development using [scaffolding app template](https://github.com/hashicorp/terraform-provider-scaffolding-framework) |
| 33 | +then you see like this code for resource: |
| 34 | + |
| 35 | +```go |
| 36 | +func (r *ExampleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { |
| 37 | + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) |
| 38 | +} |
| 39 | +``` |
| 40 | + |
| 41 | +I initially didn't pay attention much, but what is written here actually linked how |
| 42 | +you would implement your `Read` function. Basically it means when "import" is |
| 43 | +happening your `Read` function will be executed, and from fields only `id` |
| 44 | +will be available. It impacts how you define your ID, you should be able to |
| 45 | +read resource purely by ID. It sounds easy, but in some cases resource could |
| 46 | +be available only in nested way. For example, let's say you have resource |
| 47 | +by this path: `/policy/<policyID>/rules/<ruleID>`. |
| 48 | + |
| 49 | +Initially you could select `ruleID` as id for your `PolicyRule` resource, which |
| 50 | +brings a problem with import. For that reason probably you should make your |
| 51 | +ID in this format `<policyID>/<ruleID>`, doing this you can fetch rule purely |
| 52 | +by ID: |
| 53 | + |
| 54 | +```go |
| 55 | +func (r *ExampleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { |
| 56 | + var resourceID types.String |
| 57 | + req.State.GetAttribute(ctx, path.Root("resourceID"), &resourceID) |
| 58 | + |
| 59 | + parts := strings.Split(resourceID, "/") |
| 60 | + policyID := parts[0] |
| 61 | + ruleID := parts[1] |
| 62 | + |
| 63 | + // Define your http req |
| 64 | + |
| 65 | + // If applicable, this is a great opportunity to initialize any necessary |
| 66 | + // provider client data and make a call using it. |
| 67 | + // httpResp, err := r.client.Do(httpReq) |
| 68 | + // if err != nil { |
| 69 | + // resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read example, got error: %s", err)) |
| 70 | + // return |
| 71 | + // } |
| 72 | + |
| 73 | + // Save updated data into Terraform state |
| 74 | + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) |
| 75 | +} |
| 76 | +``` |
| 77 | + |
| 78 | +This of course means import should be run like this: |
| 79 | + |
| 80 | +``` |
| 81 | +terraform import <policyID>/<ruleID> |
| 82 | +``` |
| 83 | + |
| 84 | +## Keeping client code in same repo as your provider |
| 85 | + |
| 86 | +There was recommendation like better to not keep your client code to service |
| 87 | +in same repository as your provider. But for me it was better option |
| 88 | +to avoid additional maintenance as service client wasn't going |
| 89 | +to be used by anyone else. Probably it means if your company big enough |
| 90 | +to have internal services, but not big emough to have Go sdk's for them |
| 91 | +don't hesitate to keep your service's client code in same repository. |
| 92 | +Of course keep isolation between client code and provider's (mixing them |
| 93 | +definetely not a good idea). |
| 94 | + |
| 95 | +For me it simlified overall development, as service client and provider |
| 96 | +implemented in same time. |
| 97 | + |
| 98 | +## Logging approach |
| 99 | + |
| 100 | +In terraform structured [logging](https://developer.hashicorp.com/terraform/tutorials/providers-plugin-framework/providers-plugin-framework-logging) is used. |
| 101 | +In my case there was need to log not only from provider, but to have logs |
| 102 | +from client code as well. Also I wanted to keep some isolation (in case I would need to |
| 103 | +separate client code to separate repository). For that I implemented |
| 104 | +interface: |
| 105 | + |
| 106 | +```go |
| 107 | +interface Logger { |
| 108 | + Debug(ctx context.Context, msg string) |
| 109 | + Info(ctx context.Context, msg string) |
| 110 | +} |
| 111 | +``` |
| 112 | + |
| 113 | +In the provider side add logger structure: |
| 114 | + |
| 115 | +```go |
| 116 | +type ClientLogger struct { |
| 117 | +} |
| 118 | + |
| 119 | +func (c *ClientLogger) Debug(ctx context.Context, msg string) { |
| 120 | + tflog.Debug(ctx, msg) |
| 121 | +} |
| 122 | + |
| 123 | +func (c *ClientLogger) Info(ctx context.Context, msg string) { |
| 124 | + tflog.Info(ctx, msg) |
| 125 | +} |
| 126 | +``` |
| 127 | + |
| 128 | +In your client code add possibility to get logger as parameter: |
| 129 | + |
| 130 | +```go |
| 131 | +type ExampleClient struct{ |
| 132 | + Logger *ClientLogger |
| 133 | +} |
| 134 | + |
| 135 | +func (c *ExampleClient) Execute(ctx context.Context) { |
| 136 | + // this log appears if TF_LOG=INFO |
| 137 | + c.Logger.Info(ctx, "Starting execute something") |
| 138 | + // do something |
| 139 | + // add more debugging logs if needed, this log appears if TF_LOG=DEBUG |
| 140 | + c.Logger.Debug(ctx, "Debugging information") |
| 141 | +} |
| 142 | +``` |
| 143 | + |
| 144 | +In my case logging as much as possible information in case of error |
| 145 | +was very helpful. Specially during executions of terraform in pipelines. |
| 146 | + |
| 147 | +Also check diagnostics logis: https://developer.hashicorp.com/terraform/plugin/framework/diagnostics |
| 148 | + |
0 commit comments