# Effortlessly Deploy Your GCP Cloud Run App Using Terraform

By [Mozes721](https://paragraph.com/@mozes721) · 2024-11-01

---

Terraform is gaining more popularity for a reason as it provides high level of control flexibility as IaC(Infrastructure as Code)

Supports modules, keeps track of state of your infrastructure and is helpfull if your project is complex, multi-cloud or hybrid enviornments.

### Prerequisites

To start off be sure to follow [this](https://developer.hashicorp.com/terraform/install) guide for Terraform instalation if you haven’t done so and be sure to have [GCP](https://cloud.google.com/gcp?&gad_source=1) account already set up.

> You should have the app already prior deployed through other means like CLI to understand the deployment process, baseline configuration, incremental transition etc.

Related blog with manual deployment I added bellow 👇📖

[https://blog.stackademic.com/how-to-deploy-a-go-service-to-gcp-cloud-run-694d01cab5b5](https://blog.stackademic.com/how-to-deploy-a-go-service-to-gcp-cloud-run-694d01cab5b5)

### Project Structure

For my project structure I have these files and directory structure.

    terraform/
      ├── modules/
      │   ├── docker/
      │   │   ├── docker-artifact.tf
      │   │   └── variables.tf
      │   ├── gcp/
      │   │   ├── cloud-run.tf
      │   │   └── variables.tf
      ├── main.tf
      ├── set-prod.env.sh
      ├── terraform.tfvars
      ├── variables.tf
      └── account_key.json
    

*   `main.tf`: Including the required providers and the Google provider configuration.
    
*   `variables.tf`: Describe how to define variables for your project.
    
*   `terraform.tfvars`: Explain how to set variable values specific to your environment.
    
*   `set-prod.env.sh`: Sets the enviornment variables for terraform with **TF\_VAR** prefix flag.
    
*   **Modules**: Detail the `docker` and `cloud-run` modules, explaining their roles and how they interact.
    
    ### IaC Scripts
    
    I will showcase from parent to child modules scripts for more of a higher order guide.
    
    Most likely you will have env variables most convieneit way for me is to create shell script with the `TF_VAR_` prefix that Terraform will recoginze and use ones initialized(but for that later).
    
        #!/bin/bash
        
        #server 
        export TF_VAR_redis_url="redis_url"
        export TF_VAR_firebase_account_key="your_account_key.json"
        export TF_VAR_client_url="client_url"
        export TF_VAR_gcp_account_key="client_url"
        
        echo "Environment variables for Terraform GCP set."
        
    
    Variables that i have as well set in module level but parent will usually contain all of them but in module level i just passed the right ones.
    
        variable "project_id" {
          description = "The ID of the Google Cloud project."
          type        = string
        }
        
        variable "project_name" {
          description = "The project name of the Google Cloud Run project."
          type        = string
        }
        
        
        variable "region" {
          description = "The Google Cloud region."
          type        = string
        }
        
        
        variable "redis_url" {
          description = "The URL for the Redis instance."
          type        = string
        }
        
        
        variable "client_url" {
          description = "The URL for the client application."
          type        = string
        }
        
        variable "gcp_account_key" {
          description = "Path to the Google Cloud service account key file."
          type        = string
        }
        
        variable "firebase_account_key_location" {
          description = "Firebase account key location in Docker container."
          type        = string
        }
        
    
    There is as well other script file that I created that does **NOT** contain private or secret key values that can be easily modified and is handy for default values thats your `terraform.tfvars`
    
        project_id = "recepies-6e7c0"
        project_name = "recipe-service"
        region     = "europe-north1"
        gcp_account_key = "./account_key.json"
        firebase_account_key_location = "/app/config/account_key.json"
        
    
    Lets talk about 🐘 in the room our `main.tf` script.
    
        terraform {
          required_providers {
            google = {
              source  = "hashicorp/google"
              version = ">= 4.0.0"
            }
          }
          required_version = ">= 0.12"
        }
        
        provider "google" {
          credentials = file(var.gcp_account_key)
          project     = var.project_id
          region      = var.region
        }
        
        # Get project information
        data "google_project" "project" {
          project_id = var.project_id
        }
        
        module "docker" {
          source      = "./modules/docker"
          project_id  = var.project_id
        }
        
        module "cloud_run" {
          source      = "./modules/gcp"
          project_id  = var.project_id
          region      = var.region
          redis_url   = var.redis_url
          client_url  = var.client_url
          firebase_account_key_location = var.firebase_account_key_location
          cloudrun_image = "gcr.io/${var.project_id}/recipe-server:latest"
        
          depends_on = [
            module.docker
          ]
        }
        
    
    At the begining I define the **PaaS** provider as i use **GCP** google is added you can add **AWS**, **Azure** or other providers. Creditentials are essential to approve your request to any cloud provider the gcp\_account\_key you pass as a json file that i have in parent terraform directory.
    

![](https://storage.googleapis.com/papyrus_images/40d8e395ef598c402592078ba99bfca56ad1aaa6d38ce10b8e11c983453d1484.png)

At above screenshot you can see i have created a Service account key in GCP and passed the right IAM access rights.

> It crucial to assign the correct IAM (Identity and Access Management) access rights to the `account_key.json` as otherwise you will have different permission issues when trying to run Terraform. Roles viewer, editor, storage.admin, cloudrun.admin, Docker artifacts.

There is an alternative as well to just assign roles and permission through IaC but for me it’s more an hastle at least until i get more familiar with it.

    gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
      --member="serviceAccount:YOUR_SERVICE_ACCOUNT_EMAIL" \
      --role="roles/editor"
    

Above ilustrates how it could be done.

Then next steps are running your modules I start off with docker as need to create Docker Artifact in GCP and after that is completed I do the same with Cloud Run. Keep in mind I access the dir with `"./modules/docker"` and pass needed variables from parent to child `modules/docker/variables.tf` _._

    resource "google_project_service" "container_registry_api" {
      project = var.project_id
      service = "containerregistry.googleapis.com"
      disable_on_destroy = false
    }
    
    resource "null_resource" "docker_build_push" {
      triggers = {
        always_run = timestamp()
      }
    
      provisioner "local-exec" {
        command = <<-EOT
          # Build the Docker image
          docker build -t gcr.io/${var.project_id}/recipe-server:latest .
          
          # Configure docker to authenticate with GCP
          gcloud auth configure-docker --quiet
          
          # Push the image
          docker push gcr.io/${var.project_id}/recipe-server:latest
        EOT
      }
    
      depends_on = [
        google_project_service.container_registry_api
      ]
    }
    

The _docker-artifact.tf_ is quite short as only think we need is to define the resources used starting with **container\_registry\_api** and secondly **docker\_build\_push** add provisioning for local execution and end it with building and deploying the grc docker image with passed in _var.project\_id +_ add that it depends on container\_registry\_api as its required.

Lastly in our **IaC** we deploy it running our last module with `"./modules/gcp"`

    resource "google_project_service" "required_apis" {
      for_each = toset([
        "run.googleapis.com",
        "containerregistry.googleapis.com"
      ])
      
      project = var.project_id
      service = each.key
      disable_on_destroy = false
    }
    
    resource "google_cloud_run_service" "recipe_service" {
      name     = var.project_name
      location = var.region
      project  = var.project_id
    
      template {
        spec {
          containers {
            image = var.cloudrun_image
            
            env {
              name  = "REDIS_URL"
              value = var.redis_url
            }
            env {
              name  = "CLIENT_URL"
              value = var.client_url
            }
            env {
              name  = "FIREBASE_ACCOUNT_KEY"
              value = var.firebase_account_key_location
            }
          }
        }
      }
    
      depends_on = [
        google_project_service.required_apis
      ]
    }
    
    resource "google_cloud_run_service_iam_member" "public_access" {
      location = google_cloud_run_service.recipe_service.location
      project  = google_cloud_run_service.recipe_service.project
      service  = google_cloud_run_service.recipe_service.name
      role     = "roles/run.invoker"
      member   = "allUsers"
    }
    

Same as for docker module we define required resources for `"google_cloud_run_service"` we select the name, region, project\_id then select the image thats been passed from main.

If you have required env variables pass them as well.

IAM member resource is added to give permission for deployment to Cloud Run.

### Deploying Your Application

Now when architecture is set and done we do the following steps.

1.Initialize Terraform

    terraform init
    

2\. Run the Shell Script or manually set your env variables

    source set-prod.env.sh
    

For terraform to access the .env variables.

3\. Preview the changes in terraform or dirrectly deploy it.

    terraform plan //Helps you preview the changes that Terraform will make to your infrastructure. 
    
    terraform apply //Run the terraform script to deploy your app through IaC.
    

If all is good you will end up with something like this.

![](https://storage.googleapis.com/papyrus_images/e81e78d2f8b18fa415da16c57ae13af68cc971fb6526b29a213e70534a7790e9.png)

If commiting to GitHub be note worthy to add some files in _.gitignore_ as terraform generates artifacts and backup etc.

    terraform/set-prod-env.sh
    terraform/account_key.json
    terraform/.terraform
    terraform/.terraform.lock.hcl
    terraform/.terraform.tfstate.lock.info
    
    # Ignore Terraform working directory
    terraform/.terraform/
    
    # Ignore tfstate files and backups
    *.tfstate
    *.tfstate.backup
    

### Conclusion

While IaC adds some complexity compared to manual setup it adds as well levarage as mentioned before of more maintainability and automation espectially of interact between multiple cloud providers etc. As well for me personally it gives more power to me as a developer!

Repo you can find [here](https://github.com/Mozes721/RecipesApp).

---

*Originally published on [Mozes721](https://paragraph.com/@mozes721/effortlessly-deploy-your-gcp-cloud-run-app-using-terraform)*
