amoghrajesh opened a new pull request, #71756:
URL: https://github.com/apache/airflow/pull/71756

    <!-- SPDX-License-Identifier: Apache-2.0
         https://www.apache.org/licenses/LICENSE-2.0 -->
   
   <!--
   Thank you for contributing!
   
   Please provide above a brief description of the changes made in this pull 
request.
   Write a good git commit message following this guide: 
https://chris.beams.io/posts/git-commit/
   
   Please make sure that your code changes are covered with tests.
   And in case of new features or big changes remember to adjust the 
documentation.
   
   For user-facing UI changes, please attach before/after screenshots (or a 
short
   screen recording) so reviewers can assess the visual impact.
   
   Feel free to ping (in general) for the review if you do not see reaction for 
a few days
   (72 Hours is the minimum reaction time you can expect from volunteers) - we 
sometimes miss notifications.
   
   In case of an existing issue, reference it using one of the following:
   
   * closes: #ISSUE
   * related: #ISSUE
   -->
   
   ---
   
   ##### Was generative AI tooling used to co-author this PR?
   
   <!--
   If generative AI tooling has been used in the process of authoring this PR, 
please
   change below checkbox to `[X]` followed by the name of the tool, uncomment 
the "Generated-by".
   -->
   
   - [ ] Yes (please specify the tool below)
   
   <!--
   Generated-by: [Tool Name] following [the 
guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions)
   -->
   
   
   Follow-up to https://github.com/apache/airflow/pull/71701 but for generic 
secrets backends, which removed the two token validation calls the HashiCorp 
provider made per secret. This removes the repeated authentication underneath 
them.
   
   ## The problem
   
   `_get_connection` and `_get_variable` call `ensure_secrets_backend_loaded()` 
inside the
   lookup itself. That reaches `sdk/configuration.py` and ends at a bare 
`secrets_backend_cls(**backend_kwargs)` in the shared config parser, so every 
lookup gets a new backend, a new client, and a cold `cached_property`. For 
Vault that means a fresh AppRole login per secret.
   
   Measured against a real Vault: two variables fetched in one process produced 
**two** AppRole logins.
   
   The SDK function is inspired was copied from `airflow.configuration`. One 
line differs:
   
   ```python
   # airflow-core
   secrets_backend_list                                   # module global, 
built once
   
   # task-sdk, before this change
   secrets_backend_list = initialize_secrets_backends()   # local, rebuilt 
every call
   ```
   
   The copy turned a reference to a cached global into a fresh call to build 
one. Everything downstream follows from that substitution, which is why core 
never showed the problem and the SDK always did.
   
   ## Why not simply mirror core
   
   Core keeps one module-level list and returns it only when the requested 
search path is the
   server default. Any other path takes the rebuild branch. The task runner 
always asks for `DEFAULT_SECRETS_SEARCH_PATH_WORKERS`, which is not the server 
default, so a faithful copy of
   core would leave worker lookups rebuilding exactly as they do today.
   
   Caching per search path is what makes both chains cacheable. The SDK also 
defers config initialisation deliberately via `__getattr__`, so populating at 
import the way core does would regress that.
   
   ## Preserved behaviour
   
   Core's `len(secrets_backend_list) == 2` check exists so that a custom 
backend configured after
   startup is still picked up. That is kept, expressed as `len(backends) == 
len(default_backends)`
   so it holds for the worker chain too rather than hardcoding the server 
chain's length. When no
   custom backend is found nothing is stored and every call rebuilds, exactly 
as before.
   
   I traced the old and new implementations across every combination. Chain 
contents are identical
   in all eight:
   
   | `[workers]` / `[secrets]` | chain | backends | cached |
   |---|---|---|---|
   | set / unset | server | env, metastore | no |
   | set / unset | worker | **metastore**, env, execapi | yes |
   | set / set | server | **env**, env, metastore | yes |
   | set / set | worker | **metastore**, env, execapi | yes |
   | unset / set | server | **env**, env, metastore | yes |
   | unset / set | worker | **env**, env, execapi | yes |
   | unset / unset | server | env, metastore | no |
   | unset / unset | worker | env, execapi | no |
   
   Bold is the custom backend prepended by `initialize_secrets_backends`. 
Caching engages only
   where one exists for the chain being requested.
   
   ## One behaviour difference
   
   The old code always built the server chain first and discarded it, even when 
the worker chain
   was requested. That discarded construction could raise:
   
   ```
   [secrets] backend         = VaultBackend (misconfigured)
   [workers] secrets_backend = MetastoreBackend (fine)
   requesting the worker chain:
   
   before: VaultError: The 'token' authentication type requires 'token' or 
'token_path'
   after:  ['MetastoreBackend', 'EnvironmentVariablesBackend', 
'ExecutionAPISecretsBackend']
   ```
   
   A worker with a valid backend no longer fails because of an unrelated 
server-side
   misconfiguration it never uses. It needs `[workers] secrets_backend` set to 
something different
   and `[secrets] backend` broken enough to raise on construction; with 
`[workers]` unset the
   worker falls back to `[secrets] backend` and both versions raise identically.
   
   ## Credential expiry across backends
   
   Holding an instance for the life of a process means an expiring credential 
has to refresh. All seven external backends were checked:
   
   | Backend | Auth state held | Refreshes? |
   |---|---|---|
   | amazon (secrets manager, SSM) | boto3 session | yes, botocore credential 
resolver |
   | google | google-auth credentials | yes |
   | microsoft/azure | azure-identity credentials | yes |
   | akeyless | own `_cached_token` with `_token_expiry`, TTL 600s | yes, 
itself |
   | yandex | `yandexcloud.SDK` from OAuth token or SA key | SA key path 
refreshes; OAuth tokens are long-lived |
   | hashicorp | static hvac token | yes, via the 403 retry added in 
#<FIX_A_PR> |
   | cncf/kubernetes | `load_incluster_config()` in a `cached_property` | 
**no** |
   
   The kubernetes backend reads the projected service account token from disk 
once. Bound tokens
   expire and kubelet rotates the file, and the Python client does not re-read 
it.
   
   That gap is not introduced here. Core has cached backend instances for the 
life of the process
   on the server path for years, so the same defect already exists in the API 
server, scheduler and
   Dag processor. This change makes it reachable in a worker whose task 
outlives the token. It is
   worth fixing separately, with the same shape used for Vault: catch the 401, 
drop the cached
   client, retry once.
   
   ## Improvements
   
   Setup breeze with `VaultBackend` and create 5 variables:
   
   ```shell
   alias v='docker exec -e VAULT_ADDR=http://127.0.0.1:8200 -e VAULT_TOKEN=root 
vault-test vault'
   
   v write auth/approle/role/airflow token_policies=airflow token_type=batch
   for i in 1 2 3 4 5; do v kv put -mount=secret variables/var$i 
value=value-$i; done
   ```
   
   Use this dag:
   ```python
   from __future__ import annotations
   
   from datetime import datetime
   
   from airflow.sdk import DAG, Variable, task
   
   with DAG("five_vars", schedule=None, start_date=datetime(2024, 1, 1), 
catchup=False):
   
       @task
       def read_five():
           return [Variable.get(f"var{i}") for i in range(1, 6)]
   
       read_five()
   
   ```
   
   ### Before the changes
   
   From audit file:
   ```shell
   amoghrajesh.desai  …/airflow   main $?    orbstack  ♥ 10:27  docker 
exec vault-test cat /vault/logs/audit.log \
     | jq -r 'select(.type=="request") | .request.path' | sort | uniq -c        
                                                                                
                                                             5 
auth/approle/login
      1 secret/data/variables/var1
      1 secret/data/variables/var2
      1 secret/data/variables/var3
      1 secret/data/variables/var4
      1 secret/data/variables/var5
   ```
   
   ### After the changes:
   
   <img width="2560" height="509" alt="image" 
src="https://github.com/user-attachments/assets/e52f5b9f-c9c6-4001-8101-738af8f0de72";
 />
   
   
   From audit file:
   
   ```shell
   amoghrajesh.desai  …/airflow   vault-login-cost-reduction-part-2 $?   
 orbstack  ♥ 10:32  docker exec vault-test cat /vault/logs/audit.log \
     | jq -r 'select(.type=="request") | .request.path' | sort | uniq -c        
                                                                                
                                                             1 
auth/approle/login
      1 secret/data/variables/var1
      1 secret/data/variables/var2
      1 secret/data/variables/var3
      1 secret/data/variables/var4
      1 secret/data/variables/var5
   ```
   
   One Airflow task reading five distinct variables, counted from Vault's own 
file audit device.
   
   | Path | main | this PR |
   |---|---|---|
   | `auth/approle/login` | 5 | **1** |
   | `secret/data/variables/var1` … `var5` | 5 | 5 |
   | **total** | **10** | **6** |
   
   Cumulative across both changes, same workload:
   
   | | logins | `lookup-self` | reads | total |
   |---|---|---|---|---|
   | Before either change | 5 | 10 | 5 | 20 |
   | #71701 | 5 | 0 | 5 | 10 |
   | This PR | 1 | 0 | 5 | 6 |
   
   Six is the floor for five distinct secrets: one authentication plus one read 
each. Reads do not
   drop because the secrets backend interface fetches one key at a time and 
there is no bulk read.
   
   
   ---
   
   * Read the **[Pull Request 
Guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#pull-request-guidelines)**
 for more information. Note: commit author/co-author name and email in commits 
become permanently public when merged.
   * For fundamental code changes, an Airflow Improvement Proposal 
([AIP](https://cwiki.apache.org/confluence/display/AIRFLOW/Airflow+Improvement+Proposals))
 is needed.
   * When adding dependency, check compliance with the [ASF 3rd Party License 
Policy](https://www.apache.org/legal/resolved.html#category-x).
   * For significant user-facing changes create newsfragment: 
`{pr_number}.significant.rst`, in 
[airflow-core/newsfragments](https://github.com/apache/airflow/tree/main/airflow-core/newsfragments).
 You can add this file in a follow-up commit after the PR is created so you 
know the PR number.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to