
# 구성 계획 적용

`POST /v1/config-plans/{plan_id}:apply` · 권한 `config:write`, 계획의 읽기 권한과 변경되는 각 종류의 쓰기 권한

저장된 [계획](create.md)을 환경에 적용합니다. 모든 변경은 하나의 트랜잭션으로 기록됩니다. 하나라도 실패하면 아무것도 저장되지 않습니다. 계획은 한 번만 적용할 수 있습니다.

## 요청

| 매개변수 | 설명 |
|---|---|
| `plan_id` | 계획 ID, `cfgp_…`. |

요청 본문은 선택 사항입니다.

| 필드 | 설명 |
|---|---|
| `resolved_secrets` | `requires_secret: true`이 있는 모든 변경에 대한 `secret_ref` 이름 → 값 객체입니다(예: `KEENAUTH_CONNECTION_GITHUB_CLIENT_SECRET`). 값은 최대 8 KiB입니다. 계획에서 저장되거나 기록되거나 반환되지 않습니다. |
| `confirm_deletions` | 확인하는 `<kind>/<key>` 삭제 목록입니다(예: `connection/legacy`). 계획의 `deletion_requires_confirmation` 경고에 이 확인이 필요한 삭제가 나열됩니다. |
| `sp_host` | 환경의 서비스 제공자 호스트입니다(예: `login.example.com`). 계획이 엔터프라이즈 연결의 OIDC 자료를 생성할 때만 필요합니다(콜백 `https://<sp_host>/sso/<connection id>/callback`이 이 값으로 고정됩니다). 필요하지 않은 요청에서는 보내지 않아야 합니다. |

- 쓰기 권한은 `environment-settings:write`, `identity-settings:write`, `attack-protection:write`, `branding:write`(`branding` 및 `application_branding`의 경우), `connections:write`, `apis:write`, `roles:write` 및 `applications:write`이며, `no_op` 이외의 변경이 있는 종류에만 필요합니다.
- 쓰기 전에 대상을 다시 읽습니다. 계획이 만들어진 후 계획된 리소스의 버전이나 계획 종류의 내용이 변경되었다면 아무것도 기록되지 않고, 계획은 `conflicted` 상태가 되며 요청은 `409 config_conflict`로 실패합니다. 새 계획을 만드십시오. `prune`를 사용하면 계획된 리소스를 참조하는 종류도 비교되며, 리소스 간 참조가 다시 확인됩니다. 깨진 참조 역시 계획을 `conflicted` 상태로 만듭니다.
- 연결 또는 지난 30일 이내에 토큰을 받은 애플리케이션을 삭제하는 경우 `confirm_deletions`에 반복해서 입력해야 합니다. 그렇지 않으면 요청은 `409 deletion_not_confirmed`로 실패하고 계획은 `planned` 상태로 남습니다.
- 엔터프라이즈 연결의 OIDC 자료(`spec.oidc`)와 클라이언트 비밀은 연결 레코드 다음에 페더레이션 서비스에 기록됩니다. 두 번째 기록이 거부되면 계획은 `planned` 상태로 남고, 연결 레코드는 이미 존재할 수 있으며, 새 계획은 누락된 자료만 기록합니다. `null`의 `spec.oidc`은 자료를 변경하지 않습니다. 소셜 연결의 클라이언트 비밀은 아직 기록할 수 없습니다. 그러한 `requires_secret` 변경이 있는 계획은 비밀을 확인한 후 `501 not_implemented`를 반환합니다. 아무것도 기록되지 않습니다.
- `Idempotency-Key`를 지원합니다. 가능하면 본문에 비밀을 포함하지 않고 보내십시오. 키의 레코드에는 본문의 해시가 저장됩니다.

## 응답

`200 OK`와 `Cache-Control: no-store`를 함께 반환합니다: [구성 계획 조회](get.md) 형식의 계획이며 `status: applied` 및 `applied_at`가 포함됩니다.

변경 전 영향을 받은 리소스의 구성은 리비전으로 보관됩니다(환경당 최신 50개, 최대 90일). 적용 시 생성, 수정, 삭제, 변경되지 않은 리소스 수를 담은 `config.applied` 항목 하나를 환경의 관리 로그에 기록합니다. 변경된 각 리소스도 자체 항목을 기록합니다.

## 오류

| HTTP | 코드 | 발생 시점 |
|---|---|---|
| 400 | `malformed_request`, `unknown_field` | 본문이 JSON이 아니거나 `resolved_secrets`, `confirm_deletions`, `sp_host` 이외의 필드가 있는 경우입니다. |
| 401 | `unauthenticated` | 자격 증명이 없거나 잘못되었거나 취소된 경우입니다. |
| 403 | `insufficient_permissions` | 자격 증명에 필요한 권한이 없습니다. `required_permissions`에 모든 권한이 나열됩니다. |
| 404 | `not_found` | 계획이 없거나 다른 환경에 속하거나 삭제되었습니다. |
| 409 | `config_conflict` | 계획이 만들어진 후 대상이 변경되었습니다. 계획은 이제 `conflicted` 상태입니다. |
| 409 | `config_plan_not_applicable` | 계획이 이미 적용되었거나 `conflicted` 상태이거나, 다른 요청이 지금 적용 중입니다(`plan_status: applying`). `plan_status`에서 상태를 확인할 수 있습니다. |
| 409 | `deletion_not_confirmed` | 보호된 삭제가 `confirm_deletions`에 없습니다. `unconfirmed_deletions`에 나열됩니다. |
| 409 | `conflict` | 다른 리소스가 변경의 고유 값을 이미 사용하고 있습니다(`errors[].pointer`은 `/changes/<i>/spec/<field>`, 코드 `duplicate`); 계획은 `planned` 상태로 남습니다. |
| 409 | `connection_in_use` | 삭제할 연결이 아직 사용 중입니다. 계획은 `planned` 상태로 남습니다. |
| 409 | `connection_test_required` | 변경이 `enterprise` 연결 `active`을 생성하거나, 테스트 로그인에 성공하지 않은 연결을 활성화합니다(`errors[].pointer`은 `/changes/<i>/spec`, 코드 `test_required`); 계획은 `planned` 상태로 남습니다. |
| 410 | `config_plan_expired` | 계획의 유효 시간이 지났습니다. 새 계획을 만드십시오. |
| 413 | `config_document_too_large` | 변경 사항이나 이에 대해 보관된 리비전(최대 10 MiB)이 한 요청으로 적용하기에 너무 큽니다. 종류를 줄여 계획하십시오. |
| 422 | `validation_failed` | 아래를 참조하십시오. |
| 422 | `policy_conflict` | 변경이 환경 정책과 충돌합니다. 계획은 `planned` 상태로 남습니다. |
| 429 | `rate_limited` | `config_plan` 속도 제한(계획 생성과 공유)을 초과했거나, 이 환경에 대해 한 번에 계산 또는 적용 중인 계획이 너무 많습니다. 계획은 `planned` 상태로 남으며, `Retry-After` 기간 후에 다시 시도하십시오. |
| 501 | `not_implemented` | 계획에 아직 기록할 수 없는 소셜 연결의 클라이언트 비밀이 필요합니다. |
| 503 | `service_unavailable` | 종속 서비스(디렉터리 또는 엔터프라이즈 OIDC 자료의 경우 페더레이션 서비스)를 일시적으로 사용할 수 없거나 계획 용량이 일시적으로 찼습니다. 아무것도 기록되지 않았거나, 페더레이션 서비스의 경우 연결 레코드만 기록되었습니다. `Retry-After` 기간 후에 다시 시도하십시오. |

| `errors[].pointer` | `errors[].code` | 의미 |
|---|---|---|
| `/resolved_secrets/<NAME>` | `secret_missing` | 계획에 이 비밀이 필요합니다. |
| `/resolved_secrets/<NAME>` | `secret_unused` | 계획에 이 비밀이 필요하지 않습니다. |
| `/resolved_secrets/<NAME>` | `secret_invalid` | 값이 비어 있거나, 8 KiB보다 길거나, 텍스트가 아닙니다. |
| `/resolved_secrets/<NAME>` | `invalid` | 이름이 `KEENAUTH_…` 형식이 아닙니다(`A-Z`, `0-9`, `_`). |
| `/sp_host` | `sp_host_required`, `sp_host_unused`, `invalid` | 계획이 OIDC 자료를 생성하는데 호스트가 주어지지 않았거나, 호스트가 필요하지 않거나, 호스트가 소문자 DNS 이름이 아닙니다. |
| `/changes/<i>/spec/oidc/...` | 연결 OIDC API와 동일 | 페더레이션 서비스가 계획의 `changes`에서 이 위치의 자료를 거부했습니다. |
| `/confirm_deletions/<i>` | `duplicate`, `not_a_deletion` | 항목이 반복되었거나 계획에서 삭제가 아닙니다. |
| `/confirm_deletions/<i>` | `invalid` | 항목이 `<kind>/<key>` 형식이 아닙니다. |
| `/resolved_secrets`, `/confirm_deletions` | `too_many` | 5,000개를 초과하는 항목입니다. |
| `/changes/<i>/spec/...` | 리소스 고유 API와 동일 | 환경이 계획의 `changes`에서 이 위치의 변경을 거부했습니다. |

## 예제

```bash
curl -X POST "https://api.eu.keenauth.com/v1/config-plans/cfgp_01m4qpqtywfs7930tb6kqn5rc5:apply" \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"confirm_deletions": ["connection/legacy"]}'

keenauth config apply cfgp_01m4qpqtywfs7930tb6kqn5rc5 --confirm-deletion connection/legacy
```

`keenauth config apply --secrets-from-env`는 계획에 필요한 각 비밀을 같은 이름의 환경 변수에서 읽으며, 이를 출력하지 않습니다.
