app.yaml을 사용하여 GAE에 환경 변수를 안전하게 저장


98

app.yamlGAE에 배포 하기 위해 API 키 및 기타 민감한 정보 를 환경 변수 로 저장해야합니다 . 문제는 내가 app.yamlGitHub에 푸시 하면이 정보가 공개된다는 것입니다 (좋지 않음). 프로젝트에 적합하지 않기 때문에 데이터 저장소에 정보를 저장하고 싶지 않습니다. 오히려 .gitignore앱의 각 배포에 나열된 파일의 값을 바꾸고 싶습니다 .

내 app.yaml 파일은 다음과 같습니다.

application: myapp
version: 3 
runtime: python27
api_version: 1
threadsafe: true

libraries:
- name: webapp2
  version: latest
- name: jinja2
  version: latest

handlers:
- url: /static
  static_dir: static

- url: /.*
  script: main.application  
  login: required
  secure: always
# auth_fail_action: unauthorized

env_variables:
  CLIENT_ID: ${CLIENT_ID}
  CLIENT_SECRET: ${CLIENT_SECRET}
  ORG: ${ORG}
  ACCESS_TOKEN: ${ACCESS_TOKEN}
  SESSION_SECRET: ${SESSION_SECRET}

어떤 아이디어?


73
GAE가 개발자 콘솔을 통해 인스턴스 환경 변수를 설정하는 옵션을 추가하기를 바랍니다 (내가 익숙한 다른 모든 PaaS와 마찬가지로).
Spain Train

4
데이터 스토어를 사용할 수 있습니다. 이 답변을 참조하십시오 : stackoverflow.com/a/35254560/1027846
Mustafa İlhan

데이터 저장소 사용에 대한 위의 mustilica의 의견을 확장합니다. 이 작업을 수행하기 위해 프로젝트에서 사용하는 코드는 아래의 내 대답을 참조하십시오. stackoverflow.com/a/35261091#35261091 . 실제로 개발자 콘솔에서 환경 변수를 편집 할 수 있으며 자리 표시 자 값이 자동으로 생성됩니다.
마틴 Omander

mustilica와 Martin에게 감사드립니다. 우리는 실제로 한동안 데이터 저장소 접근 방식을 사용해 왔으며 이것이이 문제에 대한 최상의 솔루션이라는 데 동의합니다. json 파일 접근 방식 인 IMO보다 CI / CD 설정을 사용하는 것이 더 쉽습니다.
스페인 기차

1
2019 년 및 GAE는 여전히이 문제를 해결하지 않았습니다 : /
Josh Noe

답변:


53

민감한 데이터 인 경우 소스 제어에 체크인되므로 소스 코드에 저장해서는 안됩니다. 조직 내부 또는 외부에서 잘못된 사람들이 찾을 수 있습니다. 또한 개발 환경은 프로덕션 환경과 다른 구성 값을 사용합니다. 이러한 값이 코드에 저장되면 개발 및 프로덕션에서 다른 코드를 실행해야하는데 이는 지저분하고 나쁜 습관입니다.

내 프로젝트에서 다음 클래스를 사용하여 데이터 저장소에 구성 데이터를 넣습니다.

from google.appengine.ext import ndb

class Settings(ndb.Model):
  name = ndb.StringProperty()
  value = ndb.StringProperty()

  @staticmethod
  def get(name):
    NOT_SET_VALUE = "NOT SET"
    retval = Settings.query(Settings.name == name).get()
    if not retval:
      retval = Settings()
      retval.name = name
      retval.value = NOT_SET_VALUE
      retval.put()
    if retval.value == NOT_SET_VALUE:
      raise Exception(('Setting %s not found in the database. A placeholder ' +
        'record has been created. Go to the Developers Console for your app ' +
        'in App Engine, look up the Settings record with name=%s and enter ' +
        'its value in that record\'s value field.') % (name, name))
    return retval.value

응용 프로그램은 다음을 수행하여 값을 얻습니다.

API_KEY = Settings.get('API_KEY')

데이터 저장소에 해당 키에 대한 값이 있으면 가져옵니다. 없는 경우 자리 표시 자 레코드가 생성되고 예외가 발생합니다. 예외는 Developers Console로 이동하여 자리 표시 자 레코드를 업데이트하도록 알려줍니다.

구성 값 설정에서 추측이 필요하다는 것을 알았습니다. 설정할 구성 값이 확실하지 않은 경우 코드를 실행하면 알려줍니다!

위의 코드는 내부적으로 Memcache와 데이터 저장소를 사용하는 ndb 라이브러리를 사용하므로 빠릅니다.


최신 정보:

jelder 는 App Engine 콘솔에서 Datastore 값을 찾고 설정하는 방법을 물었습니다. 방법은 다음과 같습니다.

  1. https://console.cloud.google.com/datastore/로 이동합니다.

  2. 아직 선택하지 않은 경우 페이지 상단에서 프로젝트를 선택합니다.

  3. 에서 종류 드롭 다운 상자, 선택 설정 .

  4. 위의 코드를 실행하면 키가 표시됩니다. 모두 NOT SET 값을 갖습니다 . 각각을 클릭하고 값을 설정하십시오.

도움이 되었기를 바랍니다!

Settings 클래스에서 생성 한 설정

편집하려면 클릭

실제 값을 입력하고 저장


2
제공된 모든 답변 중에서 이것은 Heroku가 작업을 처리하는 방법과 가장 가까운 것 같습니다. GAE를 처음 접하기 때문에 개발자 콘솔에서 자리 표시 자 레코드를 찾을 수있는 위치를 잘 모르겠습니다. 설명해 주실 수 있습니까? 아니면 보너스 포인트에 대해 스크린 샷을 게시 할 수 있습니까?
jelder

2
dam ~… gcloud와 관련하여 모든면에서이 특정 요구에 대해 다른 서비스를 사용해야하는 것은 매우 나빠 보입니다. 그 외에도 Google은 firebase 함수 내에서 환경 변수에 대해 "100 % 영웅적인"접근 방식을 제공하지만 gcloud 함수에는 제공하지 않습니다 (적어도 문서화되지 않은 경우… 내가 틀리지 않은 경우)
Ben

1
여기 고유성 및 환경 변수 대체를 추가합니다 당신의 접근 방식을 기반으로 요점입니다 - gist.github.com/SpainTrain/6bf5896e6046a5d9e7e765d0defc8aa8
스페인 기차

3
@Ben 비 Firebase 함수는 환경 변수를 지원합니다 (최소한 현재).
NReilingh

3
@obl-App Engine 앱은 자체 데이터 저장소에 자동으로 인증되며 인증 세부 정보가 필요하지 않습니다. 꽤 깔끔합니다 :-)
Martin Omander 19

49

이 솔루션은 간단하지만 모든 팀에 적합하지 않을 수 있습니다.

먼저 환경 변수를 env_variables.yaml에 넣습니다 . 예 :

env_variables:
  SECRET: 'my_secret'

그런 다음이 포함 env_variables.yaml의를app.yaml

includes:
  - env_variables.yaml

마지막으로, 추가 env_variables.yaml.gitignore비밀 변수가 리포지터리 (repository) 내에 존재하지 않습니다 그래서.

이 경우 env_variables.yaml배치 관리자간에 공유해야합니다.


1
일부 사람들에게 분명하지 않은 것을 추가하기 위해 환경 변수를에서 찾을 수 있으며 process.env.MY_SECRET_KEY로컬 개발 환경에서 이러한 환경 변수가 필요한 경우 노드 dotenv패키지를 사용할 수 있습니다
Dave Kiss

2
env_variables.yaml모든 인스턴스에 도달 하는 방법은 퍼즐의 누락 된 부분입니다.
Christopher Oezbek

1
또한 : 이것을 로컬에서 사용하는 방법?
Christopher Oezbek

@ChristopherOezbek 1. 배포하는 방법? gcloud app deploy평소처럼 Google Cloud에 배포하기 만하면 됩니다. 2. 비밀 환경 변수를 로컬로 설정하는 방법은 무엇입니까? 여러 가지 방법이 있습니다. 당신은 사용할 수 있습니다 export프롬프트 명령 또는 @DaveKiss 제안처럼 어떤 도구를 사용합니다.
Shih-Wen Su

이것이 가장 간단한 해결책입니다. 를 통해 애플리케이션에서 비밀에 액세스 할 수 있습니다 os.environ.get('SECRET').
Quinn Comendant

19

내 접근 방식은 App Engine 앱 자체 내 에서만 클라이언트 비밀을 저장하는 것입니다. 클라이언트 비밀은 소스 제어 나 로컬 컴퓨터에 없습니다. 이는 모든 App Engine 공동 작업자가 클라이언트 보안 비밀에 대해 걱정할 필요없이 코드 변경 사항을 배포 할 수 있다는 이점이 있습니다.

클라이언트 암호를 Datastore에 직접 저장하고 Memcache를 사용하여 암호 액세스 지연 시간을 개선합니다. Datastore 항목은 한 번만 생성하면되며 향후 배포시 유지됩니다. 물론 App Engine 콘솔을 사용하여 언제든지 이러한 항목을 업데이트 할 수 있습니다.

일회성 엔티티 생성을 수행하는 두 가지 옵션이 있습니다.

  • App Engine Remote API 대화 형 셸을 사용하여 항목을 만듭니다.
  • 더미 값으로 엔티티를 초기화하는 관리자 전용 핸들러를 만듭니다. 이 관리 핸들러를 수동으로 호출 한 다음 App Engine 콘솔을 사용하여 프로덕션 클라이언트 비밀번호로 항목을 업데이트합니다.

7
전혀 복잡하지 않습니다. 감사합니다 앱 엔진.
courtsimas

17

게시 당시에는 존재하지 않았지만 여기에서 우연히 발견 한 모든 사용자를 위해 Google은 이제 Secret Manager 라는 서비스를 제공합니다 .

Google 클라우드 플랫폼의 안전한 위치에 비밀을 저장하는 것은 간단한 REST 서비스입니다 (물론 SDK가 래핑되어 있음). 이는 저장된 비밀을 확인하기위한 추가 단계가 필요하고 세분화 된 권한 모델을 갖는 데이터 저장소보다 더 나은 접근 방식입니다. 필요한 경우 프로젝트의 여러 측면에 대해 개별 비밀을 다르게 보호 할 수 있습니다.

버전 관리를 제공하므로 비교적 쉽게 암호 변경을 처리 할 수있을뿐만 아니라 강력한 쿼리 및 관리 계층을 통해 필요한 경우 런타임에 비밀을 검색하고 만들 수 있습니다.

Python SDK

사용 예 :

from google.cloud import secretmanager_v1beta1 as secretmanager

secret_id = 'my_secret_key'
project_id = 'my_project'
version = 1    # use the management tools to determine version at runtime

client = secretmanager.SecretManagerServiceClient()

secret_path = client.secret_verion_path(project_id, secret_id, version)
response = client.access_secret_version(secret_path)
password_string = response.payload.data.decode('UTF-8')

# use password_string -- set up database connection, call third party service, whatever

3
이것은 새로운 정답이어야합니다. Secret Manager는 아직 베타 버전이지만 환경 변수로 작업 할 때 진행되는 방법입니다.
King Leon

@ KingLeon, 이것을 사용하면 os.getenv('ENV_VAR')s 무리를 리팩터링해야 함을 의미 합니까?
Alejandro

위와 비슷한 코드를 함수에 넣은 다음 SECRET_KEY = env('SECRET_KEY', default=access_secret_version(GOOGLE_CLOUD_PROJECT_ID, 'SECRET_KEY', 1)). 사용 기본값 설정access_secret_version
King Leon

또한 django-environ을 사용하고 있습니다. github.com/joke2k/django-environ
King Leon

16

이를 수행하는 가장 좋은 방법은 client_secrets.json 파일에 키를 저장하고 .gitignore 파일에 나열하여 git에 업로드되지 않도록 제외하는 것입니다. 환경마다 다른 키가있는 경우 app_identity api를 사용하여 앱 ID가 무엇인지 확인하고 적절하게로드 할 수 있습니다.

여기에 상당히 포괄적 인 예제가 있습니다-> https://developers.google.com/api-client-library/python/guide/aaa_client_secrets .

다음은 몇 가지 예제 코드입니다.

# declare your app ids as globals ...
APPID_LIVE = 'awesomeapp'
APPID_DEV = 'awesomeapp-dev'
APPID_PILOT = 'awesomeapp-pilot'

# create a dictionary mapping the app_ids to the filepaths ...
client_secrets_map = {APPID_LIVE:'client_secrets_live.json',
                      APPID_DEV:'client_secrets_dev.json',
                      APPID_PILOT:'client_secrets_pilot.json'}

# get the filename based on the current app_id ...
client_secrets_filename = client_secrets_map.get(
    app_identity.get_application_id(),
    APPID_DEV # fall back to dev
    )

# use the filename to construct the flow ...
flow = flow_from_clientsecrets(filename=client_secrets_filename,
                               scope=scope,
                               redirect_uri=redirect_uri)

# or, you could load up the json file manually if you need more control ...
f = open(client_secrets_filename, 'r')
client_secrets = json.loads(f.read())
f.close()

2
확실히 올바른 방향이지만 app.yaml앱 배포시 값을 바꾸는 문제는 해결되지 않습니다 . 거기에 어떤 아이디어가 있습니까?

1
따라서 각 환경에 대해 다른 client_secrets 파일을 사용하십시오. 예 : client_secrets_live.json, client_secrets_dev.json, client_secrets_pilot.json 등, 그런 다음 Python 논리를 사용하여 어떤 서버에 있는지 확인하고 적절한 json 파일을로드합니다. app_identity.get_application_id () 메서드는 현재 어떤 서버에 있는지 자동으로 감지하는 데 유용 할 수 있습니다. 이게 무슨 뜻인가요?
Gwyn Howell

@ BenGrunfeld는 내 대답을 참조하십시오. 내 솔루션은 정확히 이것을 수행합니다. 이 답변이 질문을 어떻게 해결하는지 모르겠습니다. 목표는 git에서 비밀 구성을 유지하고 배포의 일부로 git을 사용하는 것이라고 가정합니다. 여기에서이 파일은 여전히 ​​어딘가에 있어야하며 배포 프로세스로 푸시되어야합니다. 이것은 앱에서 할 수있는 일이 될 수 있지만, 제가 강조한 기술을 사용하고 app.yaml을 사용하려는 경우 다른 파일에 저장할 수 있습니다. 내가 그 질문을 이해한다면 그것은 라이브러리 제작자의 실제 클라이언트 비밀 또는 찌르는 것과 함께 오픈 소스 앱을 제공하는 것과 비슷합니다. 키.
therewillbesnacks

1
머리를 돌리는 데 시간이 좀 걸렸지 만 이것이 올바른 접근 방식이라고 생각합니다. 응용 프로그램 설정 ( app.yaml)을 비밀 키 및 기밀 정보와 혼합하지 않습니다. 제가 정말 좋아하는 것은 작업을 수행하기 위해 Google 워크 플로를 사용한다는 것입니다. 감사합니다 @GwynHowell. =)
Ben

1
유사한 접근 방식은 해당 JSON 파일을 앱의 기본 GCS 버킷 ( cloud.google.com/appengine/docs/standard/python/… ) 의 알려진 위치에 배치하는 것 입니다.
스페인 기차

15

이 솔루션은 더 이상 사용되지 않는 appcfg.py에 의존합니다.

앱을 GAE에 배포 할 때 appcfg.py의 -E 명령 줄 옵션을 사용하여 환경 변수를 설정할 수 있습니다 (appcfg.py 업데이트).

$ appcfg.py
...
-E NAME:VALUE, --env_variable=NAME:VALUE
                    Set an environment variable, potentially overriding an
                    env_variable value from app.yaml file (flag may be
                    repeated to set multiple variables).
...

배포 후 어딘가에서 이러한 환경 변수를 쿼리 할 수 ​​있습니까? (난 안 바랍니다.)
Ztyx

gcloud유틸리티를 사용하여 이와 같이 환경 변수를 전달하는 방법이 있습니까?
Trevor

6

대부분의 답변은 구식입니다. Google 클라우드 데이터 저장소를 사용하는 것은 실제로 현재 약간 다릅니다. https://cloud.google.com/python/getting-started/using-cloud-datastore

예를 들면 다음과 같습니다.

from google.cloud import datastore
client = datastore.Client()
datastore_entity = client.get(client.key('settings', 'TWITTER_APP_KEY'))
connection_string_prod = datastore_entity.get('value')

여기서는 항목 이름이 'TWITTER_APP_KEY'이고 종류가 'settings'이며 'value'가 TWITTER_APP_KEY 항목의 속성이라고 가정합니다.


3

몇 가지 접근 방식을 할 수있는 것 같습니다. 유사한 문제가 있으며 다음을 수행합니다 (사용 사례에 맞게 조정 됨).

  • 동적 app.yaml 값을 저장하는 파일을 만들고 빌드 환경의 보안 서버에 배치합니다. 정말 편집증이라면 값을 비대칭 적으로 암호화 할 수 있습니다. 버전 제어 / 동적 가져 오기가 필요한 경우 개인 저장소에 보관하거나 셸 스크립트를 사용하여 적절한 위치에서 복사 / 가져올 수도 있습니다.
  • 배포 스크립트 중에 git에서 가져 오기
  • git pull 후 yaml 라이브러리를 사용하여 순수 Python으로 읽고 쓰는 방식으로 app.yaml을 수정합니다.

이를 수행하는 가장 쉬운 방법은 Hudson , Bamboo 또는 Jenkins 와 같은 지속적 통합 서버를 사용하는 것 입니다. 위에서 언급 한 모든 항목을 수행하는 플러그인, 스크립트 단계 또는 워크 플로를 추가하기 만하면됩니다. 예를 들어 Bamboo 자체에 구성된 환경 변수를 전달할 수 있습니다.

요약하면 액세스 권한 만있는 환경에서 빌드 프로세스 중에 값을 푸시하면됩니다. 빌드를 아직 자동화하지 않았다면 그렇게해야합니다.

또 다른 옵션 옵션은 당신이 말한 것입니다. 데이터베이스에 넣으십시오. 그렇게하지 않는 이유가 너무 느리기 때문이라면 값을 두 번째 계층 캐시로 memcache에 푸시하고 값을 첫 번째 계층 캐시로 인스턴스에 고정하면됩니다. 값이 변경 될 수 있고 인스턴스를 재부팅하지 않고 업데이트해야하는 경우 해시를 유지하여 변경시기를 확인하거나 수행 한 작업이 값을 변경할 때 어떻게 든 트리거 할 수 있습니다. 그게 다야.


1
FWIW,이 접근 방식은 12 Factor 앱 가이드 라인 ( 12factor.net ) 의 구성 요소를 가장 가깝게 따릅니다
Spain Train

3

google kms로 변수를 암호화하고 소스 코드에 포함해야합니다. ( https://cloud.google.com/kms/ )

echo -n the-twitter-app-key | gcloud kms encrypt \
> --project my-project \
> --location us-central1 \
> --keyring THEKEYRING \
> --key THECRYPTOKEY \
> --plaintext-file - \
> --ciphertext-file - \
> | base64

스크램블 된 (암호화되고 base64로 인코딩 된) 값을 환경 변수 (yaml 파일)에 넣습니다.

암호 해독을 시작하는 데 도움이되는 비단뱀 코드입니다.

kms_client = kms_v1.KeyManagementServiceClient()
name = kms_client.crypto_key_path_path("project", "global", "THEKEYRING", "THECRYPTOKEY")

twitter_app_key = kms_client.decrypt(name, base64.b64decode(os.environ.get("TWITTER_APP_KEY"))).plaintext

3

Google Datastore 사용을 기반으로 한 @Jason F의 답변 은 비슷하지만 코드는 라이브러리 문서 입니다. 나를 위해 일한 스 니펫은 다음과 같습니다.

from google.cloud import datastore

client = datastore.Client('<your project id>')
key = client.key('<kind e.g settings>', '<entity name>') # note: entity name not property
# get by key for this entity
result = client.get(key)
print(result) # prints all the properties ( a dict). index a specific value like result['MY_SECRET_KEY'])

Medium 게시물에서 부분적으로 영감을 얻었 습니다.


2

javascript / nodejs에서이 문제를 어떻게 해결했는지 기록하고 싶었습니다. 로컬 개발을 위해 .env 파일에서 process.env로 환경 변수를로드하는 'dotenv'npm 패키지를 사용했습니다. GAE를 사용하기 시작했을 때 환경 변수를 'app.yaml'파일에 설정해야한다는 것을 알게되었습니다. 글쎄, 나는 로컬 개발에 'dotenv'를 사용하고 GAE에 'app.yaml'을 사용하고 싶지 않았기 때문에 (그리고 두 파일 사이에 내 환경 변수를 복제), app.yaml 환경 변수를 프로세스에로드하는 작은 스크립트를 작성했습니다. .env, 로컬 개발 용. 이것이 누군가에게 도움이되기를 바랍니다.

yaml_env.js :

(function () {
    const yaml = require('js-yaml');
    const fs = require('fs');
    const isObject = require('lodash.isobject')

    var doc = yaml.safeLoad(
        fs.readFileSync('app.yaml', 'utf8'), 
        { json: true }
    );

    // The .env file will take precedence over the settings the app.yaml file
    // which allows me to override stuff in app.yaml (the database connection string (DATABASE_URL), for example)
    // This is optional of course. If you don't use dotenv then remove this line:
    require('dotenv/config');

    if(isObject(doc) && isObject(doc.env_variables)) {
        Object.keys(doc.env_variables).forEach(function (key) {
            // Dont set environment with the yaml file value if it's already set
            process.env[key] = process.env[key] || doc.env_variables[key]
        })
    }
})()

이제 코드에 가능한 한 빨리이 파일을 포함하면 완료됩니다.

require('../yaml_env')

이것이 여전히 사실입니까? .env비밀 변수가 있는 파일을 사용하고 있기 때문 입니다. 내 app.yaml파일에 복제하지 않고 배포 된 코드가 여전히 작동합니다. .env그래도 클라우드 의 파일이 어떻게되는지 걱정 됩니다. 암호화되거나 다른 것입니까? .env배포 된 gcloud 파일 변수에 아무도 액세스하지 못하도록하려면 어떻게해야 하나요?
Gus

GAE는 app.yaml 파일에 정의 된 모든 변수를 노드 환경에 자동으로 추가하기 때문에 이것은 전혀 필요하지 않습니다. 기본적으로 이것은 .env 패키지에 정의 된 변수로 dotenv가하는 것과 동일합니다. 하지만 env vars가있는 app.yaml을 VCS 또는 파이프 라인으로 푸시 할 수 없기 때문에 CD를 어떻게 설정해야하는지 궁금합니다.
Jornve

1

Martin의 대답 확장

from google.appengine.ext import ndb

class Settings(ndb.Model):
    """
    Get sensitive data setting from DataStore.

    key:String -> value:String
    key:String -> Exception

    Thanks to: Martin Omander @ Stackoverflow
    https://stackoverflow.com/a/35261091/1463812
    """
    name = ndb.StringProperty()
    value = ndb.StringProperty()

    @staticmethod
    def get(name):
        retval = Settings.query(Settings.name == name).get()
        if not retval:
            raise Exception(('Setting %s not found in the database. A placeholder ' +
                             'record has been created. Go to the Developers Console for your app ' +
                             'in App Engine, look up the Settings record with name=%s and enter ' +
                             'its value in that record\'s value field.') % (name, name))
        return retval.value

    @staticmethod
    def set(name, value):
        exists = Settings.query(Settings.name == name).get()
        if not exists:
            s = Settings(name=name, value=value)
            s.put()
        else:
            exists.value = value
            exists.put()

        return True

1

Cloud Datastore에 appengine 환경 변수를 저장할 수있는 gae_env 라는 pypi 패키지 가 있습니다. 내부적으로는 Memcache도 사용하므로

용법:

import gae_env

API_KEY = gae_env.get('API_KEY')

데이터 저장소에 해당 키에 대한 값이 있으면 반환됩니다. 없는 경우 자리 표시 자 레코드 __NOT_SET__가 생성되고 a ValueNotSetError가 발생합니다. 예외는 Developers Console 로 이동 하여 자리 표시 자 레코드를 업데이트하도록 알려줍니다 .


Martin의 답변과 유사하게 Datastore에서 키 값을 업데이트하는 방법은 다음과 같습니다.

  1. 개발자 콘솔의 데이터 저장소 섹션 으로 이동

  2. 아직 선택하지 않은 경우 페이지 상단에서 프로젝트를 선택합니다.

  3. 에서 종류 드롭 다운 상자를 선택합니다 GaeEnvSettings.

  4. 예외가 발생한 키는 값을 갖습니다 __NOT_SET__.

Settings 클래스에서 생성 한 설정

편집하려면 클릭

실제 값을 입력하고 저장


사용 / 구성에 대한 자세한 정보 는 패키지의 GitHub 페이지 로 이동 하십시오.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.