【问题标题】:Cloud SQL import permissions issues for Cloud Storage bucketCloud Storage 存储分区的 Cloud SQL 导入权限问题
【发布时间】:2019-07-12 21:19:57
【问题描述】:

我正在编写一个云函数来:

  • Export Cloud SQL (postgresql) DB 到 Cloud Storage 存储桶中的文件
  • Import 回到另一个 Cloud SQL 实例/数据库(仍然是 postgresql)

注意: 我希望此代码每晚自行运行,以将生产数据库复制到暂存环境,因此我计划使用 Cloud Scheduler 触发它。
如果您有更好/更简单的解决方案可以在 GCP 中解决这个问题,我会全力以赴 :)

这是我的代码(实际函数是文件底部的clone_db):

from os import getenv
from datetime import datetime
from time import sleep

from googleapiclient import discovery
from googleapiclient.errors import HttpError
from oauth2client.client import GoogleCredentials
from google.cloud import storage

GS_BUCKET = getenv("GS_BUCKET")
GS_FOLDER = "sql-exports"
GS_EXPORT_PATH = f"gs://{GS_BUCKET}/{GS_FOLDER}"


def __sql_file_name(db: str, timestamp: datetime):
    return f"{db}-{timestamp.strftime('%Y-%m-%d')}.sql.gz"


def __sql_file_uri(db: str, timestamp: datetime):
    return f"{GS_EXPORT_PATH}/{__sql_file_name(db, timestamp)}"


def __export_source_db(service, project: str, timestamp: datetime, instance: str, db: str):
    context = {
        "exportContext": {
            "kind": "sql#exportContext",
            "fileType": "SQL",
            "uri": __sql_file_uri(db, timestamp),
            "databases": [db],
        }
    }

    return service.instances().export(project=project, instance=instance, body=context).execute()


def __import_target_db(service, project: str, timestamp: datetime, instance: str, db: str):
    context = {
        "importContext": {
            "kind": "sql#importContext",
            "fileType": "SQL",
            "uri": __sql_file_uri(db, timestamp),
            "database": db,
        }
    }

    return service.instances().import_(project=project, instance=instance, body=context).execute()


def __drop_db(service, project: str, instance: str, db: str):
    try:
        return service.databases().delete(project=project, instance=instance, database=db).execute()
    except HttpError as e:
        if e.resp.status == 404:
            return {"status": "DONE"}
        else:
            raise e


def __create_db(service, project: str, instance: str, db: str):
    database = {
        "name": db,
        "project": project,
        "instance": instance,
    }

    return service.databases().insert(project=project, instance=instance, body=database).execute()


def __update_export_permissions(file_name: str):
    client = storage.Client()
    file = client.get_bucket(GS_BUCKET).get_blob(f"{GS_FOLDER}/{file_name}")
    file.acl.user(getenv("TARGET_DB_SERVICE_ACCOUNT")).grant_read()
    file.acl.save()


def __delete_sql_file(file_name: str):
    client = storage.Client()
    bucket = client.get_bucket(GS_BUCKET)
    bucket.delete_blob(f"{GS_FOLDER}/{file_name}")


def __wait_for(operation_type, operation, service, project):
    if operation["status"] in ("PENDING", "RUNNING", "UNKNOWN"):
        print(f"{operation_type} operation in {operation['status']} status. Waiting for completion...")

        while operation['status'] != "DONE":
            sleep(1)
            operation = service.operations().get(project=project, operation=operation['name']).execute()

    print(f"{operation_type} operation completed!")


def clone_db(_):
    credentials = GoogleCredentials.get_application_default()
    service = discovery.build('sqladmin', 'v1beta4', credentials=credentials)

    # Project ID of the project that contains the instance to be exported.
    project = getenv('PROJECT_ID')

    # Cloud SQL instance ID. This does not include the project ID.
    source = {
        "instance": getenv("SOURCE_INSTANCE_ID"),
        "db": getenv("SOURCE_DB_NAME")
    }

    timestamp = datetime.utcnow()

    print(f"Exporting database {source['instance']}:{source['db']} to Cloud Storage...")
    operation = __export_source_db(service, project, timestamp, **source)

    __wait_for("Export", operation, service, project)

    print("Updating exported file permissions...")
    __update_export_permissions(__sql_file_name(source["db"], timestamp))
    print("Done.")

    target = {
        "instance": getenv("TARGET_INSTANCE_ID"),
        "db": getenv("TARGET_DB_NAME")
    }

    print(f"Dropping target database {target['instance']}:{target['db']}")
    operation = __drop_db(service, project, **target)
    __wait_for("Drop", operation, service, project)

    print(f"Creating database {target['instance']}:{target['db']}...")
    operation = __create_db(service, project, **target)
    __wait_for("Creation", operation, service, project)

    print(f"Importing data into {target['instance']}:{target['db']}...")
    operation = __import_target_db(service, project, timestamp, **target)
    __wait_for("Import", operation, service, project)

    print("Deleting exported SQL file")
    __delete_sql_file(__sql_file_name(source["db"], timestamp))
    print("Done.")

在我尝试将导出的数据导入到我的目标实例之前,一切正常。

在调用import_ 时,函数失败并出现以下错误:

Error: function crashed. Details:
<HttpError 403 when requesting https://www.googleapis.com/sql/v1beta4/projects/<project_id>/instances/<instance_id>/import?alt=json returned "The service account does not have the required permissions for the bucket.">

我已在此处和网络上的许多其他问答中了解到此错误,但我不知道如何使事情正常进行。
这是我所做的:

  • Cloud Function 作为我的“Compute Engine 默认服务帐户”运行,该帐户在 IAM 中设置了 Project Editor 角色
  • 目标 Cloud SQL 实例的服务帐号以 Storage Object Admin 的形式添加到存储桶的权限中。我尝试了各种其他角色组合(旧版阅读器/所有者、存储对象查看器......)但无济于事
  • 正如您在函数代码中看到的那样,我专门授予目标实例的服务帐户对导出文件的读取权限,并且它正确反映在对象在云存储中的权限上:

  • 我已尝试禁用此存储桶的对象级权限,并确保我上面第一点的权限设置正确,但它也不起作用

有趣的是,当我尝试从 GCP Cloud SQL 控制台手动导入同一实例上的同一文件时,一切正常。
完成后,我可以看到我导出的文件的权限已更新,以将实例的服务帐户包含为 Reader,就像我最后在代码中所做的那样尝试重现该行为。

那么我在这里错过了什么?
我应该为哪个服务帐户设置哪些权限才能使其正常工作?

【问题讨论】:

    标签: python google-cloud-platform google-cloud-storage google-cloud-functions google-cloud-sql


    【解决方案1】:

    问题在于您的代码而不是 Cloud SQL。

    当调用 _import_target_db 函数时,您正在寻找您的 Cloud Storage 存储桶中不存在的文件。

    进入细节:

    您使用以下名称将数据库导出到您的存储桶:

    gs://yourBucket/sql-exports/exportedDatabaseName-yyyy-mm-dd.sql.gz

    但是,当您尝试导入它时,导入函数正在寻找一个名为:

    gs://yourBucket/sql-exports/importDatabaseName-yyyy-mm-dd.sql.gz

    此文件在您的存储桶中不存在,出于安全原因,返回 403 Forbidden 错误。

    【讨论】:

    • 是的,你完全正确!之后我们发现了这一点,我忘了更新问题......访问权限让我失望了,当它发生时我没有质疑我的代码的有效性,这使得这成为一个复杂的案例?谷歌对“可能有一个更好的错误信息 » 是出于安全原因,最好不要泄露“找不到文件”与“没有访问权限”的信息,这是有道理的。无论如何,感谢您的调查,很好!
    【解决方案2】:

    我遇到了同样的问题并尝试了很多不同的方法。即使在授予项目、存储桶和 SQL 文件的 DB-service-account 所有者权限之后,它也无法正常工作,而从其他文件导入/导出/导出到其他文件始终有效。

    所以我最终重命名了我的导入文件,并且令人惊讶的是它工作了(以前的文件名很长,并且像您的示例一样在其中包含下划线)。但是我在文档中找不到有关此类命名限制的任何内容,此时我什至无法判断此问题是否与文件名或下划线的使用有关。但可能值得一试。

    【讨论】:

    • 伙计,这太不可思议了 :D 在 Google 的支持下,我已经研究了这个主题整整一个月,并上报给 Cloud Functions 团队的工程师,但我们找不到问题所在。该错误消息显然具有误导性,我想那里有一个很好的错误要修复;我将用这种解决方法跟进他们;谢谢!!
    【解决方案3】:

    CloudSQL 实例在不属于您项目的 Google 服务帐号下运行。

    您需要找到您的实例的服务帐号 - Cloud SQL-> 集群名称 ->服务帐号

    然后,您获取上述服务帐户并为其授予相关存储桶的写入/读取权限

    【讨论】:

    • 这就是我说我尝试过以下操作时的意思:“目标 Cloud SQL 实例的服务帐户作为存储对象管理员添加到存储桶的权限中。我尝试了各种其他角色组合(旧reader/owner, storage object viewer, ...) 无济于事”
    猜你喜欢
    • 2019-10-12
    • 1970-01-01
    • 2019-04-05
    • 2014-12-11
    • 1970-01-01
    • 1970-01-01
    • 2023-02-21
    • 2017-11-15
    • 2020-12-03
    相关资源
    最近更新 更多