Secure Cloud Run services tutorial

This tutorial walks through how to create a secure two-service applicationrunning on Cloud Run. This application is aMarkdown editor whichincludes a public "frontend" service which anyone can use to compose markdowntext, and a private "backend" service which renders Markdown text to HTML.

Diagram showing the request flow from the frontend 'editor' to the backend 'renderer'.
The "Renderer" backend is a private service. This allows guaranteeing a text transformation standard across an organization without tracking changes across libraries in multiple languages.

The backend service is private using Cloud Run's built-in,IAM-based service-to-service authenticationfeature, that limits who can call the service. Both services are built with theprinciple of least privilege,with no access to the rest of Google Cloud except where necessary.

Limitations or non-goals of this tutorial

Objectives

  • Create a dedicated service account with minimal permissions for service-to-serviceauthentication and service access to the rest of Google Cloud.
  • Write, build, and deploy two services to Cloud Run which interact.
  • Make requests between a public and private Cloud Run service.

Costs

In this document, you use the following billable components of Google Cloud:

To generate a cost estimate based on your projected usage, use thepricing calculator.

New Google Cloud users might be eligible for afree trial.

Before you begin

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains theresourcemanager.projects.create permission.Learn how to grant roles.
    Note: If you don't plan to keep the resources that you create in this procedure, create a project instead of selecting an existing project. After you finish these steps, you can delete the project, removing all resources associated with the project.

    Go to project selector

  3. Verify that billing is enabled for your Google Cloud project.

  4. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains theresourcemanager.projects.create permission.Learn how to grant roles.
    Note: If you don't plan to keep the resources that you create in this procedure, create a project instead of selecting an existing project. After you finish these steps, you can delete the project, removing all resources associated with the project.

    Go to project selector

  5. Verify that billing is enabled for your Google Cloud project.

  6. Enable the Cloud Run API.

    Roles required to enable APIs

    To enable APIs, you need the Service Usage Admin IAM role (roles/serviceusage.serviceUsageAdmin), which contains theserviceusage.services.enable permission.Learn how to grant roles.

    Enable the API

  7. Install and initialize the gcloud CLI.
  8. Installcurl to try out the service

Required roles

To get the permissions that you need to complete the tutorial, ask your administrator to grant you the following IAM roles on your project:

For more information about granting roles, seeManage access to projects, folders, and organizations.

You might also be able to get the required permissions throughcustom roles or otherpredefined roles.

Note:IAM basic roles might also contain permissions to complete the tutorial. You shouldn't grant basic roles in a production environment, but you can grant them in a development or test environment.

Set up gcloud defaults

To configure gcloud with defaults for your Cloud Run service:

  1. Set your default project:

    gcloudconfigsetprojectPROJECT_ID

    ReplacePROJECT_ID with the name of the project you created forthis tutorial.

  2. Configure gcloud for your chosen region:

    gcloudconfigsetrun/regionREGION

    ReplaceREGION with the supported Cloud Runregionof your choice.

Cloud Run locations

Cloud Run is regional, which means the infrastructure thatruns your Cloud Run services is located in a specific region and ismanaged by Google to be redundantly available acrossall the zones within that region.

Meeting your latency, availability, or durability requirements are primaryfactors for selecting the region where your Cloud Run services are run.You can generally select the region nearest to your users but you should considerthe location of theother Google Cloudproducts that are used by your Cloud Run service.Using Google Cloud products together across multiple locations can affectyour service's latency as well as cost.

Cloud Run is available in the following regions:

Subject toTier 1 pricing

  • asia-east1 (Taiwan)
  • asia-northeast1 (Tokyo)
  • asia-northeast2 (Osaka)
  • asia-south1 (Mumbai, India)
  • asia-southeast3 (Bangkok)
  • europe-north1 (Finland)leaf iconLow CO2
  • europe-north2 (Stockholm)leaf iconLow CO2
  • europe-southwest1 (Madrid)leaf iconLow CO2
  • europe-west1 (Belgium)leaf iconLow CO2
  • europe-west4 (Netherlands)leaf iconLow CO2
  • europe-west8 (Milan)
  • europe-west9 (Paris)leaf iconLow CO2
  • me-west1 (Tel Aviv)
  • northamerica-south1 (Mexico)
  • us-central1 (Iowa)leaf iconLow CO2
  • us-east1 (South Carolina)
  • us-east4 (Northern Virginia)
  • us-east5 (Columbus)
  • us-south1 (Dallas)leaf iconLow CO2
  • us-west1 (Oregon)leaf iconLow CO2

Subject toTier 2 pricing

  • africa-south1 (Johannesburg)
  • asia-east2 (Hong Kong)
  • asia-northeast3 (Seoul, South Korea)
  • asia-southeast1 (Singapore)
  • asia-southeast2 (Jakarta)
  • asia-south2 (Delhi, India)
  • australia-southeast1 (Sydney)
  • australia-southeast2 (Melbourne)
  • europe-central2 (Warsaw, Poland)
  • europe-west10 (Berlin)
  • europe-west12 (Turin)
  • europe-west2 (London, UK)leaf iconLow CO2
  • europe-west3 (Frankfurt, Germany)
  • europe-west6 (Zurich, Switzerland)leaf iconLow CO2
  • me-central1 (Doha)
  • me-central2 (Dammam)
  • northamerica-northeast1 (Montreal)leaf iconLow CO2
  • northamerica-northeast2 (Toronto)leaf iconLow CO2
  • southamerica-east1 (Sao Paulo, Brazil)leaf iconLow CO2
  • southamerica-west1 (Santiago, Chile)leaf iconLow CO2
  • us-west2 (Los Angeles)
  • us-west3 (Salt Lake City)
  • us-west4 (Las Vegas)

If you already created a Cloud Run service, you can view theregion in the Cloud Run dashboard in theGoogle Cloud console.

Retrieve the code sample

To retrieve the code sample for use:

  1. Clone the sample app repository to your Cloud Shell or local machine:

    Node.js

    gitclonehttps://github.com/GoogleCloudPlatform/nodejs-docs-samples.git

    Alternatively, you can download the sample as a zip file and extract it.

    Python

    gitclonehttps://github.com/GoogleCloudPlatform/python-docs-samples.git

    Alternatively, you can download the sample as a zip file and extract it.

    Go

    gitclonehttps://github.com/GoogleCloudPlatform/golang-samples.git

    Alternatively, you can download the sample as a zip file and extract it.

    Java

    gitclonehttps://github.com/GoogleCloudPlatform/java-docs-samples.git

    Alternatively, you can download the sample as a zip file and extract it.

    C#

    gitclonehttps://github.com/GoogleCloudPlatform/dotnet-docs-samples.git

    Alternatively, you can download the sample as a zip file and extract it.

  2. Change to the directory that contains the Cloud Run samplecode:

    Node.js

    cdnodejs-docs-samples/run/markdown-preview/

    Python

    cdpython-docs-samples/run/markdown-preview/

    Go

    cdgolang-samples/run/markdown-preview/

    Java

    cdjava-docs-samples/run/markdown-preview/

    C#

    cddotnet-docs-samples/run/markdown-preview/

Review the private Markdown rendering service

From the perspective of the frontend there is a simple API specification forthe Markdown service:

  • One endpoint at/
  • Expects POST requests
  • The body of the POST request is Markdown text

You may want to review all of the code for any security concerns or just tolearn more about it by exploring the./renderer/ directory. Note that thetutorial does not explain the Markdown transformation code.

Ship the private Markdown rendering service

To ship your code, build with Cloud Build, upload toArtifact Registry, and deploy to Cloud Run:

  1. Change to therenderer directory:

    Node.js

    cdrenderer/

    Python

    cdrenderer/

    Go

    cdrenderer/

    Java

    cdrenderer/

    C#

    cdSamples.Run.MarkdownPreview.Renderer/

  2. Create an Artifact Registry:

    gcloudartifactsrepositoriescreateREPOSITORY\--repository-formatdocker\--locationREGION

    Replace:

    • REPOSITORY with a unique name for the repository. For each repository location in a project, repository names must be unique.
    • REGION with the Google Cloud region to be used for theArtifact Registry repository.
  3. Run the following command to build your container and publish onArtifact Registry.

    Node.js

    gcloudbuildssubmit--tagREGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/renderer

    WherePROJECT_ID is your Google Cloud project ID, andrenderer is thename you want to give your service.

    Upon success, you will see a SUCCESS message containing the ID, creationtime, and image name. The image is stored in Artifact Registry and can bereused if needed.

    Python

    gcloudbuildssubmit--tagREGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/renderer

    WherePROJECT_ID is your Google Cloud project ID, andrenderer is thename you want to give your service.

    Upon success, you will see a SUCCESS message containing the ID, creationtime, and image name. The image is stored in Artifact Registry and can bereused if needed.

    Go

    gcloudbuildssubmit--tagREGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/renderer

    WherePROJECT_ID is your Google Cloud project ID, andrenderer is thename you want to give your service.

    Upon success, you will see a SUCCESS message containing the ID, creationtime, and image name. The image is stored in Artifact Registry and can bereused if needed.

    Java

    This sample usesJib to buildDocker images using common Java tools. Jib optimizes container builds withoutthe need for a Dockerfile or havingDockerinstalled. Learn more aboutbuilding Java containers with Jib.

    1. Use thegcloud credential helperto authorize Docker to push to your Artifact Registry.

      gcloudauthconfigure-docker

    2. Use the Jib Maven Plugin to build and push the container to Artifact Registry.

      mvncompilejib:build-Dimage=REGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/renderer

    WherePROJECT_ID is your Google Cloud project ID, andrenderer is thename you want to give your service.

    Upon success, you will see a BUILD SUCCESS message. The image is stored inArtifact Registry and can be reused if needed.

    C#

    gcloudbuildssubmit--tagREGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/renderer

    WherePROJECT_ID is your Google Cloud project ID, andrenderer is thename you want to give your service.

    Upon success, you will see a SUCCESS message containing the ID, creationtime, and image name. The image is stored in Artifact Registry and can bereused if needed.

  4. Deploy as a private service with restricted access.

    Cloud Run provides out-of-the-boxaccess controlandservice identity features. Access control provides anauthentication layer that restricts users and other services from invoking theservice. Service identity allows restricting your service from accessing otherGoogle Cloud resources by creating a dedicated service account with limitedpermissions.

    1. Create a service account to serve as the "compute identity" of the renderservice. By default this has no privileges other than project membership.

      Command line

      gcloudiamservice-accountscreaterenderer-identity

      Terraform

      To learn how to apply or remove a Terraform configuration, seeBasic Terraform commands.

      resource"google_service_account""renderer"{account_id="renderer-identity"display_name="Service identity of the Renderer (Backend) service."}

      The Markdown rendering service does not integrate directly with anythingelse in Google Cloud. It needs no further permissions.

    2. Deploy with therenderer-identity service account and deny unauthenticatedaccess.

      Command line

      gcloudrundeployrenderer\--imageREGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/renderer\--service-accountrenderer-identity\--no-allow-unauthenticated

      Cloud Run can use the short form service account name instead of the full email address if the service account is part of the same project.

      Terraform

      To learn how to apply or remove a Terraform configuration, seeBasic Terraform commands.

      resource"google_cloud_run_v2_service""renderer"{name="renderer"location="us-central1"deletion_protection=false # set to "true" in productiontemplate{containers{      # Replace with the URL of your Secure Services > Renderer image.      #   gcr.io/<PROJECT_ID>/rendererimage="us-docker.pkg.dev/cloudrun/container/hello"}service_account=google_service_account.renderer.email}}

Try out the private Markdown rendering service

Private services cannot be directly loaded by a web browser. Instead, usecurlor a similar HTTP request CLI tool that allows injecting anAuthorization header.

To send some bold text to the service and see it convert the markdown asterisks toHTML<strong> tags:

  1. Get the URL from the deployment output.

  2. Usegcloud to derive a special development-only identity token for authentication:

    TOKEN=$(gcloudauthprint-identity-token)
  3. Create a curl request that passes the raw Markdown text as a URL-escaped query string parameter:

    curl-H"Authorization: Bearer$TOKEN"\-H'Content-Type: text/plain'\-d'**Hello Bold Text**'\SERVICE_URL

    ReplaceSERVICE_URL with the URL provided after deploying theMarkdown rendering service.

  4. The response should be an HTML snippet:

     <strong>Hello Bold Text</strong>

Review the integration between editor and rendering services

The editor service provides a simple text-entry UI and a space to see the HTMLpreview. Before continuing, review the code retrieved earlier by opening the./editor/ directory.

Next, explore the following few sections of code that securelyintegrates the two services.

Node.js

Therender.js module creates authenticated requests to the private rendererservice. It uses the Google Cloud metadata server in theCloud Run environment to create an identity token and addit to the HTTP request as part of anAuthorization header.

In other environments,render.js usesApplication Default Credentialsto request a token from Google's servers.

import{GoogleAuth}from'google-auth-library';importgotfrom'got';constauth=newGoogleAuth();letclient,serviceUrl;// renderRequest creates a new HTTP request with IAM ID Token credential.// This token is automatically handled by private Cloud Run (fully managed) and Cloud Functions.constrenderRequest=asyncmarkdown=>{if(!process.env.EDITOR_UPSTREAM_RENDER_URL)throwError('EDITOR_UPSTREAM_RENDER_URL needs to be set.');serviceUrl=process.env.EDITOR_UPSTREAM_RENDER_URL;// Build the request to the Renderer receiving service.constserviceRequestOptions={method:'POST',headers:{'Content-Type':'text/plain',},body:markdown,timeout:{request:3000,},};try{// Create a Google Auth client with the Renderer service url as the target audience.if(!client)client=awaitauth.getIdTokenClient(serviceUrl);// Fetch the client request headers and add them to the service request headers.// The client request headers include an ID token that authenticates the request.constclientHeaders=awaitclient.getRequestHeaders();serviceRequestOptions.headers['Authorization']=clientHeaders['Authorization'];}catch(err){throwError('could not create an identity token: '+err.message);}try{// serviceResponse converts the Markdown plaintext to HTML.constserviceResponse=awaitgot(serviceUrl,serviceRequestOptions);returnserviceResponse.body;}catch(err){throwError('request to rendering service failed: '+err.message);}};

Parse the markdown from JSON and send it to the Renderer service to betransformed into HTML.

app.post('/render',async(req,res)=>{try{constmarkdown=req.body.data;constresponse=awaitrenderRequest(markdown);res.status(200).send(response);}catch(err){console.error('Error rendering markdown:',err);res.status(500).send(err);}});

Python

Thenew_request method creates authenticated requests to private services.It uses the Google Cloud metadata server in the Cloud Runenvironment to create an identity token and add it to the HTTP requestas part of anAuthorization header.

In other environments,new_request requests an identity token from Google'sservers by authenticating withApplication Default Credentials.

importosimporturllibimportgoogle.auth.transport.requestsimportgoogle.oauth2.id_tokendefnew_request(data):"""Creates a new HTTP request with IAM ID Token credential.    This token is automatically handled by private Cloud Run and Cloud Functions.    Args:        data: data for the authenticated request    Returns:        The response from the HTTP request    """url=os.environ.get("EDITOR_UPSTREAM_RENDER_URL")ifnoturl:raiseException("EDITOR_UPSTREAM_RENDER_URL missing")req=urllib.request.Request(url,data=data.encode())auth_req=google.auth.transport.requests.Request()target_audience=urlid_token=google.oauth2.id_token.fetch_id_token(auth_req,target_audience)req.add_header("Authorization",f"Bearer{id_token}")response=urllib.request.urlopen(req)returnresponse.read()

Parse the markdown from JSON and send it to the Renderer service to betransformed into HTML.

@app.route("/render",methods=["POST"])defrender_handler():"""Parse the markdown from JSON and send it to the Renderer service to be    transformed into HTML.    """body=request.get_json(silent=True)ifnotbody:return"Error rendering markdown: Invalid JSON",400data=body["data"]try:parsed_markdown=render.new_request(data)returnparsed_markdown,200exceptExceptionaserr:returnf"Error rendering markdown:{err}",500

Go

RenderService creates authenticated requests to private services. It uses the Google Cloud metadata server in the Cloud Run environment tocreate an identity token and add it to the HTTP request as part of anAuthorization header.

In other environments,RenderService requests an identity token from Google'sservers by authenticating withApplication Default Credentials.

import("bytes""context""fmt""io""net/http""time""golang.org/x/oauth2""google.golang.org/api/idtoken")// RenderService represents our upstream render service.typeRenderServicestruct{// URL is the render service address.URLstring// tokenSource provides an identity token for requests to the Render Service.tokenSourceoauth2.TokenSource}// NewRequest creates a new HTTP request to the Render service.// If authentication is enabled, an Identity Token is created and added.func(s*RenderService)NewRequest(methodstring)(*http.Request,error){req,err:=http.NewRequest(method,s.URL,nil)iferr!=nil{returnnil,fmt.Errorf("http.NewRequest: %w",err)}ctx,cancel:=context.WithTimeout(context.Background(),30*time.Second)defercancel()// Create a TokenSource if none exists.ifs.tokenSource==nil{s.tokenSource,err=idtoken.NewTokenSource(ctx,s.URL)iferr!=nil{returnnil,fmt.Errorf("idtoken.NewTokenSource: %w",err)}}// Retrieve an identity token. Will reuse tokens until refresh needed.token,err:=s.tokenSource.Token()iferr!=nil{returnnil,fmt.Errorf("TokenSource.Token: %w",err)}token.SetAuthHeader(req)returnreq,nil}

The request is sent to the Renderer service after adding the markdowntext to be transformed into HTML. Response errors are handled to differentiatecommunication problems from rendering functionality.

varrenderClient=&http.Client{Timeout:30*time.Second}// Render converts the Markdown plaintext to HTML.func(s*RenderService)Render(in[]byte)([]byte,error){req,err:=s.NewRequest(http.MethodPost)iferr!=nil{returnnil,fmt.Errorf("RenderService.NewRequest: %w",err)}req.Body=io.NopCloser(bytes.NewReader(in))deferreq.Body.Close()resp,err:=renderClient.Do(req)iferr!=nil{returnnil,fmt.Errorf("http.Client.Do: %w",err)}out,err:=io.ReadAll(resp.Body)iferr!=nil{returnnil,fmt.Errorf("ioutil.ReadAll: %w",err)}ifresp.StatusCode!=http.StatusOK{returnout,fmt.Errorf("http.Client.Do: %s (%d): request not OK",http.StatusText(resp.StatusCode),resp.StatusCode)}returnout,nil}

Java

makeAuthenticatedRequest creates authenticated requests to privateservices. It uses the Google Cloud metadata server in theCloud Run environment to create an identity token and add it to theHTTP request as part of anAuthorization header.

In other environments,makeAuthenticatedRequest requests an identity tokenfrom Google's servers by authenticating withApplication Default Credentials.

// makeAuthenticatedRequest creates a new HTTP request authenticated by a JSON Web Tokens (JWT)// retrievd from Application Default Credentials.publicStringmakeAuthenticatedRequest(Stringurl,Stringmarkdown){Stringhtml="";try{// Retrieve Application Default CredentialsGoogleCredentialscredentials=GoogleCredentials.getApplicationDefault();IdTokenCredentialstokenCredentials=IdTokenCredentials.newBuilder().setIdTokenProvider((IdTokenProvider)credentials).setTargetAudience(url).build();// Create an ID tokenStringtoken=tokenCredentials.refreshAccessToken().getTokenValue();// Instantiate HTTP requestMediaTypecontentType=MediaType.get("text/plain; charset=utf-8");okhttp3.RequestBodybody=okhttp3.RequestBody.create(markdown,contentType);Requestrequest=newRequest.Builder().url(url).addHeader("Authorization","Bearer "+token).post(body).build();Responseresponse=ok.newCall(request).execute();html=response.body().string();}catch(IOExceptione){logger.error("Unable to get rendered data",e);}returnhtml;}

Parse the markdown from JSON and send it to the Renderer service to betransformed into HTML.

// '/render' expects a JSON body payload with a 'data' property holding plain text// for rendering.@PostMapping(value="/render",consumes="application/json")publicStringrender(@RequestBodyDatadata){Stringmarkdown=data.getData();Stringurl=System.getenv("EDITOR_UPSTREAM_RENDER_URL");if(url==null){Stringmsg="No configuration for upstream render service: "+"add EDITOR_UPSTREAM_RENDER_URL environment variable";logger.error(msg);thrownewIllegalStateException(msg);}Stringhtml=makeAuthenticatedRequest(url,markdown);returnhtml;}

C#

GetAuthenticatedPostResponse creates authenticated requests to privateservices. It uses the Google Cloud metadata server in theCloud Run environment to create an identity token and add it to theHTTP request as part of anAuthorization header.

In other environments,GetAuthenticatedPostResponse requests an identity tokenfrom Google's servers by authenticating withApplication Default Credentials.

privateasyncTask<string>GetAuthenticatedPostResponse(stringurl,stringpostBody){// Get the OIDC access token from the service account via Application Default CredentialsGoogleCredentialcredential=awaitGoogleCredential.GetApplicationDefaultAsync();OidcTokentoken=awaitcredential.GetOidcTokenAsync(OidcTokenOptions.FromTargetAudience(url));stringaccessToken=awaittoken.GetAccessTokenAsync();// Create request to the upstream service with the generated OAuth access token in the Authorization headervarupstreamRequest=newHttpRequestMessage(HttpMethod.Post,url);upstreamRequest.Headers.Authorization=newAuthenticationHeaderValue("Bearer",accessToken);upstreamRequest.Content=newStringContent(postBody);varupstreamResponse=await_httpClient.SendAsync(upstreamRequest);upstreamResponse.EnsureSuccessStatusCode();returnawaitupstreamResponse.Content.ReadAsStringAsync();}

Parse the markdown from JSON and send it to the Renderer service to betransformed into HTML.

publicasyncTask<IActionResult>Index([FromBody]RenderModelmodel){varmarkdown=model.Data??string.Empty;varrenderedHtml=awaitGetAuthenticatedPostResponse(_editorUpstreamRenderUrl,markdown);returnContent(renderedHtml);}

Ship the public editor service

To build and deploy your code:

  1. Change to theeditor directory:

    Node.js

    cd../editor

    Python

    cd../editor

    Go

    cd../editor

    Java

    cd../editor

    C#

    cd../Samples.Run.MarkdownPreview.Editor/

  2. Run the following command to build your container and publish onArtifact Registry.

    Node.js

    gcloudbuildssubmit--tagREGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/editor

    WherePROJECT_ID is your Google Cloud project ID, andeditor is thename you want to give your service.

    Upon success, you will see a SUCCESS message containing the ID, creationtime, and image name. The image is stored in Container Registry and can bereused if needed.

    Python

    gcloudbuildssubmit--tagREGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/editor

    WherePROJECT_ID is your Google Cloud project ID, andeditor is thename you want to give your service.

    Upon success, you will see a SUCCESS message containing the ID, creationtime, and image name. The image is stored in Artifact Registry and can bereused if needed.

    Go

    gcloudbuildssubmit--tagREGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/editor

    WherePROJECT_ID is your Google Cloud project ID, andeditor is thename you want to give your service.

    Upon success, you will see a SUCCESS message containing the ID, creationtime, and image name. The image is stored in Artifact Registry and can bereused if needed.

    Java

    This sample usesJib to buildDocker images using common Java tools. Jib optimizes container builds withoutthe need for a Dockerfile or havingDockerinstalled. Learn more aboutbuilding Java containers with Jib.

    mvncompilejib:build-Dimage=REGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/editor

    WherePROJECT_ID is your Google Cloud project ID, andeditor is thename you want to give your service.

    Upon success, you will see a BUILD SUCCESS message. The image is stored inArtifact Registry and can be reused if needed.

    C#

    gcloudbuildssubmit--tagREGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/editor

    WherePROJECT_ID is your Google Cloud project ID, andeditor is thename you want to give your service.

    Upon success, you will see a SUCCESS message containing the ID, creationtime, and image name. The image is stored in Artifact Registry and can bereused if needed.

  3. Deploy as a private service with special access to the rendering service.

    1. Create a service account to serve as the "compute identity" of the privateservice. By default this has no privileges other than project membership.

      Command line

      gcloudiamservice-accountscreateeditor-identity

      Terraform

      To learn how to apply or remove a Terraform configuration, seeBasic Terraform commands.

      resource"google_service_account""editor"{account_id="editor-identity"display_name="Service identity of the Editor (Frontend) service."}

      The Editor service does not need to interact with anything else in Google Cloud other than the Markdown rendering service.

    2. Grant access to theeditor-identity compute identity to invoke the Markdownrendering service. Any service which uses this as a compute identity willhave this privilege.

      Command line

      gcloudrunservicesadd-iam-policy-bindingrenderer\--memberserviceAccount:editor-identity@PROJECT_ID.iam.gserviceaccount.com\--roleroles/run.invoker

      Terraform

      To learn how to apply or remove a Terraform configuration, seeBasic Terraform commands.

      resource"google_cloud_run_service_iam_member""editor_invokes_renderer"{location=google_cloud_run_v2_service.renderer.locationservice=google_cloud_run_v2_service.renderer.namerole="roles/run.invoker"member="serviceAccount:${google_service_account.editor.email}"}

      Because this is given the invoker role in the context of the render service,the render service is the only private Cloud Run service the editorcan invoke.

    3. Deploy with theeditor-identity service account and allow public,unauthenticated access.

      Command line

      gcloudrundeployeditor--imageREGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/editor\--service-accounteditor-identity\--set-env-varsEDITOR_UPSTREAM_RENDER_URL=SERVICE_URL\--allow-unauthenticated

      Replace:

      • PROJECT_ID with your project ID
      • SERVICE_URL with the URL provided after deploying the Markdown rendering service.

      Terraform

      To learn how to apply or remove a Terraform configuration, seeBasic Terraform commands.

      Deploy the editor service:

      resource"google_cloud_run_v2_service""editor"{name="editor"location="us-central1"deletion_protection=false # set to "true" in productiontemplate{containers{      # Replace with the URL of your Secure Services > Editor image.      #   gcr.io/<PROJECT_ID>/editorimage="us-docker.pkg.dev/cloudrun/container/hello"env{name="EDITOR_UPSTREAM_RENDER_URL"value=google_cloud_run_v2_service.renderer.uri}}service_account=google_service_account.editor.email}}

      GrantallUsers permission to invoke the service:

      data"google_iam_policy""noauth"{binding{role="roles/run.invoker"members=["allUsers",]}}resource"google_cloud_run_service_iam_policy""noauth"{location=google_cloud_run_v2_service.editor.locationproject=google_cloud_run_v2_service.editor.projectservice=google_cloud_run_v2_service.editor.namepolicy_data=data.google_iam_policy.noauth.policy_data}

Understand the HTTPS traffic

There are three HTTP requests involved in rendering markdown using these services.

Diagram showing the request flow from the user to the editor, editor to get a token from Metadata server, editor to make request to render service, render service to return HTML to editor.
The frontend service with theeditor-identity invokes the render service. Botheditor-identity andrenderer-identity have limited permissions so any security exploit or code injection has limited access to other Google Cloud resources.

Trying it out

To try out the complete two-service application:

  1. Navigate your browser to the URL provided by the deployment step above.

  2. Try editing the Markdown text on the left and click the button to see itpreview on the right.

    It should look like this:

    Screenshot of the Markdown Editor User Interface

    Success: You created a secure two-service application running on Cloud Run.

If you choose to continue developing these services, remember that they haverestricted Identity and Access Management (IAM) access to the rest of Google Cloud andwill need to be given additional IAM roles to access many otherservices.

Clean up

If you created a new project for this tutorial,delete the project.If you used an existing project and wish to keep it without the changes addedin this tutorial,delete resources created for the tutorial.

Delete the project

The easiest way to eliminate billing is to delete the project that you created for the tutorial.

To delete the project:

    Caution: Deleting a project has the following effects:
    • Everything in the project is deleted. If you used an existing project for the tasks in this document, when you delete it, you also delete any other work you've done in the project.
    • Custom project IDs are lost. When you created this project, you might have created a custom project ID that you want to use in the future. To preserve the URLs that use the project ID, such as anappspot.com URL, delete selected resources inside the project instead of deleting the whole project.

    If you plan to explore multiple architectures, tutorials, or quickstarts, reusing projects can help you avoid exceeding project quota limits.

  1. In the Google Cloud console, go to theManage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then clickDelete.
  3. In the dialog, type the project ID, and then clickShut down to delete the project.

Delete tutorial resources

  1. Delete the Cloud Run services you deployed in this tutorial:

    gcloud

    gcloudrunservicesdeleteeditorgcloudrunservicesdeleterenderer

    You can also delete Cloud Run services from theGoogle Cloud console.

  2. Remove the gcloud default configurations you added during tutorial setup.

    gcloudconfigunsetrun/region
  3. Remove the project configuration:

     gcloud config unset project
  4. Delete other Google Cloud resources created in this tutorial:

What's next

Except as otherwise noted, the content of this page is licensed under theCreative Commons Attribution 4.0 License, and code samples are licensed under theApache 2.0 License. For details, see theGoogle Developers Site Policies. Java is a registered trademark of Oracle and/or its affiliates.

Last updated 2026-02-19 UTC.