커스텀 데이터베이스 플러그인
커스텀 데이터베이스 플러그인 (Custom database plugins)
데이터베이스 시크릿 엔진에 커스텀 데이터베이스 타입을 추가하는 플러그인 인터페이스와 작성 방법을 다룹니다.
출처: 문서
본문
커스텀 데이터베이스 플러그인의 인터페이스는 Vault 1.6에서 바뀌었어요. Vault는 이제 사용되지 않는(deprecated) 이전 버전의 인터페이스를 당분간 계속 인식합니다. 더 이상 사용되지 않는 인터페이스의 플러그인을 사용 중이라면 최신 버전으로 업그레이드해야 합니다. 자세한 내용은 Upgrading database plugins을 참고하세요.
고급 주제예요! 플러그인 개발은 Vault에서 매우 고급 주제이며, 일상적인 사용에 필요한 지식은 아닙니다. 플러그인을 작성할 계획이 없다면 이 문서 섹션은 건너뛰어도 됩니다.
데이터베이스 시크릿 엔진은 Vault의 코어 코드를 수정하지 않고도 플러그인 인터페이스를 통해 새 기능을 추가할 수 있게 해 줍니다. 이를 통해 원하는 어떤 데이터베이스에서든 자격 증명을 생성하는 자신의 코드를 작성할 수 있어요. 또한 동적으로 링크된 라이브러리를 요구하는 데이터베이스도 Vault 자체는 정적으로 링크된 상태로 유지하면서 플러그인으로 사용할 수 있게 합니다.
Database 플러그인을 빌드하기 시작하기 전에 플러그인 시스템에 대해 자세히 알아보려면 Plugins internals 문서를 읽어 주세요.
데이터베이스 플러그인은 플러그인 멀티플렉싱을 구현할 수 있는데, 이를 통해 단일 플러그인 프로세스를 여러 데이터베이스 연결에 사용할 수 있습니다. 멀티플렉싱을 활성화하려면 플러그인을 Vault의 dbplugin 패키지의 ServeMultiplex 함수 호출로 컴파일해야 해요.
플러그인 인터페이스
데이터베이스 시크릿 엔진의 모든 플러그인은 같은 인터페이스를 구현해야 해요. 이 인터페이스는 sdk/database/dbplugin/v5/database.go에 있습니다.
type Database interface {
// 데이터베이스 플러그인을 초기화합니다. 데이터베이스 객체 자체의 생성자와 같은 역할입니다.
Initialize(ctx context.Context, req InitializeRequest) (InitializeResponse, error)
// NewUser는 데이터베이스 안에 새 사용자를 만듭니다. 이 사용자는 TTL이 만료될 때까지 존재하는 임시 사용자입니다.
NewUser(ctx context.Context, req NewUserRequest) (NewUserResponse, error)
// UpdateUser는 데이터베이스 안의 기존 사용자를 업데이트합니다.
UpdateUser(ctx context.Context, req UpdateUserRequest) (UpdateUserResponse, error)
// DeleteUser는 데이터베이스에서 사용자를 삭제합니다. 사용자가 이 호출 전에
// 존재하지 않았더라도 오류를 반환해서는 안 됩니다.
DeleteUser(ctx context.Context, req DeleteUserRequest) (DeleteUserResponse, error)
// Type은 특정 데이터베이스 백엔드 구현의 Name을 반환합니다.
// 이 타입 이름은 보통 데이터베이스 백엔드 구현 안에서 상수로 설정됩니다.
// 예를 들어 MySQL 데이터베이스 백엔드의 경우 "mysql"입니다.
// 메트릭·로깅 같은 것에 사용됩니다. 이 값에 따라 동작이 바뀌지는 않습니다.
Type() (string, error)
// Close는 백엔드가 설정한 기본 데이터베이스 연결을 닫으려고 시도합니다.
Close() error
}
각 요청·응답 객체도 sdk/database/dbplugin/v5/database.go에서 찾을 수 있어요.
각 요청에는 최소 1개의 Statements 객체(UpdateUserRequest에서는 하위 필드에)가 있습니다. 이 객체는 해당 특정 작업에 대해 실행할 명령 집합을 나타냅니다. NewUser 함수의 경우 이는 사용자를 만들(그리고 흔히 그 사용자에 대한 권한을 설정) 명령 집합입니다. 이 문들은 API의 다음 필드에서 옵니다.
| API 인자 | 요청 객체 |
|---|---|
| creation_statements | NewUserRequest.Statements.Commands |
| revocation_statements | DeleteUserRequest.Statements.Commands |
| rollback_statements | NewUserRequest.RollbackStatements.Commands |
| renew_statements | UpdateUserRequest.Expiration.Statements.Commands |
| rotation_statements | UpdateUserRequest.Password.Statements.Commands |
| root_rotation_statements | UpdateUserRequest.Password.Statements.Commands |
많은 내장 플러그인은 {{name}}(또는 {{username}}), {{password}}, {{expiration}}을 관련 값으로 교체합니다. 이 문자열 교체를 수행하는 것은 플러그인의 몫입니다. 이 문자열 교체를 돕는 sdk/database/helper/dbutil에 있는 QueryHelper라는 헬퍼 함수가 있습니다. 꼭 사용해야 하는 것은 아니지만, 플러그인의 동작을 내장 플러그인과 일관되게 만들 수 있습니다.
InitializeRequest 객체는 키-값 맵을 담고 있어요. 이 데이터는 사용자가 플러그인의 구성으로 지정한 것입니다. 플러그인은 이 데이터로 데이터베이스에 연결해야 합니다. 응답 객체는 비슷한 구성 맵을 담고 있어요. 응답 객체는 Vault 안에 저장해야 하는 구성 맵을 담아야 합니다. 이는 플러그인이 저장하기 전에 구성을 조작할 수 있게 해 줍니다.
또한 Initialize 호출 동안 플러그인이 데이터베이스에 연결을 초기화해야 하는지 나타내는 부울 값(InitializeRequest.VerifyConnection)이 전달돼요. 이 함수는 구성이 기록될 때 호출됩니다. 이를 통해 사용자는 구성이 유효하고 해당 데이터베이스에 연결할 수 있는지 알 수 있습니다. false로 설정되면 Initialize 호출 동안은 연결을 만들지 말아야 하지만, 다른 함수의 후속 호출은 연결을 열어야 합니다.
플러그인 서빙 (Serving a plugin)
멀티플렉싱으로 플러그인 서빙
플러그인 멀티플렉싱에는 github.com/hashicorp/vault/sdk v0.4.0 이상이 필요합니다.
플러그인은 Vault 밖에서 별도 바이너리로 실행되므로 플러그인 자체에 main 함수가 필요해요. 멀티플렉싱된 플러그인을 서빙하려면 sdk/database/dbplugin/v5의 ServeMultiplex 함수를 사용하세요.
아래는 예제 설정입니다.
package main
import (
"github.com/hashicorp/vault/api"
dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
)
func main() {
apiClientMeta := &api.PluginAPIClientMeta{}
flags := apiClientMeta.FlagSet()
flags.Parse(os.Args[1:])
err := Run()
if err != nil {
log.Println(err)
os.Exit(1)
}
}
func Run() error {
dbplugin.ServeMultiplex(dbType.(dbplugin.New))
return nil
}
func New() (interface{}, error) {
db, err := newDatabase()
if err != nil {
return nil, err
}
// 이 미들웨어는 엄격히 필수는 아니지만 오류 메시지에 비밀번호 같은 값을
// 실수로 노출하지 않도록 강력히 권장됩니다. 예시는 아래에 포함되어 있습니다.
db = dbplugin.NewDatabaseErrorSanitizerMiddleware(db, db.secretValues)
return db, nil
}
type MyDatabase struct {
// 데이터베이스용 변수
password string
}
func newDatabase() (MyDatabase, error) {
// ...
db := &MyDatabase{
// ...
}
return db, nil
}
func (db *MyDatabase) secretValues() map[string]string {
return map[string]string{
db.password: "[password]",
}
}
MyDatabase를 실제 데이터베이스 플러그인 구현으로 교체합니다.
멀티플렉싱 없이 플러그인 서빙
멀티플렉싱 없이 플러그인을 서빙하려면 sdk/database/dbplugin/v5의 Serve 함수를 호출해 플러그인을 서빙해야 해요.
설정은 멀티플렉싱 케이스와 정확히 같지만, Run 함수만 다릅니다.
func Run() error {
dbType, err := New()
if err != nil {
return err
}
dbplugin.Serve(dbType.(dbplugin.Database))
return nil
}
플러그인 실행 (Running your plugin)
위 main 패키지는 빌드되면 플러그인의 바이너리를 제공합니다. 플러그인을 배포할 계획이라면 크로스 플랫폼 빌드를 위해 gox로 빌드하는 것을 권장합니다.
데이터베이스 시크릿 엔진에서 플러그인을 사용하려면 plugin internals 문서에 지정된 플러그인 디렉터리에 바이너리를 배치해야 해요.
이제 Vault 카탈로그에 플러그인을 등록할 수 있어야 합니다. 이렇게 하려면 토큰에 sudo 권한이 필요해요.
$ vault write sys/plugins/catalog/database/mydatabase-database-plugin \
sha256="..." \
command="mydatabase"
Success! Data written to: sys/plugins/catalog/database/mydatabase-database-plugin
이제 다른 것과 마찬가지로 플러그인을 구성할 수 있어야 합니다.
$ vault write database/config/mydatabase \
plugin_name=mydatabase-database-plugin \
allowed_roles="readonly" \
myplugins_connection_details="..."
플러그인 버전 관리를 활용하도록 데이터베이스 플러그인 업데이트
플러그인은 선택적으로 자신의 시맨틱 버전을 자체 보고할 수 있습니다. 그렇게 하는 플러그인의 경우 Vault는 사용자가 제공하지 않아도 카탈로그의 플러그인 버전을 자동으로 채웁니다. 등록 중 사용자가 버전을 제공한다면, 제공된 버전이 플러그인이 보고하는 것과 일치하지 않으면 Vault는 오류를 냅니다. 비어 있지 않은 버전을 보고하는 플러그인은 앞에 'v'가 붙은 유효한 Semantic Version을 반드시 보고해야 하며, 그렇지 않으면 등록이 실패합니다. 예: v1.0.0 또는 v2.3.2-beta.
이 동작을 선택하려는 플러그인은 version 인터페이스를 구현할 수 있어요. 그러나 필수는 아니며, 플러그인이 version 인터페이스를 구현하지 않아도 사용자가 등록 중 버전을 제공할 수 있습니다.
version 인터페이스를 구현하려면 플러그인이 먼저 Vault SDK 패키지를 최소 v0.6.0으로 업그레이드해야 해요.
위의 Database 인터페이스에 더해 데이터베이스 플러그인은 PluginVersioner 인터페이스도 구현할 수 있습니다.
// PluginVersioner는 버전 정보를 반환하는 선택적 인터페이스입니다.
type PluginVersioner interface {
// PluginVersion은 백엔드의 버전을 반환합니다.
PluginVersion() PluginVersion
}
type PluginVersion struct {
Version string
}
플러그인 멀티플렉싱을 활용하도록 데이터베이스 플러그인 업그레이드
배경 (Background)
많은 외부 플러그인을 확장하는 것은 리소스 집약적일 수 있어요. 외부 플러그인 확장의 성능 문제를 해결하기 위해 데이터베이스 플러그인은 플러그인 멀티플렉싱을 구현할 수 있는데, 단일 플러그인 프로세스를 여러 데이터베이스 연결에 사용할 수 있게 해 줍니다. 멀티플렉싱을 활성화하려면 플러그인을 Vault의 dbplugin 패키지의 ServeMultiplex 함수 호출로 컴파일해야 해요.
플러그인 멀티플렉싱을 활용하도록 업그레이드
비멀티플렉싱에서 멀티플렉싱 데이터베이스 플러그인으로 업그레이드하는 데 필요한 단계는 단 하나예요: Serve 함수 호출을 ServeMultiplex로 바꾸기입니다.
이렇게 하면 이전과 마찬가지로 플러그인의 RPC 서버가 실행됩니다. 그러나 ServeMultiplex 함수는 인자로 팩토리 함수를 직접 받습니다. 이 팩토리 함수는 dbplugin.Database 인터페이스를 구현하는 객체를 반환하는 함수예요.
플러그인 멀티플렉싱을 피해야 하는 때는?
플러그인 멀티플렉싱을 피해야 하는 사용 사례:
- 플러그인 프로세스 수준 분리가 필요할 때
- 크래시나 플러그인 재로드 호출 시 플러그인 타입에 대한 모든 마운트/데이터베이스 연결에 걸쳐 재시작을 피할 때
v5 인터페이스로 데이터베이스 플러그인 업그레이드
배경 (Background)
Vault 1.6에서 데이터베이스 인터페이스가 바뀌었어요. 새 버전은 version 5, 이전 버전은 version 4라고 합니다. 명시적으로 노출되지 않았던 인터페이스의 이전 버전 관리 때문이에요.
새 인터페이스가 도입된 이유는 여러 가지입니다.
- Vault 1.5에서 도입된 Password policies은 Vault가 비밀번호 생성 책임을 져야 했어요. 이전 버전에서는 데이터베이스 플러그인이 비밀번호 생성을 담당했는데, 이는 비밀번호 정책과의 통합을 막았습니다.
- 비밀번호는 데이터베이스 플러그인이 생성해야 했어요. 즉 플러그인 작성자가 안전한 비밀번호 생성 책임을 졌습니다. 이는 Vault SDK 안의 헬퍼 함수로 해야 하지만, 작성자가 안전하지 않은 비밀번호를 생성하는 것을 막는 것은 없었어요.
- version 4 인터페이스에는 작성자를 혼란스럽게 만드는 많은 불일치가 있었어요. 예를 들어 비밀번호는 3가지 다른 방식으로 처리됐습니다. CreateUser가 비밀번호를 생성해 반환하고, SetCredentials가 구성 struct를 통해 비밀번호를 받아 반환하고, RotateRootCredentials가 비밀번호를 생성해 새 비밀번호와 함께 전체 구성을 업데이트된 사본 반환할 것으로 기대했습니다.
- 정적 자격 증명 회전에 사용되는 SetCredentials와 루트 사용자 자격 증명 회전에 사용되는 RotateRootCredentials는 본질적으로 같은 작업이었습니다: 사용자의 비밀번호 변경. 실제 차이는 그것이 어떤 사용자를 가리키는지뿐이었어요. 특히 루트 자격 증명 회전 시 SetCredentials가 사용될 때 명확했습니다(해당 플러그인이 정적 자격 증명 회전을 지원하지 않는 한).
- 이전 인터페이스에는 Init와 Initialize가 모두 포함되어 혼란을 더했습니다.
새 인터페이스는 대략 gRPC 인터페이스를 모델로 합니다. 요청이나 응답에 추가 데이터를 넣기 위해 인터페이스 정의를 변경할 필요가 없어 미래 호환성이 개선되었고, 여러 함수를 단일 함수 호출로 병합해 인터페이스를 단순화합니다.
커스텀 데이터베이스 업그레이드
Vault 1.6은 version 4와 version 5 데이터베이스 플러그인을 모두 지원합니다. version 4 플러그인 지원은 향후 릴리스에서 제거될 거예요. Version 5 데이터베이스 플러그인은 Vault 1.6 이전 버전에서는 동작하지 않습니다. 데이터베이스 플러그인을 업그레이드한다면 Vault 1.6 이상만 사용하고 있는지 확인하세요. 플러그인이 version 4 또는 version 5를 사용하는지 확인하려면, 플러그인에 대해 확인할 수 있는 순서 없는 변경 목록은 다음과 같아요.
- version 4의 import 경로는 github.com/hashicorp/vault/sdk/database/dbplugin이고, version 5의 import 경로는 github.com/hashicorp/vault/sdk/database/dbplugin/v5입니다.
- Version 4에는 다음 함수가 있습니다: Initialize, Init, CreateUser, RenewUser, RevokeUser, SetCredentials, RotateRootCredentials, Type, Close. 전체 함수 시그니처는 sdk/database/dbplugin/plugin.go에서 확인할 수 있습니다.
- Version 5에는 다음 함수가 있습니다: Initialize, NewUser, UpdateUser, DeleteUser, Type, Close. 전체 함수 시그니처는 sdk/database/dbplugin/v5/database.go에서 확인할 수 있습니다.
version 4 커스텀 데이터베이스 플러그인을 사용한다면 다음이 version 5로 업그레이드하는 기본 지침입니다.
version 4에서는 비밀번호 생성을 플러그인이 담당했어요. version 5에서는 더 이상 그렇지 않습니다. Vault가 비밀번호를 생성해 NewUserRequest.Password와 UpdateUserRequest.Password.NewPassword를 통해 플러그인에 전달해요.
- import 경로를 github.com/hashicorp/vault/sdk/database/dbplugin에서 github.com/hashicorp/vault/sdk/database/dbplugin/v5로 바꿔요. 패키지 이름은 같으므로, 새 패키지 안에 해당 심볼들이 존재한다면(예: Serve 함수) dbplugin에 대한 참조는 유지할 수 있어요.
- 구현해야 할 함수를 쉽게 보는 방법: 패키지 안에
var _ dbplugin.Database = (*MyDatabase)(nil)를 전역 변수로 넣으세요. 이는 MyDatabase 타입이 dbplugin.Database 인터페이스를 따르지 않으면 컴파일에 실패합니다. - Init과 Initialize를 새 Initialize 함수 정의로 교체해요. Init가 취하던 필드(config와 verifyConnection)는 이제 InitializeRequest로 감쌌습니다. 반환되던 map[string]interface{} 객체는 이제 InitializeResponse로 감쌌습니다. Database 인터페이스를 따르려면 Initialize만 필요합니다.
- CreateUser를 NewUser로 업데이트해요. NewUserRequest 객체에는 만들 사용자의 사용자명과 비밀번호가 들어 있어요. 또한 사용자를 만들 문 목록과 적용될 수도 있고 아닐 수도 있는 몇 가지 다른 필드를 포함합니다. 커스텀 플러그인은 요청에 제공된 비밀번호를 사용해야 하며 직접 생성하면 안 됩니다. 대신 비밀번호를 생성하면 Vault가 그것을 알지 못해 호출자에게 잘못된 비밀번호를 줄 거예요.
- SetCredentials, RotateRootCredentials, RenewUser가 UpdateUser로 결합됩니다. 요청 객체 UpdateUserRequest에는 세 부분이 있습니다: 변경할 사용자명, ChangePassword, ChangeExpiration. 하나의 객체가 nil이 아니면 그 특정 필드(비밀번호 또는 만료)를 변경해야 함을 나타냅니다. 예를 들어 ChangePassword 필드가 nil이 아니면 사용자의 비밀번호를 변경해야 합니다. 이는 SetCredentials를 호출하는 것과 동등합니다. ChangeExpiration 필드가 nil이 아니면 사용자의 만료 날짜를 변경해야 합니다. 이는 RenewUser를 호출하는 것과 동등합니다. 많은 데이터베이스는 업데이트된 만료에 대해 별도로 할 일이 없습니다.
- RevokeUser를 DeleteUser로 업데이트해요. 가장 단순한 변경입니다. 삭제할 사용자명은 DeleteUserRequest 객체에 들어 있습니다.