terraform/modules/cicd/main.tf (207 lines of code) (raw):
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
locals {
repository_name = split("/", replace(var.github_repository_url, "/(.*github.com/)/", ""))[1]
repository_owner = split("/", replace(var.github_repository_url, "/(.*github.com/)/", ""))[0]
artifact_registry_repo = "${google_artifact_registry_repository.default.location}-docker.pkg.dev/${google_artifact_registry_repository.default.project}/${google_artifact_registry_repository.default.name}"
github_repository_url = replace(var.github_repository_url, "/(.*github.com)/", "https://github.com")
new_release_config = templatefile("${path.module}/cloudbuild/new-release.cloudbuild.yaml.tftpl", {})
app_build_config = templatefile("${path.module}/cloudbuild/app-build.cloudbuild.yaml.tftpl", {})
skaffold_config = templatefile("${path.module}/cloudbuild/skaffold.yaml.tftpl", { name = var.run_service_name })
run_config = templatefile("${path.module}/cloudbuild/app-prod.yaml.tftpl",
{ run_service_name = var.run_service_name
lb_ip_address = data.google_compute_global_address.default.address
project_id = var.project_id
run_service_account = data.google_service_account.cloud_run.email
}
)
}
# Retrieve existing resources
data "google_cloud_run_service" "default" {
name = var.run_service_name
project = var.project_id
location = var.region
}
data "google_service_account" "cloud_run" {
account_id = data.google_cloud_run_service.default.template[0].spec[0].service_account_name
}
data "google_compute_global_address" "default" {
project = var.project_id
name = "${var.run_service_name}-reserved-ip"
}
# Create Artifact Registry and the gcr Pub/Sub topic
resource "google_artifact_registry_repository" "default" {
project = var.project_id
location = var.region
repository_id = "dev-journey-repo"
description = "Dev journey artifact registry repo."
format = "DOCKER"
labels = var.labels
depends_on = [
time_sleep.project_services
]
}
resource "google_pubsub_topic" "gcr" {
project = var.project_id
name = "gcr"
}
# Cloud Build resources (triggers, service account, and IAM bindings)
resource "google_service_account" "cloud_build" {
project = var.project_id
account_id = "${substr(var.run_service_name, 0, 22)}-builder"
display_name = "Service Account for Cloud Build deployment to Cloud Run."
}
resource "google_project_iam_member" "builder_logwriter" {
project = var.project_id
role = "roles/logging.logWriter"
member = "serviceAccount:${google_service_account.cloud_build.email}"
}
resource "google_project_iam_member" "builder_deploy_admin" {
project = var.project_id
role = "roles/clouddeploy.admin"
member = "serviceAccount:${google_service_account.cloud_build.email}"
}
resource "google_project_iam_member" "builder_builds_builder" {
project = var.project_id
role = "roles/cloudbuild.builds.builder"
member = "serviceAccount:${google_service_account.cloud_build.email}"
}
resource "google_project_iam_member" "builder_sa_user" {
project = var.project_id
role = "roles/iam.serviceAccountUser"
member = "serviceAccount:${google_service_account.cloud_build.email}"
}
resource "google_project_iam_member" "builder_run_developer" {
project = var.project_id
role = "roles/run.developer"
member = "serviceAccount:${google_service_account.cloud_build.email}"
}
resource "google_cloudbuild_trigger" "app_new_build" {
project = var.project_id
name = "dev-journey-app-build"
description = "Initiates new build of ${var.run_service_name}. Triggers by changes to app on main branch of source repo."
service_account = google_service_account.cloud_build.id
included_files = [
"src/**",
]
github {
name = local.repository_name
owner = local.repository_owner
push {
branch = "^main$"
invert_regex = false
}
}
build {
images = ["${local.artifact_registry_repo}/app:$${SHORT_SHA}"]
substitutions = {
_IMAGE = "${local.artifact_registry_repo}/app"
}
tags = []
options {
logging = "CLOUD_LOGGING_ONLY"
}
dynamic "step" {
for_each = yamldecode(local.app_build_config).steps
content {
args = step.value.args
name = step.value.name
entrypoint = step.value.entrypoint
id = step.value.id
}
}
}
}
resource "google_cloudbuild_trigger" "app_new_release" {
project = var.project_id
name = "dev-journey-new-release"
description = "Triggers on any new build pushed to Artifact Registry. Creates a new release in Cloud Deploy."
service_account = google_service_account.cloud_build.id
pubsub_config {
topic = google_pubsub_topic.gcr.id
}
approval_config {
approval_required = false
}
source_to_build {
uri = local.github_repository_url
ref = "refs/heads/main"
repo_type = "GITHUB"
}
build {
images = []
substitutions = {
_REGION = var.region
_RUN_SERVICE_NAME = var.run_service_name
_PIPELINE_NAME = google_clouddeploy_delivery_pipeline.default.name
_IMAGE = "${local.artifact_registry_repo}/app"
_RUN_CONFIG = local.run_config
_SKAFFOLD_CONFIG = local.skaffold_config
}
tags = []
options {
logging = "CLOUD_LOGGING_ONLY"
}
dynamic "step" {
for_each = yamldecode(local.new_release_config).steps
content {
args = step.value.args
name = step.value.name
entrypoint = step.value.entrypoint
id = step.value.id
}
}
}
}
# Cloud Deploy resources (pipeline, target service account, and IAM bindings)
resource "google_clouddeploy_delivery_pipeline" "default" {
project = var.project_id
location = var.region
name = "dev-journey-delivery"
description = "Basic delivery pipeline for ${var.run_service_name} app."
labels = var.labels
serial_pipeline {
stages {
profiles = ["prod"]
target_id = google_clouddeploy_target.prod.name
}
}
depends_on = [
time_sleep.project_services
]
}
resource "google_service_account" "cloud_deploy" {
project = var.project_id
account_id = "${substr(var.run_service_name, 0, 21)}-deployer"
display_name = "Service Account for Cloud Deploy deployment to Cloud Run."
}
resource "google_project_iam_member" "deploy_job_runner" {
project = var.project_id
role = "roles/clouddeploy.jobRunner"
member = "serviceAccount:${google_service_account.cloud_deploy.email}"
}
resource "google_project_iam_member" "deploy_run_admin" {
project = var.project_id
role = "roles/run.admin"
member = "serviceAccount:${google_service_account.cloud_deploy.email}"
}
resource "google_service_account_iam_binding" "deploy_sa_user_run" {
service_account_id = data.google_service_account.cloud_run.id
role = "roles/iam.serviceAccountUser"
members = [
"serviceAccount:${google_service_account.cloud_deploy.email}",
]
}
resource "google_clouddeploy_target" "prod" {
project = var.project_id
provider = google
location = var.region
name = "dev-journey-prod-target"
description = "Prod target for ${var.run_service_name} app."
execution_configs {
usages = ["RENDER", "DEPLOY", "VERIFY"]
service_account = google_service_account.cloud_deploy.email
}
labels = var.labels
require_approval = true
run {
location = "projects/${var.project_id}/locations/${data.google_cloud_run_service.default.location}"
}
depends_on = [
time_sleep.project_services
]
}