Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Could not create or update Cloud Run service (Firebase functions v2) #312

Closed
fuelkoy opened this issue Mar 11, 2024 · 16 comments
Closed

Could not create or update Cloud Run service (Firebase functions v2) #312

fuelkoy opened this issue Mar 11, 2024 · 16 comments
Assignees
Labels
priority: p2 Moderately-important priority. Fix may not be included in next release. type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns.

Comments

@fuelkoy
Copy link

fuelkoy commented Mar 11, 2024

Bug Description

Could not create or update Cloud Run service using Firebase Functions. Functions emulator is working locally correctly if using user and password. If using Automatic IAM Authentication it does not work locally nor while deploying.

Example code (or command)

import "dotenv/config";
import * as dotenv from "dotenv";
import { initializeApp } from "firebase-admin/app";
import { onRequest } from "firebase-functions/v2/https";
import mysql from 'mysql2/promise';
import { Connector } from '@google-cloud/cloud-sql-connector';

dotenv.config({ path: "../.env" });

// Initialize the Firebase Admin SDK
initializeApp();

const connector = new Connector();
const clientOpts = await connector.getOptions({
  instanceConnectionName: 'fuelkdev:europe-north1:dev',
  // authType: AuthTypes.IAM,
});
const pool = await mysql.createPool({
  ...clientOpts,
  user: process.env.CLOUD_SQL_USER,
  password: process.env.CLOUD_SQL_PASSWORD,
  database: process.env.CLOUD_SQL_DATABASE,
});

export const helloWorld = onRequest(async (request, response) => {
  response.send("Hello from Firebase!");

  const conn = await pool.getConnection();
  const [result] = await conn.query(`SELECT NOW();`);
  console.log(123);
  console.table(result); // prints returned time value from server
});

Stacktrace

If trying to use Automatic IAM:

!  functions: Error: Access denied for user 'functionsServiceAccount'@'cloudsqlproxy~[IpAddress]' (using password: YES)
    at Packet.asError (C:\Users\Kehitys\Documents\GitHub\nodeJsSqlConnector\functions\node_modules\.pnpm\mysql2@3.9.2\node_modules\mysql2\lib\packets\packet.js:728:17)
    at ClientHandshake.execute (C:\Users\Kehitys\Documents\GitHub\nodeJsSqlConnector\functions\node_modules\.pnpm\mysql2@3.9.2\node_modules\mysql2\lib\commands\command.js:29:26)
    at PoolConnection.handlePacket (C:\Users\Kehitys\Documents\GitHub\nodeJsSqlConnector\functions\node_modules\.pnpm\mysql2@3.9.2\node_modules\mysql2\lib\connection.js:481:34)
    at PacketParser.onPacket (C:\Users\Kehitys\Documents\GitHub\nodeJsSqlConnector\functions\node_modules\.pnpm\mysql2@3.9.2\node_modules\mysql2\lib\connection.js:97:12)
    at PacketParser.executePayload (C:\Users\Kehitys\Documents\GitHub\nodeJsSqlConnector\functions\node_modules\.pnpm\mysql2@3.9.2\node_modules\mysql2\lib\packet_parser.js:139:14)
    at TLSSocket.<anonymous> (C:\Users\Kehitys\Documents\GitHub\nodeJsSqlConnector\functions\node_modules\.pnpm\mysql2@3.9.2\node_modules\mysql2\lib\connection.js:104:25)
    at TLSSocket.emit (node:events:514:28)
    at addChunk (node:internal/streams/readable:545:12)
    at readableAddChunkPushByteMode (node:internal/streams/readable:495:3)
    at Readable.push (node:internal/streams/readable:375:5)
!  Your function was killed because it raised an unhandled error.

The rest is the same whether using user, password or Automatic IAM

If not setting GOOGLE_APPLICATION_CREDENTIALS in .env cannot deploy errors with the following error: Error: Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information. at GoogleAuth.getApplicationDefaultAsync (C:\Users\Kehitys\Documents\GitHub\nodeJsSqlConnector\functions\node_modules\.pnpm\google-auth-library@9.6.3\node_modules\google-auth-library\build\src\auth\googleauth.js:271:15) at process.processTicksAndRejections (node:internal/process/task_queues:95:5) at async GoogleAuth.getClient (C:\Users\Kehitys\Documents\GitHub\nodeJsSqlConnector\functions\node_modules\.pnpm\google-auth-library@9.6.3\node_modules\google-auth-library\build\src\auth\googleauth.js:695:17) at async GoogleAuth.request (C:\Users\Kehitys\Documents\GitHub\nodeJsSqlConnector\functions\node_modules\.pnpm\google-auth-library@9.6.3\node_modules\google-auth-library\build\src\auth\googleauth.js:748:24) at async SQLAdminFetcher.getInstanceMetadata (file:///C:/Users/Kehitys/Documents/GitHub/nodeJsSqlConnector/functions/node_modules/.pnpm/@google-cloud+cloud-sql-connector@1.2.3/node_modules/@google-cloud/cloud-sql-connector/dist/mjs/sqladmin-fetcher.js:88:21) at async CloudSQLInstance.performRefresh (file:///C:/Users/Kehitys/Documents/GitHub/nodeJsSqlConnector/functions/node_modules/.pnpm/@google-cloud+cloud-sql-connector@1.2.3/node_modules/@google-cloud/cloud-sql-connector/dist/mjs/cloud-sql-instance.js:117:26) at async CloudSQLInstance.getCloudSQLInstance (file:///C:/Users/Kehitys/Documents/GitHub/nodeJsSqlConnector/functions/node_modules/.pnpm/@google-cloud+cloud-sql-connector@1.2.3/node_modules/@google-cloud/cloud-sql-connector/dist/mjs/cloud-sql-instance.js:21:9) at async CloudSQLInstanceMap.loadInstance (file:///C:/Users/Kehitys/Documents/GitHub/nodeJsSqlConnector/functions/node_modules/.pnpm/@google-cloud+cloud-sql-connector@1.2.3/node_modules/@google-cloud/cloud-sql-connector/dist/mjs/connector.js:46:36) at async Connector.getOptions (file:///C:/Users/Kehitys/Documents/GitHub/nodeJsSqlConnector/functions/node_modules/.pnpm/@google-cloud+cloud-sql-connector@1.2.3/node_modules/@google-cloud/cloud-sql-connector/dist/mjs/connector.js:106:9) at async file:///C:/Users/Kehitys/Documents/GitHub/nodeJsSqlConnector/functions/lib/index.js:8:18

If setting GOOGLE_APPLICATION_CREDENTIALS in .env cannot deploy errors with the following error: Could not create or update Cloud Run service helloworld, Container Healthcheck failed. Revision 'helloworld-00001-hox' is not ready and cannot serve traffic. The user-provided container failed to start and listen on the port defined provided by the PORT=8080 environment variable. Logs for this revision might contain more information.
With a Logs Explorer error: Detailed stack trace: Error: Failed to read credentials from file [Path to application_default_credentials.json]: Error: ENOENT: no such file or directory, open '[Path to application_default_credentials.json]'

ERROR 2024-03-11T09:46:29.942735Z Default STARTUP TCP probe failed 1 time consecutively for container "trpc-1" on port 8080. The instance was not started.
WARNING 2024-03-11T09:46:29.801817146Z Container called exit(1).
DEFAULT 2024-03-11T09:46:29.471439Z Could not load the function, shutting down.
DEFAULT 2024-03-11T09:46:29.471208Z at getUserFunction (/workspace/node_modules/.pnpm/@google-cloud+functions-framework@3.3.0/node_modules/@google-cloud/functions-framework/build/src/loader.js:95:30)
DEFAULT 2024-03-11T09:46:29.471200Z at importModuleDynamicallyWrapper (node:internal/vm/module:431:15)
DEFAULT 2024-03-11T09:46:29.471193Z at ModuleLoader.import (node:internal/modules/esm/loader:329:24)
DEFAULT 2024-03-11T09:46:29.471187Z at ModuleJob.run (node:internal/modules/esm/module_job:218:25)
DEFAULT 2024-03-11T09:46:29.471180Z at file:///workspace/lib/index.js:25:1
DEFAULT 2024-03-11T09:46:29.471173Z at initializeApp (/workspace/node_modules/.pnpm/firebase-admin@12.0.0/node_modules/firebase-admin/lib/app/lifecycle.js:102:36)
DEFAULT 2024-03-11T09:46:29.471165Z at AppStore.initializeApp (/workspace/node_modules/.pnpm/firebase-admin@12.0.0/node_modules/firebase-admin/lib/app/lifecycle.js:34:82)
DEFAULT 2024-03-11T09:46:29.471157Z at getApplicationDefault (/workspace/node_modules/.pnpm/firebase-admin@12.0.0/node_modules/firebase-admin/lib/app/credential-internal.js:401:16)
DEFAULT 2024-03-11T09:46:29.471149Z at credentialFromFile (/workspace/node_modules/.pnpm/firebase-admin@12.0.0/node_modules/firebase-admin/lib/app/credential-internal.js:480:29)
DEFAULT 2024-03-11T09:46:29.471141Z at readCredentialFile (/workspace/node_modules/.pnpm/firebase-admin@12.0.0/node_modules/firebase-admin/lib/app/credential-internal.js:507:15)

How to reproduce

Environment details

  • OS:
Host Name:                 DESKTOP-6BMEHR6
OS Name:                   Microsoft Windows 11 Pro
OS Version:                10.0.22621 N/A Build 22621
OS Manufacturer:           Microsoft Corporation
OS Configuration:          Standalone Workstation
OS Build Type:             Multiprocessor Free
Registered Owner:          Kehitys
Registered Organization:   N/A
Product ID:                00330-52779-93116-AAOEM
Original Install Date:     17.10.2023, 9.52.34
System Boot Time:          15.2.2024, 8.09.01
System Manufacturer:       Dell Inc.
System Model:              Latitude 5500
System Type:               x64-based PC
  • Node.js version: v20.10.0
  • npm version: 10.2.3

Steps to reproduce

If using Automatic IAM set IAM principal (user, service account, etc.) with the Cloud SQL Client role and enable The Cloud SQL Admin API

  1. gcloud iam service-accounts create SERVICE_ACCOUNT_NAME

  2. gcloud projects add-iam-policy-binding PROJECT_ID --member="serviceAccount:SERVICE_ACCOUNT_NAME@PROJECT_ID.iam.gserviceaccount.com" --role=ROLE
    Näyttökuva 2024-03-11 130143

  3. Follow instructions at: https://cloud.google.com/sql/docs/postgres/create-edit-iam-instances#configure-iam-db-instance
    Näyttökuva 2024-03-11 130053

  4. Follow instructions at: https://cloud.google.com/sql/docs/postgres/add-manage-iam-users#creating-a-database-user
    Näyttökuva 2024-03-11 130015
    (I tried this by creating a new service account, using the user that is the owner of the, project default app engine service account (I suppose, the one with Cloud SQL Client and Cloud SQL Instance User roles)).

  5. firebase login

  6. firebase init functions, choose typescript, remove npm run lint from firebase.json predeploy

  7. Set the project ready to use esm with esbuild as it seems to be the only viable solution to use esm in firebase functions
    (based on the following: Support ES Modules for Node 14 functions firebase/firebase-tools#2994 (comment) and https://stackoverflow.com/questions/73953672/problem-deploying-firebase-function-with-typescript-using-es-module-system)
    : type to "module" in package.json, and add the following scripts

    "dev:tsc": "tsc --watch --preserveWatchOutput",
    "dev:node": "firebase emulators:start --only functions",
    "dev:esbuild": "pnpm run build --watch",
    "dev": "run-p dev:*",
    "build": "esbuild src/index.ts --bundle --platform=node --outfile=lib/index.js --format=esm --external:./node_modules/*",
  1. Set the following in tsconfig.json to be able to use ESM
    "allowJs": true,
    "esModuleInterop": true,
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "noImplicitReturns": true,
    "noUnusedLocals": true,
    "noEmit": true,
    "strict": true,
    "lib": ["ES2022"],
    "target": "ES2022",
  1. install packages,
pnpm i @google-cloud/cloud-sql-connector @google-cloud/functions-framework (I'm using pnpm) dotenv google-auth-library mysql2 esbuild npm-run-all
  1. set environment variables (And GOOGLE_APPLICATION_CREDENTIALS pointing to the application_default_credentials.json)
  2. gcloud init
  3. gcloud auth application-default login
  4. npm run deploy

Repo

https://github.com/fuelkoy/nodeJsSqlConnector

Thanks!

@fuelkoy fuelkoy added the type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns. label Mar 11, 2024
@jackwotherspoon
Copy link
Collaborator

@fuelkoy Thanks for raising the issue! 😄

Quick follow-up for you while we debug and look into the issue for you...

When setting the environment variable for the database IAM user ( user: process.env.CLOUD_SQL_USER), are you using functionsServiceAccount as the username? MySQL expects you to remove the domain suffix (i.e the @ and everything after it)

image

@fuelkoy
Copy link
Author

fuelkoy commented Mar 12, 2024

@jackwotherspoon Good morning and thank you for the quick response.

And yes. If I use IAM index.ts file looks like:

import "dotenv/config";
import * as dotenv from "dotenv";
import { initializeApp } from "firebase-admin/app";
import { onRequest } from "firebase-functions/v2/https";
import mysql from 'mysql2/promise';
import { AuthTypes, Connector } from '@google-cloud/cloud-sql-connector';

dotenv.config({ path: "../.env" });

// Initialize the Firebase Admin SDK
initializeApp();

const connector = new Connector();
const clientOpts = await connector.getOptions({
  instanceConnectionName: 'fuelkdev:europe-north1:dev',
  authType: AuthTypes.IAM,
});
const pool = await mysql.createPool({
  ...clientOpts,
  user: "functionsServiceAccount",
  // user: process.env.CLOUD_SQL_USER,
  // password: process.env.CLOUD_SQL_PASSWORD,
  database: process.env.CLOUD_SQL_DATABASE,
});

export const helloWorld = onRequest(async (request, response) => {
  response.send("Hello from Firebase!");

  const conn = await pool.getConnection();
  const [result] = await conn.query(`SELECT NOW();`);
  console.log(123);
  console.table(result); // prints returned time value from server
});

@jackwotherspoon
Copy link
Collaborator

Thanks for the clarification @fuelkoy 😄 We will look into reproducing this and report back

@jackwotherspoon jackwotherspoon added the priority: p2 Moderately-important priority. Fix may not be included in next release. label Mar 12, 2024
@ttosta-google
Copy link
Contributor

Hello @fuelkoy

Your deployment may be failing because the Runtime Service Account used by Cloud Run (which Cloud Functions gen2 uses under the hood - documentation) does not match the service account you are setting as the user for IAM database AuthN

To resolve this, follow these steps to set your Service Account as the Runtime SA:

  1. Make sure your service account has the following roles:

Service Account Token Creator
Firebase App Distribution Admin
Cloud SQL Instance User
Cloud SQL Client

  1. Make sure GOOGLE_APPLICATION_CREDENTIALS point to a valid path for SA key JSON file (it's only required for local development)
  2. Add the following to index.ts file:
import { setGlobalOptions } from 'firebase-functions/v2';

setGlobalOptions({serviceAccount: "functionsServiceAccount@<YOUR PROJECT>.iam.gserviceaccount.com"});

Please let us know if you have any questions.

@fuelkoy
Copy link
Author

fuelkoy commented Mar 14, 2024

@ttosta-google Good morning and thank you for the further instructions.

In short I would suggest the reason it is not working is because of GOOGLE_APPLICATION_CREDENTIALS (my findings to suggest this are found below). I found a non viable work around which I will shortly describe:

  1. Try to deploy GOOGLE_APPLICATION_CREDENTIALS set in .env (only way to get deploy to pass through at the moment)
  2. Deploy will fail because it tries to find GOOGLE_APPLICATION_CREDENTIALS on the Google server from that location which exists only locally
  3. Go to Cloud Functions in console.cloud.google.com
  4. Choose helloWorld
  5. Edit
  6. Next
  7. Manually remove GOOGLE_APPLICATION_CREDENTIALS from the .env file
  8. Deploy
  9. Deploy successful.

So based on this test the problem is purely with GOOGLE_APPLICATION_CREDENTIALS, not finding it on deploy but still requiring it of course, and then failing becauses of its existense in the Google server side.

There seems to be 2 problems.

  1. I cannot use IAM Cloud SQL login from local testing
  2. I cannot deploy the function in a viable way

So the problem still remains. This is was my road testing and applying your inscructions.
I followed your instrucions, double checked env variables are correct and GOOGLE_APPLICATION_CREDENTIALS points to a valid path, and updating the values based on your instructions and ran the following getting the following logs:

gcloud auth application-default login

Your browser has been opened to visit:

    https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A8085%2F&scope=openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fsqlservice.login&state=DpX0ikvNRRtx7YuGlJiFuKY7SnmTtC&access_type=offline&code_challenge=mVHDORh3Aq1SjH87J03TEo_Sy7DsjLS0xCbg7aCVqik&code_challenge_method=S256


Credentials saved to file: [C:\Users\Kehitys\AppData\Roaming\gcloud\application_default_credentials.json]

These credentials will be used by any library that requests Application Default Credentials (ADC).

Quota project "myproject" was added to ADC which can be used by Google client libraries for billing and quota. Note that some services may still bill the project owning the resource.

Then I set the env varibale in the terminal as follows:
$env:GOOGLE_APPLICATION_CREDENTIALS="C:\Users\Kehitys\AppData\Roaming\gcloud\application_default_credentials.json"

Then I tested locally to use the function:

pnpm dev

> functions@ dev C:\Users\Kehitys\Documents\GitHub\nodeJsSqlConnector\functions
> run-p dev:*


> functions@ dev:esbuild C:\Users\Kehitys\Documents\GitHub\nodeJsSqlConnector\functions
> pnpm run build --watch


> functions@ dev:tsc C:\Users\Kehitys\Documents\GitHub\nodeJsSqlConnector\functions
> tsc --watch --preserveWatchOutput


> functions@ dev:node C:\Users\Kehitys\Documents\GitHub\nodeJsSqlConnector\functions
> firebase emulators:start --only functions

[9.00.07] Starting compilation in watch mode...


> functions@ build C:\Users\Kehitys\Documents\GitHub\nodeJsSqlConnector\functions
> esbuild src/index.ts --bundle --platform=node --outfile=lib/index.js --format=esm --external:./node_modules/* "--watch"

i  emulators: Starting emulators: functions
!  functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: auth, firestore, database, hosting, pubsub, storage
!  functions: Your GOOGLE_APPLICATION_CREDENTIALS environment variable points to C:/Users/Kehitys/AppData/Roaming/gcloud/application_default_credentials.json. Non-emulated services will access production using these credentials. Be careful!
i  ui: Emulator UI logging to ui-debug.log
i  functions: Watching "C:\Users\Kehitys\Documents\GitHub\nodeJsSqlConnector\functions" for Cloud Functions...
+  functions: Using node@20 from host.
i  functions: Loaded environment variables from .env.
Serving at port 8160

+  functions: Loaded functions definitions from source: helloWorld.
+  functions[europe-north1-helloWorld]: http function initialized (http://127.0.0.1:5001/myproject/europe-north1/helloWorld).

┌─────────────────────────────────────────────────────────────┐
│ ✔  All emulators ready! It is now safe to connect your app. │
│ i  View Emulator UI at http://127.0.0.1:4000/               │
└─────────────────────────────────────────────────────────────┘

┌───────────┬────────────────┬─────────────────────────────────┐
│ Emulator  │ Host:Port      │ View in Emulator UI             │
├───────────┼────────────────┼─────────────────────────────────┤
│ Functions │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │
└───────────┴────────────────┴─────────────────────────────────┘
  Emulator Hub running at 127.0.0.1:4400
  Other reserved ports: 4500

Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.

i  functions: Loaded environment variables from .env.
!  Google API requested!
   - URL: "https://oauth2.googleapis.com/token"
   - Be careful, this may be a production service.
!  Google API requested!
   - URL: "https://sqladmin.googleapis.com/sql/v1beta4/projects/myproject/instances/dev/connectSettings"
   - Be careful, this may be a production service.
!  Google API requested!
   - URL: "https://sqladmin.googleapis.com/sql/v1beta4/projects/myproject/instances/dev:generateEphemeralCert"
   - Be careful, this may be a production service.
i  functions: Beginning execution of "europe-north1-helloWorld"
!  functions: Error: Access denied for user 'functionsServiceAccount'@'cloudsqlproxy~87.95.128.29' (using password: YES)
    at Packet.asError (C:\Users\Kehitys\Documents\GitHub\nodeJsSqlConnector\functions\node_modules\.pnpm\mysql2@3.9.2\node_modules\mysql2\lib\packets\packet.js:728:17)
    at ClientHandshake.execute (C:\Users\Kehitys\Documents\GitHub\nodeJsSqlConnector\functions\node_modules\.pnpm\mysql2@3.9.2\node_modules\mysql2\lib\commands\command.js:29:26)
    at PoolConnection.handlePacket (C:\Users\Kehitys\Documents\GitHub\nodeJsSqlConnector\functions\node_modules\.pnpm\mysql2@3.9.2\node_modules\mysql2\lib\connection.js:481:34)
    at PacketParser.onPacket (C:\Users\Kehitys\Documents\GitHub\nodeJsSqlConnector\functions\node_modules\.pnpm\mysql2@3.9.2\node_modules\mysql2\lib\connection.js:97:12)
    at PacketParser.executePayload (C:\Users\Kehitys\Documents\GitHub\nodeJsSqlConnector\functions\node_modules\.pnpm\mysql2@3.9.2\node_modules\mysql2\lib\packet_parser.js:139:14)
    at TLSSocket.<anonymous> (C:\Users\Kehitys\Documents\GitHub\nodeJsSqlConnector\functions\node_modules\.pnpm\mysql2@3.9.2\node_modules\mysql2\lib\connection.js:104:25)
    at TLSSocket.emit (node:events:514:28)
    at addChunk (node:internal/streams/readable:545:12)
    at readableAddChunkPushByteMode (node:internal/streams/readable:495:3)
    at Readable.push (node:internal/streams/readable:375:5)
!  Your function was killed because it raised an unhandled error.
i  functions: Finished "europe-north1-helloWorld" in 361.2223ms

For this i searched the logs further in Logs Explorer to find the following:
2024-03-14T07:00:29.444687Z 199989 [Note] [MY-000000] [Server] CloudSQL Instance's IAM access denied for user functionsServiceAccount@myproject.iam.gserviceaccount.com: Error code :PERMISSION_DENIED Error Message :The database username "functionsServiceAccount@myproject.iam.gserviceaccount.com" provided by the client does not match the authenticated user's email ID "owner@gmail.com" that was likely provided to the Cloud SQL proxy or Connector.

Then I ran the following with results following:

pnpm run deploy

> functions@ deploy C:\Users\Kehitys\Documents\GitHub\nodeJsSqlConnector\functions
> firebase deploy --only functions


=== Deploying to 'myproject'...

i  deploying functions
Running command: npm --prefix "$RESOURCE_DIR" run build

> build
> esbuild src/index.ts --bundle --platform=node --outfile=lib/index.js --format=esm --external:./node_modules/*


  lib\index.js  1.3kb

Done in 21ms
+  functions: Finished running predeploy script.
i  functions: preparing codebase default for deployment
i  functions: ensuring required API cloudfunctions.googleapis.com is enabled...
i  functions: ensuring required API cloudbuild.googleapis.com is enabled...
i  artifactregistry: ensuring required API artifactregistry.googleapis.com is enabled...
+  functions: required API cloudbuild.googleapis.com is enabled
+  artifactregistry: required API artifactregistry.googleapis.com is enabled
+  functions: required API cloudfunctions.googleapis.com is enabled
i  functions: Loading and analyzing source code for codebase default to determine what to deploy
Serving at port 8913

Error: Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.
    at GoogleAuth.getApplicationDefaultAsync (C:\Users\Kehitys\Documents\GitHub\nodeJsSqlConnector\functions\node_modules\.pnpm\google-auth-library@9.6.3\node_modules\google-auth-library\build\src\auth\googleauth.js:271:15)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async GoogleAuth.getClient (C:\Users\Kehitys\Documents\GitHub\nodeJsSqlConnector\functions\node_modules\.pnpm\google-auth-library@9.6.3\node_modules\google-auth-library\build\src\auth\googleauth.js:695:17)       
    at async GoogleAuth.request (C:\Users\Kehitys\Documents\GitHub\nodeJsSqlConnector\functions\node_modules\.pnpm\google-auth-library@9.6.3\node_modules\google-auth-library\build\src\auth\googleauth.js:748:24)
    at async SQLAdminFetcher.getInstanceMetadata (file:///C:/Users/Kehitys/Documents/GitHub/nodeJsSqlConnector/functions/node_modules/.pnpm/@google-cloud+cloud-sql-connector@1.2.3/node_modules/@google-cloud/cloud-sql-connector/dist/mjs/sqladmin-fetcher.js:88:21)
    at async CloudSQLInstance.performRefresh (file:///C:/Users/Kehitys/Documents/GitHub/nodeJsSqlConnector/functions/node_modules/.pnpm/@google-cloud+cloud-sql-connector@1.2.3/node_modules/@google-cloud/cloud-sql-connector/dist/mjs/cloud-sql-instance.js:117:26)
    at async CloudSQLInstance.getCloudSQLInstance (file:///C:/Users/Kehitys/Documents/GitHub/nodeJsSqlConnector/functions/node_modules/.pnpm/@google-cloud+cloud-sql-connector@1.2.3/node_modules/@google-cloud/cloud-sql-connector/dist/mjs/cloud-sql-instance.js:21:9)
    at async CloudSQLInstanceMap.loadInstance (file:///C:/Users/Kehitys/Documents/GitHub/nodeJsSqlConnector/functions/node_modules/.pnpm/@google-cloud+cloud-sql-connector@1.2.3/node_modules/@google-cloud/cloud-sql-connector/dist/mjs/connector.js:46:36)
    at async Connector.getOptions (file:///C:/Users/Kehitys/Documents/GitHub/nodeJsSqlConnector/functions/node_modules/.pnpm/@google-cloud+cloud-sql-connector@1.2.3/node_modules/@google-cloud/cloud-sql-connector/dist/mjs/connector.js:106:9)
    at async file:///C:/Users/Kehitys/Documents/GitHub/nodeJsSqlConnector/functions/lib/index.js:18:18


Error: Functions codebase could not be analyzed successfully. It may have a syntax or runtime error

I don't know the reason but I can make the following notes from the above

  1. Having set the service account and using it locally for some reason the access is denied. It seems based on Logs Explorer logs that it waits that login with (as I understand) gcloud auth application-default login has been made with the same email. As of my understanding login in with owner should not cause such a problem but I'm not sure if I'm interpreting the source of this problem correctly.
  2. Deploying fails due not finding the GOOGLE_APPLICATION_CREDENTIALS. If I set it to env through terminal and not in .env file starting the deploy it seems to find it because it prints:
i  emulators: Starting emulators: functions
!  functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: auth, firestore, database, hosting, pubsub, storage
!  functions: Your GOOGLE_APPLICATION_CREDENTIALS environment variable points to C:/Users/Kehitys/AppData/Roaming/gcloud/application_default_credentials.json. Non-emulated services will access production using these credentials. Be careful!

but then the deploy is still failing because it cannot find it Error: Could not load the default credentials.. But if I set it the deploy will proceed but because it is set it will fail later because it is set. So if not setting there is a problem but if setting it there is a problem because of setting it.

  1. Just a note that I'm not sure if IAM db connection would work when deployed if it does not work locally. But this I cannot prove as I haven't yet been able to proceed that far.

Thank you for help thus far and looking into this. Our company is a pretty happy customer of Google's products and connection Firebase Functions (v2) with Cloud SQL is really important to us. So we appreciate your help greatly.
Of course for us this is a big problem as this makes using Cloud SQL with cloud-slq-dodejs-connector non working combination for us. And as this is the suggested way to use Cloud Functions/Run with Cloud SQL this is even bigger problem. But we believe you can help us to solve this and to use these services together

(Note: I have updated my repo to contain the latest changes even tho they are not many)

@fuelkoy
Copy link
Author

fuelkoy commented Mar 18, 2024

@ttosta-google Good Monday and the start of the week!

I have tested the local Automatic IAM Authentication little bit further now using the project owner account and the Default compute service account. In both cases, the message, in short, is the same: 'Error:: Access denied for user 'placeholder'@'%' to database 'my-db'. I also added the roles Service Account Token Creator Firebase App Distribution Admin Cloud SQL Instance User Cloud SQL Client just in case for them both, even though I believe they already have them.
When using the owner, Log Explorer logs the following:

severity: "INFO"
textPayload: "2024-03-18T06:22:29.424872Z 279192 [Note] [MY-010914] [Server] Access denied for user 'placeholder'@'%' to database 'my-db'"
severity: "WARNING"
textPayload: "2024-03-18T06:22:29.424918Z 279192 [Warning] [MY-000000] [Server] Failed to connect to mysql: 279192. Server-side error number: 1044"

When using the Default compute service account, Log Explorer logs the following:

severity: "INFO"
textPayload: "2024-03-18T06:23:44.527370Z 279210 [Note] [MY-010926] [Server] Access denied for user 'placeholder-compute'@'cloudsqlproxy~87.95.128.29' (using password: YES)"
severity: "INFO"
textPayload: "2024-03-18T06:23:44.526488Z 279210 [Note] [MY-000000] [Server] CloudSQL Instance's IAM access denied for user placeholder-compute@developer.gserviceaccount.com: Error code :PERMISSION_DENIED Error Message :The database username "placeholder-compute@developer.gserviceaccount.com" provided by the client does not match the authenticated user's email ID "placeholder@placeholder.com" that was likely provided to the Cloud SQL proxy or Connector."

I don't know what I'm doing wrong, as even these accounts can't connect to the database using IAM authentication. Your help is greatly appreciated. 
Thank you

EDIT

I could also even use the generated DB root user (all though this might not be the best practice), but I cannot proceed with the deployment with it because of the reasons mentioned before relating to GOOGLE_APPLICATION_CREDENTIALS. 

@fuelkoy
Copy link
Author

fuelkoy commented Mar 19, 2024

@ttosta-google @jackwotherspoon I got IAM authentication working if I use GOOGLE_APPLICATION_CREDENTIALS in -env. Also I fixed typos in env file which prevented the owner account to work right away.

I needed to gcloud auth with impersonate service account so that the Cloud SQL proxy or Connector is provided with the same account than the function.
gcloud auth application-default login --impersonate-service-account=functionsServiceAccount@myproject.iam.gserviceaccount.com
, grant the service account privileges to read all tables and add IAM Role Service Usage Consumer.
After this it works correctly locally.

But if I don't set the value in .env it fails right away both when starting the emulator and when trying to deploy.
I have tried to set then env value through different methods (powershell, windows environment variables, cross-env). For all of those there is the following print, but still console.log(GOOGLE_APPLICATION_CREDENTIALS ) logs undefined and thus the following fail: Error:

Could not load the default credentials.
i  emulators: Starting emulators: functions
!  ui: Emulator UI unable to start on port 4000, starting on 4001 instead.
!  functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: auth, firestore, database, hosting, pubsub, storage
!  functions: Your GOOGLE_APPLICATION_CREDENTIALS environment variable points to C:/Users/Kehitys/AppData/Roaming/gcloud/application_default_credentials.json. Non-emulated services will access production using these credentials. Be careful!

Can you guide me further how to set up ADC correctly for local testing and for deployment please?

@jackwotherspoon
Copy link
Collaborator

Hi @fuelkoy! Thanks for the detailed steps and explanations 😄 I believe I understand the issue so let's see if I can help clear up the confusion.

There is one underlying detail that may need to be more clearly surfaced in our documentation for IAM database authentication. I will happily update our documentation to make it more clear.

  • At any given time ONLY the IAM principal belonging to the Application Default Credentials (ADC) can successfully login with IAM DB authentication. The IAM user or service account principal who owns the ADC creds MUST match that of the IAM database user or service account being passed to the database as the user argument.

The above statement is the reason for the issues you are seeing and hopefully it helps make sense of the error messages you are seeing.

@fuelkoy
Copy link
Author

fuelkoy commented Mar 19, 2024

Hello @jackwotherspoon. Thank you for the explanation and now I have proceeded to get things working if using the value in .env file.

Can you help me how to set the GOOGLE_APPLICATION_CREDENTIALS so that it wont be deployed and this making a successful deploy. At the moment I don't know how we can do a deployment

@fuelkoy
Copy link
Author

fuelkoy commented Mar 19, 2024

To test the presence of GOOGLE_APPLICATION_CREDENTIALS I simply added console.log above the connector initialization:

console.log(process.env.GOOGLE_APPLICATION_CREDENTIALS);
const connector = new Connector();
const clientOpts = await connector.getOptions({
  instanceConnectionName: process.env.CLOUD_INSTANCE_CONNECTION_NAME as string,
  authType: AuthTypes.IAM,
});

@jackwotherspoon
Copy link
Collaborator

Hi @fuelkoy,

You definitely should NOT have to set GOOGLE_APPLICATION_CREDENTIALS in your .env for a successful deployment and to have things run smoothly. This definitely seems odd and not the proper behavior. I wonder if this is a firebase functions Windows issue or something to do with the Firebase configuration. As @ttosta-google was able to deploy the app no problem on Ubuntu.

Now that we have got to the bottom of the mismatched IAM users it seems the deployment issue is unrelated to the codebase of the Cloud SQL Node Connector. I wonder if filing an issue on https://github.com/firebase/firebase-tools/issues may be able to shed some more light into if this is a Firebase configuration issue.

TLDR from our side;

For locally setting the ADC creds:

gcloud auth application-default login --impersonate-service-account=functionsServiceAccount@myproject.iam.gserviceaccount.com

For Cloud Functions gen2: The underlying Runtime Service Account will be used as the ADC credentials and can be set in your index.ts file:

import { setGlobalOptions } from 'firebase-functions/v2';

setGlobalOptions({serviceAccount: "functionsServiceAccount@myproject.iam.gserviceaccount.com"});

@fuelkoy
Copy link
Author

fuelkoy commented Mar 20, 2024

@jackwotherspoon Good morning.

Thank you for your answer, and before reading your answer, I was going to write here the very same thing. 1 question regarding my solution defined later in this message: if I define connector and pool globally, is it important to call  await pool.end();  connector.close();. There was no problem when I did not call those when testing Cloud SQL.

Indeed, when starting the emulator, firebase functions populate the ADC by itself to the location: C:\Users\Kehitys\AppData\Roaming\firebase\ownerEmail_application_default_credentials.json. But on deployment, they are empty, and when a connector is made globally, it cannot find GOOGLE_APPLICATION_CREDENTIALS, even though it clearly is located at C:\Users\Kehitys\AppData\Roaming\gcloud\application_default_credentials.json.

I noticed that if I define a connector inside the function, then there is no problem. And the deployment was also successful. I will file an issue with Firebase Tools and mention this issue there so that people can find a solution if they stumble on the same thing.

One thing to note that could be changed in the Cloud SQL Node Connector documentation. If I define connector in global scope, it will auto-fail in Firebase functions because, as a comment in a relevant issue points out:

.env variables are only accessible from inside the function! (not outside, meaning inside onRequest( request, response) => {...} ). That's it.
 
As the document says, "Once your custom environment variables are deployed, your function code can access them."

 firebase/firebase-tools#6499 (comment)
This should probably be pointed out to the users using the Cloud SQL Node Connector inside Firebase functions, as it can help save a lot of time. So the only way to use the Cloud SQL Node Connector inside Firebase functions is to define a connector inside an async function and then get the pool and connector. But I consider this also a non-viable solution due to the performance penalty, as this will increase the warm function duration from ~10 ms to ~520–950 ms (500–930 ms coming just from initializing the connection inside the function). So the following is the code I got the function working with:.

import "dotenv/config";
import * as dotenv from "dotenv";
import { initializeApp } from "firebase-admin/app";
import { onRequest } from "firebase-functions/v2/https";
import mysql from 'mysql2/promise';
import { AuthTypes, Connector } from '@google-cloud/cloud-sql-connector';
import { setGlobalOptions } from "firebase-functions/v2";

dotenv.config({ path: "../.env" });

// Initialize the Firebase Admin SDK
initializeApp();

// Set global options for all functions
setGlobalOptions({
	region: "europe-north1",
	serviceAccount: process.env.CLOUD_SERVICE_ACCOUNT,
});


const createPool = async() => {
  
console.log(process.env.GOOGLE_APPLICATION_CREDENTIALS);
const connector = new Connector();
const clientOpts = await connector.getOptions({
  instanceConnectionName: process.env.CLOUD_INSTANCE_CONNECTION_NAME as string,
  authType: AuthTypes.IAM,
});

const pool = mysql.createPool({
  ...clientOpts,
  user: "functionsServiceAccount",
  // user: process.env.CLOUD_SQL_USER,
  // password: process.env.CLOUD_SQL_PASSWORD,
  database: process.env.CLOUD_SQL_DATABASE,
});

  return { connector, pool  };
}


export const helloWorld = onRequest(async (request, response) => {
  console.log("⏳ Running...");
  const start = Date.now();
  const { connector, pool } = await createPool();
  const conn = await pool.getConnection();
  
  const end1 = Date.now();
  console.log(`✅ Connection made in ${end1 - start}ms`);
  
  const [result] = await conn.query(`SELECT NOW();`);
  console.table(result); // prints returned time value from server

  response.send("Hello from Firebase v2!");

  const end = Date.now();
  console.log(`✅ Function call completed in ${end - start}ms`);

  await pool.end();
  connector.close();
});

And the following one that works in emulator start when connector is defined in global scope (the 10 ms one)

import "dotenv/config";
import * as dotenv from "dotenv";
import { initializeApp } from "firebase-admin/app";
import { onRequest } from "firebase-functions/v2/https";
import mysql from 'mysql2/promise';
import { Connector } from '@google-cloud/cloud-sql-connector';
import { setGlobalOptions } from "firebase-functions/v2";

dotenv.config({ path: "../.env" });

// Initialize the Firebase Admin SDK
initializeApp();

// Set global options for all functions
setGlobalOptions({
	region: "europe-north1",
});


console.log(process.env.GOOGLE_APPLICATION_CREDENTIALS);
const connector = new Connector();
const clientOpts = await connector.getOptions({
  instanceConnectionName: process.env.CLOUD_INSTANCE_CONNECTION_NAME as string,
  // authType: AuthTypes.IAM,
});

const pool = mysql.createPool({
  ...clientOpts,
  user: process.env.CLOUD_SQL_USER,
  password: process.env.CLOUD_SQL_PASSWORD,
  database: process.env.CLOUD_SQL_DATABASE,
});
const conn = await pool.getConnection();

export const helloWorld = onRequest(async (request, response) => {
  console.log("⏳ Running...");
  const start = Date.now();

  const [result] = await conn.query(`SELECT NOW();`);
  console.table(result); // prints returned time value from server

  response.send("Hello from Firebase v2!");

  const end = Date.now();
  console.log(`✅ Function call completed in ${end - start}ms`);
});

I will now close this issue.

@jackwotherspoon
Copy link
Collaborator

jackwotherspoon commented Mar 20, 2024

Hi @fuelkoy thanks again for the detailed solution! Glad you were able to get a working solution.

One thing to note that could be changed in the Cloud SQL Node Connector documentation. If I define connector in global scope, it will auto-fail in Firebase functions because, as a comment in a relevant issue points out:

.env variables are only accessible from inside the function! (not outside, meaning inside onRequest( request, response) => > {...} ). That's it.

As the document says, "Once your custom environment variables are deployed, your function code can access them.

TIL, I was unaware of this detail of Firebase functions. Thanks for pointing it out. We can definitely look at adding something to our README for future users to find (feel free to send a PR if you would like to contribute the note, otherwise I happily will)

So the only way to use the Cloud SQL Node Connector inside Firebase functions is to define a connector inside an async function and then get the pool and connector. But I consider this also a non-viable solution due to the performance penalty, as this will increase the warm function duration from ~10 ms to ~520–950 ms (500–930 ms coming just from initializing the connection inside the function.

One thing you could try to improve performance would be to lazy init a global pool and connector. This way it is initialized during the first request when you have access to the credentials but then saved for subsequent requests. This is actually mentioned as a tip for Cloud Functions. The only caveat being that you would probably also want to turn on "Always on CPU" so that the Cloud SQL Node Connector's background refreshes have access to CPU and are not throttled (we will be implementing a lazy refresh feature soon so that this is no longer required #285)

@fuelkoy
Copy link
Author

fuelkoy commented Mar 21, 2024

Thank you very much for the follow-up. I did not know about this. Indeed, lazy init a global pool and connector works. Thank you very much for pointing this out, as this seems to tackle the performance penalty, which is really good. I will edit and close my issue in firebase-tools, pointing to the lazy init as it resolves this thing.
Thank you again, and can you update the README, please?

@fuelkoy
Copy link
Author

fuelkoy commented Mar 21, 2024

@jackwotherspoon Just asking, when will you be implementing lazy certificate refresh (#285)? If I understood correctly from Cloud Run pricing always allocating CPU will cost ~47e per function. 

I will at least bump that issue or request, as adding ~47 per function running multiple of them would be quite expensive for us 😅. Thank you again for your help. You have been really helpful.

@jackwotherspoon
Copy link
Collaborator

Hi @fuelkoy, glad to have helped you with the lazy init 😄

Just asking, when will you be implementing lazy certificate refresh (#285)?

We realize that the "Always on CPU" can make the application a bit more expensive and because of this the lazy refresh functionality is a top priority for our team. We are currently working out the design still for the feature as it slightly complex and our team needs to make sure that it will work across all our Cloud SQL Connector libraries. We are hoping to get to the implementation phase sometime in the next couple of months, we will update the feature request issue with any progress updates to keep you and others in the loop.

Thanks again for the great feedback and have an amazing rest of your week 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
priority: p2 Moderately-important priority. Fix may not be included in next release. type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns.
Projects
None yet
Development

No branches or pull requests

3 participants