├── .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 |
--------------------------------------------------------------------------------