refactor: reorganize GitOps control plane

This commit is contained in:
donghyeon-ka
2026-07-25 23:55:31 +09:00
parent d507ac6ee9
commit 293ee6fc97
191 changed files with 7046 additions and 9034 deletions
+114
View File
@@ -0,0 +1,114 @@
name: Promote Dev Image by Pull Request
on:
workflow_dispatch:
inputs:
service:
description: Workload to promote
required: true
type: choice
options:
- auth-server
- api-server
image_digest:
description: Immutable OCI digest including the sha256 prefix
required: true
type: string
concurrency:
group: promote-dev-${{ inputs.service }}
cancel-in-progress: false
jobs:
promote:
runs-on:
- self-hosted
- linux
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
token: ${{ secrets.GITOPS_BOT_TOKEN }}
- name: Update immutable digest
env:
SERVICE: ${{ inputs.service }}
IMAGE_DIGEST: ${{ inputs.image_digest }}
run: |
case "$SERVICE" in
auth-server)
image_name="ghcr.io/donghyeonka/project-auth-server"
;;
api-server)
image_name="ghcr.io/donghyeonka/project-api-server"
;;
*)
echo "Unsupported service: $SERVICE" >&2
exit 1
;;
esac
if [[ ! "$IMAGE_DIGEST" =~ ^sha256:[a-f0-9]{64}$ ]]; then
echo "image_digest must be an immutable sha256 digest" >&2
exit 1
fi
manifest="clusters/dev-k3s/manifests/${SERVICE}/kustomization.yaml"
temporary="$(mktemp "${manifest}.XXXXXX")"
awk -v image_name="$image_name" -v image_digest="$IMAGE_DIGEST" '
$1 == "-" && $2 == "name:" { target = ($3 == image_name) }
target && $1 == "newTag:" {
match($0, /^[[:space:]]*/)
print substr($0, RSTART, RLENGTH) "digest: " image_digest
target = 0
updated = 1
next
}
target && $1 == "digest:" {
match($0, /^[[:space:]]*/)
print substr($0, RSTART, RLENGTH) "digest: " image_digest
target = 0
updated = 1
next
}
{ print }
END { if (!updated) exit 42 }
' "$manifest" >"$temporary"
mv "$temporary" "$manifest"
kubectl kustomize "$(dirname "$manifest")" >/dev/null
- name: Create promotion branch
env:
SERVICE: ${{ inputs.service }}
RUN_NUMBER: ${{ gitea.run_number }}
run: |
branch="gitops/promote-${SERVICE}-${RUN_NUMBER}"
git config user.name "gitops-bot"
git config user.email "gitops-bot@hyeonworks.local"
git switch -c "$branch"
git add "clusters/dev-k3s/manifests/${SERVICE}/kustomization.yaml"
git commit -m "chore(gitops): promote ${SERVICE} dev digest"
git push --set-upstream origin "$branch"
echo "PROMOTION_BRANCH=$branch" >>"$GITHUB_ENV"
- name: Open Gitea pull request
env:
GITEA_TOKEN: ${{ secrets.GITOPS_BOT_TOKEN }}
GITEA_API_URL: ${{ gitea.api_url }}
REPOSITORY: ${{ gitea.repository }}
DEFAULT_BRANCH: ${{ gitea.event.repository.default_branch }}
SERVICE: ${{ inputs.service }}
IMAGE_DIGEST: ${{ inputs.image_digest }}
run: |
payload="$(
jq -n \
--arg base "$DEFAULT_BRANCH" \
--arg head "$PROMOTION_BRANCH" \
--arg title "chore(gitops): promote ${SERVICE} dev digest" \
--arg body "Promotes ${SERVICE} to immutable digest ${IMAGE_DIGEST}." \
'{base: $base, head: $head, title: $title, body: $body}'
)"
curl -fsS \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "$payload" \
"${GITEA_API_URL}/repos/${REPOSITORY}/pulls"
+23
View File
@@ -0,0 +1,23 @@
name: Validate GitOps Repository
on:
pull_request:
push:
branches:
- main
concurrency:
group: validate-gitops-${{ gitea.ref }}
cancel-in-progress: true
jobs:
validate:
runs-on:
- self-hosted
- linux
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Validate
run: ./hack/validate.sh
+22
View File
@@ -0,0 +1,22 @@
# Terraform working directories and local state
**/.terraform/*
**/.terraform-state/*
*.tfstate
*.tfstate.*
*.tfplan
*.tfvars
!*.tfvars.example
crash.log
crash.*.log
# Bootstrap material and local operator files
.local/*
!.local/.gitkeep
*init*.json
# Tool and editor output
.DS_Store
.idea/
.vscode/
*.swp
*.tmp
+14
View File
@@ -0,0 +1,14 @@
# Repository operating rules
- Treat the Gitea `origin` as the canonical deployment repository.
- Production is disabled; do not create or enable production Applications
without a complete production design and explicit approval.
- Never commit Terraform state, provider directories, plan files, tfvars,
Vault init JSON, unseal/recovery material or plaintext credentials.
- A Vault API object may be owned by only one Terraform state.
- Keep secret payloads outside Terraform resources and data sources.
- Routine GitOps automation changes Git only; direct cluster mutation is
reserved for documented bootstrap and recovery runbooks.
- Run `make validate` before handing off repository changes.
- Do not apply to a live cluster unless the user explicitly requests live
deployment and the kube context has been verified.
+31
View File
@@ -0,0 +1,31 @@
# Changelog
## 2026-07-25
- Established the internal Gitea repository as the single GitOps source.
- Replaced staged Argo roots with one bootstrap seed and one cluster-owned
root Application.
- Reorganized the repository around `clusters/dev-k3s`, environment-neutral
`platform`/`workloads` bases and separate `iac/terraform`.
- Corrected the Sealed Secrets Helm repository and added controller resources
and key renewal configuration.
- Removed incomplete production skeletons; production remains unsupported.
- Changed application promotion to a Gitea pull request carrying an immutable
OCI digest and removed direct writes to `main`.
- Pinned Vault, PostgreSQL, Keycloak, Vault Injector and Sealed Secrets images
to verified multi-architecture digests.
- Removed routine `kubectl apply`, port-forward orchestration and reusable
Vault operator-token scripts.
- Added a guarded, dev-only Vault init/unseal/root-revoke entrypoint.
- Fixed auth migration and Keycloak sync Job lifecycle and ordering.
- Removed selective sync from Applications containing Sync hooks.
- Enabled ConfigMap hash rollouts and projected Vault authentication tokens.
- Removed the same-cluster Transit Vault and its credential rotation cycle.
- Consolidated Terraform into `vault-core` and `vault-database` remote states
with write-only/ephemeral credential inputs.
- Removed tracked Terraform providers and backend metadata.
- Moved machine-consumed Vault policies out of runbooks and narrowed routine
automation permissions.
- Recorded Gateway API first and deferred Istio ambient adoption criteria.
- Reduced shell entrypoints from 18 files/1,205 lines to 3 files/300 lines.
- Split current documentation from archived historical material.
+46 -3016
View File
File diff suppressed because it is too large Load Diff
+34
View File
@@ -0,0 +1,34 @@
.PHONY: bootstrap validate vault-init terraform-init terraform-plan terraform-apply check-terraform-inputs
KUBE_CONTEXT ?=
TF_ROOT ?=
BACKEND_CONFIG ?=
TF_DIR = iac/terraform/live/dev-k3s/$(TF_ROOT)
bootstrap:
@test -n "$(KUBE_CONTEXT)" || (echo "KUBE_CONTEXT is required" >&2; exit 1)
./hack/bootstrap-argocd.sh --context "$(KUBE_CONTEXT)"
validate:
./hack/validate.sh
vault-init:
./hack/vault-init.sh init
check-terraform-inputs:
@case "$(TF_ROOT)" in \
vault-core|vault-database) ;; \
*) echo "TF_ROOT must be vault-core or vault-database" >&2; exit 1 ;; \
esac
@test -f "$(BACKEND_CONFIG)" || (echo "BACKEND_CONFIG must point to a readable backend file" >&2; exit 1)
terraform-init: check-terraform-inputs
terraform -chdir="$(TF_DIR)" init -input=false -reconfigure -backend-config="$(abspath $(BACKEND_CONFIG))"
terraform-plan: terraform-init
terraform -chdir="$(TF_DIR)" plan -input=false -lock-timeout=5m
terraform-apply: terraform-init
@test "$(APPROVE_APPLY)" = "dev-k3s/$(TF_ROOT)" || \
(echo "Set APPROVE_APPLY=dev-k3s/$(TF_ROOT) to continue" >&2; exit 1)
terraform -chdir="$(TF_DIR)" apply -input=false -lock-timeout=5m
+77 -1578
View File
File diff suppressed because it is too large Load Diff
@@ -1,7 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: api-server-config
data:
APP_SERVER_PORT: "8082"
APP_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: http://auth-public.auth-dev.svc.cluster.local
@@ -1,13 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: api-prod
resources:
- ../../base
- namespace.yaml
images:
- name: ghcr.io/donghyeonka/project-api-server
newName: ghcr.io/donghyeonka/project-api-server
newTag: fd097c9
@@ -1,11 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: api-prod
labels:
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/enforce-version: latest
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: latest
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: latest
@@ -1,26 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: auth-server-config
data:
SPRING_PROFILES_ACTIVE: dev
SERVER_FORWARD_HEADERS_STRATEGY: framework
APP_DOCS_TITLE: Project Auth Server API
APP_DOCS_DESCRIPTION: dev auth-server OpenAPI
APP_DOCS_VERSION: v1
APP_DATASOURCE_URL: jdbc:postgresql://postgres.platform.svc.cluster.local:5432/project_auth
APP_PERSISTENCE_MIGRATION_RUN_ON_STARTUP: "false"
APP_SECURITY_OAUTH2_KEYCLOAK_ISSUER_URI: http://keycloak-public.platform.svc.cluster.local/realms/project-auth
APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_ID: project-auth-server
APP_SECURITY_OAUTH2_GOOGLE_REGISTRATION_ID: keycloak-google
APP_SECURITY_OAUTH2_GOOGLE_IDP_HINT: google
APP_SECURITY_OAUTH2_GITHUB_REGISTRATION_ID: keycloak-github
APP_SECURITY_OAUTH2_GITHUB_IDP_HINT: github
APP_SECURITY_JWT_ISSUER: http://auth-public.auth-dev.svc.cluster.local
APP_SECURITY_JWT_ACTIVE_KEY_ID: dev-vault-rsa-1
APP_SECURITY_JWT_GENERATE_KEY_PAIR_ON_STARTUP: "false"
APP_SECURITY_JWT_ACCESS_TOKEN_EXPIRATION: PT30M
APP_SECURITY_JWT_VAULT_ENABLED: "true"
APP_SECURITY_JWT_VAULT_ADDRESS: http://vault.vault.svc.cluster.local:8200
APP_SECURITY_JWT_VAULT_MOUNT_PATH: transit
APP_SECURITY_JWT_VAULT_TRANSIT_KEY_NAME: project-auth-jwt
@@ -1,13 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: auth-prod
resources:
- ../../base
- namespace.yaml
images:
- name: ghcr.io/donghyeonka/project-auth-server
newName: ghcr.io/donghyeonka/project-auth-server
newTag: 5648fd2
@@ -1,11 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: auth-prod
labels:
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/enforce-version: latest
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: latest
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: latest
-18
View File
@@ -1,18 +0,0 @@
## Argo CD Structure
`argocd/` 디렉터리는 환경(`dev`, `prod`)과 성격(`apps`, `infra`) 기준으로 나눠 관리합니다.
- `applications/<env>/apps`: 서비스 애플리케이션 선언
- `applications/<env>/infra`: 공용 인프라/컨트롤러 선언
- `projects/<env>/apps-project.yaml`: 서비스 애플리케이션용 AppProject
- `projects/<env>/infra-project.yaml`: 공용 인프라용 AppProject
현재 `dev`에는 실제 선언을 두고, `prod`는 이후 운영 확장을 위한 구조와 프로젝트 골격을 먼저 유지합니다.
현재 dev `infra`에는 대표적으로 아래 Application이 포함됩니다.
- `vault-transit`: workload Vault transit auto-unseal provider
- `vault`: workload Vault
- `platform`: postgres, keycloak
- `vault-agent-injector`: workload secret injection
- `sealed-secrets`: image pull secret 같은 예외 secret 처리
@@ -1,34 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: vault-transit-dev
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "10"
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: infra-dev
source:
repoURL: https://github.com/DongHyeonka/Project-Auth-GitOps
targetRevision: main
path: infra/vault-transit/overlays/dev
destination:
server: https://kubernetes.default.svc
namespace: vault-transit
syncPolicy:
automated:
enabled: true
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- PruneLast=true
- ApplyOutOfSyncOnly=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
revisionHistoryLimit: 5
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
-68
View File
@@ -1,68 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: infra-dev
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
description: Dev shared infrastructure managed by Argo CD
sourceRepos:
- https://github.com/DongHyeonka/Project-Auth-GitOps
- https://bitnami-labs.github.io/sealed-secrets
- https://helm.releases.hashicorp.com
destinations:
- namespace: platform
server: https://kubernetes.default.svc
- namespace: vault
server: https://kubernetes.default.svc
- namespace: vault-transit
server: https://kubernetes.default.svc
- namespace: kube-system
server: https://kubernetes.default.svc
clusterResourceWhitelist:
- group: ""
kind: Namespace
- group: "apiextensions.k8s.io"
kind: CustomResourceDefinition
- group: "rbac.authorization.k8s.io"
kind: ClusterRole
- group: "rbac.authorization.k8s.io"
kind: ClusterRoleBinding
- group: "admissionregistration.k8s.io"
kind: MutatingWebhookConfiguration
namespaceResourceWhitelist:
- group: ""
kind: ConfigMap
- group: ""
kind: Secret
- group: ""
kind: Service
- group: ""
kind: ServiceAccount
- group: ""
kind: PersistentVolumeClaim
- group: "bitnami.com"
kind: SealedSecret
- group: "rbac.authorization.k8s.io"
kind: Role
- group: "rbac.authorization.k8s.io"
kind: RoleBinding
- group: "apps"
kind: Deployment
- group: "apps"
kind: StatefulSet
- group: "apps"
kind: ReplicaSet
- group: "autoscaling"
kind: HorizontalPodAutoscaler
- group: "batch"
kind: Job
- group: "networking.k8s.io"
kind: Ingress
- group: "networking.k8s.io"
kind: NetworkPolicy
- group: "policy"
kind: PodDisruptionBudget
orphanedResources:
warn: true
-50
View File
@@ -1,50 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: apps-prod
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
description: Prod application workloads managed by Argo CD
sourceRepos:
- https://github.com/DongHyeonka/Project-Auth-GitOps
destinations:
- namespace: auth-prod
server: https://kubernetes.default.svc
- namespace: api-prod
server: https://kubernetes.default.svc
clusterResourceWhitelist:
- group: ""
kind: Namespace
namespaceResourceWhitelist:
- group: ""
kind: ConfigMap
- group: ""
kind: Secret
- group: ""
kind: Service
- group: ""
kind: ServiceAccount
- group: ""
kind: PersistentVolumeClaim
- group: "bitnami.com"
kind: SealedSecret
- group: "apps"
kind: Deployment
- group: "apps"
kind: StatefulSet
- group: "apps"
kind: ReplicaSet
- group: "autoscaling"
kind: HorizontalPodAutoscaler
- group: "batch"
kind: Job
- group: "networking.k8s.io"
kind: Ingress
- group: "networking.k8s.io"
kind: NetworkPolicy
- group: "policy"
kind: PodDisruptionBudget
orphanedResources:
warn: true
+23
View File
@@ -0,0 +1,23 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: project-gitops-dev-k3s
namespace: argocd
spec:
project: default
source:
repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
targetRevision: main
path: clusters/dev-k3s
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
enabled: true
prune: true
selfHeal: true
syncOptions:
- PruneLast=true
- FailOnSharedResource=true
revisionHistoryLimit: 10
+2
View File
@@ -0,0 +1,2 @@
ARGOCD_VERSION=v3.4.2
ARGOCD_INSTALL_SHA256=69114b8c9eb48a1d08598e6f654a0869b10ae902456ea4b70796cb563760f5ec
@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- sealed-secrets.yaml
- vault.yaml
- vault-agent-injector.yaml
@@ -3,17 +3,31 @@ kind: Application
metadata:
name: sealed-secrets-dev
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "0"
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: infra-dev
project: cluster-addons-dev
source:
repoURL: https://bitnami-labs.github.io/sealed-secrets
repoURL: https://bitnami.github.io/sealed-secrets
chart: sealed-secrets
targetRevision: 2.17.9
helm:
values: |
fullnameOverride: sealed-secrets-controller
keyrenewperiod: 720h
image:
repository: bitnami/sealed-secrets-controller
tag: "0.33.1@sha256:e7fad65c2d2f47e48d9ca17408ed56961bfa6a6dd74ccd4a1a214664156534bc"
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 250m
memory: 256Mi
destination:
server: https://kubernetes.default.svc
namespace: kube-system
@@ -23,11 +37,11 @@ spec:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- PruneLast=true
- ApplyOutOfSyncOnly=true
- FailOnSharedResource=true
retry:
limit: 5
refresh: true
backoff:
duration: 5s
factor: 2
@@ -4,11 +4,12 @@ metadata:
name: vault-agent-injector-dev
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "10"
argocd.argoproj.io/sync-wave: "2"
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: infra-dev
project: cluster-addons-dev
source:
repoURL: https://helm.releases.hashicorp.com
chart: vault
@@ -23,6 +24,9 @@ spec:
injector:
enabled: true
authPath: auth/kubernetes
image:
repository: hashicorp/vault-k8s
tag: "1.7.2@sha256:ae3d307658b72a1cf35dab9bdf92c995d45cdc7183af0516857714b5bd0ba84d"
webhook:
failurePolicy: Fail
namespaceSelector:
@@ -37,7 +41,7 @@ spec:
memory: 256Mi
agentImage:
repository: hashicorp/vault
tag: "1.18"
tag: "1.18.5@sha256:750bb37c1638fa194ab37053a81618c61bb0491ddec6fccac87c07a8e6cd8166"
destination:
server: https://kubernetes.default.svc
namespace: vault
@@ -47,11 +51,11 @@ spec:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- PruneLast=true
- ApplyOutOfSyncOnly=true
- FailOnSharedResource=true
retry:
limit: 5
refresh: true
backoff:
duration: 5s
factor: 2
@@ -4,15 +4,14 @@ metadata:
name: vault-dev
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "10"
finalizers:
- resources-finalizer.argocd.argoproj.io
argocd.argoproj.io/sync-wave: "1"
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
spec:
project: infra-dev
project: platform-dev
source:
repoURL: https://github.com/DongHyeonka/Project-Auth-GitOps
repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
targetRevision: main
path: infra/vault/overlays/dev
path: clusters/dev-k3s/manifests/vault
destination:
server: https://kubernetes.default.svc
namespace: vault
@@ -24,9 +23,10 @@ spec:
syncOptions:
- CreateNamespace=true
- PruneLast=true
- ApplyOutOfSyncOnly=true
- FailOnSharedResource=true
retry:
limit: 5
refresh: true
backoff:
duration: 5s
factor: 2
@@ -1,8 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: vault-prod
resources:
- ../../base
- namespace.yaml
- foundation
- platform
- workloads
@@ -4,15 +4,14 @@ metadata:
name: platform-dev
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "20"
finalizers:
- resources-finalizer.argocd.argoproj.io
argocd.argoproj.io/sync-wave: "10"
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
spec:
project: infra-dev
project: platform-dev
source:
repoURL: https://github.com/DongHyeonka/Project-Auth-GitOps
repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
targetRevision: main
path: infra/platform/overlays/dev
path: clusters/dev-k3s/manifests/auth-system
destination:
server: https://kubernetes.default.svc
namespace: platform
@@ -24,9 +23,10 @@ spec:
syncOptions:
- CreateNamespace=true
- PruneLast=true
- ApplyOutOfSyncOnly=true
- FailOnSharedResource=true
retry:
limit: 5
refresh: true
backoff:
duration: 5s
factor: 2
@@ -1,8 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: vault-transit
resources:
- ../../base
- namespace.yaml
- auth-system.yaml
@@ -4,15 +4,16 @@ metadata:
name: api-server-dev
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "40"
argocd.argoproj.io/sync-wave: "20"
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: apps-dev
source:
repoURL: https://github.com/DongHyeonka/Project-Auth-GitOps
repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
targetRevision: main
path: apps/api-server/overlays/dev
path: clusters/dev-k3s/manifests/api-server
destination:
server: https://kubernetes.default.svc
namespace: api-dev
@@ -24,9 +25,10 @@ spec:
syncOptions:
- CreateNamespace=true
- PruneLast=true
- ApplyOutOfSyncOnly=true
- FailOnSharedResource=true
retry:
limit: 5
refresh: true
backoff:
duration: 5s
factor: 2
@@ -4,15 +4,16 @@ metadata:
name: auth-server-dev
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "30"
argocd.argoproj.io/sync-wave: "20"
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: apps-dev
source:
repoURL: https://github.com/DongHyeonka/Project-Auth-GitOps
repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
targetRevision: main
path: apps/auth-server/overlays/dev
path: clusters/dev-k3s/manifests/auth-server
destination:
server: https://kubernetes.default.svc
namespace: auth-dev
@@ -24,9 +25,10 @@ spec:
syncOptions:
- CreateNamespace=true
- PruneLast=true
- ApplyOutOfSyncOnly=true
- FailOnSharedResource=true
retry:
limit: 5
refresh: true
backoff:
duration: 5s
factor: 2
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- auth-server.yaml
- api-server.yaml
+6
View File
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- projects
- applications
@@ -0,0 +1,2 @@
APP_SERVER_PORT=8082
APP_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=http://auth-public.auth-dev.svc.cluster.local
@@ -3,6 +3,7 @@ kind: Ingress
metadata:
name: api-server
annotations:
argocd.argoproj.io/sync-wave: "20"
traefik.ingress.kubernetes.io/router.entrypoints: web
spec:
ingressClassName: traefik
@@ -4,16 +4,21 @@ kind: Kustomization
namespace: api-dev
resources:
- ../../base
- ../../../../workloads/api-server/base
- namespace.yaml
- configmap.yaml
- ingress.yaml
- public-access.yaml
- networkpolicy.yaml
- ghcr-regcred.sealedsecret.yaml
generatorOptions:
disableNameSuffixHash: true
annotations:
argocd.argoproj.io/sync-wave: "0"
configMapGenerator:
- name: api-server-config
envs:
- config.env
images:
- name: ghcr.io/donghyeonka/project-api-server
@@ -0,0 +1,21 @@
SPRING_PROFILES_ACTIVE=dev
SERVER_FORWARD_HEADERS_STRATEGY=framework
APP_DOCS_TITLE=Project Auth Server API
APP_DOCS_DESCRIPTION=dev auth-server OpenAPI
APP_DOCS_VERSION=v1
APP_DATASOURCE_URL=jdbc:postgresql://postgres.platform.svc.cluster.local:5432/project_auth
APP_PERSISTENCE_MIGRATION_RUN_ON_STARTUP=false
APP_SECURITY_OAUTH2_KEYCLOAK_ISSUER_URI=http://keycloak-public.platform.svc.cluster.local/realms/project-auth
APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_ID=project-auth-server
APP_SECURITY_OAUTH2_GOOGLE_REGISTRATION_ID=keycloak-google
APP_SECURITY_OAUTH2_GOOGLE_IDP_HINT=google
APP_SECURITY_OAUTH2_GITHUB_REGISTRATION_ID=keycloak-github
APP_SECURITY_OAUTH2_GITHUB_IDP_HINT=github
APP_SECURITY_JWT_ISSUER=http://auth-public.auth-dev.svc.cluster.local
APP_SECURITY_JWT_ACTIVE_KEY_ID=dev-vault-rsa-1
APP_SECURITY_JWT_GENERATE_KEY_PAIR_ON_STARTUP=false
APP_SECURITY_JWT_ACCESS_TOKEN_EXPIRATION=PT30M
APP_SECURITY_JWT_VAULT_ENABLED=true
APP_SECURITY_JWT_VAULT_ADDRESS=http://vault.vault.svc.cluster.local:8200
APP_SECURITY_JWT_VAULT_MOUNT_PATH=transit
APP_SECURITY_JWT_VAULT_TRANSIT_KEY_NAME=project-auth-jwt
@@ -7,6 +7,7 @@ spec:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-service-account-token-volume-name: vault-token
vault.hashicorp.com/agent-inject-secret-migration-env: database/creds/auth-db-migration-dev
vault.hashicorp.com/agent-inject-template-migration-env: |
{{- with secret "database/creds/auth-db-migration-dev" -}}
@@ -18,7 +19,16 @@ spec:
vault.hashicorp.com/agent-run-as-user: "10001"
vault.hashicorp.com/role: auth-db-migration-dev
spec:
automountServiceAccountToken: true
automountServiceAccountToken: false
volumes:
- name: vault-token
projected:
defaultMode: 0444
sources:
- serviceAccountToken:
audience: vault
expirationSeconds: 3600
path: token
containers:
- name: auth-db-migration
command:
@@ -8,6 +8,7 @@ spec:
annotations:
vault.hashicorp.com/agent-cache-enable: "true"
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-service-account-token-volume-name: vault-token
vault.hashicorp.com/agent-inject-secret-runtime-env: kv/data/dev/platform/postgres/auth-server
vault.hashicorp.com/agent-inject-template-runtime-env: |
{{ with secret "kv/data/dev/platform/postgres/auth-server" }}
@@ -23,7 +24,16 @@ spec:
vault.hashicorp.com/agent-run-as-user: "10001"
vault.hashicorp.com/role: auth-server-dev
spec:
automountServiceAccountToken: true
automountServiceAccountToken: false
volumes:
- name: vault-token
projected:
defaultMode: 0444
sources:
- serviceAccountToken:
audience: vault
expirationSeconds: 3600
path: token
containers:
- name: auth-server
command:
@@ -3,6 +3,7 @@ kind: Ingress
metadata:
name: auth-server
annotations:
argocd.argoproj.io/sync-wave: "20"
traefik.ingress.kubernetes.io/router.entrypoints: web
spec:
ingressClassName: traefik
@@ -4,9 +4,8 @@ kind: Kustomization
namespace: auth-dev
resources:
- ../../base
- ../../../../workloads/auth-server/base
- namespace.yaml
- configmap.yaml
- ingress.yaml
- public-access.yaml
- networkpolicy.yaml
@@ -17,9 +16,15 @@ patches:
- path: db-migration-job.vault-patch.yaml
generatorOptions:
disableNameSuffixHash: true
annotations:
argocd.argoproj.io/sync-wave: "0"
configMapGenerator:
- name: auth-server-config
envs:
- config.env
images:
- name: ghcr.io/donghyeonka/project-auth-server
newName: ghcr.io/donghyeonka/project-auth-server
newTag: 1f47f2c
- name: ghcr.io/donghyeonka/project-auth-server
newName: ghcr.io/donghyeonka/project-auth-server
newTag: 1f47f2c
@@ -0,0 +1,9 @@
POSTGRES_SUPERUSER=postgres
POSTGRES_DEFAULT_DB=postgres
AUTH_DB_NAME=project_auth
AUTH_DB_USER=project_auth
KEYCLOAK_DB_NAME=keycloak
KEYCLOAK_DB_USER=keycloak
KEYCLOAK_BOOTSTRAP_ADMIN_USERNAME=admin
KEYCLOAK_CLIENT_ID=project-auth-server
AUTH_SERVER_BASE_URL=http://auth-public.auth-dev.svc.cluster.local
@@ -7,6 +7,7 @@ spec:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-service-account-token-volume-name: vault-token
vault.hashicorp.com/agent-inject-perms-keycloak-sync-env: "0644"
vault.hashicorp.com/agent-inject-secret-keycloak-sync-env: kv/data/dev/platform/keycloak/bootstrap-admin
vault.hashicorp.com/agent-inject-template-keycloak-sync-env: |
@@ -20,7 +21,16 @@ spec:
vault.hashicorp.com/agent-pre-populate-only: "true"
vault.hashicorp.com/role: keycloak-client-sync-dev
spec:
automountServiceAccountToken: true
automountServiceAccountToken: false
volumes:
- name: vault-token
projected:
defaultMode: 0444
sources:
- serviceAccountToken:
audience: vault
expirationSeconds: 3600
path: token
containers:
- name: keycloak-client-sync
command:
@@ -31,13 +41,19 @@ spec:
. /vault/secrets/keycloak-sync-env
set -eu
until /opt/keycloak/bin/kcadm.sh config credentials \
--server http://keycloak.platform.svc.cluster.local \
--realm master \
--user "$KC_BOOTSTRAP_ADMIN_USERNAME" \
--password "$KC_BOOTSTRAP_ADMIN_PASSWORD" >/dev/null 2>&1; do
ready=false
for _ in $(seq 1 60); do
if /opt/keycloak/bin/kcadm.sh config credentials \
--server http://keycloak.platform.svc.cluster.local \
--realm master \
--user "$KC_BOOTSTRAP_ADMIN_USERNAME" \
--password "$KC_BOOTSTRAP_ADMIN_PASSWORD" >/dev/null 2>&1; then
ready=true
break
fi
sleep 5
done
test "$ready" = true
CLIENT_UUID=$(/opt/keycloak/bin/kcadm.sh get clients \
-r project-auth \
@@ -7,6 +7,7 @@ spec:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-service-account-token-volume-name: vault-token
vault.hashicorp.com/agent-inject-perms-keycloak-env: "0644"
vault.hashicorp.com/agent-inject-secret-keycloak-env: kv/data/dev/platform/postgres/keycloak
vault.hashicorp.com/agent-inject-template-keycloak-env: |
@@ -19,7 +20,16 @@ spec:
{{ end }}
vault.hashicorp.com/role: keycloak-dev
spec:
automountServiceAccountToken: true
automountServiceAccountToken: false
volumes:
- name: vault-token
projected:
defaultMode: 0444
sources:
- serviceAccountToken:
audience: vault
expirationSeconds: 3600
path: token
containers:
- name: keycloak
command:
@@ -4,13 +4,21 @@ kind: Kustomization
namespace: platform
resources:
- ../../base
- ../../../../platform/auth-system/base
- namespace.yaml
- configmap.yaml
- keycloak-ingress.yaml
- public-access.yaml
- networkpolicy.yaml
generatorOptions:
annotations:
argocd.argoproj.io/sync-wave: "0"
configMapGenerator:
- name: platform-config
envs:
- config.env
patches:
- path: postgres.vault-patch.yaml
- path: keycloak.vault-patch.yaml
@@ -2,6 +2,8 @@ apiVersion: v1
kind: Namespace
metadata:
name: platform
annotations:
argocd.argoproj.io/sync-options: Prune=false
labels:
vault-injection: enabled
pod-security.kubernetes.io/enforce: baseline
@@ -7,6 +7,7 @@ spec:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-service-account-token-volume-name: vault-token
vault.hashicorp.com/agent-inject-perms-postgres-env: "0644"
vault.hashicorp.com/agent-inject-secret-postgres-env: kv/data/dev/platform/postgres/superuser
vault.hashicorp.com/agent-inject-template-postgres-env: |
@@ -23,7 +24,16 @@ spec:
{{ end }}
vault.hashicorp.com/role: postgres-dev
spec:
automountServiceAccountToken: true
automountServiceAccountToken: false
volumes:
- name: vault-token
projected:
defaultMode: 0444
sources:
- serviceAccountToken:
audience: vault
expirationSeconds: 3600
path: token
containers:
- name: postgres
command:
@@ -4,6 +4,6 @@ kind: Kustomization
namespace: vault
resources:
- ../../base
- ../../../../platform/security/vault/base
- namespace.yaml
- networkpolicy.yaml
@@ -2,6 +2,8 @@ apiVersion: v1
kind: Namespace
metadata:
name: vault
annotations:
argocd.argoproj.io/sync-options: Prune=false
labels:
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/enforce-version: latest
@@ -36,28 +36,6 @@ spec:
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: vault-server-allow-transit-egress
spec:
podSelector:
matchLabels:
app: vault
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: vault-transit
podSelector:
matchLabels:
app: vault-transit
ports:
- protocol: TCP
port: 8200
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: vault-server-allow-postgres-egress
spec:
@@ -3,12 +3,15 @@ kind: AppProject
metadata:
name: apps-dev
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "-10"
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
description: Dev application workloads managed by Argo CD
sourceRepos:
- https://github.com/DongHyeonka/Project-Auth-GitOps
- https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
destinations:
- namespace: auth-dev
server: https://kubernetes.default.svc
@@ -20,31 +23,19 @@ spec:
namespaceResourceWhitelist:
- group: ""
kind: ConfigMap
- group: ""
kind: Secret
- group: ""
kind: Service
- group: ""
kind: ServiceAccount
- group: ""
kind: PersistentVolumeClaim
- group: "bitnami.com"
kind: SealedSecret
- group: "apps"
kind: Deployment
- group: "apps"
kind: StatefulSet
- group: "apps"
kind: ReplicaSet
- group: "autoscaling"
kind: HorizontalPodAutoscaler
- group: "batch"
kind: Job
- group: "networking.k8s.io"
kind: Ingress
- group: "networking.k8s.io"
kind: NetworkPolicy
- group: "policy"
kind: PodDisruptionBudget
orphanedResources:
warn: true
@@ -0,0 +1,48 @@
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: cluster-addons-dev
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "-10"
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
description: Cluster-scoped controllers required by the dev GitOps stack
sourceRepos:
- https://bitnami.github.io/sealed-secrets
- https://helm.releases.hashicorp.com
destinations:
- namespace: kube-system
server: https://kubernetes.default.svc
- namespace: vault
server: https://kubernetes.default.svc
clusterResourceWhitelist:
- group: ""
kind: Namespace
- group: apiextensions.k8s.io
kind: CustomResourceDefinition
- group: rbac.authorization.k8s.io
kind: ClusterRole
- group: rbac.authorization.k8s.io
kind: ClusterRoleBinding
- group: admissionregistration.k8s.io
kind: MutatingWebhookConfiguration
namespaceResourceWhitelist:
- group: ""
kind: ConfigMap
- group: ""
kind: Secret
- group: ""
kind: Service
- group: ""
kind: ServiceAccount
- group: apps
kind: Deployment
- group: rbac.authorization.k8s.io
kind: Role
- group: rbac.authorization.k8s.io
kind: RoleBinding
orphanedResources:
warn: true
@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- apps.yaml
- cluster-addons.yaml
- platform.yaml
@@ -1,25 +1,25 @@
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: infra-prod
name: platform-dev
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "-10"
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
description: Prod shared infrastructure managed by Argo CD
description: Dev shared infrastructure managed by Argo CD
sourceRepos:
- https://github.com/DongHyeonka/Project-Auth-GitOps
- https://bitnami-labs.github.io/sealed-secrets
- https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
destinations:
- namespace: platform-prod
- namespace: platform
server: https://kubernetes.default.svc
- namespace: kube-system
- namespace: vault
server: https://kubernetes.default.svc
clusterResourceWhitelist:
- group: ""
kind: Namespace
- group: "apiextensions.k8s.io"
kind: CustomResourceDefinition
- group: "rbac.authorization.k8s.io"
kind: ClusterRole
- group: "rbac.authorization.k8s.io"
@@ -27,16 +27,12 @@ spec:
namespaceResourceWhitelist:
- group: ""
kind: ConfigMap
- group: ""
kind: Secret
- group: ""
kind: Service
- group: ""
kind: ServiceAccount
- group: ""
kind: PersistentVolumeClaim
- group: "bitnami.com"
kind: SealedSecret
- group: "rbac.authorization.k8s.io"
kind: Role
- group: "rbac.authorization.k8s.io"
@@ -45,17 +41,11 @@ spec:
kind: Deployment
- group: "apps"
kind: StatefulSet
- group: "apps"
kind: ReplicaSet
- group: "autoscaling"
kind: HorizontalPodAutoscaler
- group: "batch"
kind: Job
- group: "networking.k8s.io"
kind: Ingress
- group: "networking.k8s.io"
kind: NetworkPolicy
- group: "policy"
kind: PodDisruptionBudget
orphanedResources:
warn: true
+12
View File
@@ -0,0 +1,12 @@
# ADR 0001: Internal Gitea is canonical
Status: accepted
The internal repository
`https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops` is the only
writable deployment source.
Argo CD and Gitea Actions use this URL. A GitHub copy may exist only as a
read-only mirror with monitored replication; it must never be an independent
deployment branch. GHCR remains an image registry and does not require GitHub
to host the GitOps source.
+24
View File
@@ -0,0 +1,24 @@
# ADR 0002: Terraform state ownership
Status: accepted
Terraform은 VM/네트워크뿐 아니라 provider가 제공되는 Vault API 객체도
관리할 수 있다. 현재 저장소의 Terraform 범위는 Vault API이고 실제
machine provisioning은 provider가 확정될 때 별도 root로 추가한다.
`dev-k3s`는 두 state만 사용한다.
- `vault-core`: mounts, auth backends, policies, Kubernetes/JWT roles,
application Transit key
- `vault-database`: PostgreSQL connection과 dynamic roles
resource/API path 하나는 한 state에만 속한다. state는 암호화, versioning,
access control, locking이 가능한 remote backend에 저장한다.
`vault-core`는 privilege-escalation 가능한 객체를 포함하므로 제한된
관리자 실행만 허용한다. `vault-database`는 core가 생성한
`vault-database-automation-dev` 정책의 short-lived identity로 실행한다.
Secret payload는 Terraform resource/data source로 관리하지 않는다.
필수 credential은 ephemeral variable과 provider write-only argument를
통해서만 apply에 전달한다.
+25
View File
@@ -0,0 +1,25 @@
# ADR 0003: Vault topology
Status: accepted for dev, production decision pending
동일한 단일 노드 K3s 안의 두 Vault는 failure domain을 분리하지 못하면서
초기화, Transit credential, rotation과 staged apply 절차를 추가했다.
따라서 `dev-k3s`는 단일 self-hosted Vault로 단순화한다.
Dev profile:
- single-node integrated Raft
- Shamir 1-of-1 init/unseal
- TLS 미적용
- 명시적 backup/recovery runbook
이 구성은 production에 사용할 수 없다. production은 다음 중 하나를
선택해야 한다.
- managed Vault
- workload cluster 밖의 독립 HA Vault
- 최소 3-node integrated-Raft + TLS + KMS/HSM auto-unseal + PDB,
anti-affinity와 정기 restore exercise
production Vault가 결정되기 전에는 production manifest와 Terraform root를
만들지 않는다.
+18
View File
@@ -0,0 +1,18 @@
# ADR 0004: Single Argo CD root
Status: accepted
Argo CD 설치 후 `bootstrap/argocd/root-application.yaml` 하나만 seed한다.
root는 `clusters/dev-k3s`의 AppProject와 모든 child Application을 소유한다.
반복 `kubectl apply`와 foundation/platform/application별 root wrapper는
제거한다. routine deployment는 Git merge만으로 시작한다.
Child Application의 sync wave는 객체 생성 순서를 가독성 있게 표현하지만
서로 다른 Application의 readiness dependency로 간주하지 않는다.
Workload와 hook은 Vault/DB가 늦게 준비되는 상황을 retry할 수 있어야 한다.
Root가 child Application을 prune하거나 삭제하려면 확인이 필요하다.
shared resource 소유권 충돌은 sync를 실패시킨다. 현재 규모에서는 명시적
Application을 사용하고 두 번째 클러스터가 생길 때 ApplicationSet을
검토한다.
+17
View File
@@ -0,0 +1,17 @@
# ADR 0005: Cluster-first repository layout
Status: accepted
현재는 하나의 platform 팀, 하나의 dev cluster와 소수 workload를 가지므로
GitOps configuration monorepo를 유지한다. application source repository와
deployment configuration repository는 분리한다.
- `platform/`, `workloads/`: 환경 중립 base
- `clusters/<cluster>/manifests`: cluster-specific final composition
- `clusters/<cluster>/applications`: Argo reconciliation inventory
- `iac/terraform`: Kubernetes manifest와 분리된 external API IaC
- `bootstrap`: controller가 존재하기 전의 최소 seed
production 접근권한, 소유 팀, Terraform backend 또는 release cadence가
실제로 갈라질 때 platform GitOps, workload GitOps, IaC repo 분리를
재검토한다. 존재하지 않는 환경의 skeleton은 유지하지 않는다.
+33
View File
@@ -0,0 +1,33 @@
# ADR 0006: Gateway API first, Istio deferred
Status: accepted
현재 단일 노드 K3s와 auth/api 중심 workload에는 service mesh 운영 비용을
정당화할 mTLS identity, L7 authorization, canary traffic policy 또는
multi-team 요구가 없다. 이번 개편에는 Istio를 설치하지 않는다.
선행 작업:
1. Traefik Gateway API provider와 GatewayClass 검증
2. Ingress를 Gateway/HTTPRoute로 이관
3. north-south TLS
4. 내부 호출의 ingress hairpin 제거
5. Vault/PostgreSQL native TLS
6. NetworkPolicy regression test와 observability/SLO
Istio 요구가 실제화되면 sidecar가 아니라 ambient mode로 제한 pilot한다.
초기 범위는 api-server와 auth-server이며 Vault, Vault injector,
PostgreSQL은 제외한다. ztunnel L4부터 시작하고 L7 정책이 필요할 때만
waypoint를 추가한다.
다음 기능 요구 중 두 개 이상과 운영 선행조건이 모두 충족될 때 ADR을
재검토한다.
- ServiceAccount identity 기반 east-west mTLS
- path/JWT 기반 L7 authorization
- canary traffic split/retry/timeout/outlier detection
- 지속적인 서비스·namespace·팀 증가
- application instrumentation만으로 해결하기 어려운 장애 분석
현재 Ingress를 즉시 제거하지 않는다. TLS, DNS, GatewayClass 계약이
확정되기 전 가상의 Gateway 설정을 배포하지 않기 위함이다.
+38
View File
@@ -0,0 +1,38 @@
# Argo CD layout
`bootstrap/argocd/root-application.yaml`이 유일한 수동 seed입니다. 이
Application은 `clusters/dev-k3s`를 source로 사용하고 다음 리소스를
소유합니다.
```text
clusters/dev-k3s
├── projects
└── applications
├── foundation
│ ├── sealed-secrets
│ ├── vault
│ └── vault-agent-injector
├── platform
│ └── auth-system
└── workloads
├── auth-server
└── api-server
```
AppProject는 root sync wave `-10`, foundation은 `0~1`, platform은 `10`,
workload는 `20`입니다. 이 wave는 child Application 객체 생성 순서만
표현하며 서로 다른 Application의 readiness dependency로 사용하지
않습니다. Vault Agent와 workload는 필요한 Vault/DB API가 준비될 때까지
자체 retry 가능한 형태여야 합니다.
모든 child Application은 auto-sync, prune, self-heal을 사용합니다.
Application 삭제와 parent prune은 확인이 필요하며, shared resource
소유권 충돌은 `FailOnSharedResource=true`로 실패시킵니다.
Sync hook이 있는 `auth-server``auth-system`에는 selective sync 옵션을
사용하지 않습니다. DB migration과 Keycloak client sync는 같은
Application 내부 wave로 순서를 제어합니다.
클러스터가 하나이고 child Application 수가 적으므로 현재는 명시적
Application을 사용합니다. 두 번째 클러스터나 실제 production이 생길
때 foundation/platform/workload별 ApplicationSet 도입을 검토합니다.
+55
View File
@@ -0,0 +1,55 @@
# Deployment architecture
## Reconciliation boundaries
```text
Gitea main
|
+-- Argo CD root -> AppProjects + child Applications -> Kubernetes
|
+-- approved Terraform runner -> Vault API
```
Argo CD는 Kubernetes desired state만 관리합니다. 최초 Argo 설치/root
seed와 문서화된 recovery 외에는 직접 cluster mutation을 하지 않습니다.
Terraform은 Config Management Plugin이나 Argo hook 안에서 실행하지
않습니다.
## Kustomize ownership
- `platform/`, `workloads/`: 환경 중립 base
- `clusters/dev-k3s/manifests/`: namespace, host, image, Vault role 및
NetworkPolicy를 포함하는 최종 cluster composition
- Argo CD Application: final composition만 source로 사용
지원하지 않는 production overlay는 존재하지 않습니다. production
계약과 승인 경계가 확정될 때 별도로 생성합니다.
## In-application ordering
`auth-server`의 한 sync operation 안에서:
- generated ConfigMap과 일반 리소스: wave `0`
- database migration Sync hook: wave `5`
- Deployment: wave `10`
- north-south route: wave `20`
`auth-system`의 Keycloak client sync도 idempotent Sync hook이며 deadline,
backoff, `BeforeHookCreation,HookSucceeded` cleanup을 사용합니다.
## Stateful lifecycle
Vault와 PostgreSQL PVC는 `Prune=false`로 보호합니다. child Application
prune/delete는 확인이 필요합니다. path 이동이나 Application rename 전에는
새 owner가 동일 live resource를 정상적으로 추적하는지 확인한 후 이전
owner를 non-cascading 방식으로 제거합니다.
## Image promotion
첫-party image는 애플리케이션 CI가 얻은 정확한 GHCR digest를 Gitea
workflow에 전달합니다. workflow는 digest 변경 PR을 만들고, validation과
승인을 거쳐 merge된 뒤 Argo CD가 배포합니다.
현재 short-SHA tag는 migration 시점의 예외입니다. private GHCR을 읽을
자격증명이 이 저장소 실행 환경에 없으므로 임의 digest로 바꾸지 않았고,
다음 정상 promotion에서 `digest:`로 교체됩니다.
+55
View File
@@ -0,0 +1,55 @@
# Secret trust boundaries
## Dev Vault
`dev-k3s`는 단일 self-hosted Vault를 사용합니다. 동일 workload
클러스터에 별도의 Transit Vault를 두지 않습니다. 단일 Vault는 다음을
소유합니다.
- KV-v2 runtime secret path
- Kubernetes auth와 workload role
- dynamic PostgreSQL credential
- 애플리케이션 JWT signing용 Transit key
dev Vault는 Shamir 1-of-1로 한 번 초기화하고 재시작 시 명시적으로
unseal합니다. 이 방식은 개발 환경 전용입니다. production에서는 managed
Vault 또는 독립 failure domain의 HA integrated-Raft와 KMS/HSM
auto-unseal을 사용해야 합니다.
## Terraform
`vault-core` state는 mounts, auth, policies, roles와 JWT key를 소유하며
제한된 관리자만 적용합니다. `vault-database`는 PostgreSQL connection과
dynamic roles만 소유하고 `vault-database-automation-dev` 정책을 사용합니다.
Terraform variable로 전달되는 token과 PostgreSQL password는 ephemeral/
write-only 경계를 사용합니다. KV payload는 Terraform resource/data
source로 읽거나 쓰지 않습니다.
## Workload authentication
workload는 audience `vault`, TTL 1시간의 projected ServiceAccount token으로
Vault Kubernetes auth에 로그인합니다. token은 Vault Agent가 사용하며
application container에 Kubernetes bearer token을 직접 노출하지 않습니다.
Secret payload는 승인된 운영자가 Vault에 직접 기록합니다. 값은 Git,
Gitea Actions log, Terraform state, Kubernetes manifest에 남기지 않습니다.
## Bootstrap material
Vault init output은 기본적으로 `.local/vault/dev-k3s-init.json`에 mode
`0600`으로 생성됩니다. encrypted custody로 이동한 후 working copy를
제거합니다. initial root token은 `vault-core`와 operator auth 검증 직후
폐기합니다.
Sealed Secrets는 private GHCR pull credential에만 사용합니다. controller
private key는 별도 복구 저장소에 백업해야 합니다.
## Dev limitations
- Vault, PostgreSQL, ingress가 아직 TLS를 사용하지 않음
- single-node Vault와 PostgreSQL
- Kubernetes API egress CIDR가 현재 dev cluster에 종속
- 정적 bootstrap secret은 coordinated rotation 필요
이 제약은 production에서 허용되지 않습니다.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
# Archived documentation
Files in this directory describe historical repository states. Paths,
workflows, credentials and operational commands may no longer exist.
Do not execute archived procedures. Use the root README and `docs/runbooks/`.
+209
View File
@@ -0,0 +1,209 @@
# Bootstrap an empty dev-k3s cluster
이 runbook은 폐기 가능한 개발 클러스터만 대상으로 합니다. production에
사용하지 않습니다.
## 1. Preflight
```bash
kubectl config current-context
kubectl cluster-info
make validate
```
의도한 dev cluster가 아니면 중단합니다. 내부 Gitea가 private이면 Argo CD가
root repository를 읽을 수 있는 read-only credential을 외부 secret
authority에서 먼저 provision해야 합니다. credential은 이 저장소에
commit하지 않습니다.
remote state backend 파일을 준비합니다.
```bash
mkdir -p .local/terraform-backend/dev-k3s
cp iac/terraform/backend/dev-k3s/vault-core.s3.hcl.example \
.local/terraform-backend/dev-k3s/vault-core.s3.hcl
cp iac/terraform/backend/dev-k3s/vault-database.s3.hcl.example \
.local/terraform-backend/dev-k3s/vault-database.s3.hcl
```
실제 bucket, endpoint와 workload identity를 설정합니다. backend credential은
파일에 넣지 않습니다.
## 2. Argo CD와 root Application
```bash
make bootstrap KUBE_CONTEXT="$(kubectl config current-context)"
kubectl -n argocd get application project-gitops-dev-k3s
```
이 명령이 수행하는 직접 cluster mutation은 Argo CD 설치와 root seed뿐입니다.
Child Application은 root가 생성합니다.
## 3. Dev Vault 초기화
Vault Pod가 생성될 때까지 기다린 뒤 operator workstation에서 forward합니다.
이 port-forward는 최초 dev bootstrap용이며 routine runner 모델이 아닙니다.
```bash
kubectl -n vault wait --for=create pod -l app=vault --timeout=300s
kubectl -n vault port-forward deployment/vault 8200:8200
```
별도 terminal:
```bash
export VAULT_ADDR=http://127.0.0.1:8200
./hack/vault-init.sh init
```
`.local/vault/dev-k3s-init.json`을 즉시 encrypted custody로 복사합니다.
dev-only 1-of-1 unseal key와 initial root token이 있으므로 일반 backup과
분리합니다.
## 4. Vault core
초기 root token을 shell history에 직접 적지 않습니다.
```bash
export TF_VAR_vault_addr="$VAULT_ADDR"
export TF_VAR_vault_token="$(
jq -r '.root_token' .local/vault/dev-k3s-init.json
)"
make terraform-plan \
TF_ROOT=vault-core \
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-core.s3.hcl
make terraform-apply \
TF_ROOT=vault-core \
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-core.s3.hcl \
APPROVE_APPLY=dev-k3s/vault-core
```
plan에서 mount, auth backend, policy, role, JWT Transit key 이외 객체가
나오면 apply하지 않습니다.
## 5. Runtime secret seed
Secret 값은 Git/Terraform을 통과하지 않습니다. 아래 변수는 terminal
session에만 유지합니다.
```bash
read -r -s -p "PostgreSQL superuser password: " POSTGRES_SUPERUSER_PASSWORD
echo
read -r -s -p "Auth database password: " AUTH_DB_PASSWORD
echo
read -r -s -p "Keycloak database password: " KEYCLOAK_DB_PASSWORD
echo
read -r -s -p "Keycloak bootstrap admin password: " KEYCLOAK_ADMIN_PASSWORD
echo
read -r -s -p "Auth-server Keycloak client secret: " KEYCLOAK_CLIENT_SECRET
echo
secret_file="$(mktemp)"
trap 'rm -f "$secret_file"' EXIT
chmod 0600 "$secret_file"
jq -n --arg password "$POSTGRES_SUPERUSER_PASSWORD" \
'{POSTGRES_SUPERUSER_PASSWORD: $password}' >"$secret_file"
vault kv put kv/dev/platform/postgres/superuser @"$secret_file"
jq -n \
--arg password "$AUTH_DB_PASSWORD" \
'{AUTH_DB_PASSWORD: $password, APP_DATASOURCE_USERNAME: "project_auth", APP_DATASOURCE_PASSWORD: $password}' >"$secret_file"
vault kv put kv/dev/platform/postgres/auth-server @"$secret_file"
jq -n --arg password "$KEYCLOAK_DB_PASSWORD" \
'{KEYCLOAK_DB_PASSWORD: $password}' >"$secret_file"
vault kv put kv/dev/platform/postgres/keycloak @"$secret_file"
jq -n --arg password "$KEYCLOAK_ADMIN_PASSWORD" \
'{KC_BOOTSTRAP_ADMIN_PASSWORD: $password}' >"$secret_file"
vault kv put kv/dev/platform/keycloak/bootstrap-admin @"$secret_file"
jq -n --arg secret "$KEYCLOAK_CLIENT_SECRET" \
'{KEYCLOAK_CLIENT_SECRET: $secret, APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_SECRET: $secret}' >"$secret_file"
vault kv put kv/dev/platform/keycloak/client-auth-server @"$secret_file"
rm -f "$secret_file"
trap - EXIT
```
PostgreSQL이 Vault Agent 주입 후 시작하는지 확인합니다.
```bash
kubectl -n platform rollout status statefulset/postgres --timeout=600s
```
## 6. Vault database state
초기 root token으로 TTL이 짧은 database 전용 token을 발급합니다.
```bash
export TF_VAR_vault_token="$(
vault token create \
-policy=vault-database-automation-dev \
-ttl=30m \
-format=json |
jq -r '.auth.client_token'
)"
export TF_VAR_postgres_admin_password="$POSTGRES_SUPERUSER_PASSWORD"
export TF_VAR_postgres_admin_password_version=1
make terraform-plan \
TF_ROOT=vault-database \
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-database.s3.hcl
make terraform-apply \
TF_ROOT=vault-database \
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-database.s3.hcl \
APPROVE_APPLY=dev-k3s/vault-database
```
## 7. Root token 폐기
Kubernetes auth operator login이 동작하는지 먼저 검증합니다.
```bash
operator_jwt="$(
kubectl -n vault create token vault-operator \
--audience=vault \
--duration=10m
)"
operator_token="$(
VAULT_TOKEN= vault write \
-format=json \
auth/kubernetes/login \
role=vault-operator-dev \
jwt="$operator_jwt" |
jq -r '.auth.client_token'
)"
VAULT_TOKEN="$operator_token" vault token lookup >/dev/null
```
검증 후 initial root token을 폐기합니다.
```bash
./hack/vault-init.sh revoke-root
unset operator_jwt operator_token
unset TF_VAR_vault_token TF_VAR_postgres_admin_password
unset POSTGRES_SUPERUSER_PASSWORD AUTH_DB_PASSWORD KEYCLOAK_DB_PASSWORD
unset KEYCLOAK_ADMIN_PASSWORD KEYCLOAK_CLIENT_SECRET
```
encrypted custody로 옮긴 init material의 local working copy는 조직의
dev recovery 정책에 따라 제거합니다.
## 8. 확인
```bash
kubectl -n argocd get applications
kubectl -n vault get pods
kubectl -n platform get pods
kubectl -n auth-dev get pods
kubectl -n api-dev get pods
```
모든 Application의 sync/health를 확인하고 DB migration 및 Keycloak client
sync hook 결과를 검토합니다. 실패한 hook을 고치기 위해 child manifest를
직접 apply하지 말고 Git PR을 사용합니다.
+46
View File
@@ -0,0 +1,46 @@
# Sealed Secrets backup and recovery
Existing SealedSecrets are decryptable only with a controller private key.
Back up every key after first install and after rotation.
## Backup
Use an encrypted operator workstation:
```bash
umask 077
kubectl -n kube-system get secret \
-l sealedsecrets.bitnami.com/sealed-secrets-key \
-o json > .local/sealed-secrets-keys.json
chmod 0600 .local/sealed-secrets-keys.json
```
Encrypt the file with the organization's recovery mechanism, store at least
two independently controlled copies, and delete the plaintext working copy.
Record cluster, date and checksum without recording private key data.
## Restore
Restore keys before application SealedSecrets sync:
```bash
kubectl -n kube-system apply -f .local/sealed-secrets-keys.json
kubectl -n kube-system rollout restart deployment/sealed-secrets-controller
kubectl -n kube-system rollout status deployment/sealed-secrets-controller
```
Verify both `auth-dev/ghcr-regcred` and `api-dev/ghcr-regcred` are created.
If no key backup exists, old ciphertext cannot be recovered. Create a new
controller key and reseal every Secret from the original credential source.
## Rotation
The chart requests periodic key renewal. Old keys must remain until all
ciphertext has been resealed and verified. GHCR credential rotation requires:
1. issue an organization-owned read-only package credential;
2. create namespace-scoped SealedSecrets with the current controller cert;
3. merge and verify image pulls;
4. revoke the previous credential;
5. update the encrypted key backup.
@@ -0,0 +1,88 @@
# Terraform v2 state migration
새 클러스터에는 이 runbook이 필요하지 않습니다. legacy local state 또는
이전 `provider-foundation`, `workload-foundation`, `workload-config`,
`database-config` remote state가 실제로 존재할 때만 수행합니다.
State 이동은 live object 삭제보다 위험할 수 있습니다. maintenance window와
독립 backup 없이 진행하지 않습니다.
## 목표
| 이전 state | 목표 |
|---|---|
| provider Transit Vault state | archive 후 provider Vault 폐기 절차에서 별도 처리 |
| workload foundation | `vault-core`의 기준 state |
| workload config | 소유 객체를 `vault-core`로 이동 |
| database config | `vault-database`로 backend key migration |
동일 클러스터 Transit Vault는 더 이상 desired state가 아닙니다. Terraform
state에서 먼저 삭제하거나 destroy하지 않습니다. snapshot과 seal dependency
해제 확인 후 별도 decommission 승인을 받아 처리합니다.
## 1. Inventory와 backup
모든 operator machine, runner, remote backend에서 state 위치를 확인합니다.
```bash
find . -type f \
\( -name 'terraform.tfstate*' -o -name '*.tfplan' \) \
-not -path './.git/*'
```
각 state를 `terraform state pull`로 encrypted offline custody에 저장하고
checksum을 기록합니다. backup에는 secret data가 포함될 수 있습니다.
## 2. Backend key migration
`workload-foundation` backend에 연결한 상태에서 새 `vault-core` backend
configuration으로 `terraform init -migrate-state`를 수행합니다.
`database-config`도 같은 방식으로 `vault-database` key로 이동합니다.
실제 backend 파일과 이전 key는 환경마다 다르므로 명령에 값을 하드코딩하지
않습니다. migration 전후 `terraform state pull` checksum과 `state list`
비교합니다.
## 3. Workload configuration ownership 이동
이전 `workload-config` state의 다음 객체를 `vault-core` state의 선언된
address로 이동합니다.
- application Vault policies
- workload Kubernetes auth roles
`terraform state mv -state=<source-backup> -state-out=<target-working-copy>`
사용해 offline copy에서 먼저 연습합니다. target address는 현재
`module.workload_policies``module.workload_roles``terraform state list`
결과를 기준으로 합니다. resource 이름을 추측하지 않습니다.
이동 후 두 state 모두 plan합니다.
- `vault-core`: 변경 없음 또는 address-only 이동
- legacy workload-config: 삭제할 live object 없음
두 plan 중 하나라도 destroy를 제안하면 중단하고 backup state를 복원합니다.
## 4. Database state
기존 database state가 없고 live Vault 객체만 존재할 때만 다음 import ID를
사용합니다.
```text
vault_database_secret_backend_connection.platform_postgres database/config/platform-postgres-dev
vault_database_secret_backend_role.auth_db_migration database/roles/auth-db-migration-dev
vault_database_secret_backend_role.postgres_operator database/roles/postgres-operator-dev
```
이미 다른 state에 address가 있으면 import하지 말고 state ownership을 먼저
이동합니다.
## 5. Cutover 완료 조건
- 두 목표 state가 remote backend와 locking을 사용
- 동일 Vault path가 두 state list에 나타나지 않음
- plan에 예상하지 않은 create/delete가 없음
- legacy state와 backup은 immutable archive
- repo와 runner에 local state/provider directory가 없음
검증이 끝나기 전 legacy backend를 삭제하지 않습니다.
+44
View File
@@ -0,0 +1,44 @@
# Vault backup and recovery
`dev-k3s` Vault는 single-node integrated Raft입니다. upgrade, Terraform
state migration, storage 작업 전에 snapshot을 생성합니다.
## Snapshot
short-lived authorized token과 Vault API 연결을 준비합니다.
```bash
umask 077
vault operator raft snapshot save .local/vault/dev-k3s.snap
sha256sum .local/vault/dev-k3s.snap
chmod 0600 .local/vault/dev-k3s.snap
```
즉시 암호화해 init/unseal material과 다른 위치에 보관합니다. snapshot에는
secret payload가 포함됩니다.
## Recovery material
- Raft snapshot
- unseal key
- 현재 manifest revision
- Terraform remote state backup
- Sealed Secrets controller key
Initial root token은 recovery material이 아닙니다. bootstrap 완료 후
폐기해야 합니다.
## Restore exercise
분기마다 isolated disposable cluster에서 다음을 검증합니다.
1. 동일 Vault version과 storage 설정 배포
2. Vault init 후 snapshot restore
3. 복구 key로 unseal
4. Kubernetes auth 재연결 확인
5. KV read, JWT signing, dynamic database lease issue/revoke 확인
6. recovery time과 누락된 의존성 기록
현재 dev 구성은 production recovery 설계가 아닙니다. production은
independent HA Vault, TLS, auto-unseal과 더 엄격한 backup custody가
필요합니다.
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
usage() {
echo "Usage: $0 --context <kube-context>" >&2
}
if [[ "${1:-}" != "--context" || -z "${2:-}" || -n "${3:-}" ]]; then
usage
exit 1
fi
expected_context="$2"
for cmd in curl kubectl sha256sum; do
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "$cmd is required" >&2
exit 1
fi
done
current_context="$(kubectl config current-context)"
if [[ "$current_context" != "$expected_context" ]]; then
echo "Refusing bootstrap: current context is ${current_context}, expected ${expected_context}." >&2
exit 1
fi
source "${REPO_ROOT}/bootstrap/argocd/version.env"
manifest="$(mktemp)"
cleanup() {
rm -f "$manifest"
}
trap cleanup EXIT INT TERM
curl -fsSL \
"https://raw.githubusercontent.com/argoproj/argo-cd/${ARGOCD_VERSION}/manifests/install.yaml" \
-o "$manifest"
printf '%s %s\n' "$ARGOCD_INSTALL_SHA256" "$manifest" | sha256sum -c -
kubectl --context "$expected_context" create namespace argocd --dry-run=client -o yaml |
kubectl --context "$expected_context" apply -f -
kubectl --context "$expected_context" apply \
--server-side \
--force-conflicts \
-n argocd \
-f "$manifest"
kubectl --context "$expected_context" -n argocd \
rollout status deployment/argocd-server --timeout=300s
kubectl --context "$expected_context" -n argocd \
rollout status deployment/argocd-repo-server --timeout=300s
kubectl --context "$expected_context" apply \
-f "${REPO_ROOT}/bootstrap/argocd/root-application.yaml"
echo "Argo CD ${ARGOCD_VERSION} and the dev-k3s root Application are installed."
+138
View File
@@ -0,0 +1,138 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
for cmd in bash git helm jq kubectl rg terraform; do
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "$cmd is required" >&2
exit 1
fi
done
cd "$REPO_ROOT"
forbidden_files="$(
git ls-files |
rg '(^|/)(\\.terraform|\\.terraform-state|\\.local)(/|$)|(^|/)terraform\\.tfstate($|\\.)|\\.tfplan$' ||
true
)"
if [[ -n "$forbidden_files" ]]; then
echo "Generated or state files are tracked:" >&2
echo "$forbidden_files" >&2
exit 1
fi
while IFS= read -r script; do
bash -n "$script"
done < <(rg --files -g '*.sh')
while IFS= read -r script; do
if [[ ! -x "$script" ]]; then
echo "Shell entry point is not executable: ${script}" >&2
exit 1
fi
done < <(rg --files hack -g '*.sh')
jq empty platform/auth-system/base/files/keycloak/project-auth-realm.json
overlays=(
clusters/dev-k3s
clusters/dev-k3s/manifests/api-server
clusters/dev-k3s/manifests/auth-server
clusters/dev-k3s/manifests/auth-system
clusters/dev-k3s/manifests/vault
)
for overlay in "${overlays[@]}"; do
kubectl kustomize "$overlay" >/dev/null
done
sealed_secrets_render="$(
helm template sealed-secrets sealed-secrets \
--repo https://bitnami.github.io/sealed-secrets \
--version 2.17.9 \
--namespace kube-system \
--set fullnameOverride=sealed-secrets-controller \
--set image.repository=bitnami/sealed-secrets-controller \
--set-string image.tag=0.33.1@sha256:e7fad65c2d2f47e48d9ca17408ed56961bfa6a6dd74ccd4a1a214664156534bc
)"
if [[ "$sealed_secrets_render" != *'image: docker.io/bitnami/sealed-secrets-controller:0.33.1@sha256:e7fad65c2d2f47e48d9ca17408ed56961bfa6a6dd74ccd4a1a214664156534bc'* ]]; then
echo "Sealed Secrets image digest was not rendered by the Helm chart." >&2
exit 1
fi
vault_injector_render="$(
helm template vault-agent-injector vault \
--repo https://helm.releases.hashicorp.com \
--version 0.32.0 \
--namespace vault \
--set server.enabled=false \
--set injector.enabled=true \
--set global.externalVaultAddr=http://vault.vault.svc.cluster.local:8200 \
--set global.tlsDisable=true \
--set injector.image.repository=hashicorp/vault-k8s \
--set-string injector.image.tag=1.7.2@sha256:ae3d307658b72a1cf35dab9bdf92c995d45cdc7183af0516857714b5bd0ba84d \
--set injector.agentImage.repository=hashicorp/vault \
--set-string injector.agentImage.tag=1.18.5@sha256:750bb37c1638fa194ab37053a81618c61bb0491ddec6fccac87c07a8e6cd8166
)"
if [[ "$vault_injector_render" != *'image: "hashicorp/vault-k8s:1.7.2@sha256:ae3d307658b72a1cf35dab9bdf92c995d45cdc7183af0516857714b5bd0ba84d"'* ||
"$vault_injector_render" != *'value: "hashicorp/vault:1.18.5@sha256:750bb37c1638fa194ab37053a81618c61bb0491ddec6fccac87c07a8e6cd8166"'* ]]; then
echo "Vault injector or Agent image digest was not rendered by the Helm chart." >&2
exit 1
fi
rendered_images="$(
{
kubectl kustomize clusters/dev-k3s/manifests/auth-system
kubectl kustomize clusters/dev-k3s/manifests/vault
} | rg '^[[:space:]]+image: (hashicorp/vault|postgres|quay\\.io/keycloak)'
)"
if printf '%s\n' "$rendered_images" | rg -v '@sha256:[a-f0-9]{64}$'; then
echo "A third-party runtime image is not pinned by digest." >&2
exit 1
fi
terraform fmt -check -recursive iac/terraform
terraform_roots=(
iac/terraform/live/dev-k3s/vault-core
iac/terraform/live/dev-k3s/vault-database
)
for root in "${terraform_roots[@]}"; do
data_dir="$(mktemp -d)"
TF_DATA_DIR="$data_dir" terraform -chdir="$root" init \
-backend=false \
-input=false \
-lockfile=readonly >/dev/null
TF_DATA_DIR="$data_dir" terraform -chdir="$root" validate
rm -rf "$data_dir"
done
if rg -n \
'github\\.com/DongHyeonka/Project-Auth-GitOps|bitnami-labs\\.github\\.io/sealed-secrets|/home/donghyeon/dev/Project-Auth-GitOps|terraform/vault(-transit)?/(dev|reconcile)' \
--glob '!docs/archive/**' \
--glob '!policies/legacy/**' \
--glob '!hack/validate.sh' \
.; then
echo "Current files contain a legacy repository URL, Helm URL, absolute path, or Terraform root." >&2
exit 1
fi
if rg -n 'uses:[[:space:]]+[^#[:space:]]+@v[0-9]' .gitea/workflows; then
echo "Gitea Actions must be pinned to an immutable commit SHA." >&2
exit 1
fi
if rg -n 'ApplyOutOfSyncOnly=true' bootstrap clusters; then
echo "ApplyOutOfSyncOnly is incompatible with hook-based migrations and must not be enabled." >&2
exit 1
fi
if rg -n 'git[[:space:]]+push[^#]*(HEAD:)?main([[:space:]]|$)' .gitea hack; then
echo "Automation must promote changes through a branch and review, not push directly to main." >&2
exit 1
fi
git diff --check
echo "Repository validation passed."
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
VAULT_ADDR="${VAULT_ADDR:-http://127.0.0.1:8200}"
VAULT_INIT_OUTPUT="${VAULT_INIT_OUTPUT:-${REPO_ROOT}/.local/vault/dev-k3s-init.json}"
for cmd in jq vault; do
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "$cmd is required" >&2
exit 1
fi
done
export VAULT_ADDR
status_json() {
local output=""
local status_code=0
set +e
output="$(vault status -format=json 2>/dev/null)"
status_code=$?
set -e
if [[ "$status_code" -ne 0 && "$status_code" -ne 2 ]]; then
echo "Vault is not reachable at ${VAULT_ADDR}." >&2
return "$status_code"
fi
printf '%s\n' "$output"
}
unseal() {
local status=""
local unseal_key=""
status="$(status_json)"
if [[ "$(jq -r '.sealed' <<<"$status")" == "false" ]]; then
echo "Vault is already unsealed."
return
fi
if [[ ! -f "$VAULT_INIT_OUTPUT" ]]; then
echo "Init material is unavailable: ${VAULT_INIT_OUTPUT}" >&2
exit 1
fi
unseal_key="$(jq -er '.unseal_keys_b64[0]' "$VAULT_INIT_OUTPUT")"
vault operator unseal "$unseal_key" >/dev/null
echo "Vault is unsealed."
}
init() {
local status=""
status="$(status_json)"
if [[ "$(jq -r '.initialized' <<<"$status")" == "true" ]]; then
echo "Refusing initialization: Vault is already initialized." >&2
exit 1
fi
if [[ -e "$VAULT_INIT_OUTPUT" ]]; then
echo "Refusing to overwrite existing init material: ${VAULT_INIT_OUTPUT}" >&2
exit 1
fi
umask 077
mkdir -p "$(dirname "$VAULT_INIT_OUTPUT")"
vault operator init \
-key-shares=1 \
-key-threshold=1 \
-format=json >"$VAULT_INIT_OUTPUT"
chmod 0600 "$VAULT_INIT_OUTPUT"
unseal
echo "Dev Vault was initialized with a dev-only 1-of-1 Shamir key."
echo "Move ${VAULT_INIT_OUTPUT} to encrypted custody before continuing."
}
revoke_root() {
local root_token=""
local temporary=""
if [[ ! -f "$VAULT_INIT_OUTPUT" ]]; then
echo "Init material is unavailable: ${VAULT_INIT_OUTPUT}" >&2
exit 1
fi
root_token="$(jq -er '.root_token' "$VAULT_INIT_OUTPUT")"
VAULT_TOKEN="$root_token" vault token revoke -self
temporary="$(mktemp "${VAULT_INIT_OUTPUT}.XXXXXX")"
jq 'del(.root_token)' "$VAULT_INIT_OUTPUT" >"$temporary"
chmod 0600 "$temporary"
mv "$temporary" "$VAULT_INIT_OUTPUT"
echo "The initial root token was revoked and removed from the local init file."
}
case "${1:-}" in
init)
init
;;
unseal)
unseal
;;
revoke-root)
revoke_root
;;
*)
echo "Usage: $0 <init|unseal|revoke-root>" >&2
exit 1
;;
esac
@@ -0,0 +1,5 @@
bucket = "project-gitops-terraform-state"
key = "dev-k3s/vault-core.tfstate"
region = "us-east-1"
encrypt = true
use_lockfile = true
@@ -0,0 +1,5 @@
bucket = "project-gitops-terraform-state"
key = "dev-k3s/vault-database.tfstate"
region = "us-east-1"
encrypt = true
use_lockfile = true
@@ -0,0 +1,22 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/vault" {
version = "5.7.0"
constraints = "~> 5.7.0"
hashes = [
"h1:Pm0AcUSYmBPZgRahQX/ahiYcjtZODSAEc2rK8r8MQ18=",
"zh:1dd9ab6d23f61a5e522efcb462f1fd6f4a210c77b9038c8e12fa5fa663b45d01",
"zh:3c98d37ead857c980f7b9285f8c3e1eb7a8fd6d6799275c311c6997973389cc9",
"zh:3df895fbaed383e3748ba1b50f5f1046f75503483bc3d783992059f85c85ba31",
"zh:3e9faaa0a85c6f03c7fd7f8b7008bb3fbb8777f26c001875947cafa47f91c657",
"zh:52a057d0c6cde7cbfd9ceb78a3781dcfc81cf108c533f454530ea6bb87a9bea8",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:8521c3825254a5f7fbff8f42ca57cabf052366f0420f5f239ebebf8292c03d0e",
"zh:953563d429e40087eb34faf22f28e781e50eee27cfc9ac1ad04308ba592a647f",
"zh:a52dd76bb7f5b86cb8de7380d2e68b47ec4445782c16ee205e6a013be35a57b6",
"zh:bdad38c95a14c8cce1eeadcc539cf9bf74902ce7c662b79105ad993bb48ec073",
"zh:d3c676d7d12c15b58518fa3ee7fc398a13893b4057fe9bf4bc1fe635f3fb995a",
"zh:f8673b6c06da80e912c9e32dd4853f07bfca386968d5b33c9fceb6f68b519959",
]
}
@@ -0,0 +1,171 @@
terraform {
required_version = ">= 1.11.0"
required_providers {
vault = {
source = "hashicorp/vault"
version = "~> 5.7.0"
}
}
backend "s3" {}
}
provider "vault" {
address = var.vault_addr
skip_child_token = true
token = var.vault_token
}
locals {
policy_dir = "${path.module}/../../../../../policies/vault/dev-k3s"
workload_policies = {
auth-server-dev = file("${local.policy_dir}/auth-server-dev.hcl")
auth-db-migration-dev = file("${local.policy_dir}/auth-db-migration-dev.hcl")
postgres-dev = file("${local.policy_dir}/postgres-dev.hcl")
keycloak-dev = file("${local.policy_dir}/keycloak-dev.hcl")
keycloak-client-sync-dev = file("${local.policy_dir}/keycloak-client-sync-dev.hcl")
postgres-operator-dev = file("${local.policy_dir}/postgres-operator-dev.hcl")
keycloak-operator-dev = file("${local.policy_dir}/keycloak-operator-dev.hcl")
}
}
resource "vault_mount" "kv" {
path = var.kv_mount_path
type = "kv"
options = {
version = "2"
}
lifecycle {
prevent_destroy = true
}
}
resource "vault_mount" "database" {
path = var.database_mount_path
type = "database"
lifecycle {
prevent_destroy = true
}
}
resource "vault_mount" "transit" {
path = var.transit_mount_path
type = "transit"
lifecycle {
prevent_destroy = true
}
}
resource "vault_auth_backend" "kubernetes" {
path = var.kubernetes_auth_path
type = "kubernetes"
}
resource "vault_kubernetes_auth_backend_config" "cluster" {
backend = vault_auth_backend.kubernetes.path
disable_iss_validation = true
disable_local_ca_jwt = false
kubernetes_host = var.kubernetes_host
}
resource "vault_transit_secret_backend_key" "project_auth_jwt" {
backend = vault_mount.transit.path
name = var.jwt_transit_key_name
type = "rsa-2048"
}
resource "vault_policy" "platform_admin" {
name = var.platform_admin_policy_name
policy = file("${local.policy_dir}/platform-admin-dev.hcl")
}
resource "vault_policy" "database_automation" {
name = var.database_automation_policy_name
policy = file("${local.policy_dir}/vault-database-automation-dev.hcl")
}
module "workload_policies" {
source = "../../../modules/vault-policy-set"
policies = local.workload_policies
}
resource "vault_kubernetes_auth_backend_role" "operator" {
audience = var.kubernetes_token_audience
backend = vault_auth_backend.kubernetes.path
bound_service_account_names = [var.operator_service_account_name]
bound_service_account_namespaces = [var.operator_service_account_namespace]
role_name = var.operator_role_name
token_policies = [vault_policy.platform_admin.name]
token_ttl = var.operator_token_ttl_seconds
}
module "workload_roles" {
source = "../../../modules/vault-kubernetes-roles"
backend = vault_auth_backend.kubernetes.path
roles = {
auth-server-dev = {
audiences = [var.kubernetes_token_audience]
service_account_names = ["auth-server"]
service_account_namespaces = ["auth-dev"]
token_policies = [module.workload_policies.names["auth-server-dev"]]
token_ttl = var.kubernetes_role_ttl_seconds
}
auth-db-migration-dev = {
audiences = [var.kubernetes_token_audience]
service_account_names = ["auth-db-migration"]
service_account_namespaces = ["auth-dev"]
token_policies = [module.workload_policies.names["auth-db-migration-dev"]]
token_ttl = var.kubernetes_role_ttl_seconds
}
postgres-dev = {
audiences = [var.kubernetes_token_audience]
service_account_names = ["postgres"]
service_account_namespaces = ["platform"]
token_policies = [module.workload_policies.names["postgres-dev"]]
token_ttl = var.kubernetes_role_ttl_seconds
}
keycloak-dev = {
audiences = [var.kubernetes_token_audience]
service_account_names = ["keycloak"]
service_account_namespaces = ["platform"]
token_policies = [module.workload_policies.names["keycloak-dev"]]
token_ttl = var.kubernetes_role_ttl_seconds
}
keycloak-client-sync-dev = {
audiences = [var.kubernetes_token_audience]
service_account_names = ["keycloak-client-sync"]
service_account_namespaces = ["platform"]
token_policies = [module.workload_policies.names["keycloak-client-sync-dev"]]
token_ttl = var.kubernetes_role_ttl_seconds
}
}
}
resource "vault_jwt_auth_backend" "ci" {
count = var.ci_jwt_oidc_discovery_url == null ? 0 : 1
bound_issuer = var.ci_jwt_bound_issuer
oidc_discovery_url = var.ci_jwt_oidc_discovery_url
path = var.ci_jwt_auth_path
}
resource "vault_jwt_auth_backend_role" "ci" {
count = var.ci_jwt_oidc_discovery_url == null ? 0 : 1
backend = vault_jwt_auth_backend.ci[0].path
bound_audiences = var.ci_jwt_bound_audiences
bound_claims = var.ci_jwt_bound_claims
bound_claims_type = "glob"
role_name = var.ci_jwt_role_name
role_type = "jwt"
token_explicit_max_ttl = var.ci_token_ttl_seconds
token_policies = [vault_policy.database_automation.name]
user_claim = var.ci_jwt_user_claim
}
@@ -0,0 +1,146 @@
variable "database_automation_policy_name" {
description = "Least-privilege policy used by the approved Vault database runner."
type = string
default = "vault-database-automation-dev"
}
variable "ci_jwt_auth_path" {
description = "JWT auth mount used by external CI."
type = string
default = "jwt-ci"
}
variable "ci_jwt_bound_audiences" {
description = "Accepted CI JWT audiences."
type = set(string)
default = []
}
variable "ci_jwt_bound_claims" {
description = "Claims that bind CI JWTs to the canonical repository and protected branch."
type = map(string)
default = {}
}
variable "ci_jwt_bound_issuer" {
description = "Expected issuer for CI JWTs."
type = string
default = null
nullable = true
}
variable "ci_jwt_oidc_discovery_url" {
description = "CI OIDC discovery URL. Null keeps JWT auth disabled until the issuer is confirmed."
type = string
default = null
nullable = true
}
variable "ci_jwt_role_name" {
description = "Vault role used by the GitOps configuration workflow."
type = string
default = "project-gitops-dev"
}
variable "ci_jwt_user_claim" {
description = "JWT claim used as the Vault identity alias."
type = string
default = "sub"
}
variable "ci_token_ttl_seconds" {
description = "Maximum lifetime for a CI Vault token."
type = number
default = 3600
}
variable "database_mount_path" {
description = "Workload Vault database secrets mount path."
type = string
default = "database"
}
variable "jwt_transit_key_name" {
description = "Transit key used for application JWT signing."
type = string
default = "project-auth-jwt"
}
variable "kubernetes_auth_path" {
description = "Kubernetes auth backend path."
type = string
default = "kubernetes"
}
variable "kubernetes_host" {
description = "Kubernetes TokenReview API address."
type = string
default = "https://kubernetes.default.svc.cluster.local:443"
}
variable "kubernetes_role_ttl_seconds" {
description = "TTL for workload Kubernetes auth tokens."
type = number
default = 3600
}
variable "kubernetes_token_audience" {
description = "Audience used by projected service account tokens."
type = string
default = "vault"
}
variable "kv_mount_path" {
description = "Workload Vault KV-v2 mount path."
type = string
default = "kv"
}
variable "operator_role_name" {
description = "Workload Vault Kubernetes auth role for human operators."
type = string
default = "vault-operator-dev"
}
variable "operator_service_account_name" {
description = "Service account authorized to open workload Vault operator sessions."
type = string
default = "vault-operator"
}
variable "operator_service_account_namespace" {
description = "Namespace of the workload Vault operator service account."
type = string
default = "vault"
}
variable "operator_token_ttl_seconds" {
description = "TTL for workload Vault operator sessions."
type = number
default = 1800
}
variable "platform_admin_policy_name" {
description = "Policy used only for short-lived break-glass administration."
type = string
default = "platform-admin-dev"
}
variable "transit_mount_path" {
description = "Workload Vault Transit mount path for application cryptography."
type = string
default = "transit"
}
variable "vault_addr" {
description = "Workload Vault API address reachable by the approved runner."
type = string
default = "http://127.0.0.1:8200"
}
variable "vault_token" {
description = "Short-lived token used only for this Terraform run."
type = string
sensitive = true
ephemeral = true
}
@@ -0,0 +1,22 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/vault" {
version = "5.7.0"
constraints = "~> 5.7.0"
hashes = [
"h1:Pm0AcUSYmBPZgRahQX/ahiYcjtZODSAEc2rK8r8MQ18=",
"zh:1dd9ab6d23f61a5e522efcb462f1fd6f4a210c77b9038c8e12fa5fa663b45d01",
"zh:3c98d37ead857c980f7b9285f8c3e1eb7a8fd6d6799275c311c6997973389cc9",
"zh:3df895fbaed383e3748ba1b50f5f1046f75503483bc3d783992059f85c85ba31",
"zh:3e9faaa0a85c6f03c7fd7f8b7008bb3fbb8777f26c001875947cafa47f91c657",
"zh:52a057d0c6cde7cbfd9ceb78a3781dcfc81cf108c533f454530ea6bb87a9bea8",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:8521c3825254a5f7fbff8f42ca57cabf052366f0420f5f239ebebf8292c03d0e",
"zh:953563d429e40087eb34faf22f28e781e50eee27cfc9ac1ad04308ba592a647f",
"zh:a52dd76bb7f5b86cb8de7380d2e68b47ec4445782c16ee205e6a013be35a57b6",
"zh:bdad38c95a14c8cce1eeadcc539cf9bf74902ce7c662b79105ad993bb48ec073",
"zh:d3c676d7d12c15b58518fa3ee7fc398a13893b4057fe9bf4bc1fe635f3fb995a",
"zh:f8673b6c06da80e912c9e32dd4853f07bfca386968d5b33c9fceb6f68b519959",
]
}
@@ -0,0 +1,75 @@
terraform {
required_version = ">= 1.11.0"
required_providers {
vault = {
source = "hashicorp/vault"
version = "~> 5.7.0"
}
}
backend "s3" {}
}
provider "vault" {
address = var.vault_addr
skip_child_token = true
token = var.vault_token
}
locals {
migration_role_name = "auth-db-migration-dev"
operator_role_name = "postgres-operator-dev"
creation_statements = [
<<-EOT
CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';
GRANT "${var.auth_db_role}" TO "{{name}}";
EOT
]
revocation_statements = [
<<-EOT
REASSIGN OWNED BY "{{name}}" TO "${var.auth_db_role}";
DROP OWNED BY "{{name}}";
REVOKE "${var.auth_db_role}" FROM "{{name}}";
DROP ROLE IF EXISTS "{{name}}";
EOT
]
}
resource "vault_database_secret_backend_connection" "platform_postgres" {
allowed_roles = [local.migration_role_name, local.operator_role_name]
backend = var.database_mount_path
name = var.database_config_name
plugin_name = "postgresql-database-plugin"
verify_connection = true
postgresql {
connection_url = "postgresql://{{username}}:{{password}}@${var.postgres_host}:${var.postgres_port}/${var.postgres_database}?sslmode=${var.postgres_sslmode}"
password_authentication = "scram-sha-256"
password_wo = var.postgres_admin_password
password_wo_version = var.postgres_admin_password_version
username = var.postgres_admin_username
}
}
resource "vault_database_secret_backend_role" "auth_db_migration" {
backend = var.database_mount_path
creation_statements = local.creation_statements
db_name = vault_database_secret_backend_connection.platform_postgres.name
default_ttl = var.auth_db_migration_default_ttl_seconds
max_ttl = var.auth_db_migration_max_ttl_seconds
name = local.migration_role_name
revocation_statements = local.revocation_statements
}
resource "vault_database_secret_backend_role" "postgres_operator" {
backend = var.database_mount_path
creation_statements = local.creation_statements
db_name = vault_database_secret_backend_connection.platform_postgres.name
default_ttl = var.postgres_operator_default_ttl_seconds
max_ttl = var.postgres_operator_max_ttl_seconds
name = local.operator_role_name
revocation_statements = local.revocation_statements
}
@@ -0,0 +1,101 @@
variable "auth_db_migration_default_ttl_seconds" {
description = "Default TTL for migration credentials."
type = number
default = 3600
}
variable "auth_db_migration_max_ttl_seconds" {
description = "Maximum TTL for migration credentials."
type = number
default = 86400
}
variable "auth_db_role" {
description = "Stable PostgreSQL owner role used by dynamic users."
type = string
default = "project_auth"
}
variable "database_config_name" {
description = "Vault database connection name."
type = string
default = "platform-postgres-dev"
}
variable "database_mount_path" {
description = "Foundation-owned database secrets mount path."
type = string
default = "database"
}
variable "postgres_admin_password" {
description = "PostgreSQL admin password passed only through a write-only provider field."
type = string
sensitive = true
ephemeral = true
}
variable "postgres_admin_password_version" {
description = "Increment whenever postgres_admin_password is rotated."
type = number
}
variable "postgres_admin_username" {
description = "Dedicated database administration username."
type = string
default = "postgres"
}
variable "postgres_database" {
description = "Database in which dynamic migration objects are owned and revoked."
type = string
default = "project_auth"
}
variable "postgres_host" {
description = "Platform PostgreSQL service DNS name."
type = string
default = "postgres.platform.svc.cluster.local"
}
variable "postgres_operator_default_ttl_seconds" {
description = "Default TTL for operator database credentials."
type = number
default = 3600
}
variable "postgres_operator_max_ttl_seconds" {
description = "Maximum TTL for operator database credentials."
type = number
default = 28800
}
variable "postgres_port" {
description = "Platform PostgreSQL service port."
type = number
default = 5432
}
variable "postgres_sslmode" {
description = "PostgreSQL SSL mode. Dev currently uses disable; production must use verify-full."
type = string
default = "disable"
validation {
condition = contains(["disable", "require", "verify-ca", "verify-full"], var.postgres_sslmode)
error_message = "postgres_sslmode must be disable, require, verify-ca, or verify-full."
}
}
variable "vault_addr" {
description = "Workload Vault API address."
type = string
default = "http://127.0.0.1:8200"
}
variable "vault_token" {
description = "Short-lived token carrying vault-database-automation-dev."
type = string
sensitive = true
ephemeral = true
}
@@ -0,0 +1,11 @@
resource "vault_kubernetes_auth_backend_role" "this" {
for_each = var.roles
audience = one(each.value.audiences)
backend = var.backend
bound_service_account_names = each.value.service_account_names
bound_service_account_namespaces = each.value.service_account_namespaces
role_name = each.key
token_policies = each.value.token_policies
token_ttl = each.value.token_ttl
}
@@ -0,0 +1,15 @@
variable "backend" {
description = "Kubernetes auth backend path."
type = string
}
variable "roles" {
description = "Kubernetes auth roles keyed by Vault role name."
type = map(object({
audiences = set(string)
service_account_names = set(string)
service_account_namespaces = set(string)
token_policies = set(string)
token_ttl = number
}))
}
@@ -0,0 +1,10 @@
terraform {
required_version = ">= 1.11.0"
required_providers {
vault = {
source = "hashicorp/vault"
version = "~> 5.7.0"
}
}
}
@@ -0,0 +1,11 @@
resource "vault_policy" "this" {
for_each = var.policies
name = each.key
policy = each.value
}
output "names" {
description = "Policy names keyed by their requested names."
value = { for name, policy in vault_policy.this : name => policy.name }
}
@@ -0,0 +1,4 @@
variable "policies" {
description = "Map of Vault policy names to HCL policy documents."
type = map(string)
}
@@ -0,0 +1,10 @@
terraform {
required_version = ">= 1.11.0"
required_providers {
vault = {
source = "hashicorp/vault"
version = "~> 5.7.0"
}
}
}

Some files were not shown because too many files have changed in this diff Show More