Skip to content

new IaC export command supporting functions.yaml and first TF steps#10115

Draft
inlined wants to merge 16 commits intomainfrom
inlined.cf3-terraform-building-blocks
Draft

new IaC export command supporting functions.yaml and first TF steps#10115
inlined wants to merge 16 commits intomainfrom
inlined.cf3-terraform-building-blocks

Conversation

@inlined
Copy link
Member

@inlined inlined commented Mar 17, 2026

Edit

This is long enough I'm going to convert to a draft and try to make a series of smaller PRs that compose to this.

Broken into

Description

Working to support exporting the IaC of a functions codebase, which allows you to actually do deployment in any other engine.

Scenarios Tested

So far, just had Gemini validate, but I need some eyes on it before the CL gets too big. So far we just handle exporting a v1 function and IAM bindings if it has no params. Variables are hard coded and sometimes faked. Next up are changes to the functions & tools packages to support a halfway export of a build.Build where a Field is a tf.Value instead and we track the usage of built-ins so we don't need to declare them all.

Sample Commands

From the following code:

import * as functions from 'firebase-functions/v1';
import * as admin from 'firebase-admin';

admin.initializeApp();

// HTTPS function
export const helloWorld = functions
  .region("us-central1", "europe-west1")
  .runWith({ "invoker": "secretSA@" })
  .https.onRequest((request, response) => {
    functions.logger.info("Hello logs!", { structuredData: true });
    response.send("Hello from Firebase v1!");
  });

// Callable function with memory and timeout configuration
export const addMessage = functions
  .runWith({ memory: '512MB', timeoutSeconds: 60 })
  .https.onCall((data, context) => {
    // Ensuring the user is authenticated.
    if (!context.auth) {
      throw new functions.https.HttpsError(
        'unauthenticated',
        'The function must be called while authenticated.'
      );
    }
    const text = data.text;
    if (!(typeof text === 'string') || text.length === 0) {
      throw new functions.https.HttpsError(
        'invalid-argument',
        'The function must be called with one arguments "text" containing the message text to add.'
      );
    }

    // Returning the text for testing
    return {
      text: text,
      timestamp: Date.now()
    };
  });

// Firestore event function using a wildcard
export const makeUppercase = functions.firestore.document('/messages/{documentId}')
  .onCreate((snap, context) => {
    const original = snap.data().original;
    functions.logger.log('Uppercasing', context.params.documentId, original);
    const uppercase = original.toUpperCase();
    return snap.ref.set({ uppercase }, { merge: true });
  });

// Auth event function
export const sendWelcomeEmail = functions.runWith({
  vpcConnector: "myConnector",
  vpcConnectorEgressSettings: "ALL_TRAFFIC",
  labels: {
    "my-label": "my-value"
  },
  minInstances: 1,
  maxInstances: 10,
  serviceAccount: "my-service-account@",
}).auth.user().onCreate((user) => {
  functions.logger.info('A new user signed in for the first time.', user.email);
  return null;
});

// Realtime Database event function
export const sanitizeText = functions.database.ref('/messages/{pushId}/text')
  .onWrite((change, context) => {
    if (!change.after.exists()) {
      return null;
    }

    const original = change.after.val();
    const sanitized = original.replace(/badword/g, '********');

    if (original === sanitized) {
      return null;
    }

    return change.after.ref.set(sanitized);
  });

Created the following TF files:

Manifest file: variables.tf
variable "project"  {
  description = "The ID of the project to deploy to."
}

variable "location"  {
  description = "The location to deploy to. Default us-central1 (deprecated)"
  default = "us-central1"
}

variable "gcf_bucket"  {
  description = "The name of the bucket to deploy to."
}

variable "gcf_archive"  {
  description = "The name of the archive to deploy to."
}

variable "extension_id"  {
  description = "The extension ID. Used for reverse compatibility when extensions ahve been ported. Injects an env var and adds a function name prefix"
  default = null
}
Manifest file: main.tf
locals  {
  firebaseConfig = {
    authDomain = "${var.project}.firebaseapp.com"
    databaseURL = "https://REALTIME_DATABASE_URLS_ARE_HARD_TO_INJECT.firebaseio.com"
    storageBucket = "${var.project}.appspot.com"
  }
}

resource "google_cloudfunctions_function" "add_message"  {
  name = var.extension_id == null ? "addMessage" : "ext-${var.extension_id}-addMessage"
  runtime = "nodejs22"
  project = var.project
  source_archive_bucket = var.gcf_bucket
  source_archive_object = var.gcf_archive
  docker_repository = "ARTIFACT_REGISTRY"
  region = "us-central1"
  entry_point = "addMessage"
  available_memory_mb = 512
  timeout = 60
  labels = {
    deployment-callable = "true"
  }
  https_trigger_security_level = "SECURE_ALWAYS"
  trigger_http = true
}

resource "google_cloudfunctions_function" "hello_world"  {
  name = var.extension_id == null ? "helloWorld" : "ext-${var.extension_id}-helloWorld"
  runtime = "nodejs22"
  project = var.project
  source_archive_bucket = var.gcf_bucket
  source_archive_object = var.gcf_archive
  docker_repository = "ARTIFACT_REGISTRY"
  for_each = toset(["us-central1","europe-west1"])
  region = each.value
  entry_point = "helloWorld"
  labels = {

  }
  https_trigger_security_level = "SECURE_ALWAYS"
  trigger_http = true
}

resource "google_cloudfunctions_function" "make_uppercase"  {
  name = var.extension_id == null ? "makeUppercase" : "ext-${var.extension_id}-makeUppercase"
  runtime = "nodejs22"
  project = var.project
  source_archive_bucket = var.gcf_bucket
  source_archive_object = var.gcf_archive
  docker_repository = "ARTIFACT_REGISTRY"
  region = "us-central1"
  entry_point = "makeUppercase"
  labels = {

  }
  event_trigger = {
    event_type = "providers/cloud.firestore/eventTypes/document.create"
    resource = "projects/${var.project}/databases/(default)/documents/messages/{documentId}"
  }
}

resource "google_cloudfunctions_function" "sanitize_text"  {
  name = var.extension_id == null ? "sanitizeText" : "ext-${var.extension_id}-sanitizeText"
  runtime = "nodejs22"
  project = var.project
  source_archive_bucket = var.gcf_bucket
  source_archive_object = var.gcf_archive
  docker_repository = "ARTIFACT_REGISTRY"
  region = "us-central1"
  entry_point = "sanitizeText"
  labels = {

  }
  event_trigger = {
    event_type = "providers/google.firebase.database/eventTypes/ref.write"
    resource = "projects/_/instances/REALTIME_DATABASE_URLS_ARE_HARD_TO_INJECT/refs/messages/{pushId}/text"
  }
}

resource "google_cloudfunctions_function" "send_welcome_email"  {
  name = var.extension_id == null ? "sendWelcomeEmail" : "ext-${var.extension_id}-sendWelcomeEmail"
  runtime = "nodejs22"
  project = var.project
  source_archive_bucket = var.gcf_bucket
  source_archive_object = var.gcf_archive
  docker_repository = "ARTIFACT_REGISTRY"
  region = "us-central1"
  entry_point = "sendWelcomeEmail"
  min_instances = 1
  max_instances = 10
  service_account_email = "my-service-account@${var.project}.iam.gserviceaccount.com"
  vpc_connector = "projects/${var.project}/locations/us-central1/connectors/myConnector"
  vpc_connector_egress_settings = "ALL_TRAFFIC"
  labels = {
    my-label = "my-value"
  }
  event_trigger = {
    event_type = "providers/firebase.auth/eventTypes/user.create"
    resource = "projects/${var.project}"
  }
}

resource "google_cloudfunctions_function_iam_binding" "add_message"  {
  cloud_function = google_cloudfunctions_function.add_message.name
  region = google_cloudfunctions_function.add_message.region
  project = google_cloudfunctions_function.add_message.project
  role = "roles/cloudfunctions.invoker"
  members = []
}

resource "google_cloudfunctions_function_iam_binding" "hello_world"  {
  for_each = google_cloudfunctions_function.hello_world
  cloud_function = google_cloudfunctions_function.hello_world[each.key].name
  region = google_cloudfunctions_function.hello_world[each.key].region
  project = google_cloudfunctions_function.hello_world[each.key].project
  role = "roles/cloudfunctions.invoker"
  members = ["serviceAccount:secretSA@${var.project}.iam.gserviceaccount.com"]
}


@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces foundational capabilities for exporting Firebase Cloud Functions as Infrastructure as Code. The primary goal is to enable users to manage and deploy their functions using external IaC tools like Terraform, thereby improving version control, automation, and integration with existing infrastructure workflows. This initial implementation focuses on supporting GCFv1 functions and provides a command-line interface to generate the necessary configuration files.

Highlights

  • New functions:export Command: Introduced a new command firebase functions:export to allow users to export their Cloud Functions configuration as Infrastructure as Code (IaC).
  • IaC Export Formats: The new command supports exporting functions in two formats: functions.yaml (an internal representation) and initial Terraform (variables.tf, main.tf) configurations for GCFv1 functions.
  • Terraform Generation Logic: Implemented core logic to translate Firebase Cloud Functions (specifically GCFv1) into Terraform google_cloudfunctions_function and google_cloudfunctions_function_iam_binding resources, including handling regions, VPC settings, service accounts, and secret environment variables.
  • Experimental Feature: The functions:export command is currently enabled behind an experimental flag, functionsiac, indicating it's not yet production-ready and requires explicit opt-in.
  • Terraform HCL Utilities: Added a dedicated module for generating HashiCorp Configuration Language (HCL) blocks, expressions, and value serialization, facilitating the creation of Terraform files.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • src/commands/functions-export.ts
    • Added a new command functions:export to orchestrate the export of Cloud Functions configurations.
    • Implemented logic to select the appropriate exporter based on the --format option (internal YAML or Terraform).
    • Included handling for codebase selection and validation of source directory existence.
  • src/commands/index.ts
    • Integrated the new functions:export command into the CLI, guarded by the functionsiac experimental flag.
  • src/experiments.ts
    • Defined a new experimental flag functionsiac with a short description 'Exports functions IaC code'.
  • src/functions/iac/export.spec.ts
    • Added unit tests for the getInternalIac and getTerraformIac functions, verifying correct output for functions.yaml and Terraform files respectively.
  • src/functions/iac/export.ts
    • Created getInternalIac to export function configurations to functions.yaml.
    • Developed getTerraformIac to generate variables.tf and main.tf for GCFv1 functions, including handling of Firebase configuration and environment variables for Terraform interpolation.
    • Introduced STANDARD_TF_VARS for common Terraform variables like project, location, and GCS bucket/archive.
  • src/functions/iac/terraform.spec.ts
    • Added comprehensive unit tests for Terraform utility functions, covering expression creation, field copying/renaming, service account formatting, and value serialization.
  • src/functions/iac/terraform.ts
    • Defined types for Terraform HCL expressions, values, and blocks.
    • Implemented utility functions expr, copyField, renameField, serviceAccount, serializeValue, and blockToString to aid in generating valid HCL syntax.
    • Included logic to handle parameterized fields for project ID in serializeValue.
  • src/gcp/cloudfunctions.spec.ts
    • Updated imports to include build and tf modules.
    • Added tests for terraformFromEndpoint to ensure it correctly generates compute and IAM resources for GCFv1 functions and rejects unsupported platforms/runtimes.
    • Added tests for functionTerraform to verify handling of regions, VPC settings, and secret environment variables.
    • Added tests for invokerTerraform to check correct IAM binding generation for public, private, and custom service account invokers, including for_each for multi-region deployments.
  • src/gcp/cloudfunctions.ts
    • Imported build and tf modules, and assertExhaustive.
    • Added terraformFromEndpoint function to generate Terraform resource blocks for a given Cloud Function endpoint, specifically for GCFv1.
    • Implemented functionTerraform to create a google_cloudfunctions_function resource block, mapping various endpoint properties to Terraform attributes.
    • Implemented invokerTerraform to create a google_cloudfunctions_function_iam_binding resource block for function invokers, supporting public, private, and custom service accounts.
    • Updated captureRuntimeValidationError to use regex.exec for consistency.
  • src/utils.ts
    • Added a new utility function toLowerSnakeCase for converting strings to lower snake case, useful for Terraform attribute names.
Activity
  • The author has performed initial validation of the IaC export functionality using Gemini.
  • The author is seeking early review to gather feedback before the change list grows further.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new functions:export command to generate Infrastructure as Code for Cloud Functions, with initial support for functions.yaml and Terraform. The implementation is a solid first step, adding new commands, experiments, and IaC generation logic. My review focuses on improving type safety by removing any types as per your style guide, fixing a critical bug in Terraform generation for VPC connectors, and addressing a few other issues that could lead to invalid HCL or brittle behavior. Overall, these changes establish a strong foundation for the new export feature.

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
@inlined
Copy link
Member Author

inlined commented Mar 17, 2026

Will look at minor issues in the morning

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants