【问题标题】:Securely storing environment variables in GAE with app.yaml使用 app.yaml 在 GAE 中安全地存储环境变量
【发布时间】:2014-05-05 08:36:41
【问题描述】:

我需要将 API 密钥和其他敏感信息作为环境变量存储在 app.yaml 中,以便在 GAE 上进行部署。问题在于,如果我将 app.yaml 推送到 GitHub,此信息将公开(不好)。我不想将信息存储在数据存储中,因为它不适合项目。相反,我想在每次部署应用时从.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}

有什么想法吗?

【问题讨论】:

  • 我希望 GAE 能够添加通过开发者控制台设置实例环境变量的选项(就像我熟悉的所有其他 PaaS 一样)。
  • 您可以使用数据存储。请参考这个答案:stackoverflow.com/a/35254560/1027846
  • 扩展了以上mustilica关于使用数据存储的评论。请参阅下面的答案,了解我在项目中使用的代码:stackoverflow.com/a/35261091#35261091。实际上,它允许您从开发者控制台编辑环境变量,并自动创建占位符值。
  • 感谢mustilica 和Martin。实际上,我们一直在使用数据存储方法,我同意这是解决此问题的最佳方法。与 json 文件方法 IMO 相比,使用 CI/CD 设置更容易。
  • 2019 和 GAE still 还没有解决这个问题:/

标签: python google-app-engine python-2.7 environment-variables


【解决方案1】:

最好的方法是将密钥存储在 client_secrets.json 文件中,并通过将其列在 .gitignore 文件中来将其从上传到 git 中排除。如果不同环境有不同的key,可以通过app_identity api判断app 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()

【讨论】:

  • 绝对是正确的方向,但这并不能解决在部署应用程序时换出app.yaml 中的值的问题。有什么想法吗?
  • 所以每个环境都有一个不同的 client_secrets 文件。例如 client_secrets_live.json、client_secrets_dev.json、client_secrets_pilot.json 等,然后使用 python 逻辑来确定您在哪个服务器上并加载适当的 json 文件。 app_identity.get_application_id() 方法可能有助于自动检测您在哪个服务器上。你的意思是这样的吗?
  • @BenGrunfeld 看到我的回答。我的解决方案正是这样做的。我不明白这个答案如何解决这个问题。我假设目标是将秘密配置排除在 git 之外,并将 git 用作部署的一部分。在这里,这个文件仍然需要放在某个地方并推送到部署过程中。这可以是你在你的应用程序中做的事情,但你只需使用我强调的技术,如果你想使用这个与 app.yaml 相比,可能存储在另一个文件中。如果我理解这个问题,这类似于使用图书馆制造商的实际客户秘密或产品来发布一个开源应用程序。键。
  • 我花了一段时间才明白这一点,但我认为这是正确的方法。您没有将应用程序设置 (app.yaml) 与密钥和机密信息混合在一起,我真正喜欢的是您使用 Google 工作流程来完成任务。谢谢@GwynHowell。 =)
  • 类似的方法是将 JSON 文件放在应用默认 GCS 存储桶 (cloud.google.com/appengine/docs/standard/python/…) 中的已知位置。
【解决方案2】:

听起来你可以做一些方法。我们有类似的问题并执行以下操作(适应您的用例):

  • 创建一个存储任何动态 app.yaml 值的文件,并将其放置在构建环境中的安全服务器上。如果你真的很偏执,你可以不对称地加密这些值。如果您需要版本控制/动态拉取,您甚至可以将其保存在私有仓库中,或者仅使用 shell 脚本将其复制/从适当的位置拉取。
  • 在部署脚本期间从 git 拉取
  • 在 git pull 之后,通过使用 yaml 库在纯 python 中读写 app.yaml 来修改它

最简单的方法是使用持续集成服务器,例如HudsonBambooJenkins。只需添加一些插件、脚本步骤或工作流来完成我提到的所有上述项目。例如,您可以传入在 Bamboo 中配置的环境变量。

总而言之,只需在您只能访问的环境中在构建过程中输入值。如果您还没有自动化构建,那么您应该这样做。

另一个选项选项就是你说的,放到数据库里。如果您不这样做的原因是速度太慢,只需将值作为第二层缓存推送到 memcache 中,然后将值作为第一层缓存固定到实例中。如果值可以更改并且您需要在不重新启动它们的情况下更新实例,只需保留一个哈希值,您可以检查它们何时更改或在您更改值时以某种方式触发它。应该是这样的。

【讨论】:

  • FWIW,这种方法最接近 12 Factor App 指南中的配置因素 (12factor.net)
【解决方案3】:

我的方法是将客户端机密存储在 App Engine 应用程序本身中。客户端机密既不在源代码控制中,也不在任何本地计算机上。这样做的好处是,任何 App Engine 协作者都可以部署代码更改,而不必担心客户端机密。

我将客户端机密直接存储在 Datastore 中,并使用 Memcache 来改善访问机密的延迟。 Datastore 实体只需要创建一次,并将在未来的部署中持续存在。当然,App Engine 控制台可用于随时更新这些实体。

有两个选项可以执行一次性实体创建:

  • 使用 App Engine Remote API 交互式 shell 创建实体。
  • 创建一个仅用于管理员的处理程序,该处理程序将使用虚拟值初始化实体。手动调用此管理处理程序,然后使用 App Engine 控制台使用生产客户端机密更新实体。

【讨论】:

  • 一点也不复杂。感谢应用引擎。
【解决方案4】:

如果是敏感数据,则不应将其存储在源代码中,因为它将被检入源代码管理。错误的人(组织内部或外部)可能会在那里找到它。此外,您的开发环境可能使用与生产环境不同的配置值。如果这些值存储在代码中,您将不得不在开发和生产中运行不同的代码,这是一种混乱且不好的做法。

在我的项目中,我使用此类将配置数据放入数据存储区:

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 并更新占位符记录。

我发现这消除了设置配置值的猜测。如果您不确定要设置哪些配置值,只需运行代码,它就会告诉您!

上面的代码使用了ndb库,它使用了memcache和底层的数据存储,所以速度很快。


更新:

jelder 询问如何在 App Engine 控制台中查找 Datastore 值并进行设置。方法如下:

  1. 转到https://console.cloud.google.com/datastore/

  2. 如果尚未选择,请在页面顶部选择您的项目。

  3. 种类下拉框中,选择设置

  4. 如果您运行上面的代码,您的密钥就会显示出来。它们都将具有 NOT SET 值。单击每个并设置其值。

希望这会有所帮助!

【讨论】:

  • 在所有提供的答案中,这似乎最接近 Heroku 的处理方式。作为 GAE 的新手,我不太明白在开发人员控制台中的哪里可以找到占位符记录。你能解释一下,或者为了加分,张贴截图吗?
  • dam~... 恕我直言 gcloud,为了这个特定的需求而不得不使用其他服务看起来很糟糕。除此之外,谷歌确实为 firebase 函数中的 env vars 提供了一种“100%-herokuish”的方法,但不为 gcloud 函数提供(至少没有记录……如果我没记错的话)
  • 这是一个基于您的方法的要点,它增加了唯一性和环境变量后备 - gist.github.com/SpainTrain/6bf5896e6046a5d9e7e765d0defc8aa8
  • @Ben 非 Firebase 函数确实支持环境变量(至少现在)。
  • @obl - App Engine 应用会自动对其自己的数据存储进行身份验证,无需身份验证详细信息。这很整洁:-)
【解决方案5】:

此解决方案依赖于已弃用的 appcfg.py

在将应用部署到 GAE(appcfg.py 更新)时,您可以使用 appcfg.py 的 -E 命令行选项来设置环境变量

$ 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).
...

【讨论】:

  • 你能在部署后的某个地方查询那些环境变量吗? (我希望不会。)
  • 有没有办法像这样使用gcloud 实用程序传递环境变量?
【解决方案6】:

只是想说明我是如何在 javascript/nodejs 中解决这个问题的。对于本地开发,我使用了“dotenv”npm 包,它将环境变量从 .env 文件加载到 process.env 中。当我开始使用 GAE 时,我了解到需要在“app.yaml”文件中设置环境变量。好吧,我不想使用 'dotenv' 进行本地开发和使用 'app.yaml' 进行 GAE(并在两个文件之间复制我的环境变量),所以我编写了一个小脚本,将 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 文件会发生什么。它会被加密还是什么?部署后如何确保没有人访问 gcloud .env 文件变量?
  • 这根本不需要,因为 GAE 会自动将 app.yaml 文件中定义的所有变量添加到节点环境中。基本上这与 dotenv 对 .env 包中定义的变量所做的相同。但是我想知道如何设置 CD,因为您无法将带有 env vars 的 app.yaml 推送到 VCS 或管道...
【解决方案7】:

扩展马丁的答案

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

【讨论】:

    【解决方案8】:

    有一个名为 gae_env 的 pypi 包,可让您将 appengine 环境变量保存在 Cloud Datastore 中。在引擎盖下,它还使用 Memcache,所以速度很快

    用法:

    import gae_env
    
    API_KEY = gae_env.get('API_KEY')
    

    如果数据存储区中有该键的值,则会返回该值。 如果没有,将创建一个占位符记录 __NOT_SET__ 并抛出一个 ValueNotSetError。异常会提醒您前往Developers Console 并更新占位符记录。


    与 Martin 的回答类似,以下是如何更新 Datastore 中键的值:

    1. 在开发者控制台中转到Datastore Section

    2. 如果尚未选择,请在页面顶部选择您的项目。

    3. 种类下拉框中,选择GaeEnvSettings

    4. 引发异常的键的值为__NOT_SET__


    转至package's GitHub page 了解更多关于使用/配置的信息

    【讨论】:

      【解决方案9】:

      此解决方案很简单,但可能并不适合所有不同的团队。

      首先,将环境变量放在一个env_variables.yaml中,例如,

      env_variables:
        SECRET: 'my_secret'
      

      然后,将此env_variables.yaml 包含在app.yaml

      includes:
        - env_variables.yaml
      

      最后,将env_variables.yaml 添加到.gitignore,这样存储库中就不会存在秘密变量了。

      在这种情况下,env_variables.yaml 需要在部署管理器之间共享。

      【讨论】:

      • 只是添加一些对某些人来说可能不明显的内容,然后您的环境变量将在 process.env.MY_SECRET_KEY 中找到,如果您在本地开发环境中需要这些环境变量,您可以使用节点 dotenv
      • env_variables.yaml 如何访问所有实例是一个难题。
      • 还有:如何在本地使用这个?
      • 这是最简单的解决方案。可以通过os.environ.get('SECRET') 在您的应用程序中访问秘密。
      • @这个 env_variable.yaml 将如何在谷歌应用引擎部署的代码中可用?
      【解决方案10】:

      大多数答案都已过时。现在使用谷歌云数据存储实际上有点不同。 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 实体的属性。

      【讨论】:

        【解决方案11】:

        您应该使用 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 文件中)。

        一些pythonish代码可以帮助您开始解密。

        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
        

        【讨论】:

          【解决方案12】:

          @Jason F 基于使用 Google Datastore 的 answer 已接近,但根据 library docs 上的示例用法,代码有点过时。这是对我有用的 sn-p:

          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 post

          【讨论】:

            【解决方案13】:

            这在您发帖时并不存在,但对于其他偶然发现这里的人,Google 现在提供了一项名为 Secret Manager 的服务。

            这是一个简单的 REST 服务(当然还有 SDK 包装),可以将您的秘密存储在谷歌云平台上的安全位置。这是一种比 Data Store 更好的方法,它需要额外的步骤来查看存储的机密并拥有更细粒度的权限模型——如果需要,您可以针对项目的不同方面以不同的方式保护各个机密。

            它提供版本控制,因此您可以相对轻松地处理密码更改,以及强大的查询和管理层,使您能够在运行时发现和创建机密(如有必要)。

            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_version_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
            

            【讨论】:

            • 这应该是新的正确答案。 Secret Manager 仍处于测试阶段,但这是处理环境变量时的前进方向。
            • @KingLeon,使用这个意味着必须重构一堆os.getenv('ENV_VAR')s吗?
            • 我把与上面类似的代码放在一个函数中,然后我使用类似SECRET_KEY = env('SECRET_KEY', default=access_secret_version(GOOGLE_CLOUD_PROJECT_ID, 'SECRET_KEY', 1))的东西。设置默认使用access_secret_version
            • 另外,我正在使用 django-environ。 github.com/joke2k/django-environ
            • @dierre 是的,但没关系。它是秘密值的标识符,仅此而已。
            【解决方案14】:

            使用 github 操作而不是谷歌云触发器(谷歌云触发器无法找到它自己的 app.yaml 并自行管理该死的环境变量。)

            操作方法如下:

            我的环境: 应用引擎, 标准(非弹性), Nodejs Express 应用程序, 一个 PostgreSQL CloudSql

            首先设置:

            1. Create a new Google Cloud Project (or select an existing project).
            
            2. Initialize your App Engine app with your project.
            
            [Create a Google Cloud service account][sa] or select an existing one.
            
            3. Add the the following Cloud IAM roles to your service account:
            
                App Engine Admin - allows for the creation of new App Engine apps
            
                Service Account User - required to deploy to App Engine as service account
            
                Storage Admin - allows upload of source code
            
                Cloud Build Editor - allows building of source code
            
            [Download a JSON service account key][create-key] for the service account.
            
            4. Add the following [secrets to your repository's secrets][gh-secret]:
            
                GCP_PROJECT: Google Cloud project ID
            
                GCP_SA_KEY: the downloaded service account key
            

            app.yaml

            runtime: nodejs14
            env: standard
            env_variables:
              SESSION_SECRET: $SESSION_SECRET
            beta_settings:
              cloud_sql_instances: SQL_INSTANCE
            

            然后是 github 动作

            name: Build and Deploy to GKE
            
            on: push
            
            env:
              PROJECT_ID: ${{ secrets.GKE_PROJECT }}
              DATABASE_URL: ${{ secrets.DATABASE_URL}}
            jobs:
              setup-build-publish-deploy:
                name: Setup, Build, Publish, and Deploy
                runs-on: ubuntu-latest
            
            steps:
             - uses: actions/checkout@v2
             - uses: actions/setup-node@v2
               with:
                node-version: '12'
             - run: npm install
             - uses: actions/checkout@v1
             - uses: ikuanyshbekov/app-yaml-env-compiler@v1.0
               env:
                SESSION_SECRET: ${{ secrets.SESSION_SECRET }}  
             - shell: bash
               run: |
                    sed -i 's/SQL_INSTANCE/'${{secrets.DATABASE_URL}}'/g' app.yaml
             - uses: actions-hub/gcloud@master
               env:
                PROJECT_ID: ${{ secrets.GKE_PROJECT }}
                APPLICATION_CREDENTIALS: ${{ secrets.GCLOUD_AUTH }}
                CLOUDSDK_CORE_DISABLE_PROMPTS: 1
               with:
                args: app deploy app.yaml
            

            要将秘密添加到 github 操作中,您必须转到:设置/秘密

            请注意,我可以使用 bash 脚本处理所有替换。所以我不会依赖github项目“ikuanyshbekov/app-yaml-env-compiler@v1.0”

            很遗憾,GAE 没有提供最简单的方法来处理 app.yaml 的环境变量。我不想使用 KMS,因为我需要更新 beta-settings/cloud sql 实例。我真的需要将所有内容替换为 app.yaml。

            这样我可以针对正确的环境采取特定的行动并管理秘密。

            【讨论】:

              【解决方案15】:

              我的解决方案是通过 github action 和 github secrets 替换 app.yaml 文件中的 secrets。

              app.yaml(应用引擎)

              env_variables:
                SECRET_ONE: $SECRET_ONE
                ANOTHER_SECRET: $ANOTHER_SECRET
              

              workflow.yaml (Github)

              steps:
                - uses: actions/checkout@v2
                - uses: 73h/gae-app-yaml-replace-env-variables@v0.1
                  env:
                    SECRET_ONE: ${{ secrets.SECRET_ONE }}
                    ANOTHER_SECRET: ${{ secrets.ANOTHER_SECRET }}
              

              您可以在此处找到 Github 操作。
              https://github.com/73h/gae-app-yaml-replace-env-variables

              在本地开发时,我将秘密写入 .env 文件。

              【讨论】:

                猜你喜欢
                • 2020-01-16
                • 2017-05-26
                • 2016-06-19
                • 2019-06-29
                • 2019-08-31
                • 1970-01-01
                • 2014-09-29
                • 2016-01-27
                • 1970-01-01
                相关资源
                最近更新 更多