skip to content

Your Terraform aws provider block sets profile = "dev". It plans fine on a laptop but fails in CI, where credentials are supplied as environment variables. Why, and in what order does the AWS provider look for credentials?

level: middleimportance: should knowfreq 56%

answer

  1. block arguments outrank the environment
  2. the chain has five levels
  3. laptop config does not exist on a runner
  4. null argument means unset
  5. identity belongs outside shared code

basics

~20 s

Arguments written in the provider block win over everything else, so profile = "dev" makes the provider look for a shared-config profile that does not exist on the CI runner and ignore the environment variables. Remove the argument and let AWS_PROFILE or the environment supply it.

solid answer

~40 s

The AWS provider resolves credentials in a fixed order of precedence: arguments in the `provider` block first, then environment variables, then the shared config and credentials files, then container credentials, and finally the EC2 instance metadata service. Because block arguments sit at the top, a hardcoded `profile = "dev"` beats `AWS_PROFILE` and beats `AWS_ACCESS_KEY_ID`-style variables — the provider goes looking for a `dev` profile in `~/.aws/config`, the runner has no such file, and the run fails before touching the environment credentials that were sitting right there. The fix is to keep environment-specific identity out of the configuration: drop `profile` from the block and set `AWS_PROFILE` locally instead, so the same code authenticates as a developer on a laptop and as a CI role in the pipeline without a conditional.

code

hcl · 15 lines
hcl
variable "region" {
  type    = string
  default = "eu-west-1"
}

variable "aws_profile" {
  type        = string
  default     = null
  description = "Local profile name; leave null in CI so the environment supplies credentials."
}

provider "aws" {
  region  = var.region
  profile = var.aws_profile
}

go deeper

for a junior

Know that credentials can come from the provider block, environment variables, or the shared AWS config files, and that putting a personal profile name into shared configuration will break on machines that lack that profile.

for a middle

Recite the precedence order and explain that a hardcoded argument wins, so the environment is never consulted. Say what you remove from the block to make one configuration authenticate correctly on a laptop and in CI.

for a senior

Show how you keep a config portable across execution contexts without conditionals — null-defaulted arguments, environment-supplied identity — and mention the guards that catch a wrong-identity run early, such as allowed_account_ids and checking the caller identity before a risky apply.

for a principal

Set the convention for the estate: what may appear in a provider block at all, how each execution context receives identity, and how a run proves which account and role it is operating as before it is allowed to change production.

## The resolution order The AWS provider does not have one credential source; it has a chain, and it walks it in a defined order. From highest precedence to lowest: 1. **Arguments in the `provider` block** — `access_key`/`secret_key`/`token`, `profile`, `shared_config_files`, `shared_credentials_files`, and the `assume_role` / `assume_role_with_web_identity` blocks. 2. **Environment variables** — `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, `AWS_PROFILE`, plus `AWS_CONFIG_FILE` and `AWS_SHARED_CREDENTIALS_FILE` for file locations. 3. **The shared configuration files** — `~/.aws/config` and `~/.aws/credentials`, using the `default` profile unless another is selected. 4. **Container credentials** — the endpoint an ECS task or similar runtime exposes to its containers. 5. **The EC2 instance metadata service (IMDS)** — the instance profile attached to the machine. The important structural fact is the first line: *anything written in HCL outranks everything supplied from outside*. That is exactly backwards from what most people assume when they hit this failure, because they think of the environment as "the override". ## Why the CI run fails On the laptop, `profile = "dev"` resolves against a real `~/.aws/config` containing a `[profile dev]` section, so the plan works and nobody notices the coupling. The CI runner has no home directory config — its identity arrives as environment variables, or as a federated role session. The provider still reads the block first, still selects the `dev` profile, and fails resolving it, with an error along the lines of failing to get the shared config profile. The environment credentials are never consulted, because a higher-precedence source already answered. The same shape appears with `region`. If the block hardcodes a region, `AWS_REGION` cannot move the run; if it does not, the provider falls back to `AWS_REGION`, then `AWS_DEFAULT_REGION`, then the region setting inside the selected profile. Region is required one way or another — a provider with no region from any source is an error. ## The rule this teaches Put in the provider block only what is true for *every* execution context, and leave identity to the environment: ```hcl # portable: no identity, no machine-specific paths provider "aws" { region = var.region } ``` ```bash # laptop export AWS_PROFILE=dev # CI: role session variables, or OIDC federation export AWS_REGION=eu-west-1 ``` If you genuinely need the profile expressed in code — some teams want it explicit — make it a variable that defaults to null so it can be omitted, rather than a literal: ```hcl variable "aws_profile" { type = string default = null } provider "aws" { region = var.region profile = var.aws_profile } ``` An argument set to `null` is treated as unset, so the chain proceeds to the environment. This is the honest middle ground: the knob exists, but the default configuration is portable. ## Related traps in the same family - **`shared_credentials_files` pointing at a laptop path.** Same failure, same reason: a machine-specific path baked into shared code. - **A stale `default` profile.** On a laptop with credentials in `~/.aws/credentials`, a run you *thought* was authenticating as a CI-like role may silently be using your personal `default` profile. Checking the caller identity before a scary apply is cheap insurance, and `allowed_account_ids` in the provider block turns "wrong account" into an immediate error instead of a wrong apply. - **The backend authenticates separately.** The `backend` configuration for remote state resolves its own credentials; it does not inherit the `provider` block's. A run can authenticate to the backend as one identity and to the resources as another, and both must work. - **A partially-set environment.** Exporting `AWS_ACCESS_KEY_ID` without `AWS_SESSION_TOKEN` for what is actually a temporary session produces confusing signature errors rather than a clean "no credentials" message. ## What an interviewer is checking That you know precedence runs from most-specific-in-code to most-ambient, that you can name at least the top three levels, and — the part that separates a middle answer from a junior one — that you draw the design conclusion: environment-specific identity does not belong in a file that every environment shares.

  • How does the AWS provider decide which region to use if the provider block does not set one?
    It falls back through the same style of chain: the `AWS_REGION` environment variable, then `AWS_DEFAULT_REGION`, then the region configured in the selected shared-config profile. A region must come from somewhere — if none of those supply one, the provider errors out. Leaving `region` unset in the block is a deliberate choice to make the run's target environment-controlled.
  • What happens if you set the profile argument to null instead of a string?
    Terraform treats an argument assigned `null` as though it were not written at all, so the provider continues down the chain to the environment and shared config. That makes a null-defaulted variable the clean way to offer an optional profile knob without breaking runs that supply credentials another way.
  • Does the remote state backend inherit credentials from the provider block?
    No. The backend is configured and initialised independently of providers and resolves credentials through its own configuration — its own role or profile settings, or the ambient environment. A run therefore needs working credentials for both, and they may legitimately be different identities with different permissions.

saying these in an interview costs you the question

  • Environment variables always override the provider block
  • The provider tries every source and uses whichever works
  • Setting AWS_PROFILE in CI fixes a hardcoded profile argument
  • Region is optional if credentials are present
  • The remote state backend reuses the provider's credentials

context