From 2a86d65207d6689272d6136f9a136d91bfa142a7 Mon Sep 17 00:00:00 2001 From: Lyuyang Hu Date: Mon, 19 Sep 2022 17:02:59 -0400 Subject: [PATCH] use secrets manager to distribute Cloud Storage credentials in staging and prod --- README.md | 5 ++-- backend/src/impl/.env.example | 4 +-- backend/src/impl/init.py | 42 +++++++++++++++++++++++++++++ backend/templates/__main__.mustache | 12 ++------- 4 files changed, 49 insertions(+), 14 deletions(-) create mode 100644 backend/src/impl/init.py diff --git a/README.md b/README.md index d30d241b..4109f0f7 100644 --- a/README.md +++ b/README.md @@ -62,8 +62,9 @@ This repository includes code for frontend and backend of the ExplainaBoard web - We use docker and gunicorn to deploy both frontend and backend. Frontend is built and copied into the static file folder of Flask. Please see Dockerfile for details. - To build: `docker build --pull --rm -f "Dockerfile" -t explainaboard-web:0.2.0 "."` - To run: `docker run --rm -p 5000:5000/tcp explainaboard-web:0.2.0` -- The frontend is served with the flask server at the root url so 5000 is the used to access the UI here. -- connexion is used by swagger/openapi code generation tool and it does not support gunicorn natively. So, currently we use flask server in production. Another option that connexion supports natively is tornado. +- GCP: + - For local development, developers should use their own user accounts to authenticate (please refer to quick start for details) + - For staging and production environments, the service account credentials are passed into the containers as environment variables. The credentials are stored on AWS Secrets Manager. ## More details on frontend and backend 1. Frontend: diff --git a/backend/src/impl/.env.example b/backend/src/impl/.env.example index ee550f00..db3a8cae 100644 --- a/backend/src/impl/.env.example +++ b/backend/src/impl/.env.example @@ -15,6 +15,6 @@ AWS_DEFAULT_REGION= AWS_SECRET_ACCESS_KEY= AWS_ACCESS_KEY_ID= -GOOGLE_APPLICATION_CREDENTIALS= +GCP_SERVICE_CREDENTIALS= # used for staging and prod environments only (ECS), not intended to local STORAGE_BUCKET_NAME= -GOOGLE_CLOUD_PROJECT= \ No newline at end of file +GOOGLE_CLOUD_PROJECT= diff --git a/backend/src/impl/init.py b/backend/src/impl/init.py new file mode 100644 index 00000000..5d26f030 --- /dev/null +++ b/backend/src/impl/init.py @@ -0,0 +1,42 @@ +import logging +import os +from typing import Final + +from explainaboard_web.impl.config import ( + LocalDevelopmentConfig, + ProductionConfig, + StagingConfig, +) +from flask import Flask + + +def init(app: Flask) -> Flask: + """Initializes the flask app""" + _init_config(app) + _init_gcp_credentials() + return app + + +def _init_config(app: Flask): + FLASK_ENV = os.getenv("FLASK_ENV") + if FLASK_ENV == "production": + app.config.from_object(ProductionConfig()) + elif FLASK_ENV == "development": + app.config.from_object(LocalDevelopmentConfig()) + elif FLASK_ENV == "staging": + app.config.from_object(StagingConfig()) + + +def _init_gcp_credentials(): + """If the app is running in an ECS container, the GCP credentials + are passed in as an environment variable. This function reads that + variable, writes to a file and points the Google Cloud Storage + client to the correct file to authenticate the service. + """ + if os.environ.get("GCP_SERVICE_CREDENTIALS"): + credentials = os.environ["GCP_SERVICE_CREDENTIALS"] + gcp_credentials_path: Final = "./GCP_SERVICE_CREDENTIALS.json" + with open(gcp_credentials_path, "w") as f: + f.write(credentials) + os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = gcp_credentials_path + logging.info("GCP credentials file initialized from environment variable.") diff --git a/backend/templates/__main__.mustache b/backend/templates/__main__.mustache index 990e570b..9239fbbd 100644 --- a/backend/templates/__main__.mustache +++ b/backend/templates/__main__.mustache @@ -1,6 +1,5 @@ import connexion -import os -from explainaboard_web.impl.config import ProductionConfig, StagingConfig, LocalDevelopmentConfig +from explainaboard_web.impl.init import init from {{packageName}} import encoder def create_app(): @@ -9,14 +8,7 @@ def create_app(): app.add_api('swagger.yaml', arguments={'title': '{{appName}}'}, pythonic_params=True, validate_responses=True) - FLASK_ENV = os.getenv('FLASK_ENV') - if FLASK_ENV == 'production': - app.app.config.from_object(ProductionConfig()) - elif FLASK_ENV == 'development': - app.app.config.from_object(LocalDevelopmentConfig()) - elif FLASK_ENV == 'staging': - app.app.config.from_object(StagingConfig()) - + app.app = init(app.app) return app