---
id: devops-external-secrets-atlantis
title: External Secrets / Atlantis / GitHub Actions deep
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [devops, secrets, gitops, vibe-coding]
tech_stack: { language: "YAML", applicable_to: ["DevOps"] }
applied_in: []
aliases: [external secrets operator, Atlantis, Terraform PR, GitHub Actions, OIDC, reusable workflow]
---

# External Secrets / Atlantis / GHA

> K8s + Vault + Terraform + CI 의 modern integration. **External Secrets Operator (vault → K8s), Atlantis (Terraform PR), GHA (advanced)**.

## 📖 핵심 개념
- ESO: 외부 secret store → K8s Secret 자동 sync.
- Atlantis: Terraform 의 GitOps.
- GHA OIDC: cloud auth 가 token 없이.
- Reusable workflow / matrix.

## 💻 코드 패턴

### External Secrets Operator
```yaml
# SecretStore (Vault 연결)
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: vault-store
spec:
  provider:
    vault:
      server: https://vault.example.com
      path: secret
      auth:
        kubernetes:
          mountPath: kubernetes
          role: my-role
```

```yaml
# ExternalSecret (sync)
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: db-credentials
spec:
  refreshInterval: 1m
  secretStoreRef:
    name: vault-store
    kind: SecretStore
  target:
    name: db-secret  # → K8s Secret
  data:
    - secretKey: password
      remoteRef:
        key: secret/myapp/db
        property: password
```

→ App 가 K8s Secret 만 read. Vault 가 source.

### AWS Secrets Manager 도
```yaml
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-east-1
      auth:
        jwt:
          serviceAccountRef:
            name: external-secrets-sa
```

→ AWS IRSA (IAM Role for SA).

### Auto rotation
```yaml
# AWS Secrets Manager 가 매 30 days rotate
# ESO 가 매 1 min check + sync
# K8s pod 가 secret mount = 자동 update (eventually)
```

### Pod restart
```yaml
# Stakater Reloader 가 secret 변경 시 pod restart
metadata:
  annotations:
    secret.reloader.stakater.com/reload: 'db-secret'
```

→ Secret 변경 → pod restart → 새 credential.

### Atlantis (Terraform 의 GitOps)
```yaml
# atlantis.yaml
version: 3
projects:
  - name: prod-vpc
    dir: terraform/prod/vpc
    autoplan:
      when_modified: ['*.tf', '*.tfvars']
    apply_requirements: [approved]
```

### Workflow
```
1. Engineer 가 PR 가 terraform 변경.
2. Atlantis 가 자동 `terraform plan` → PR comment 에 결과.
3. Reviewer 가 plan 검토.
4. Approved → engineer 가 `atlantis apply`.
5. Atlantis 가 `terraform apply`.
6. PR merge.
```

→ "Apply 후 merge" — drift 안.

### Atlantis 의 lock
```
매 project 가 1 PR 만 apply 가능.
- A 가 apply 중 → B 가 wait.
- "Apply" PR comment 가 lock 가져.
- 종료 시 release.
```

→ Concurrent apply 가 conflict 방지.

### terraform plan output
```diff
$ atlantis plan
+ aws_instance.web
    ami = "ami-12345"
    instance_type = "t3.micro"
- aws_security_group.old
~ aws_db_instance.main
    ~ instance_class: "db.t3.micro" -> "db.t3.small"
```

→ PR 의 visual plan.

### Atlantis 의 alternative
```
- Spacelift (managed, 더 강력)
- Terraform Cloud / HCP Terraform (HashiCorp)
- env0 (managed)
- Terragrunt + custom CI
```

→ Atlantis 가 OSS + 자체 host.

### GitHub Actions OIDC (cloud auth)
```yaml
# .github/workflows/deploy.yml
permissions:
  id-token: write    # OIDC token
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/GHA-Deploy
          aws-region: us-east-1
      
      - run: aws s3 sync ./build s3://my-bucket
```

→ Long-lived AWS key X. OIDC token 가 short-lived.

→ AWS / GCP / Azure 가 OIDC trust 설정.

### Reusable workflow
```yaml
# .github/workflows/test.yml (reusable)
on:
  workflow_call:
    inputs:
      node-version:
        type: string
        default: '20'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: ${{ inputs.node-version }} }
      - run: npm ci && npm test
```

```yaml
# Caller
jobs:
  test:
    uses: ./.github/workflows/test.yml
    with:
      node-version: '22'
```

### Matrix
```yaml
strategy:
  matrix:
    os: [ubuntu-latest, macos-latest, windows-latest]
    node: [18, 20, 22]
runs-on: ${{ matrix.os }}
steps:
  - uses: actions/setup-node@v4
    with: { node-version: ${{ matrix.node }} }
```

→ 9 jobs 가 parallel.

### Conditional matrix
```yaml
strategy:
  matrix:
    os: [ubuntu-latest, windows-latest]
    include:
      - os: macos-latest
        special: true
    exclude:
      - os: windows-latest
        node: 18
```

### Environment + secrets
```yaml
jobs:
  deploy:
    environment: production    # → manual approval gate
    steps:
      - run: ./deploy.sh
        env:
          API_KEY: ${{ secrets.API_KEY }}    # environment-scoped
```

→ Prod environment 가 별 secrets + manual approval.

### Composite action
```yaml
# .github/actions/setup/action.yml
name: 'Setup'
runs:
  using: 'composite'
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with: { node-version: '20' }
    - shell: bash
      run: npm ci
```

```yaml
# Use
- uses: ./.github/actions/setup
```

→ Reusable inline.

### Concurrency
```yaml
concurrency:
  group: ${{ github.ref }}
  cancel-in-progress: true
```

→ 같은 branch 의 새 push = 옛 cancel.

### Cache
```yaml
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-
```

→ Build 빠름.

### Dependabot
```yaml
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: 'github-actions'
    directory: '/'
    schedule:
      interval: 'weekly'
```

→ Action 도 자동 update.

### Self-hosted runner
```yaml
runs-on: self-hosted
# 자체 server 가 runner.
```

→ Cost ↓ (큰 traffic). Setup overhead.

### Action security
```
- Pin to SHA (version 가 mutable).
- uses: actions/checkout@v4.1.0
- uses: actions/checkout@b32f140b0c872d58512e0a66172253c302617b90  # SHA

→ Supply chain 안전.
```

### GitHub Actions 의 secret 주의
```
Secret value 가 log mask (자동).
하지만:
- 부분 echo 가 가능.
- Forked PR 가 secret access X.
- 외부 action 가 secret access (악성 가능).

→ Pin SHA. Secret 최소화.
```

### Workflow_dispatch (manual)
```yaml
on:
  workflow_dispatch:
    inputs:
      env:
        description: 'Deploy environment'
        required: true
        type: choice
        options: [dev, staging, prod]
```

→ UI 에서 click 가 trigger.

### Schedule
```yaml
on:
  schedule:
    - cron: '0 9 * * 1-5'    # 평일 9 AM UTC
```

→ Cron 가 GHA 안.

### Workflow run 의 cost
```
Public repo: 무료.
Private repo:
- 2000 min / month free.
- $0.008 / minute (Linux).
- $0.04 / minute (Windows / macOS).

→ Optimization (cache, matrix exclude) 가 매 cost.
```

## 🤔 의사결정 기준
| 작업 | 추천 |
|---|---|
| K8s + Vault | External Secrets Operator |
| Terraform GitOps | Atlantis |
| Cloud auth | GHA OIDC |
| 반복 logic | Reusable workflow / composite action |
| Multi-env | Environment + protection |
| Cost-sensitive | Self-hosted runner |
| Security | Pin SHA |

## ❌ 안티패턴
- **K8s Secret 직접 commit (encrypted SealedSecret 도)**: rotation 어려움.
- **Long-lived AWS key**: OIDC.
- **모든 거 inline workflow**: reuse 안 됨.
- **Secret echo**: leak.
- **No environment**: prod 가 dev 와 같은 gate.
- **No concurrency**: 중복 deploy.
- **Action @main**: supply chain.

## 🤖 LLM 활용 힌트
- ESO 가 K8s + 외부 secret store 의 답.
- Atlantis 가 Terraform 의 GitOps.
- GHA OIDC 가 modern cloud auth.
- Reusable workflow 가 DRY.

## 🔗 관련 문서
- [[DevOps_Vault_Secrets]]
- [[DevOps_Terraform_Patterns]]
- [[DevOps_CI_CD_Pipeline_Patterns]]
