├── .env ├── .gitignore ├── README.md ├── cli ├── configs │ ├── google.config.bash │ └── pulumi.config.bash ├── delete │ └── google.delete.bash ├── installs │ ├── docker.install.bash │ ├── expect.install.bash │ ├── golang.install.bash │ ├── google.install.bash │ ├── jq.install.bash │ └── pulumi.install.bash └── shared.bash ├── config.json ├── del.bash ├── gen.bash ├── images └── docker-explanation.avif └── scripts ├── _main.go └── _roles.gcp.yml /.env: -------------------------------------------------------------------------------- 1 | GOOGLE_CREDENTIALS_FILE_PATH="your config credential paths" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deploy Go to Google Cloud Run 2 | 3 | Idempotent scripts (They won't create duplicate resources if ran again) that will deploy your dockerized golang application to Google Cloud Run instantaneously with a single command `gen.bash`. 4 | 5 | It will also bring down all the resources when you don't need them `del.bash`. (This won't delete the local files) 6 | 7 | It installs CLIs in your machine and bootstraps a Pulumi code that you will be able to leverage all the required services and have your application running. (See How it Works) 8 | 9 | ## How to use: 10 | 11 | 1. Clone the repository (or download the .zip file in the releases page) 12 | 2. Configure the `config.json` with your information (At least: `project_name`, `dockerfile_relative_path`, `pulumi_relative_path`) 13 | 3. Run the `gen.bash` script. 14 | 15 | ```bash 16 | bash gen.bash 17 | ``` 18 | 19 | 4. Navigate to the recently created `pulumi` (`cd ./pulumi`) folder, and: 20 | 21 | ```bash 22 | pulumi up 23 | ``` 24 | 25 | When you want to bring everything down: 26 | 27 | ```bash 28 | bash del.bash 29 | ``` 30 | 31 | ## How it works: 32 | 33 | This repo has all the files you need to get your dockerized application deployed. 34 | 35 | Clone the repo somewhere in your project. Configure the `dockerfile_relative_path` on the `config.json`. This should point to your `Dockerfile` relative to the `gen.bash`. 36 | 37 | 38 | 39 | You should unzip them somewhere in your project (e.g. /deploy). 40 | 41 | The scripts will: 42 | 43 | 1. Install the required CLI tools (gcloud, expect, docker, jq, pulumi, golang) 44 | 2. Configure the required permissions in GCloud 45 | 3. Scaffold the right Pulumi scripts for you 46 | 47 | We check whether each of these are installed on your system and install them if not. This will install the following CLI tools: 48 | 49 | 1. [Docker](https://docker.com) 50 | 2. [Pulumi CLI](https://pulumi.com) 51 | 3. [Go](https://golang.org) 52 | 4. [Google CLI](https://cloud.google.com/sdk) 53 | 5. [jq](https://stedolan.github.io/jq/) 54 | 6. [Expect](https://core.tcl.tk/expect/index) 55 | 56 | The Google SDK is used to enable the first services. 57 | Pulumi is then used to orchestrate the entire process. jq is used to parse the JSON output from the Google SDK. Expect is used to handle certain edge cases from the Pulumi and Google Cloud CLIs. 58 | 59 | ## Configuration 60 | 61 | Open `config.json` and edit its values. 62 | 63 | - `project_name` 64 | The name of the project. This is the display value that you will see in Google Cloud's console. 65 | 66 | - `project_description` 67 | The description of the project. 68 | 69 | - `project_stack`: 70 | The stack in which Pulumi will be configured. Either dev, staging, production, prod, etc. 71 | 72 | - `project_language`: 73 | Currently supporting go, you should leave it as it is. 74 | 75 | - `gcp_project_id`: 76 | The Google Cloud's unique Project ID which the CLI will create the project. 77 | 78 | - `gcp_service_account_name` 79 | The service accounts are used by Pulumi to perform operations in Google Cloud. This is the name that will show up in IAM. The name should be unique, lowercase, have no spaces and no special characters (except hyphens). 80 | 81 | - `gcp_service_account_display_name` 82 | The service accounts are used by Pulumi to perform operations in Google Cloud. This is the name that will show up in IAM. The name should be unique, lowercase, have no spaces and no special characters (except hyphens). 83 | 84 | - `gcp_service_account_display_name` 85 | Same as above. But this will be the human readable name. 86 | 87 | - `gcp_service_account_description` 88 | The description of the service account. 89 | 90 | - `gcp_pulumi_service_account_key_path` 91 | The file path in which we'll save the the key for Pulumi to connect to Google Cloud. 92 | 93 | - `gcp_pulumi_admin_role_name` 94 | This is the Google Cloud Role that we will assign to the service account. This role will have the necessary permissions to deploy to Google Cloud with the services you've selected. 95 | 96 | - `gcp_docker_image_name` 97 | This is the name that will be visible in Google Cloud Run's Artifact Registry. This isn't the name of your local Docker image. This is part of the Pulumi code. 98 | 99 | - `gcp_artifact_registry_service_name` 100 | This is the name of the Artifact Registry service that will be created in Google Cloud. This is part of the Pulumi code. Artifact Registry is used to store the Docker image. 101 | 102 | - `gcp_artifact_registry_repository_name` 103 | This is the name of the Artifact Registry repository that will be created in Google Cloud. This is part of the Pulumi code. We need to store the Docker image in a repository inside Artifact Registry. 104 | 105 | - `gcp_cloud_run_admin_service_name` 106 | This is the name of the Cloud Run service that will be created in Google Cloud. This is part of the Pulumi code. This service will be used to manage the Cloud Run services. 107 | 108 | - `gcp_cloud_run_service_name` 109 | This is the name of the Cloud Run service that will be created in Google Cloud. This is part of the Pulumi code. This service will be used to deploy the Docker image to Google Cloud Run. 110 | 111 | - `gcp_location` 112 | This is the location where the resources will be deployed. You can find a list [here](https://cloud.google.com/about/locations). This is part of the Pulumi code 113 | -------------------------------------------------------------------------------- /cli/configs/google.config.bash: -------------------------------------------------------------------------------- 1 | source $(pwd)/cli/shared.bash 2 | 3 | while [[ $# -gt 0 ]]; do 4 | case "$1" in 5 | --project_name) 6 | project_name="$2" 7 | shift 2 8 | ;; 9 | --project_description) 10 | project_description="$2" 11 | shift 2 12 | ;; 13 | --project_stack) 14 | project_stack="$2" 15 | shift 2 16 | ;; 17 | --gcp_project_id) 18 | gcp_project_id="$2" 19 | shift 2 20 | ;; 21 | --gcp_service_account_name) 22 | gcp_service_account_name="$2" 23 | shift 2 24 | ;; 25 | --gcp_service_account_display_name) 26 | gcp_service_account_display_name="$2" 27 | shift 2 28 | ;; 29 | --gcp_service_account_description) 30 | gcp_service_account_description="$2" 31 | shift 2 32 | ;; 33 | --gcp_pulumi_service_account_key_path) 34 | gcp_pulumi_service_account_key_path="$2" 35 | shift 2 36 | ;; 37 | --gcp_pulumi_admin_role_name) 38 | gcp_pulumi_admin_role_name="$2" 39 | shift 2 40 | ;; 41 | --gcp_roles_path) 42 | gcp_roles_path="$2" 43 | shift 2 44 | ;; 45 | --config_path) 46 | config_path="$2" 47 | shift 2 48 | ;; 49 | *) 50 | echo "Unknown parameter passed: $1" 51 | exit 1 52 | ;; 53 | esac 54 | done 55 | 56 | create_service_account() { 57 | local max_retries=5 58 | local retry_count=$1 59 | local append_suffix=$2 60 | 61 | local new_service_account_name="${gcp_service_account_name}" 62 | if [[ -n "$append_suffix" ]]; then 63 | new_service_account_name="${gcp_service_account_name}-${append_suffix}" 64 | fi 65 | 66 | if [[ $retry_count -eq $max_retries ]]; then 67 | echo "Max retries reached while creating the service account. Exiting..." 68 | exit 1 69 | fi 70 | 71 | output=$(gcloud iam service-accounts create "$new_service_account_name" \ 72 | --description="$gcp_service_account_description" \ 73 | --project="$gcp_project_id" \ 74 | --display-name="$gcp_service_account_display_name" 2>&1) 75 | 76 | if [[ "$output" == *"Service account $gcp_service_account_name already exists"* ]]; then 77 | random_number=$(generate_random_number) 78 | echo "Service account already exists. Retrying with a random number: $random_number" 79 | create_service_account $((retry_count + 1)) $random_number 80 | return $? 81 | fi 82 | 83 | 84 | 85 | echo "The new name is $new_service_account_name" 86 | # Write the successful gcp_service_account_name to the config.json 87 | update_config ".gcp_service_account_name" "$new_service_account_name" 88 | echo $output 89 | } 90 | 91 | create_pulumi_role() { 92 | local max_retries=5 93 | local retry_count=$1 94 | local append_suffix=$2 95 | 96 | local new_role_name="${gcp_pulumi_admin_role_name}" 97 | if [[ -n "$append_suffix" ]]; then 98 | new_role_name="${gcp_pulumi_admin_role_name}-${append_suffix}" 99 | fi 100 | 101 | if [[ $retry_count -eq $max_retries ]]; then 102 | echo "Max retries reached while creating the role. Exiting..." 103 | exit 1 104 | fi 105 | 106 | output=$(gcloud iam roles create "$new_role_name" \ 107 | --project="$gcp_project_id" \ 108 | --file="$gcp_roles_path" 2>&1) 109 | 110 | if [[ "$output" == *"Role $gcp_pulumi_admin_role_name already exists"* ]]; then 111 | random_number=$(generate_random_number) 112 | echo "Role already exists. Retrying with a random number: $random_number" 113 | create_pulumi_role $((retry_count + 1)) $random_number 114 | return $? 115 | fi 116 | 117 | echo "Creating role: $new_role_name" 118 | # Write the successful gcp_service_account_name to the config.json 119 | update_config ".gcp_pulumi_admin_role_name" "$new_role_name" 120 | echo $output 121 | } 122 | 123 | 124 | 125 | 126 | # Check if project Id does not exist 127 | if [[ -z "$gcp_project_id" ]]; then 128 | gcp_project_id=$(gcloud projects list --format="get(projectId)" --filter="name:${project_name}" 2>&1) 129 | update_config ".gcp_project_id" "$gcp_project_id" 130 | fi 131 | 132 | 133 | service_account=$(gcloud iam service-accounts describe "$gcp_service_account_name@$gcp_project_id.iam.gserviceaccount.com" 2>&1) 134 | 135 | 136 | if [[ "$service_account" == *"NOT_FOUND: Unknown service account"* ]]; then 137 | echo "Service account "$gcp_service_account_name" was not found. Creating..." 138 | create_service_account 139 | fi 140 | 141 | # Download the credentials for the service accounts and store them locally in the keys directory 142 | if [ ! -s "$gcp_pulumi_service_account_key_path" ]; then 143 | gcloud iam service-accounts keys create "$gcp_pulumi_service_account_key_path" \ 144 | --iam-account="$gcp_service_account_name@$gcp_project_id.iam.gserviceaccount.com" \ 145 | --project="$gcp_project_id" 146 | fi 147 | 148 | 149 | gcloud_role=$(gcloud iam roles describe "$gcp_pulumi_admin_role_name" --project="$gcp_project_id" 2>&1) 150 | 151 | 152 | if [[ "$gcloud_role" == *"NOT_FOUND: The role named"* ]]; then 153 | echo "The Role $gcp_service_account_name was not found. Creating..." 154 | create_pulumi_role 155 | else 156 | echo "The role $gcp_pulumi_admin_role_name already exists. Updating..." 157 | # Google cloud will ask us if we want to update the role. We will say 158 | # yes. Additionally we do not provide an etag as we assume that the roles 159 | # will not be updated concurrently 160 | yes | gcloud iam roles update "$gcp_pulumi_admin_role_name" \ 161 | --project="$gcp_project_id" \ 162 | --file="$gcp_roles_path" 163 | fi 164 | 165 | iam_policy=$(gcloud projects get-iam-policy "$gcp_project_id" --format=json) 166 | 167 | iam_policy_role="projects/$gcp_project_id/roles/$gcp_pulumi_admin_role_name" 168 | iam_policy_member="serviceAccount:$gcp_service_account_name@$gcp_project_id.iam.gserviceaccount.com" 169 | 170 | is_present=$(echo "$iam_policy" | jq --arg role "$iam_policy_role" --arg member "$iam_policy_member" ' 171 | .bindings[] | select(.role == $role and .members[] == $member) | length > 0 172 | ') 173 | 174 | if [[ "$is_present" != "true" ]]; then 175 | echo "The specified role and member are not present." 176 | echo "Attaching the policy binding" 177 | gcloud projects add-iam-policy-binding "$gcp_project_id" --role "$iam_policy_role" --member "$iam_policy_member" 178 | fi 179 | 180 | # Check if billing is enabled for the project. 181 | # This is needed. Otherwise pulumi up will fail. 182 | # Note that the link generated may not work as expected if you have multiple accounts 183 | gcloud_billing_project=$(gcloud billing projects describe "$gcp_project_id" --format="json") 184 | 185 | is_billing_enabled=$(echo "$gcloud_billing_project" | jq '.billingEnabled') 186 | 187 | if [[ "$is_billing_enabled" != "true" ]]; then 188 | echo "Billing is not enabled for the project. Please enable billing for the project." 189 | echo "Opening the google cloud billing page in the browser" 190 | echo "If you have multiple accounts, the link may not work. Go to the billing page manually. (https://console.cloud.google.com/billing) \n\n\n" 191 | open "https://console.cloud.google.com/billing/linkedaccount?project=$gcp_project_id&hl=en&" 192 | fi 193 | -------------------------------------------------------------------------------- /cli/configs/pulumi.config.bash: -------------------------------------------------------------------------------- 1 | 2 | source $(pwd)/cli/shared.bash 3 | 4 | while [[ $# -gt 0 ]]; do 5 | case "$1" in 6 | --project_name) 7 | project_name="$2" 8 | shift 2 9 | ;; 10 | 11 | --gcp_project_id) 12 | gcp_project_id="$2" 13 | shift 2 14 | ;; 15 | --pulumi_dir) 16 | pulumi_dir="$2" 17 | shift 2 18 | ;; 19 | --original_dir) 20 | original_dir="$2" 21 | shift 2 22 | ;; 23 | --gcp_pulumi_service_account_key_path) 24 | gcp_pulumi_service_account_key_path="$2" 25 | shift 2 26 | ;; 27 | *) 28 | echo "Unknown parameter passed: $1" 29 | exit 1 30 | ;; 31 | esac 32 | done 33 | # 34 | 35 | cd "$pulumi_dir" 36 | # Install go packages, assuming go.mod file is present in the directory 37 | go mod tidy 38 | 39 | 40 | 41 | echo "Setting up gcp:credentials" 42 | pulumi config set gcp:credentials "$gcp_pulumi_service_account_key_path" 43 | 44 | echo "Setting up gcp:project" 45 | # Set the project Id 46 | pulumi config set gcp:project "$gcp_project_id" 47 | 48 | # Configure the env file to load the configuration 49 | 50 | 51 | env_path="./.env" 52 | 53 | if [ ! -f "$env_path" ]; then 54 | # File does not exist, create the file 55 | touch "$env_path" 56 | echo ".env file created." 57 | echo "GOOGLE_CREDENTIALS_FILE_PATH=\"$gcp_pulumi_service_account_key_path\"" >> "$env_path" 58 | else 59 | echo ".env file already exists. Updating..." 60 | if grep -q "^GOOGLE_CREDENTIALS_FILE_PATH=" "$env_path"; then 61 | # Replace the line 62 | sed -i '' "s|^GOOGLE_CREDENTIALS_FILE_PATH=.*|GOOGLE_CREDENTIALS_FILE_PATH=\"$gcp_pulumi_service_account_key_path\"|" "$env_path" 63 | else 64 | # Add the line if it doesn't exist 65 | echo "GOOGLE_CREDENTIALS_FILE_PATH=\"$gcp_pulumi_service_account_key_path\"" >> "$env_path" 66 | fi 67 | fi 68 | 69 | cd "$original_dir" 70 | -------------------------------------------------------------------------------- /cli/delete/google.delete.bash: -------------------------------------------------------------------------------- 1 | 2 | while [[ $# -gt 0 ]]; do 3 | case "$1" in 4 | --gcp_project_id) 5 | gcp_project_id="$2" 6 | shift 2 7 | ;; 8 | --gcp_service_account_name) 9 | gcp_service_account_name="$2" 10 | shift 2 11 | ;; 12 | --gcp_pulumi_service_account_key_path) 13 | gcp_pulumi_service_account_key_path="$2" 14 | shift 2 15 | ;; 16 | --gcp_pulumi_service_account_key_path) 17 | gcp_pulumi_service_account_key_path="$2" 18 | shift 2 19 | ;; 20 | --gcp_pulumi_admin_role_name) 21 | gcp_pulumi_admin_role_name="$2" 22 | shift 2 23 | ;; 24 | --config_path) 25 | config_path="$2" 26 | shift 2 27 | ;; 28 | *) 29 | echo "Unknown parameter passed: $1" 30 | exit 1 31 | ;; 32 | esac 33 | done 34 | 35 | # We delete the service account key json file. We use the -f flag 36 | # to avoid errors if the file does not exist 37 | rm -f "$gcp_pulumi_service_account_key_path" 38 | 39 | service_account_keys=$(gcloud iam service-accounts keys list --iam-account="$gcp_service_account_name@$gcp_project_id.iam.gserviceaccount.com" --format="json" 2>&1) 40 | 41 | if [[ ! "$service_account_keys" =~ .*"or it may not exist".* ]]; then 42 | echo "Deleting service account keys for "$gcp_service_account_name"" 43 | gcloud iam service-accounts keys delete "$gcp_pulumi_service_account_key_path" \ 44 | --iam-account="$gcp_service_account_name@$gcp_project_id.iam.gserviceaccount.com" \ 45 | --project="$gcp_project_id" 46 | fi 47 | 48 | 49 | gcloud_role=$(gcloud iam roles describe "$gcp_pulumi_admin_role_name" --project="$gcp_project_id" --format=json 2>&1) 50 | 51 | # removes the gcloud iam role if exists. 52 | if jq -e . >/dev/null 2>&1 <<<"$gcloud_role"; then 53 | if ! echo "$gcloud_role" | jq -e '.deleted == true' >/dev/null 2>&1; then 54 | gcloud iam roles delete "$gcp_pulumi_admin_role_name" --project="$gcp_project_id" 55 | fi 56 | elif [[ ! "$gcloud_role" =~ "NOT_FOUND: The role named" ]]; then 57 | echo "Deleting service account role $gcp_pulumi_admin_role_name" 58 | gcloud iam roles delete "$gcp_pulumi_admin_role_name" --project="$gcp_project_id" 59 | fi 60 | 61 | service_account=$(gcloud iam service-accounts describe "$gcp_service_account_name@$gcp_project_id.iam.gserviceaccount.com" 2>&1) 62 | 63 | if [[ ! "$service_account" == *"NOT_FOUND: Unknown service account"* && ! "$service_account" == *"denied on resource (or it may not exist"* ]]; then echo "Deleting service account role "$gcp_service_account_name" and role "$gcp_pulumi_admin_role_name"" 64 | gcloud iam service-accounts delete "$gcp_service_account_name@$gcp_project_id.iam.gserviceaccount.com" 65 | fi 66 | 67 | 68 | 69 | 70 | 71 | # Check if billing is enabled for the project. 72 | # This is needed. Otherwise pulumi up will fail. 73 | # Note that the link generated may not work as expected if you have multiple accounts 74 | gcloud_billing_project=$(gcloud billing projects describe "$gcp_project_id" --format="json") 75 | 76 | is_billing_enabled=$(echo "$gcloud_billing_project" | jq '.billingEnabled') 77 | 78 | if [[ "$is_billing_enabled" == "true" ]]; then 79 | cat << EOF 80 | Billing is enabled for the project. Please disable billing for the project by removing it. 81 | Opening the google cloud billing page in the browser 82 | If you have multiple accounts, the link may not work. Go to the billing page manually. 83 | (https://console.cloud.google.com/billing) 84 | EOF 85 | 86 | open "https://console.cloud.google.com/billing/manage?project=$gcp_project_id&hl=en&" 87 | echo "Please, run this script again to finish removing the project." 88 | echo "We won't proceed with the deletion of the project until billing is disabled." 89 | exit 0 90 | fi 91 | 92 | gcp_project=$(gcloud projects describe "$gcp_project_id" --format=json 2>&1) 93 | 94 | if jq -e . >/dev/null 2>&1 <<<"$gcp_project"; then 95 | if ! echo "$gcp_project" | jq '.lifecycleState == "DELETE_REQUESTED" or .lifecycleState == "DELETED"' >/dev/null 2>&1; then 96 | echo "Deleting project $gcp_project_id" 97 | gcloud projects delete $gcp_project_id 98 | fi 99 | elif [[ ! "$gcp_project" =~ *"(or it may not exist)"* ]]; then 100 | echo "Deleting project $gcp_project_id" 101 | gcloud projects delete $gcp_project_id 102 | fi 103 | 104 | -------------------------------------------------------------------------------- /cli/installs/docker.install.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Function to check if Docker is already installed 4 | check_docker_installed() { 5 | if which docker >/dev/null; then 6 | return 0 # return 0 if installed 7 | else 8 | return 1 # return 1 if not installed 9 | fi 10 | } 11 | 12 | # Function to install Docker on Debian/Ubuntu 13 | install_debian() { 14 | sudo apt-get update 15 | sudo apt-get install -y apt-transport-https ca-certificates curl software-properties-common 16 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - 17 | sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" 18 | sudo apt-get update 19 | sudo apt-get install -y docker-ce 20 | } 21 | 22 | # Function to install Docker on Fedora 23 | install_fedora() { 24 | sudo dnf -y install dnf-plugins-core 25 | sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo 26 | sudo dnf -y install docker-ce docker-ce-cli containerd.io 27 | sudo systemctl start docker 28 | sudo systemctl enable docker 29 | } 30 | 31 | # Function to install Docker on CentOS/RHEL 32 | install_centos() { 33 | sudo yum install -y yum-utils 34 | sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo 35 | sudo yum install -y docker-ce docker-ce-cli containerd.io 36 | sudo systemctl start docker 37 | sudo systemctl enable docker 38 | } 39 | 40 | # Function to install Docker on macOS 41 | install_macos() { 42 | # Assumes Homebrew is installed on macOS 43 | brew cask install docker 44 | open /Applications/Docker.app 45 | } 46 | 47 | # Determine the OS and run the appropriate installation command if Docker is not installed 48 | if check_docker_installed; then 49 | echo "Docker is already installed." 50 | exit 0 51 | fi 52 | 53 | if [ -f /etc/os-release ]; then 54 | . /etc/os-release 55 | case $ID in 56 | ubuntu|debian) 57 | install_debian 58 | ;; 59 | fedora) 60 | install_fedora 61 | ;; 62 | centos|rhel) 63 | install_centos 64 | ;; 65 | *) 66 | echo "Unsupported Linux distribution: $ID" 67 | exit 1 68 | ;; 69 | esac 70 | elif [[ "$OSTYPE" == "darwin"* ]]; then 71 | # Assumes Homebrew is installed on macOS 72 | install_macos 73 | else 74 | echo "Unsupported operating system" 75 | exit 1 76 | fi 77 | echo "Docker installation process completed." 78 | -------------------------------------------------------------------------------- /cli/installs/expect.install.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Function to check if expect is already installed 4 | check_expect_installed() { 5 | if which expect >/dev/null; then 6 | return 0 # return 0 if installed 7 | else 8 | return 1 # return 1 if not installed 9 | fi 10 | } 11 | 12 | # Function to install expect on Debian/Ubuntu 13 | install_debian() { 14 | sudo apt-get update 15 | sudo apt-get install -y expect 16 | } 17 | 18 | # Function to install expect on Fedora 19 | install_fedora() { 20 | sudo dnf install -y expect 21 | } 22 | 23 | # Function to install expect on CentOS/RHEL 24 | install_centos() { 25 | sudo yum install -y expect 26 | } 27 | 28 | # Function to install expect on macOS 29 | install_macos() { 30 | brew install expect 31 | } 32 | 33 | # Determine the OS and run the appropriate installation command if expect is not installed 34 | if check_expect_installed; then 35 | echo "Expect is already installed." 36 | exit 0 37 | fi 38 | 39 | if [ -f /etc/os-release ]; then 40 | . /etc/os-release 41 | case $ID in 42 | ubuntu|debian) 43 | install_debian 44 | ;; 45 | fedora) 46 | install_fedora 47 | ;; 48 | centos|rhel) 49 | install_centos 50 | ;; 51 | *) 52 | echo "Unsupported Linux distribution: $ID" 53 | exit 1 54 | ;; 55 | esac 56 | elif [[ "$OSTYPE" == "darwin"* ]]; then 57 | # Assumes Homebrew is installed on macOS 58 | install_macos 59 | else 60 | echo "Unsupported operating system" 61 | exit 1 62 | fi 63 | echo "Expect installation process completed." 64 | 65 | -------------------------------------------------------------------------------- /cli/installs/golang.install.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Function to check if Go is already installed 4 | check_golang_installed() { 5 | if which go >/dev/null; then 6 | return 0 # return 0 if installed 7 | else 8 | return 1 # return 1 if not installed 9 | fi 10 | } 11 | 12 | # Function to install Go on Debian/Ubuntu 13 | install_debian() { 14 | sudo apt-get update 15 | sudo apt-get install -y golang 16 | } 17 | 18 | # Function to install Go on Fedora 19 | install_fedora() { 20 | sudo dnf install -y golang 21 | } 22 | 23 | # Function to install Go on CentOS/RHEL 24 | install_centos() { 25 | sudo yum install -y golang 26 | } 27 | 28 | # Function to install Go on macOS 29 | install_macos() { 30 | brew install go 31 | } 32 | 33 | # Determine the OS and run the appropriate installation command if Go is not installed 34 | if check_golang_installed; then 35 | echo "Go is already installed." 36 | exit 0 37 | fi 38 | 39 | if [ -f /etc/os-release ]; then 40 | . /etc/os-release 41 | case $ID in 42 | ubuntu|debian) 43 | install_debian 44 | ;; 45 | fedora) 46 | install_fedora 47 | ;; 48 | centos|rhel) 49 | install_centos 50 | ;; 51 | *) 52 | echo "Unsupported Linux distribution: $ID" 53 | exit 1 54 | ;; 55 | esac 56 | elif [[ "$OSTYPE" == "darwin"* ]]; then 57 | # Assumes Homebrew is installed on macOS 58 | install_macos 59 | else 60 | echo "Unsupported operating system" 61 | exit 1 62 | fi 63 | echo "Go installation process completed." 64 | -------------------------------------------------------------------------------- /cli/installs/google.install.bash: -------------------------------------------------------------------------------- 1 | 2 | #!/bin/bash 3 | 4 | source $(pwd)/cli/shared.bash 5 | 6 | # This CLI script will 7 | # 1. Authenticate with Google Cloud CLI. 8 | # 2. Create the project for you. 9 | 10 | # Required Parameters 11 | # project_name="delete3" 12 | # project_description="A Pulumi Project Used to Deploy a Golang application" 13 | # project_stack="dev" 14 | 15 | # We read each of the parameters passed 16 | while [[ $# -gt 0 ]]; do 17 | case "$1" in 18 | --project_name) 19 | project_name="$2" 20 | shift 2 21 | ;; 22 | --project_description) 23 | project_description="$2" 24 | shift 2 25 | ;; 26 | --project_stack) 27 | project_stack="$2" 28 | shift 2 29 | ;; 30 | # Optional. It will be generated if not provided 31 | --gcp_project_id) 32 | gcp_project_id="$2" 33 | shift 2 34 | ;; 35 | *) 36 | echo "Unknown parameter passed: $1" 37 | exit 1 38 | ;; 39 | esac 40 | done 41 | 42 | 43 | # Check for Google Cloud CLI 44 | if command -v gcloud > /dev/null 2>&1; then 45 | echo "Google Cloud CLI is installed." 46 | else 47 | echo "Google Cloud CLI is not installed." 48 | fi 49 | 50 | authenticated_account=$(gcloud auth list --format="value(account)" --filter=status:ACTIVE) 51 | 52 | if [ -z "$authenticated_account" ]; then 53 | echo "No active authenticated user found in google." 54 | echo "We will now open a google auth login using glocud auth login" 55 | echo "Once finished open this script again" 56 | gcloud auth login 57 | else 58 | # https://cloud.google.com/sdk/gcloud/reference/auth/login 59 | echo "Authenticated as $authenticated_account" 60 | fi 61 | 62 | 63 | # Checks if the project exists. If it does, skip creation 64 | gcp_project=$(gcloud projects describe "$gcp_project_id" --format=json 2>&1) 65 | # Check if the gcloud command output is valid JSON using jq 66 | if echo "$gcp_project" | jq -e . >/dev/null 2>&1; then 67 | # Extract the lifecycleState from the JSON 68 | lifecycle_state=$(echo "$gcp_project" | jq -r '.lifecycleState') 69 | 70 | # Check if lifecycleState is DELETE_REQUESTED 71 | if [ "$lifecycle_state" == "DELETE_REQUESTED" ]; then 72 | echo "Restoring project $gcp_project_id" 73 | # Uncomment the following line to actually restore the project 74 | gcloud projects undelete $gcp_project_id 75 | fi 76 | exit 0 77 | fi 78 | 79 | 80 | # Function to generate a unique project ID 81 | generate_project_id() { 82 | local random_part=$(generate_random_number) # Random number between 100 and 999 83 | echo "${project_name}-${random_part}" 84 | } 85 | 86 | # Function to check if a project ID exists 87 | project_exists() { 88 | local project_id=$1 89 | if gcloud projects describe $project_id &> /dev/null; then 90 | return 0 # project exists 91 | else 92 | echo "Project with ID $project_id does not exist." 93 | return 1 # project does not exist 94 | fi 95 | } 96 | 97 | # Function to create a new project 98 | create_project() { 99 | local project_id=$1 100 | gcloud projects create $project_id --name $project_name 101 | } 102 | 103 | # Main logic to generate project ID and create project if it doesn't exist 104 | attempt_limit=5 105 | attempt_count=0 106 | 107 | if [ -z "$gcp_project_id" ]; then 108 | echo "Generating a new project ID..." 109 | new_project_id=$(generate_project_id) 110 | else 111 | echo "Using provided project ID: $gcp_project_id" 112 | new_project_id="$gcp_project_id" 113 | fi 114 | 115 | while [ $attempt_count -lt $attempt_limit ]; do 116 | if ! project_exists $new_project_id; then 117 | echo "Creating project with ID: $new_project_id" 118 | project_result=$(create_project $new_project_id 2>&1) 119 | if [[ "$project_result" =~ *"Please try an alternative ID"* ]]; then 120 | echo "Project ID $new_project_id is not available, generating a new one..." 121 | new_project_id=$(generate_project_id) 122 | ((attempt_count++)) 123 | continue 124 | fi 125 | echo $project_result 126 | 127 | if [[ "$project_result" =~ failed ]]; then 128 | echo "Project creation failed." 129 | exit 1 130 | fi 131 | break 132 | else 133 | echo "Project ID $new_project_id already exists, generating a new one..." 134 | new_project_id=$(generate_project_id) 135 | ((attempt_count++)) 136 | fi 137 | done 138 | 139 | if [ $attempt_count -eq $attempt_limit ]; then 140 | echo "Failed to create a unique project after $attempt_limit attempts." 141 | exit 1 142 | fi 143 | 144 | if gcloud projects list --format="get(projectId)" --filter="name:${project_name}" | grep -q .; then 145 | echo "Project with name $project_name exists." 146 | else 147 | echo "Project with name $project_name does not exist." 148 | fi 149 | 150 | exit 0 151 | -------------------------------------------------------------------------------- /cli/installs/jq.install.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Function to check if jq is already installed 4 | check_jq_installed() { 5 | if which jq >/dev/null; then 6 | return 0 # return 0 if installed 7 | else 8 | return 1 # return 1 if not installed 9 | fi 10 | } 11 | 12 | # Function to install jq on Debian/Ubuntu 13 | install_debian() { 14 | sudo apt-get update 15 | sudo apt-get install -y jq 16 | } 17 | 18 | # Function to install jq on Fedora 19 | install_fedora() { 20 | sudo dnf install -y jq 21 | } 22 | 23 | # Function to install jq on CentOS/RHEL 24 | install_centos() { 25 | sudo yum install -y jq 26 | } 27 | 28 | # Function to install jq on macOS 29 | install_macos() { 30 | brew install jq 31 | } 32 | 33 | # Determine the OS and run the appropriate installation command if jq is not installed 34 | if check_jq_installed; then 35 | echo "jq is already installed." 36 | exit 0 37 | fi 38 | 39 | if [ -f /etc/os-release ]; then 40 | . /etc/os-release 41 | case $ID in 42 | ubuntu|debian) 43 | install_debian 44 | ;; 45 | fedora) 46 | install_fedora 47 | ;; 48 | centos|rhel) 49 | install_centos 50 | ;; 51 | *) 52 | echo "Unsupported Linux distribution: $ID" 53 | exit 1 54 | ;; 55 | esac 56 | elif [[ "$OSTYPE" == "darwin"* ]]; then 57 | # Assumes Homebrew is installed on macOS 58 | install_macos 59 | else 60 | echo "Unsupported operating system" 61 | exit 1 62 | fi 63 | echo "jq installation process completed." 64 | -------------------------------------------------------------------------------- /cli/installs/pulumi.install.bash: -------------------------------------------------------------------------------- 1 | while [[ $# -gt 0 ]]; do 2 | case "$1" in 3 | --project_name) 4 | project_name="$2" 5 | shift 2 6 | ;; 7 | --project_description) 8 | project_description="$2" 9 | shift 2 10 | ;; 11 | --pulumi_dir) 12 | pulumi_dir="$2" 13 | shift 2 14 | ;; 15 | --project_language) 16 | project_language="$2" 17 | shift 2 18 | ;; 19 | --original_dir) 20 | original_dir="$2" 21 | shift 2 22 | ;; 23 | --project_stack) 24 | project_stack="$2" 25 | shift 2 26 | ;; 27 | *) 28 | echo "Unknown parameter passed: $1" 29 | exit 1 30 | ;; 31 | esac 32 | done 33 | 34 | pulumi_create() { 35 | # echo "Creating pulumi project - $project_name" 36 | # pulumi new "$project_language" --name "$project_name" --description "$project_description" --dir "$pulumi_dir" --stack "$project_stack" --non-interactive --yes 37 | echo "Creating pulumi project - $project_name" 38 | /usr/bin/expect < /dev/null 2>&1; then 85 | echo "Pulumi CLI is installed." 86 | else 87 | # Download the Pulumi install script 88 | curl -fsSL https://get.pulumi.com | sh 89 | 90 | # Add Pulumi to PATH 91 | export PATH=$PATH:$HOME/.pulumi/bin 92 | fi 93 | 94 | 95 | 96 | if [ -f "$pulumi_dir/Pulumi.yaml" ] || [ -f "$pulumi_dir/Pulumi.yml" ]; then 97 | echo "Pulumi project exists." 98 | else 99 | # Run the function and capture output 100 | # output=$(pulumi_create 2>&1) 101 | pulumi_create 102 | exit_status=$? 103 | if [ $exit_status -eq 2 ]; then 104 | echo "Don't worry about the error above. It's just a warning that the project already exists." 105 | cd "$pulumi_dir" 106 | # Install go packages, assuming go.mod file is present in the directory 107 | go mod tidy 108 | cd "$original_dir" 109 | echo "Pulumi project was created successfully" 110 | elif [ $exit_status -eq 3 ]; then 111 | echo "Error: The command timed out." 112 | elif [ $exit_status -eq 0 ]; then 113 | echo "Pulumi project was created successfully." 114 | else 115 | echo "An error occurred with exit code: $exit_status" 116 | fi 117 | 118 | fi 119 | 120 | echo "Project stack - $project_stack" 121 | cd "$pulumi_dir" 122 | pulumi stack select "$project_stack" -c 123 | cd "$original_dir" -------------------------------------------------------------------------------- /cli/shared.bash: -------------------------------------------------------------------------------- 1 | generate_random_number() { 2 | local min=${1:-0} 3 | local max=${2:-100} 4 | echo $((min + RANDOM % (max - min + 1))) 5 | } 6 | 7 | update_config() { 8 | local config_path="$(pwd)/config.json" 9 | local key="$1" 10 | local value="$2" 11 | 12 | jq "$key |= \"$value\"" "$config_path" > temp.json 13 | mv temp.json "$config_path" 14 | } 15 | 16 | failwith() { { echo -n "error: "; printf "%s\n" "$@"; } 1>&2; exit 1; } 17 | 18 | # https://codereview.stackexchange.com/a/279533/103073 19 | rpath() { # mimics a sane `realpath` for insane OSs that lack one 20 | local relbase="" relto="" 21 | while [[ x"${1-}" = x-* ]]; do case "$1" in 22 | ( "--relative-base="* ) relbase="$(rpath ${1#*=})" ;; 23 | ( "--relative-to="* ) relto="$(rpath ${1#*=})" ;; 24 | ( * ) failwith "unrecognized option '$1'" 25 | esac; shift; done 26 | if [[ "$#" -eq 0 ]]; then failwith "missing operand"; fi 27 | if [[ -n "$relto" && -n "$relbase" && "${relto#"$relbase/"}" = "$relto" ]]; then 28 | # relto is not a subdir of relbase => ignore both 29 | relto="" relbase="" 30 | elif [[ -z "$relto" && -n "$relbase" ]]; then 31 | # relbase is set but relto isn't => set relto from relbase to simplify 32 | relto="$relbase" 33 | fi 34 | local p d f n=0 up common PWD0="$PWD" 35 | for p in "$@"; do 36 | cd "$PWD0" 37 | while (( n++ < 50 )); do 38 | d="$(dirname "$p")" 39 | if [[ ! -e "$d" ]]; then failwith "$p: No such file or directory"; fi 40 | if [[ ! -d "$d" ]]; then failwith "$p: Not a directory"; fi 41 | cd -P "$d" 42 | f="$(basename "$p")" 43 | if [[ -h "$f" ]]; then p="$(readlink "$f")"; continue; fi 44 | # done getting the realpath 45 | local r="$PWD/$f" 46 | if [[ -n "$relto" && ( -z "$relbase" || "${r#"$relbase/"}" != "$r" ) ]]; then 47 | common="$relto" up="" 48 | while [[ "${r#"$common/"}" = "$r" ]]; do 49 | common="${common%/*}" up="..${up:+"/$up"}" 50 | done 51 | if [[ "$common" != "/" ]]; then 52 | r="${up:+"$up"/}${r#"$common/"}" 53 | fi 54 | fi 55 | cd "$PWD0"; echo "$r"; continue 2 56 | done 57 | cd "$PWD0"; failwith "$1: Too many levels of symbolic links" 58 | done 59 | } -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_name": "gcp-project-name", 3 | "project_description": "A Pulumi Project Used to Deploy a Golang application", 4 | "project_stack": "dev", 5 | "project_language": "go", 6 | "gcp_project_id": "", 7 | "gcp_service_account_name": "pulumi-gcp", 8 | "gcp_service_account_display_name": "Pulumi Service Account", 9 | "gcp_service_account_description": "Service Account for Pulumi GCP", 10 | "gcp_pulumi_service_account_key_path": "~/keys/gcp/gcp-project-pulumi-service-account-key-file.json", 11 | "gcp_pulumi_admin_role_name": "pulumi_admin_role", 12 | "gcp_docker_image_name": "my-docker-name", 13 | "gcp_artifact_registry_service_name": "artifact-registry-api", 14 | "gcp_artifact_registry_repository_name": "my-repo", 15 | "gcp_cloud_run_admin_service_name": "cloud-run-admin-service", 16 | "gcp_cloud_run_service_name": "cloud-run-service", 17 | "gcp_location": "us-east1", 18 | "gcp_image_tag": "latest", 19 | "dockerfile_relative_path": "./app/", 20 | "pulumi_relative_path": "./pulumi/" 21 | } 22 | -------------------------------------------------------------------------------- /del.bash: -------------------------------------------------------------------------------- 1 | cat <&1) 34 | 35 | if [[ ! "$pulumi_stack" =~ .*"no stack named".* ]]; then 36 | echo "Removing pulumi and destroying the stack" 37 | pulumi destroy -y 38 | pulumi stack rm "$project_stack" -y 39 | fi 40 | 41 | bash $google_delete_path \ 42 | --gcp_project_id "$gcp_project_id" \ 43 | --gcp_service_account_name "$gcp_service_account_name" \ 44 | --gcp_pulumi_service_account_key_path "$gcp_pulumi_service_account_key_path" \ 45 | --gcp_pulumi_admin_role_name "$gcp_pulumi_admin_role_name" \ 46 | --config_path "$config_path" 47 | 48 | echo "All of your resources have been deleted" -------------------------------------------------------------------------------- /gen.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # DEBUG ONLY 3 | source $(pwd)/cli/shared.bash 4 | 5 | echo "Welcome to Deploud! We will get the project ready." 6 | echo "This script is idempotent - Execute it as many times as you want without any side effects." 7 | 8 | cat << EOF 9 | We will install: 10 | - Docker 11 | - Expect 12 | - jq 13 | - Google Cloud SDK CLI" 14 | - Pulumi CLI 15 | 16 | Additionally, as there are one time configurations, you will need 17 | to open your browser to perform them: 18 | 19 | - Google Cloud SDK CLI: Login to your Google Account 20 | - Google Cloud Billing: Enable billing for the project 21 | 22 | EOF 23 | 24 | 25 | # PREPARE 26 | # We use jq to read from the config.json 27 | # The config.json will be updated by the bash scripts in case it 28 | # finds any conflicting values 29 | jq_install_path="$(pwd)/cli/installs/jq.install.bash" 30 | chmod +x "$jq_install_path" 31 | bash $jq_install_path 32 | config_path="$(pwd)/config.json" 33 | # END PREPARE 34 | 35 | 36 | original_dir=$(pwd) 37 | script_base_path="$(pwd)/scripts" 38 | pulumi_script="$script_base_path/_main.go" 39 | roles_script="$script_base_path/_roles.gcp.yml" 40 | 41 | project_name=$(jq -r '.project_name' "$config_path") 42 | project_description=$(jq -r '.project_description' "$config_path") 43 | project_stack=$(jq -r '.project_stack' "$config_path") 44 | project_language=$(jq -r '.project_language' "$config_path") 45 | gcp_service_account_name=$(jq -r '.gcp_service_account_name' "$config_path") 46 | gcp_service_account_display_name=$(jq -r '.gcp_service_account_display_name' "$config_path") 47 | gcp_service_account_description=$(jq -r '.gcp_service_account_description' "$config_path") 48 | gcp_project_id=$(jq -r '.gcp_project_id' "$config_path") 49 | gcp_pulumi_service_account_key_path=$(jq -r '.gcp_pulumi_service_account_key_path' "$config_path") 50 | gcp_pulumi_service_account_key_path="${gcp_pulumi_service_account_key_path/#\~/$HOME}" 51 | gcp_pulumi_admin_role_name=$(jq -r '.gcp_pulumi_admin_role_name' "$config_path") 52 | dockerfile_relative_path=$(jq -r '.dockerfile_relative_path' "$config_path") 53 | pulumi_relative_path=$(jq -r '.pulumi_relative_path' "$config_path") 54 | 55 | pulumi_relative_dir="./pulumi" 56 | pulumi_dir="$pulumi_relative_dir" 57 | # pulumi_dir=$(realpath "$pulumi_relative_dir") 58 | relative_dockerfile_path=$(rpath --relative-to="$pulumi_relative_dir" "$dockerfile_relative_path") 59 | 60 | gcp_roles_path="$pulumi_dir/roles.gcp.yml" 61 | # Add executable permissions 62 | expect_install_path="$(pwd)/cli/installs/expect.install.bash" 63 | docker_install_path="$(pwd)/cli/installs/docker.install.bash" 64 | google_install_path="$(pwd)/cli/installs/google.install.bash" 65 | pulumi_install_path="$(pwd)/cli/installs/pulumi.install.bash" 66 | golang_install_path="$(pwd)/cli/installs/golang.install.bash" 67 | 68 | google_config_path="$(pwd)/cli/configs/google.config.bash" 69 | pulumi_config_path="$(pwd)/cli/configs/pulumi.config.bash" 70 | 71 | chmod +x "$expect_install_path" 72 | chmod +x "$docker_install_path" 73 | chmod +x "$golang_install_path" 74 | 75 | chmod +x "$google_install_path" 76 | chmod +x "$pulumi_install_path" 77 | 78 | chmod +x "$google_config_path" 79 | chmod +x "$pulumi_config_path" 80 | 81 | 82 | # CLI INSTALLATIONS 83 | # We need to create the project in the respective cloud provider. 84 | # ---------------------------------- 85 | 86 | # Install Required CLI Tools 87 | bash $expect_install_path 88 | bash $docker_install_path 89 | bash $golang_install_path 90 | 91 | 92 | # Execute Google CLI Setup 93 | bash $google_install_path --project_name "$project_name" \ 94 | --project_description "$project_description" \ 95 | --project_stack "$project_stack" \ 96 | --gcp_project_id "$gcp_project_id" 97 | 98 | google_install_result=$? 99 | 100 | if [ $google_install_result -ne 0 ]; then 101 | echo "$google_install-path failed." 102 | exit $google_install_result 103 | fi 104 | 105 | bash $pulumi_install_path --project_name "$project_name" \ 106 | --project_description "$project_description" --pulumi_dir "$pulumi_dir" \ 107 | --original_dir "$original_dir" --project_stack "$project_stack"\ 108 | --project_language "$project_language" 109 | pulumi_result=$? 110 | 111 | if [ $pulumi_result -ne 0 ]; then 112 | echo "$pulumi_result-path failed." 113 | exit $pulumi_result 114 | fi 115 | 116 | # Install the script 117 | cp $pulumi_script $pulumi_dir/main.go 118 | cp $roles_script "$gcp_roles_path" 119 | 120 | 121 | # Update and read it again 122 | gcp_project_id=$(jq -r '.gcp_project_id' "$config_path") 123 | 124 | bash $google_config_path --project_name "$project_name" \ 125 | --project_description "$project_description" --project_stack "$project_stack"\ 126 | --gcp_service_account_name "$gcp_service_account_name" \ 127 | --gcp_service_account_display_name "$gcp_service_account_display_name" \ 128 | --gcp_project_id "$gcp_project_id" \ 129 | --gcp_service_account_description "$gcp_service_account_description" \ 130 | --gcp_pulumi_service_account_key_path "$gcp_pulumi_service_account_key_path" \ 131 | --gcp_pulumi_admin_role_name "$gcp_pulumi_admin_role_name" \ 132 | --gcp_roles_path "$gcp_roles_path" \ 133 | --config_path "$config_path" 134 | 135 | google_config_result=$? 136 | 137 | 138 | if [ $google_config_result -ne 0 ]; then 139 | echo "$google_config_path failed." 140 | exit $google_config_result 141 | fi 142 | 143 | bash $pulumi_config_path --project_name "$project_name" \ 144 | --gcp_project_id "$gcp_project_id" \ 145 | --pulumi_dir "$pulumi_dir" \ 146 | --original_dir "$original_dir" \ 147 | --gcp_pulumi_service_account_key_path "$gcp_pulumi_service_account_key_path" 148 | 149 | # We read it again, in case it was updated 150 | gcp_project_id=$(jq -r '.gcp_project_id' "$config_path") 151 | gcp_docker_image_name=$(jq -r '.gcp_docker_image_name' "$config_path") 152 | gcp_artifact_registry_service_name=$(jq -r '.gcp_artifact_registry_service_name' "$config_path") 153 | gcp_artifact_registry_repository_name=$(jq -r '.gcp_artifact_registry_repository_name' "$config_path") 154 | gcp_cloud_run_admin_service_name=$(jq -r '.gcp_cloud_run_admin_service_name' "$config_path") 155 | gcp_cloud_run_service_name=$(jq -r '.gcp_cloud_run_service_name' "$config_path") 156 | gcp_location=$(jq -r '.gcp_location' "$config_path") 157 | gcp_image_tag=$(jq -r '.gcp_image_tag' "$config_path") 158 | 159 | 160 | echo "Updating main.go file" 161 | 162 | # Run sed to make the replacements 163 | sed -i '' \ 164 | -e "s/const gcpProjectId = \"[^\"]*\"/const gcpProjectId = \"$gcp_project_id\"/" \ 165 | -e "s/const dockerImageName = \"[^\"]*\"/const dockerImageName = \"$gcp_docker_image_name\"/" \ 166 | -e "s/const artifactRegistryServiceName = \"[^\"]*\"/const artifactRegistryServiceName = \"$gcp_artifact_registry_service_name\"/" \ 167 | -e "s/const artifactRegistryRepoName = \"[^\"]*\"/const artifactRegistryRepoName = \"$gcp_artifact_registry_repository_name\"/" \ 168 | -e "s/const artifactRegistryRepoLocation = \"[^\"]*\"/const artifactRegistryRepoLocation = \"$gcp_location\"/" \ 169 | -e "s/const cloudRunAdminServiceName = \"[^\"]*\"/const cloudRunAdminServiceName = \"$gcp_cloud_run_admin_service_name\"/" \ 170 | -e "s/const cloudRunServiceName = \"[^\"]*\"/const cloudRunServiceName = \"$gcp_cloud_run_service_name\"/" \ 171 | -e "s/const cloudRunLocation = \"[^\"]*\"/const cloudRunLocation = \"$gcp_location\"/" \ 172 | -e "s/const imageTag = \"[^\"]*\"/const imageTag = \"$gcp_image_tag\"/" \ 173 | -e "s|Context: pulumi.String([^)]*),|Context: pulumi.String(\"$relative_dockerfile_path\"),|" \ 174 | "$pulumi_dir/main.go" 175 | 176 | 177 | # Updates the go file with proper names 178 | # Run sed to make the replacements 179 | echo "Project setup is complete. You can now start developing your application." 180 | 181 | echo "To run the application:" 182 | echo " 183 | 1. Start Docker. 184 | 2. Go to the pulumi directory, and run pulumi up. 185 | " -------------------------------------------------------------------------------- /images/docker-explanation.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superjose/deploy-golang-cloudrun/e61d0aa43123d8d524520181dc72d29a204a9446/images/docker-explanation.avif -------------------------------------------------------------------------------- /scripts/_main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "os" 7 | 8 | "github.com/joho/godotenv" 9 | "github.com/pulumi/pulumi-docker/sdk/v3/go/docker" 10 | "github.com/pulumi/pulumi-gcp/sdk/v7/go/gcp/artifactregistry" 11 | "github.com/pulumi/pulumi-gcp/sdk/v7/go/gcp/cloudrun" 12 | "github.com/pulumi/pulumi-gcp/sdk/v7/go/gcp/projects" 13 | "github.com/pulumi/pulumi/sdk/v3/go/pulumi" 14 | ) 15 | 16 | // This is the name you created in Google Cloud Platform (GCP). 17 | const gcpProjectId = "deploy-to-cloud-run-go" 18 | 19 | // The Docker Image Name 20 | const dockerImageName = "my-app-docker" 21 | const artifactRegistryServiceName = "artifact-registry-api" 22 | const artifactRegistryRepoName = "my-app-artifact-repo" 23 | const artifactRegistryRepoLocation = "us-east1" 24 | const cloudRunAdminServiceName = "cloud-run-admin-service" 25 | const cloudRunServiceName = "cloud-run-service" 26 | 27 | // For more info: https://cloud.google.com/run/docs/locations 28 | const cloudRunLocation = "us-east1" 29 | 30 | // The tag for the Docker image 31 | const imageTag = "latest" 32 | 33 | // This is a url like: us-east1-docker.pkg.dev 34 | // It is used to push the Docker image to Google Container Registry 35 | // For more info: https://cloud.google.com/container-registry/docs/pushing-and-pulling 36 | // The format is: -docker.pkg.dev 37 | var dockerGCPServer = cloudRunLocation + "-docker.pkg.dev" 38 | 39 | // The full path to the Docker image 40 | // It is used to deploy the Docker image to Google Cloud Run 41 | // The format is: -docker.pkg.dev///: 42 | // For more info: https://cloud.google.com/run/docs/deploying 43 | // Example: us-east1-docker.pkg.dev/deploy-to-cloud-run-go/my-app--artifact-repo/my-app-docker:latest 44 | var dockerImageWithPath = dockerGCPServer + "/" + gcpProjectId + "/" + artifactRegistryRepoName + "/" + dockerImageName + ":" + imageTag 45 | 46 | func main() { 47 | 48 | // Load the .env file 49 | err := godotenv.Load() 50 | 51 | if err != nil { 52 | log.Fatal("Error loading .env file") 53 | } 54 | 55 | pulumi.Run(func(ctx *pulumi.Context) error { 56 | 57 | enabledServices, serviceResultErr := enableServices(ctx) 58 | 59 | if serviceResultErr != nil { 60 | return serviceResultErr 61 | } 62 | 63 | artifactRegistryRepo, createArtifactErr := createArtifactRegistryNewRepository(ctx, &enabledServices) 64 | if createArtifactErr != nil { 65 | return createArtifactErr 66 | } 67 | 68 | dockerImage, buildAndPushErr := buildAndPushToContainerRegistry(ctx, &enabledServices, artifactRegistryRepo) 69 | 70 | if buildAndPushErr != nil { 71 | return buildAndPushErr 72 | } 73 | 74 | deployContainerErr := deployContainerToCloudRun(ctx, &enabledServices, dockerImage) 75 | 76 | if deployContainerErr != nil { 77 | return deployContainerErr 78 | } 79 | 80 | return nil 81 | }) 82 | } 83 | 84 | type EnabledServices struct { 85 | CloudRunService *projects.Service `pulumi:"cloudRunService"` 86 | ArtifactRegistryService *projects.Service `pulumi:"artifactRegistryService"` 87 | } 88 | 89 | func enableServices(ctx *pulumi.Context) (EnabledServices, error) { 90 | cloudResourceManager, cloudResourceErr := projects.NewService(ctx, "cloud-resource-manager", &projects.ServiceArgs{ 91 | Service: pulumi.String("cloudresourcemanager.googleapis.com"), 92 | Project: pulumi.String(gcpProjectId), 93 | }) 94 | 95 | if cloudResourceErr != nil { 96 | return EnabledServices{}, cloudResourceErr 97 | } 98 | 99 | cloudRunService, cloudRunAdminErr := projects.NewService(ctx, cloudRunAdminServiceName, &projects.ServiceArgs{ 100 | Service: pulumi.String("run.googleapis.com"), 101 | Project: pulumi.String(gcpProjectId), 102 | }, pulumi.DependsOn([]pulumi.Resource{cloudResourceManager})) 103 | 104 | if cloudRunAdminErr != nil { 105 | return EnabledServices{}, cloudRunAdminErr 106 | } 107 | 108 | artifactRegistryService, err := projects.NewService(ctx, artifactRegistryServiceName, &projects.ServiceArgs{ 109 | Service: pulumi.String("artifactregistry.googleapis.com"), 110 | }, pulumi.DependsOn([]pulumi.Resource{cloudResourceManager})) 111 | 112 | if err != nil { 113 | return EnabledServices{}, err 114 | } 115 | return EnabledServices{ 116 | CloudRunService: cloudRunService, 117 | ArtifactRegistryService: artifactRegistryService, 118 | }, nil 119 | } 120 | 121 | func createArtifactRegistryNewRepository(ctx *pulumi.Context, enabledServices *EnabledServices) (*artifactregistry.Repository, error) { 122 | 123 | if enabledServices == nil || enabledServices.ArtifactRegistryService == nil { 124 | return nil, errors.New("enabledServices cannot be nil") 125 | } 126 | 127 | dependingResources := []pulumi.Resource{ 128 | enabledServices.ArtifactRegistryService, 129 | } 130 | 131 | repo, err := artifactregistry.NewRepository(ctx, artifactRegistryRepoName, &artifactregistry.RepositoryArgs{ 132 | Location: pulumi.String(artifactRegistryRepoLocation), 133 | RepositoryId: pulumi.String(artifactRegistryRepoName), 134 | Format: pulumi.String("DOCKER"), 135 | Description: pulumi.String("The repository that will hold social-log Docker images."), 136 | Project: pulumi.String(gcpProjectId), 137 | }, pulumi.DependsOn(dependingResources)) 138 | 139 | if err != nil { 140 | return nil, err 141 | } 142 | 143 | return repo, nil 144 | } 145 | 146 | func buildAndPushToContainerRegistry(ctx *pulumi.Context, enabledServices *EnabledServices, artifactRegistryRepo *artifactregistry.Repository) (*docker.Image, error) { 147 | 148 | if enabledServices == nil || enabledServices.ArtifactRegistryService == nil { 149 | return nil, errors.New("enabledServices cannot be nil") 150 | } 151 | 152 | if artifactRegistryRepo == nil { 153 | return nil, errors.New("artifactRegistryRepo cannot be nil") 154 | } 155 | 156 | // Lookup GOOGLE_CREDENTIALS environment variable which should hold the path to the JSON key file 157 | jsonKeyPath, present := os.LookupEnv("GOOGLE_CREDENTIALS_FILE_PATH") 158 | if !present { 159 | return nil, errors.New("GOOGLE_CREDENTIALS_FILE_PATH environment variable is not set") 160 | } 161 | 162 | // Read the JSON key file 163 | jsonKey, err := os.ReadFile(jsonKeyPath) 164 | if err != nil { 165 | return nil, err 166 | } 167 | 168 | dependingSources := []pulumi.Resource{ 169 | enabledServices.ArtifactRegistryService, 170 | artifactRegistryRepo, 171 | } 172 | 173 | // Build and push Docker image to Google Container Registry using the JSON key 174 | image, err := docker.NewImage(ctx, dockerImageName, &docker.ImageArgs{ 175 | Build: &docker.DockerBuildArgs{ 176 | Context: pulumi.String("../"), // Adjust the context according to your project structure 177 | 178 | ExtraOptions: pulumi.StringArray{ 179 | // This option is needed for devices running on ARM architecture, such as Apple M1/M2/MX CPUs 180 | pulumi.String("--platform=linux/amd64"), 181 | }, 182 | }, 183 | ImageName: pulumi.String(dockerImageWithPath), 184 | Registry: &docker.ImageRegistryArgs{ 185 | Server: pulumi.String(dockerGCPServer), 186 | Username: pulumi.String("_json_key"), // Special username for GCP 187 | Password: pulumi.String(string(jsonKey)), // Provide the contents of the key file 188 | }, 189 | }, pulumi.DependsOn(dependingSources)) 190 | if err != nil { 191 | return nil, err 192 | } 193 | 194 | return image, nil 195 | } 196 | 197 | func deployContainerToCloudRun(ctx *pulumi.Context, enabledServices *EnabledServices, dockerImage *docker.Image) error { 198 | 199 | if enabledServices == nil || enabledServices.CloudRunService == nil { 200 | return errors.New("enabledServices cannot be nil") 201 | } 202 | 203 | if dockerImage == nil { 204 | return errors.New("dockerImage cannot be nil") 205 | } 206 | 207 | dependingSources := []pulumi.Resource{ 208 | enabledServices.CloudRunService, 209 | dockerImage, 210 | } 211 | 212 | appService, err := cloudrun.NewService(ctx, cloudRunServiceName, &cloudrun.ServiceArgs{ 213 | Project: pulumi.String(gcpProjectId), 214 | Location: pulumi.String(cloudRunLocation), // Choose the appropriate region for your service 215 | Template: &cloudrun.ServiceTemplateArgs{ 216 | Spec: &cloudrun.ServiceTemplateSpecArgs{ 217 | Containers: cloudrun.ServiceTemplateSpecContainerArray{ 218 | &cloudrun.ServiceTemplateSpecContainerArgs{ 219 | Image: dockerImage.ImageName, 220 | Resources: &cloudrun.ServiceTemplateSpecContainerResourcesArgs{ 221 | Limits: pulumi.StringMap{ 222 | "memory": pulumi.String("256Mi"), // Adjust the memory limit as needed 223 | }, 224 | }, 225 | }, 226 | }, 227 | }, 228 | }, 229 | Traffics: cloudrun.ServiceTrafficArray{ 230 | &cloudrun.ServiceTrafficArgs{ 231 | Percent: pulumi.Int(100), 232 | LatestRevision: pulumi.Bool(true), 233 | }, 234 | }, 235 | }, pulumi.DependsOn(dependingSources)) 236 | 237 | if err != nil { 238 | return err 239 | } 240 | 241 | _, iamErr := cloudrun.NewIamMember(ctx, "invoker", &cloudrun.IamMemberArgs{ 242 | Service: appService.Name, 243 | Location: appService.Location, 244 | Role: pulumi.String("roles/run.invoker"), 245 | Member: pulumi.String("allUsers"), 246 | }) 247 | 248 | if iamErr != nil { 249 | return iamErr 250 | } 251 | 252 | ctx.Export("containerUrl", appService.Statuses.Index(pulumi.Int(0)).Url().ToOutput(ctx.Context())) 253 | return nil 254 | } 255 | -------------------------------------------------------------------------------- /scripts/_roles.gcp.yml: -------------------------------------------------------------------------------- 1 | # https://cloud.google.com/iam/docs/creating-custom-roles#creating 2 | # Yaml to define the Pulumi GCP Roles that need to be created with gcloud CLI 3 | title: Pulumi GCP Roles 4 | description: | 5 | This policy ensures that all GCP roles are created using Pulumi. 6 | stage: GA 7 | # https://cloud.google.com/iam/docs/permissions-reference 8 | includedPermissions: 9 | - serviceusage.services.list 10 | - serviceusage.services.enable 11 | - serviceusage.services.disable 12 | - serviceusage.services.get 13 | - serviceusage.services.use 14 | # Service Account Permissions 15 | - iam.serviceAccounts.create 16 | - iam.serviceAccounts.delete 17 | - iam.serviceAccounts.disable 18 | - iam.serviceAccounts.enable 19 | - iam.serviceAccounts.getIamPolicy 20 | - iam.serviceAccounts.list 21 | - iam.serviceAccounts.setIamPolicy 22 | - iam.serviceAccounts.undelete 23 | - iam.serviceAccounts.update 24 | - resourcemanager.projects.get 25 | # Permissions for GCR 26 | - storage.objects.create 27 | - storage.objects.delete # Optional: only include if you need to delete images 28 | - storage.objects.get 29 | # Permissions for Google Artifact Registry 30 | - artifactregistry.repositories.create 31 | - artifactregistry.repositories.delete 32 | - artifactregistry.repositories.get 33 | - artifactregistry.repositories.list 34 | - artifactregistry.repositories.update 35 | - artifactregistry.repositories.downloadArtifacts 36 | - artifactregistry.repositories.uploadArtifacts 37 | - artifactregistry.repositories.deleteArtifacts 38 | # Permissions 39 | # Permissions 40 | - run.services.create 41 | - run.services.get 42 | - run.services.list 43 | - run.services.update 44 | - run.services.delete 45 | - run.services.getIamPolicy 46 | - run.services.setIamPolicy 47 | - iam.serviceAccounts.actAs 48 | # NOTE: This should be removed the first time you're creating a role. 49 | # This etag is to update the current active role (As GCP lets you manage multiple roles) 50 | # I'm commenting it out so I can always replace the role 51 | # etag: BwYS74Xx5y4= 52 | --------------------------------------------------------------------------------