Client Configuration
HTTP Client Configuration
Most Terraform providers communicate with APIs over HTTP. Proper client configuration is essential for reliability, performance, and user experience.
In this chapter, you’ll learn how to configure HTTP clients with timeouts, retries, custom headers, and more.
Basic HTTP Client
Simple Client
func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
// Basic HTTP client
httpClient := &http.Client{
Timeout: 30 * time.Second,
}
client := api.NewClient(httpClient)
resp.ResourceData = client
}
With Custom Transport
func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
}
httpClient := &http.Client{
Timeout: 30 * time.Second,
Transport: transport,
}
client := api.NewClient(httpClient)
resp.ResourceData = client
}
Timeout Configuration
Schema
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"timeout": schema.NumberAttribute{
Optional: true,
MarkdownDescription: "Request timeout in seconds. Defaults to 30.",
Validators: []validator.Number{
numbervalidator.Between(1, 300),
},
},
"connect_timeout": schema.NumberAttribute{
Optional: true,
MarkdownDescription: "Connection timeout in seconds. Defaults to 10.",
Validators: []validator.Number{
numbervalidator.Between(1, 60),
},
},
"read_timeout": schema.NumberAttribute{
Optional: true,
MarkdownDescription: "Read timeout in seconds. Defaults to 30.",
Validators: []validator.Number{
numbervalidator.Between(1, 300),
},
},
},
}
Implementation
func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config MyCloudProviderModel
req.Config.Get(ctx, &config)
// Parse timeouts with defaults
timeout := 30 * time.Second
if !config.Timeout.IsNull() {
timeoutFloat, _ := config.Timeout.ValueBigFloat().Int64()
timeout = time.Duration(timeoutFloat) * time.Second
}
connectTimeout := 10 * time.Second
if !config.ConnectTimeout.IsNull() {
timeoutFloat, _ := config.ConnectTimeout.ValueBigFloat().Int64()
connectTimeout = time.Duration(timeoutFloat) * time.Second
}
readTimeout := 30 * time.Second
if !config.ReadTimeout.IsNull() {
timeoutFloat, _ := config.ReadTimeout.ValueBigFloat().Int64()
readTimeout = time.Duration(timeoutFloat) * time.Second
}
// Configure transport with timeouts
transport := &http.Transport{
DialContext: (&net.Dialer{
Timeout: connectTimeout,
}).DialContext,
ResponseHeaderTimeout: readTimeout,
}
httpClient := &http.Client{
Timeout: timeout,
Transport: transport,
}
client := api.NewClient(httpClient)
resp.ResourceData = client
}
Usage
provider "mycloud" {
api_key = var.api_key
timeout = 60 # Overall request timeout
connect_timeout = 15 # Connection timeout
read_timeout = 45 # Read timeout
}
Retry Logic
Schema
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"max_retries": schema.NumberAttribute{
Optional: true,
MarkdownDescription: "Maximum number of retry attempts. Defaults to 3.",
Validators: []validator.Number{
numbervalidator.Between(0, 10),
},
},
"retry_delay": schema.NumberAttribute{
Optional: true,
MarkdownDescription: "Initial retry delay in seconds. Defaults to 1. Uses exponential backoff.",
Validators: []validator.Number{
numbervalidator.Between(1, 30),
},
},
},
}
Implementation with Retry Transport
type retryTransport struct {
Transport http.RoundTripper
MaxRetries int
RetryDelay time.Duration
}
func (t *retryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
var resp *http.Response
var err error
for attempt := 0; attempt <= t.MaxRetries; attempt++ {
// Clone request for retry
clonedReq := req.Clone(req.Context())
// Attempt request
resp, err = t.Transport.RoundTrip(clonedReq)
// Check if we should retry
if err == nil && !shouldRetry(resp.StatusCode) {
return resp, nil
}
// Don't retry on last attempt
if attempt == t.MaxRetries {
break
}
// Exponential backoff
delay := t.RetryDelay * time.Duration(1<<uint(attempt))
time.Sleep(delay)
}
return resp, err
}
func shouldRetry(statusCode int) bool {
// Retry on server errors and rate limits
return statusCode >= 500 || statusCode == 429
}
Configure Method
func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config MyCloudProviderModel
req.Config.Get(ctx, &config)
// Parse retry config
maxRetries := 3
if !config.MaxRetries.IsNull() {
retriesFloat, _ := config.MaxRetries.ValueBigFloat().Int64()
maxRetries = int(retriesFloat)
}
retryDelay := 1 * time.Second
if !config.RetryDelay.IsNull() {
delayFloat, _ := config.RetryDelay.ValueBigFloat().Int64()
retryDelay = time.Duration(delayFloat) * time.Second
}
// Create client with retry logic
baseTransport := http.DefaultTransport
transport := &retryTransport{
Transport: baseTransport,
MaxRetries: maxRetries,
RetryDelay: retryDelay,
}
httpClient := &http.Client{
Timeout: 30 * time.Second,
Transport: transport,
}
client := api.NewClient(httpClient)
resp.ResourceData = client
}
Usage
provider "mycloud" {
api_key = var.api_key
max_retries = 5
retry_delay = 2 # seconds
}
Custom Headers
Schema
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"user_agent": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Custom User-Agent header",
},
"extra_headers": schema.MapAttribute{
ElementType: types.StringType,
Optional: true,
MarkdownDescription: "Additional HTTP headers to include in all requests",
},
},
}
Implementation
type headerTransport struct {
Transport http.RoundTripper
UserAgent string
ExtraHeaders map[string]string
}
func (t *headerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Add user agent
if t.UserAgent != "" {
req.Header.Set("User-Agent", t.UserAgent)
}
// Add extra headers
for key, value := range t.ExtraHeaders {
req.Header.Set(key, value)
}
return t.Transport.RoundTrip(req)
}
func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config MyCloudProviderModel
req.Config.Get(ctx, &config)
// Parse extra headers
extraHeaders := make(map[string]string)
if !config.ExtraHeaders.IsNull() {
config.ExtraHeaders.ElementsAs(ctx, &extraHeaders, false)
}
// Default user agent
userAgent := fmt.Sprintf("terraform-provider-mycloud/%s", p.version)
if !config.UserAgent.IsNull() {
userAgent = config.UserAgent.ValueString()
}
transport := &headerTransport{
Transport: http.DefaultTransport,
UserAgent: userAgent,
ExtraHeaders: extraHeaders,
}
httpClient := &http.Client{
Timeout: 30 * time.Second,
Transport: transport,
}
client := api.NewClient(httpClient)
resp.ResourceData = client
}
Usage
provider "mycloud" {
api_key = var.api_key
user_agent = "MyCustomAgent/1.0"
extra_headers = {
"X-Custom-Header" = "value"
"X-Request-ID" = "12345"
}
}
Connection Pooling
Schema
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"max_connections": schema.NumberAttribute{
Optional: true,
MarkdownDescription: "Maximum number of idle connections. Defaults to 100.",
},
"max_connections_per_host": schema.NumberAttribute{
Optional: true,
MarkdownDescription: "Maximum idle connections per host. Defaults to 10.",
},
"idle_connection_timeout": schema.NumberAttribute{
Optional: true,
MarkdownDescription: "Idle connection timeout in seconds. Defaults to 90.",
},
},
}
Implementation
func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config MyCloudProviderModel
req.Config.Get(ctx, &config)
// Parse connection pool settings
maxConns := 100
if !config.MaxConnections.IsNull() {
connsFloat, _ := config.MaxConnections.ValueBigFloat().Int64()
maxConns = int(connsFloat)
}
maxConnsPerHost := 10
if !config.MaxConnectionsPerHost.IsNull() {
connsFloat, _ := config.MaxConnectionsPerHost.ValueBigFloat().Int64()
maxConnsPerHost = int(connsFloat)
}
idleTimeout := 90 * time.Second
if !config.IdleConnectionTimeout.IsNull() {
timeoutFloat, _ := config.IdleConnectionTimeout.ValueBigFloat().Int64()
idleTimeout = time.Duration(timeoutFloat) * time.Second
}
transport := &http.Transport{
MaxIdleConns: maxConns,
MaxIdleConnsPerHost: maxConnsPerHost,
IdleConnTimeout: idleTimeout,
MaxConnsPerHost: maxConnsPerHost * 2,
}
httpClient := &http.Client{
Timeout: 30 * time.Second,
Transport: transport,
}
client := api.NewClient(httpClient)
resp.ResourceData = client
}
TLS Configuration
Schema
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"insecure_skip_verify": schema.BoolAttribute{
Optional: true,
MarkdownDescription: "Skip TLS certificate verification. Not recommended for production.",
},
"ca_cert": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Path to CA certificate file",
},
"min_tls_version": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Minimum TLS version: '1.0', '1.1', '1.2', '1.3'. Defaults to '1.2'.",
Validators: []validator.String{
stringvalidator.OneOf("1.0", "1.1", "1.2", "1.3"),
},
},
},
}
Implementation
func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config MyCloudProviderModel
req.Config.Get(ctx, &config)
// TLS configuration
tlsConfig := &tls.Config{}
// Skip verify (for dev/testing only)
if !config.InsecureSkipVerify.IsNull() {
tlsConfig.InsecureSkipVerify = config.InsecureSkipVerify.ValueBool()
if tlsConfig.InsecureSkipVerify {
tflog.Warn(ctx, "TLS certificate verification is disabled. This is not recommended for production.")
}
}
// CA certificate
if !config.CACert.IsNull() {
caCertPath := config.CACert.ValueString()
caCert, err := os.ReadFile(caCertPath)
if err != nil {
resp.Diagnostics.AddError(
"Failed to Load CA Certificate",
fmt.Sprintf("Could not read CA certificate: %s", err.Error()),
)
return
}
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
resp.Diagnostics.AddError(
"Invalid CA Certificate",
"Could not parse CA certificate",
)
return
}
tlsConfig.RootCAs = caCertPool
}
// Minimum TLS version
minTLSVersion := config.MinTLSVersion.ValueString()
switch minTLSVersion {
case "1.0":
tlsConfig.MinVersion = tls.VersionTLS10
case "1.1":
tlsConfig.MinVersion = tls.VersionTLS11
case "1.3":
tlsConfig.MinVersion = tls.VersionTLS13
default: // "1.2" or empty
tlsConfig.MinVersion = tls.VersionTLS12
}
transport := &http.Transport{
TLSClientConfig: tlsConfig,
}
httpClient := &http.Client{
Timeout: 30 * time.Second,
Transport: transport,
}
client := api.NewClient(httpClient)
resp.ResourceData = client
}
Usage
provider "mycloud" {
api_key = var.api_key
ca_cert = "/path/to/ca.crt"
min_tls_version = "1.2"
# insecure_skip_verify = true # Don't use in production!
}
Complete Client Configuration
Comprehensive Example
func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config MyCloudProviderModel
diags := req.Config.Get(ctx, &config)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Get API key
apiKey := config.APIKey.ValueString()
if apiKey == "" {
apiKey = os.Getenv("MYCLOUD_API_KEY")
}
if apiKey == "" {
resp.Diagnostics.AddError("Missing API Key", "...")
return
}
// Parse timeouts
timeout := parseTimeout(config.Timeout, 30)
connectTimeout := parseTimeout(config.ConnectTimeout, 10)
readTimeout := parseTimeout(config.ReadTimeout, 30)
// Parse retry config
maxRetries := parseInt(config.MaxRetries, 3)
retryDelay := parseTimeout(config.RetryDelay, 1)
// Parse connection pool config
maxConns := parseInt(config.MaxConnections, 100)
maxConnsPerHost := parseInt(config.MaxConnectionsPerHost, 10)
idleTimeout := parseTimeout(config.IdleConnectionTimeout, 90)
// TLS configuration
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
}
// Build transport
transport := &http.Transport{
DialContext: (&net.Dialer{
Timeout: connectTimeout,
}).DialContext,
ResponseHeaderTimeout: readTimeout,
MaxIdleConns: maxConns,
MaxIdleConnsPerHost: maxConnsPerHost,
IdleConnTimeout: idleTimeout,
TLSClientConfig: tlsConfig,
}
// Add authentication
transport = &apiKeyTransport{
APIKey: apiKey,
Transport: transport,
}
// Add retry logic
transport = &retryTransport{
Transport: transport,
MaxRetries: maxRetries,
RetryDelay: retryDelay,
}
// Add custom headers
userAgent := fmt.Sprintf("terraform-provider-mycloud/%s", p.version)
transport = &headerTransport{
Transport: transport,
UserAgent: userAgent,
}
// Create HTTP client
httpClient := &http.Client{
Timeout: timeout,
Transport: transport,
}
// Create API client
endpoint := config.Endpoint.ValueString()
if endpoint == "" {
endpoint = "https://api.mycloud.com"
}
client := api.NewClient(api.Config{
HTTPClient: httpClient,
BaseURL: endpoint,
})
// Test connection
if err := client.TestConnection(ctx); err != nil {
resp.Diagnostics.AddError(
"Connection Test Failed",
fmt.Sprintf("Could not connect to API: %s", err.Error()),
)
return
}
// Share with resources
resp.ResourceData = client
resp.DataSourceData = client
}
func parseTimeout(attr types.Number, defaultSeconds int) time.Duration {
if attr.IsNull() {
return time.Duration(defaultSeconds) * time.Second
}
seconds, _ := attr.ValueBigFloat().Int64()
return time.Duration(seconds) * time.Second
}
func parseInt(attr types.Number, defaultValue int) int {
if attr.IsNull() {
return defaultValue
}
value, _ := attr.ValueBigFloat().Int64()
return int(value)
}
Best Practices
1. Provide Sensible Defaults
// ✅ Good: Defaults for all timeouts
timeout := 30 * time.Second
if !config.Timeout.IsNull() {
timeout = parseTimeout(config.Timeout)
}
2. Validate Configuration
// ✅ Validate timeout ranges
"timeout": schema.NumberAttribute{
Optional: true,
Validators: []validator.Number{
numbervalidator.Between(1, 300),
},
}
3. Test Connection
// ✅ Test connection during Configure
if err := client.TestConnection(ctx); err != nil {
resp.Diagnostics.AddError("Connection Failed", err.Error())
}
4. Use Connection Pooling
// ✅ Configure connection pooling
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
}
5. Implement Retry Logic
// ✅ Retry on transient errors
func shouldRetry(statusCode int) bool {
return statusCode >= 500 || statusCode == 429
}
6. Log Configuration
// ✅ Log configuration (without secrets)
tflog.Debug(ctx, "Configuring HTTP client", map[string]any{
"timeout": timeout.Seconds(),
"max_retries": maxRetries,
"endpoint": endpoint,
})
Key Takeaways
- ✅ Configure appropriate timeouts for all operations
- ✅ Implement retry logic for transient failures
- ✅ Use connection pooling for performance
- ✅ Support custom headers for flexibility
- ✅ Configure TLS properly for security
- ✅ Test connections during provider configuration
- ✅ Provide sensible defaults
- ✅ Document all configuration options
- ✅ Use layered transports for separation of concerns
What’s Next?
Now let’s learn about handling configuration from multiple sources.
Previous: ← Authentication Methods
Last updated: June 30, 2026