provider-configuration

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Terraform Provider Configuration and Authentication

Terraform Provider配置与认证

How a provider accepts connection settings and resolves credentials. Poor authentication UX is the first thing every user of a provider hits; a well-designed credential provider chain is what separates a production-grade provider from a demo. The examples use a fictional
examplecloud
provider and the Plugin Framework.
References (load when needed):
  • references/credential-chain.md
    — complete, compilable credential chain implementation (providers, chain, file profiles, Configure wiring, tests)
  • references/case-studies.md
    — how the AWS provider (
    aws-sdk-go-base
    ) and smaller providers structure real credential chains

本文介绍Provider如何接收连接设置并解析凭证。糟糕的认证体验是每个Provider用户首先遇到的问题;设计良好的凭证提供链是区分生产级Provider与演示版Provider的关键。示例使用虚构的
examplecloud
Provider以及Plugin Framework
参考资料(按需查看):
  • references/credential-chain.md
    — 完整可编译的凭证链实现代码(包含Provider、链逻辑、文件配置文件、Configure关联代码、测试)
  • references/case-studies.md
    — AWS Provider(基于
    aws-sdk-go-base
    )及小型Provider如何构建实际凭证链的案例分析

Provider Schema for Authentication

用于认证的Provider Schema

Every authentication attribute must be
Optional
, never
Required
— a
Required
attribute forces users to put credentials in configuration and makes environment-variable and credentials-file resolution impossible. Mark secrets
Sensitive
so Terraform redacts them in plan output, and state the environment-variable fallback in each description so
tfplugindocs
publishes the resolution rules.
go
func (p *examplecloudProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
    resp.Schema = schema.Schema{
        Attributes: map[string]schema.Attribute{
            "endpoint": schema.StringAttribute{
                Optional:            true,
                MarkdownDescription: "API endpoint. May also be set via the `EXAMPLECLOUD_ENDPOINT` environment variable.",
            },
            "api_key": schema.StringAttribute{
                Optional:            true,
                MarkdownDescription: "API key. May also be set via the `EXAMPLECLOUD_API_KEY` environment variable, or in a shared credentials file.",
            },
            "api_secret": schema.StringAttribute{
                Optional:            true,
                Sensitive:           true,
                MarkdownDescription: "API secret. May also be set via the `EXAMPLECLOUD_API_SECRET` environment variable, or in a shared credentials file.",
            },
            "profile": schema.StringAttribute{
                Optional:            true,
                MarkdownDescription: "Named profile in the shared credentials file. May also be set via the `EXAMPLECLOUD_PROFILE` environment variable. Defaults to `default`.",
            },
            "skip_credentials_validation": schema.BoolAttribute{
                Optional:            true,
                MarkdownDescription: "Skip the identity check normally performed during provider configuration.",
            },
        },
    }
}
Never add a
Default
to a credential attribute, and never hardcode a credential anywhere in the provider. Defaults belong in the resolution logic (where environment variables and files can override them), not in the schema.
每个认证属性必须设置为
Optional
,绝不能是
Required
——
Required
属性会强制用户将凭证写入配置文件,导致无法通过环境变量或凭证文件解析凭证。将敏感信息标记为
Sensitive
,这样Terraform会在计划输出中自动遮蔽这些内容,并在每个属性的描述中注明环境变量回退机制,以便
tfplugindocs
发布凭证解析规则。
go
func (p *examplecloudProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
    resp.Schema = schema.Schema{
        Attributes: map[string]schema.Attribute{
            "endpoint": schema.StringAttribute{
                Optional:            true,
                MarkdownDescription: "API endpoint. May also be set via the `EXAMPLECLOUD_ENDPOINT` environment variable.",
            },
            "api_key": schema.StringAttribute{
                Optional:            true,
                MarkdownDescription: "API key. May also be set via the `EXAMPLECLOUD_API_KEY` environment variable, or in a shared credentials file.",
            },
            "api_secret": schema.StringAttribute{
                Optional:            true,
                Sensitive:           true,
                MarkdownDescription: "API secret. May also be set via the `EXAMPLECLOUD_API_SECRET` environment variable, or in a shared credentials file.",
            },
            "profile": schema.StringAttribute{
                Optional:            true,
                MarkdownDescription: "Named profile in the shared credentials file. May also be set via the `EXAMPLECLOUD_PROFILE` environment variable. Defaults to `default`.",
            },
            "skip_credentials_validation": schema.BoolAttribute{
                Optional:            true,
                MarkdownDescription: "Skip the identity check normally performed during provider configuration.",
            },
        },
    }
}
切勿为凭证属性添加
Default
值,也绝不能在Provider中硬编码任何凭证。默认值应放在解析逻辑中(环境变量和文件可覆盖默认值),而非Schema中。

The Credential Provider Chain

凭证提供链

Resolve credentials by consulting an ordered list of sources and taking the first one that produces a complete set. This is the pattern the AWS provider uses via
aws-sdk-go-base
, and it generalizes to any provider. The canonical precedence, highest first:
  1. Static configuration — values set directly in the
    provider
    block. Explicit always wins.
  2. Environment variables
    EXAMPLECLOUD_API_KEY
    , etc. The CI-friendly path.
  3. Shared credentials file — named profiles in
    ~/.examplecloud/credentials
    , for humans with multiple accounts.
  4. Platform identity — instance metadata, workload identity, or OIDC token exchange, where the platform offers it. Credentials nobody has to store.
Two rules make the chain predictable:
  • Resolve secrets as a set, not field-by-field. If the environment supplies an API key but no secret, that source offers nothing — fall through to the next source for both values. Mixing an env-var key with a file-profile secret produces authentication failures that are nearly impossible for users to debug.
  • Resolve non-secret connection settings field-by-field.
    endpoint
    ,
    profile
    , or
    insecure
    can each independently follow config > env > file > default, because a mismatch there is visible and harmless.
The core abstraction is a single-method interface with a sentinel error that distinguishes "this source has nothing to offer" (fall through) from "this source is misconfigured" (surface it):
go
// ErrNoCredentials signals a source had nothing to offer. The chain falls
// through to the next source. Any other error means the source was
// configured but unusable (e.g. malformed credentials file) and is
// preserved so the final diagnostics can surface it.
var ErrNoCredentials = errors.New("no credentials found")

type Credentials struct {
    APIKey    string
    APISecret string
    Source    string // which provider supplied them, for logging
}

func (c Credentials) Complete() bool {
    return c.APIKey != "" && c.APISecret != ""
}

type Provider interface {
    Retrieve(ctx context.Context) (Credentials, error)
    Name() string
}
A
Chain
(itself a
Provider
, so chains compose) walks the providers in order and returns the first complete set of credentials. Every skipped source is recorded into an aggregate
ChainError
whose
Error()
lists each source with the reason it was skipped, and whose
Is
method makes
errors.Is(err, ErrNoCredentials)
true only when every source fell through cleanly — so
Configure
can tell "nothing supplied" from "something supplied but broken" with one check. The full implementation — the chain loop, the static, environment, and file providers, and the
NewDefaultChain
constructor that owns the canonical order — lives in
references/credential-chain.md
.
通过按顺序查询一系列凭证来源,取第一个能提供完整凭证集的来源来解析凭证。这是AWS Provider通过
aws-sdk-go-base
使用的模式,可推广到任意Provider。标准优先级从高到低为:
  1. 静态配置——直接在
    provider
    块中设置的值。显式配置始终优先。
  2. 环境变量——
    EXAMPLECLOUD_API_KEY
    等。适合CI环境使用。
  3. 共享凭证文件——
    ~/.examplecloud/credentials
    中的命名配置文件,适合拥有多个账户的用户。
  4. 平台身份——实例元数据、工作负载身份或OIDC令牌交换,适用于支持该功能的平台。无需存储的凭证方式。
以下两条规则可确保链的可预测性:
  • 按完整集合解析敏感信息:如果环境变量提供了API密钥但未提供密钥秘钥,则该来源视为无效——直接跳过并尝试下一个来源获取全部所需值。混合使用环境变量的密钥和文件配置文件的秘钥会导致几乎无法调试的认证失败。
  • 按字段解析非敏感连接设置
    endpoint
    profile
    insecure
    可各自独立遵循「配置→环境变量→文件→默认值」的优先级,因为此类不匹配是可见且无害的。
核心抽象是一个单方法接口,通过哨兵错误区分「该来源无可用凭证」(跳过)和「该来源配置错误」(抛出错误):
go
// ErrNoCredentials signals a source had nothing to offer. The chain falls
// through to the next source. Any other error means the source was
// configured but unusable (e.g. malformed credentials file) and is
// preserved so the final diagnostics can surface it.
var ErrNoCredentials = errors.New("no credentials found")

type Credentials struct {
    APIKey    string
    APISecret string
    Source    string // which provider supplied them, for logging
}

func (c Credentials) Complete() bool {
    return c.APIKey != "" && c.APISecret != ""
}

type Provider interface {
    Retrieve(ctx context.Context) (Credentials, error)
    Name() string
}
Chain
本身也是
Provider
(支持链组合),会按顺序遍历所有Provider,返回第一个完整的凭证集。每个被跳过的来源会被记录到聚合的
ChainError
中,其
Error()
方法会列出每个来源及被跳过的原因;同时
Is
方法仅当所有来源都无可用凭证时,才会使
errors.Is(err, ErrNoCredentials)
返回true——这样
Configure
就能通过一次检查区分「无可用凭证」和「提供的凭证无效」两种情况。完整实现(包括链循环、静态/环境变量/文件Provider,以及定义标准优先级的
NewDefaultChain
构造函数)可查看
references/credential-chain.md

Wiring the Chain into Configure

将凭证链关联到Configure方法

Configure
runs once per Terraform operation, before any resource CRUD. The shape:
go
func (p *examplecloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var config examplecloudProviderModel
    resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
    if resp.Diagnostics.HasError() {
        return
    }

    // 1. Guard against unknown values (e.g. api_key = some_resource.output).
    if config.APIKey.IsUnknown() {
        resp.Diagnostics.AddAttributeError(
            path.Root("api_key"),
            "Unknown API Key",
            "The provider cannot connect because api_key depends on a value known only after apply. "+
                "Set a static value, or use the EXAMPLECLOUD_API_KEY environment variable.",
        )
    }
    // ... repeat for each auth attribute, then:
    if resp.Diagnostics.HasError() {
        return
    }

    // 2. Resolve credentials through the chain.
    chain := credentials.NewDefaultChain(
        config.APIKey.ValueString(),
        config.APISecret.ValueString(),
        credentials.Options{Profile: config.Profile.ValueString()},
    )
    creds, err := chain.Retrieve(ctx)
    if err != nil {
        if errors.Is(err, credentials.ErrNoCredentials) {
            resp.Diagnostics.AddError(
                "No Valid Credential Sources Found",
                "No examplecloud credentials were found. Sources tried, in order:\n\n"+err.Error()+
                    "\n\nSet api_key and api_secret in the provider block, export "+
                    "EXAMPLECLOUD_API_KEY and EXAMPLECLOUD_API_SECRET, or add a profile to "+
                    "~/.examplecloud/credentials. See https://example.com/docs/auth.",
            )
        } else {
            resp.Diagnostics.AddError("Failed to Resolve Credentials", err.Error())
        }
        return
    }
    tflog.Debug(ctx, "resolved credentials", map[string]any{"source": creds.Source})

    // 3. Build the client once; share it with every resource and data source.
    client := examplecloud.NewClient(endpoint, creds.APIKey, creds.APISecret)
    resp.DataSourceData = client
    resp.ResourceData = client
}
Why each step matters:
  • Unknown-value guards. During planning, an attribute wired to another resource's output is unknown, not null. Without the guard the provider silently treats it as empty, falls through the chain, and authenticates as the wrong identity — or fails with a misleading "missing credentials" error. Name the environment-variable workaround in the guard message.
  • The sentinel check picks the right message. "You gave me nothing" (actionable list of options) is a different failure from "you gave me something broken" (show the parse error). Collapsing them into one message is how providers end up with users pasting secrets into config to debug.
  • Log the source, never the secret. Knowing which source won is the single most useful debugging fact and costs nothing to log.
Configure
会在每次Terraform操作前运行一次,早于任何资源的增删改查操作。其基本结构如下:
go
func (p *examplecloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var config examplecloudProviderModel
    resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
    if resp.Diagnostics.HasError() {
        return
    }

    // 1. Guard against unknown values (e.g. api_key = some_resource.output).
    if config.APIKey.IsUnknown() {
        resp.Diagnostics.AddAttributeError(
            path.Root("api_key"),
            "Unknown API Key",
            "The provider cannot connect because api_key depends on a value known only after apply. "+
                "Set a static value, or use the EXAMPLECLOUD_API_KEY environment variable.",
        )
    }
    // ... repeat for each auth attribute, then:
    if resp.Diagnostics.HasError() {
        return
    }

    // 2. Resolve credentials through the chain.
    chain := credentials.NewDefaultChain(
        config.APIKey.ValueString(),
        config.APISecret.ValueString(),
        credentials.Options{Profile: config.Profile.ValueString()},
    )
    creds, err := chain.Retrieve(ctx)
    if err != nil {
        if errors.Is(err, credentials.ErrNoCredentials) {
            resp.Diagnostics.AddError(
                "No Valid Credential Sources Found",
                "No examplecloud credentials were found. Sources tried, in order:\n\n"+err.Error()+
                    "\n\nSet api_key and api_secret in the provider block, export "+
                    "EXAMPLECLOUD_API_KEY and EXAMPLECLOUD_API_SECRET, or add a profile to "+
                    "~/.examplecloud/credentials. See https://example.com/docs/auth.",
            )
        } else {
            resp.Diagnostics.AddError("Failed to Resolve Credentials", err.Error())
        }
        return
    }
    tflog.Debug(ctx, "resolved credentials", map[string]any{"source": creds.Source})

    // 3. Build the client once; share it with every resource and data source.
    client := examplecloud.NewClient(endpoint, creds.APIKey, creds.APISecret)
    resp.DataSourceData = client
    resp.ResourceData = client
}
各步骤的重要性:
  • 未知值防护:在计划阶段,关联到其他资源输出的属性是「未知」状态而非空值。如果没有防护逻辑,Provider会将其视为空值,跳过当前链并使用错误的身份认证——或抛出误导性的「凭证缺失」错误。在防护提示中注明环境变量的替代方案。
  • 哨兵错误检查选择正确提示:「未提供任何凭证」(列出可操作选项)与「提供的凭证无效」(显示解析错误)是两种不同的故障,将它们合并为一条提示会导致用户为了调试而将敏感信息粘贴到配置文件中。
  • 记录凭证来源,而非凭证内容:知道哪个来源提供了凭证是最有用的调试信息,且无需记录敏感内容。

Diagnostics That Unblock Users

帮助用户解决问题的诊断信息

An authentication error message is the provider's most-read documentation. Every credential failure diagnostic should name:
  • Every source tried, in order, with why it was skipped — the
    ChainError
    provides this.
    aws-sdk-go-base
    does the same with its
    NoValidCredentialSourcesError
    .
  • The exact environment variable names and the credentials file path and profile that were consulted — not "set the appropriate environment variables".
  • A documentation URL for the provider's authentication guide.
Use warnings (not errors) for conditions that are suspicious but not fatal, naming what took precedence: a
profile
set while environment credentials are also present (which wins?), or a credentials file with group/world-read permissions (suggest
chmod 0600
).
认证错误提示是Provider被阅读最多的文档。每个凭证失败的诊断信息应包含:
  • 所有尝试过的来源及顺序,以及被跳过的原因——
    ChainError
    提供了这些信息。
    aws-sdk-go-base
    NoValidCredentialSourcesError
    也采用了相同的方式。
  • 确切的环境变量名称、凭证文件路径及查询的配置文件名称——而非模糊的「设置相应环境变量」。
  • Provider认证指南的文档链接
对于可疑但非致命的情况,使用警告(而非错误)提示,并说明优先级:例如设置了
profile
但同时存在环境变量凭证(哪个会优先?),或凭证文件具有组/全局可读权限(建议执行
chmod 0600
)。

Secret Hygiene

敏感信息安全规范

  • Give the
    Credentials
    type
    String()
    and
    GoString()
    methods that redact secret fields, so a stray
    %v
    ,
    %+v
    , or error wrap can never leak a secret into logs or diagnostics.
  • Never include credential values in diagnostics, log lines, or wrapped errors — log the source name and non-secret identifiers only.
  • Warn when a credentials file is readable by other users (
    info.Mode().Perm()&0o077 != 0
    ); skip this check on Windows, where POSIX permission bits are not meaningful.
  • Credentials
    类型添加
    String()
    GoString()
    方法,遮蔽敏感字段,避免因意外使用
    %v
    %+v
    或错误包装导致敏感信息泄露到日志或诊断信息中。
  • 绝不在诊断信息、日志行或包装错误中包含凭证内容——仅记录来源名称和非敏感标识符。
  • 当凭证文件可被其他用户读取时发出警告(
    info.Mode().Perm()&0o077 != 0
    );Windows系统跳过此检查,因为POSIX权限位在该系统中无意义。

Configure-Time Validation

配置阶段验证

Resolve the chain eagerly in
Configure
— never lazily on first resource use — so a credentials problem fails one time, at plan, with a good message, instead of failing in the middle of an apply. If the API has a cheap identity endpoint (the equivalent of AWS
sts:GetCallerIdentity
or a
/whoami
), call it after resolving credentials so invalid (not just missing) credentials also fail at configure time. Gate it behind a
skip_credentials_validation
attribute for air-gapped or stubbed environments.
Configure
中提前解析凭证链——而非在首次使用资源时延迟解析——这样凭证问题会在计划阶段一次性失败并给出清晰提示,而非在应用过程中中途失败。如果API提供了轻量的身份验证端点(类似AWS的
sts:GetCallerIdentity
/whoami
),解析凭证后调用该端点,使无效(不仅是缺失)的凭证也能在配置阶段失败。为隔离环境或模拟环境添加
skip_credentials_validation
属性,用于跳过该检查。

Unit Testing the Chain

凭证链的单元测试

The chain is pure logic — test it with unit tests (
Test
prefix, no
TF_ACC
), not acceptance tests. Make the environment injectable (a
getenv func(string) string
field defaulting to
os.Getenv
, or use
t.Setenv
) and point the file provider at
t.TempDir()
fixtures. The tests that matter:
  • Per-source: each provider returns its credentials when set and
    ErrNoCredentials
    when incomplete (a key with no secret is incomplete).
  • Precedence: static beats env; env beats file; chain falls through to the file when nothing above supplies a complete set.
  • Failure aggregation: with all sources empty,
    errors.Is(err, ErrNoCredentials)
    is true and the message names every source.
  • Hard errors: a malformed credentials file or an explicitly requested profile that does not exist surfaces a descriptive error rather than silently falling through (a merely defaulted profile falls through).
  • Redaction:
    fmt.Sprintf("%v")
    and
    %+v
    of a
    Credentials
    value never contain the secret.
Full test examples are in
references/credential-chain.md
.
凭证链是纯逻辑——使用单元测试(前缀为
Test
,无需
TF_ACC
)而非验收测试。使环境变量可注入(例如默认使用
os.Getenv
getenv func(string) string
字段,或使用
t.Setenv
),并将文件Provider指向
t.TempDir()
中的测试文件。需要覆盖的测试场景:
  • 单来源测试:每个Provider在设置完整凭证时返回凭证,凭证不完整时返回
    ErrNoCredentials
    (仅提供密钥未提供秘钥视为不完整)。
  • 优先级测试:静态配置优先于环境变量;环境变量优先于文件;当上方来源无完整凭证时,链会回退到文件来源。
  • 错误聚合测试:所有来源均无凭证时,
    errors.Is(err, ErrNoCredentials)
    返回true,且提示信息列出所有来源。
  • 硬错误测试:格式错误的凭证文件或明确指定但不存在的配置文件会抛出描述性错误,而非静默跳过(仅默认配置文件不存在时才会跳过)。
  • 脱敏测试:对
    Credentials
    值使用
    fmt.Sprintf("%v")
    %+v
    时,绝不会显示敏感内容。
完整测试示例可查看
references/credential-chain.md

Checklist

检查清单

  • All auth attributes
    Optional
    ; secrets marked
    Sensitive: true
  • Attribute descriptions name their environment-variable fallbacks
  • Unknown-value guards on every auth attribute in
    Configure
  • Chain precedence: static config > env vars > credentials file > platform identity
  • Secrets resolved as a complete set; non-secret settings field-by-field
  • Sentinel
    ErrNoCredentials
    distinguishes fall-through from hard failure
  • Missing-credentials diagnostic lists every source tried + docs URL
  • Credentials
    type redacts secrets in
    String()
    /
    GoString()
  • Credentials-file permission warning (non-Windows)
  • Eager resolution in
    Configure
    ; optional identity check with
    skip_credentials_validation
  • Unit tests cover per-source behavior, precedence, aggregation, redaction
  • No credential value ever logged or embedded in an error
  • 所有认证属性均为
    Optional
    ;敏感信息标记为
    Sensitive: true
  • 属性描述中注明环境变量回退机制
  • Configure
    中为每个认证属性添加未知值防护
  • 链优先级:静态配置 > 环境变量 > 凭证文件 > 平台身份
  • 敏感信息按完整集合解析;非敏感设置按字段解析
  • 使用哨兵错误
    ErrNoCredentials
    区分跳过与硬错误
  • 凭证缺失诊断信息列出所有尝试过的来源及文档链接
  • Credentials
    类型在
    String()
    /
    GoString()
    中遮蔽敏感信息
  • 凭证文件权限警告(非Windows系统)
  • Configure
    中提前解析凭证;通过
    skip_credentials_validation
    可选启用身份检查
  • 单元测试覆盖单来源行为、优先级、错误聚合、脱敏逻辑
  • 凭证内容绝不会被记录或嵌入错误信息

Related Skills

相关技能

Use the
new-terraform-provider
skill (if available) to scaffold the provider this configuration lives in, and the
provider-resources
skill for consuming the configured client from resources and data sources.
使用
new-terraform-provider
技能(若可用)搭建该配置所属的Provider框架,使用
provider-resources
技能从资源和数据源中调用已配置的客户端。