From c2ae91f12266c1789b94f43448f08354b811ba9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikuro=E3=81=95=E3=81=84=E3=81=AA?= Date: Wed, 27 Sep 2023 14:11:02 +0900 Subject: [PATCH] feat: Add base for services (#22) Co-authored-by: ITO Manaki (Colk) <53868423+Colk-tech@users.noreply.github.com> --- .github/actions/list_dockerfile/action.yml | 17 +++ .github/workflows/hadolint-else.yaml | 14 +++ .github/workflows/hadolint.yaml | 53 +++++++++ .github/workflows/staging-apply.yaml | 102 ++++++++++++++++++ .github/workflows/staging-plan.yml | 90 ++++++++++++++++ deno.jsonc | 9 -- deno.lock | 21 ---- services/hello/.dockerignore | 2 + services/hello/Dockerfile | 13 +++ main.ts => services/hello/main.ts | 2 +- staging/backend.conf | 1 + staging/main.tf | 120 +++++++++++++++++++++ staging/outputs.tf | 9 ++ staging/terraform.tfvars | 10 ++ staging/variables.tf | 49 +++++++++ 15 files changed, 481 insertions(+), 31 deletions(-) create mode 100644 .github/actions/list_dockerfile/action.yml create mode 100644 .github/workflows/hadolint-else.yaml create mode 100644 .github/workflows/hadolint.yaml create mode 100644 .github/workflows/staging-apply.yaml create mode 100644 .github/workflows/staging-plan.yml delete mode 100644 deno.jsonc delete mode 100644 deno.lock create mode 100644 services/hello/.dockerignore create mode 100644 services/hello/Dockerfile rename main.ts => services/hello/main.ts (80%) create mode 100644 staging/backend.conf create mode 100644 staging/main.tf create mode 100644 staging/outputs.tf create mode 100644 staging/terraform.tfvars create mode 100644 staging/variables.tf diff --git a/.github/actions/list_dockerfile/action.yml b/.github/actions/list_dockerfile/action.yml new file mode 100644 index 00000000..d7094a33 --- /dev/null +++ b/.github/actions/list_dockerfile/action.yml @@ -0,0 +1,17 @@ +# From https://code.dblock.org/2021/09/03/generating-task-matrix-by-looping-over-repo-files-with-github-actions.html + +name: List Dockerfile +description: List all Dockerfile as a matrix +outputs: + matrix: + description: Matrix of all Dockerfile + value: ${{ steps.set_matrix.outputs.matrix }} + +runs: + using: composite + steps: + - uses: actions/checkout@v4 + - id: set_matrix + shell: bash + run: echo "matrix=$(find services -name Dockerfile -maxdepth 2 -print0 | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT + diff --git a/.github/workflows/hadolint-else.yaml b/.github/workflows/hadolint-else.yaml new file mode 100644 index 00000000..3fe299a3 --- /dev/null +++ b/.github/workflows/hadolint-else.yaml @@ -0,0 +1,14 @@ +name: Dockerfile lint + +on: + pull_request: + paths-ignore: + - "Dockerfile" + +jobs: + lint: + name: Dockerfile lint + runs-on: ubuntu-latest + steps: + - run: | + echo "No Dockerfile updated" diff --git a/.github/workflows/hadolint.yaml b/.github/workflows/hadolint.yaml new file mode 100644 index 00000000..9874883b --- /dev/null +++ b/.github/workflows/hadolint.yaml @@ -0,0 +1,53 @@ +name: Dockerfile lint + +on: + pull_request: + paths: + - "Dockerfile" + +jobs: + list_dockerfile: + runs-on: ubuntu-latest + steps: + - uses: "actions/checkout@v2" + - uses: "./github/actions/list_dockerfile" + id: set_matrix + outputs: + matrix: ${{ steps.set_matrix.outputs.matrix }} + + lint: + needs: list_dockerfile + name: Dockerfile lint by Hadolint Action + runs-on: ubuntu-latest + strategy: + matrix: + dockerfile: ${{ fromJson(needs.list_dockerfile.outputs.matrix) }} + + steps: + - uses: "actions/checkout@v4" + + - uses: "hadolint/hadolint-action@v3.1.0" + id: run_hadolint + with: + dockerfile: ${{ matrix.dockerfile }} + no-color: false + no-fail: false + + - name: Create pull request comment + uses: "actions/github-script@v6" + if: github.event_name == 'pull_request' + with: + script: | + const hadolintOutput = ` + #### Hadolint: \`${{ steps.run_hadolint.outcome }}\` + \`\`\` + ${process.env.HADOLINT_RESULTS} + \`\`\` + `; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: hadolintOutput, + }); diff --git a/.github/workflows/staging-apply.yaml b/.github/workflows/staging-apply.yaml new file mode 100644 index 00000000..5c5c5bed --- /dev/null +++ b/.github/workflows/staging-apply.yaml @@ -0,0 +1,102 @@ +name: Staging Terraform Apply + +on: + push: + branches: + - "main" + +env: + TF_CLOUD_ORGANIZATION: "pulsate-dev" + TF_API_TOKEN: "${{ secrets.TF_API_TOKEN }}" + TF_WORKSPACE: "pulsate-staging" + CONFIG_DIRECTORY: "./staging/" + +jobs: + list_dockerfile: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: ./github/actions/list_dockerfile + id: set_matrix + outputs: + matrix: ${{ steps.set_matrix.outputs.matrix }} + images: + needs: list_dockerfile + name: Push Docker images + runs-on: ubuntu-latest + strategy: + matrix: + dockerfile: ${{ fromJson(needs.list_dockerfile.outputs.matrix) }} + + steps: + - uses: "actions/checkout@v4" + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/approvers/oreorebot2 + tags: | + ${{ github.sha }} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.0.0 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ github.token }} + - name: Build and push + uses: docker/build-push-action@v5 + with: + push: true + file: ${{ matrix.dockerfile }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + apply: + needs: images + if: github.repository == 'approvers/pulsate' + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: "actions/checkout@v4" + + - id: "auth" + name: Authenticate to Google Cloud + uses: "google-github-actions/auth@v1" + with: + workload_identity_provider: "projects/1065320521129/locations/global/workloadIdentityPools/ci-pool/providers/ci-provider" + service_account: "staging-deploy-from-github-act@pulsate-staging-400117.iam.gserviceaccount.com" + token_format: "access_token" + + - name: Upload Configuration + uses: "hashicorp/tfc-workflows-github/actions/upload-configuration@v1.0.4" + id: apply-upload + with: + workspace: ${{ env.TF_WORKSPACE }} + directory: ${{ env.CONFIG_DIRECTORY }} + - name: Create Apply Run + uses: "hashicorp/tfc-workflows-github/actions/create-run@v1.0.4" + id: apply-run + with: + workspace: ${{ env.TF_WORKSPACE }} + configuration_version: ${{ steps.apply-upload.outputs.configuration_version_id }} + env: + TF_VAR_access_token: "\"${{ steps.auth.outputs.access_token }}\"" + - name: Apply + uses: "hashicorp/tfc-workflows-github/actions/apply-run@v1.0.4" + if: fromJSON(steps.apply-run.outputs.payload).data.attributes.actions.IsConfirmable + id: apply + with: + run: ${{ steps.apply-run.outputs.run_id }} + comment: "Apply Run from GitHub Actions CI ${{ github.sha }}" diff --git a/.github/workflows/staging-plan.yml b/.github/workflows/staging-plan.yml new file mode 100644 index 00000000..1e8bdbc9 --- /dev/null +++ b/.github/workflows/staging-plan.yml @@ -0,0 +1,90 @@ +name: Staging Terraform Plan + +on: + pull_request: + branches: + - "main" + +env: + TF_CLOUD_ORGANIZATION: "pulsate-dev" + TF_API_TOKEN: "${{ secrets.TF_API_TOKEN }}" + TF_WORKSPACE: "pulsate-staging" + CONFIG_DIRECTORY: "./staging/" + +jobs: + plan: + if: github.repository == 'approvers/pulsate' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + id-token: write + steps: + - uses: "actions/checkout@v4" + + - id: "auth" + name: Authenticate to Google Cloud + uses: "google-github-actions/auth@v1" + with: + workload_identity_provider: "projects/1065320521129/locations/global/workloadIdentityPools/ci-pool/providers/ci-provider" + service_account: "staging-deploy-from-github-act@pulsate-staging-400117.iam.gserviceaccount.com" + token_format: "access_token" + + - name: Upload Configuration + uses: "hashicorp/tfc-workflows-github/actions/upload-configuration@v1.0.4" + id: "plan-upload" + with: + workspace: ${{ env.TF_WORKSPACE }} + directory: ${{ env.CONFIG_DIRECTORY }} + speculative: true + - name: Create Plan Run + uses: "hashicorp/tfc-workflows-github/actions/create-run@v1.0.4" + id: plan-run + with: + workspace: ${{ env.TF_WORKSPACE }} + configuration_version: ${{ steps.plan-upload.outputs.configuration_version_id }} + plan_only: true + env: + TF_VAR_access_token: "\"${{ steps.auth.outputs.access_token }}\"" + - name: Get Plan Output + uses: "hashicorp/tfc-workflows-github/actions/plan-output@v1.0.0" + id: plan-output + with: + plan: ${{ fromJSON(steps.plan-run.outputs.payload).data.relationships.plan.data.id }} + - name: Update PR + uses: "actions/github-script@v6" + id: plan-comment + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const botComment = comments.find(comment => { + return comment.user.type === 'Bot' && comment.body.includes('Terraform Cloud Plan Output') + }); + const output = `#### Terraform Cloud Plan Output + \`\`\` + Plan: ${{ steps.plan-output.outputs.add }} to add, ${{ steps.plan-output.outputs.change }} to change, ${{ steps.plan-output.outputs.destroy }} to destroy. + \`\`\` + [Terraform Cloud Plan](${{ steps.plan-run.outputs.run_link }}) + `; + // 3. Delete previous comment so PR timeline makes sense + if (botComment !== undefined) { + github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: output, + }); + } else { + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output, + }); + } + diff --git a/deno.jsonc b/deno.jsonc deleted file mode 100644 index dead40a3..00000000 --- a/deno.jsonc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "tasks": { - "dev": "deno run --watch main.ts", - "start": "deno run main.ts" - }, - "imports": { - "nhttp": "https://deno.land/x/nhttp@1.3.9/mod.ts" - } -} diff --git a/deno.lock b/deno.lock deleted file mode 100644 index 690f642e..00000000 --- a/deno.lock +++ /dev/null @@ -1,21 +0,0 @@ -{ - "version": "2", - "remote": { - "https://deno.land/x/nhttp@1.3.9/mod.ts": "d0ae92ddd1c18486873544a91ec3bffb8f44d94167bc86a847b3aa19de46d7ad", - "https://deno.land/x/nhttp@1.3.9/src/body.ts": "e2f40220be2f9e4b1d4db936d479569a012865626c65f52421601ebd91f4499e", - "https://deno.land/x/nhttp@1.3.9/src/constant.ts": "0c22374722e9afb53109714b2e9613e3bcbc48b903aa227c87ed295b2f2f5dc5", - "https://deno.land/x/nhttp@1.3.9/src/cookie.ts": "e189d64f76b8e6ce43f496ee64648e8bd6a512206655257ef34414e27b3ca364", - "https://deno.land/x/nhttp@1.3.9/src/error.ts": "13924b12562b33af0d0e5dd9249a8e8a600d284a803f116401d4835e0243ea2c", - "https://deno.land/x/nhttp@1.3.9/src/http_response.ts": "8c209420739ebc86eaada21660cbefdb887315f9c8988c4acd6722b8c90b9c6a", - "https://deno.land/x/nhttp@1.3.9/src/index.ts": "3b5b8c07aa5af67b21b4e009334ba38dd04b19f228ae5789e28a011388143e87", - "https://deno.land/x/nhttp@1.3.9/src/inspect.ts": "9d0b7f20ae2aa117ec2a0d46b19e5fb284bfd671e89ef067148aa22dbb6c6d47", - "https://deno.land/x/nhttp@1.3.9/src/multipart.ts": "5a85d2c210e44a3de04e988d46c558ea4decef9417c75fbf62cde285fb01a451", - "https://deno.land/x/nhttp@1.3.9/src/nhttp.ts": "8430d18d390b7e7a4e388b0ee66c2295d36356d81396cdcc8ea116b40af36e4d", - "https://deno.land/x/nhttp@1.3.9/src/nhttp_util.ts": "12c48684364001b9c46bfff1d72d89e9a859e69ff04935047ded017de80b92e2", - "https://deno.land/x/nhttp@1.3.9/src/request_event.ts": "ef6a8576f8db4b1c4eba9e178d87df552289da3dc17e94223a1981351e4f5c7d", - "https://deno.land/x/nhttp@1.3.9/src/router.ts": "342d15455ffb9787461b423dca88306b048f9a0d5300ef95439e2c0d7369297c", - "https://deno.land/x/nhttp@1.3.9/src/symbol.ts": "7d03c057f4edd6b9797f9220fd46e03c2efa31e4bea7b0a2d41f2d883595efb7", - "https://deno.land/x/nhttp@1.3.9/src/types.ts": "66a19173c55258f2a81746012247c0a2e5004d1cc1063f58a140ffc839375663", - "https://deno.land/x/nhttp@1.3.9/src/utils.ts": "5deb034777702eaac39521ac469aa277d527ad189f7e5bccdc64939d14bf43ed" - } -} diff --git a/services/hello/.dockerignore b/services/hello/.dockerignore new file mode 100644 index 00000000..662789c3 --- /dev/null +++ b/services/hello/.dockerignore @@ -0,0 +1,2 @@ +*.env +Dockerfile diff --git a/services/hello/Dockerfile b/services/hello/Dockerfile new file mode 100644 index 00000000..1ea8405e --- /dev/null +++ b/services/hello/Dockerfile @@ -0,0 +1,13 @@ +FROM denoland/deno:1.37.0 + +EXPOSE 8000 + +WORKDIR /app + +USER deno + +COPY . . + +RUN deno cache main.ts + +CMD ["run", "--allow-net", "main.ts"] diff --git a/main.ts b/services/hello/main.ts similarity index 80% rename from main.ts rename to services/hello/main.ts index c43f25dd..6608bce7 100644 --- a/main.ts +++ b/services/hello/main.ts @@ -1,4 +1,4 @@ -import nhttp from "nhttp"; +import nhttp from "https://deno.land/x/nhttp@1.3.9/mod.ts"; if (import.meta.main) { const app = nhttp(); diff --git a/staging/backend.conf b/staging/backend.conf new file mode 100644 index 00000000..8e2646ef --- /dev/null +++ b/staging/backend.conf @@ -0,0 +1 @@ +bucket = "tf-state-xxxxxxxxx" diff --git a/staging/main.tf b/staging/main.tf new file mode 100644 index 00000000..d6c2f828 --- /dev/null +++ b/staging/main.tf @@ -0,0 +1,120 @@ +terraform { + required_version = "~> 1.5.7" + required_providers { + google = { + source = "hashicorp/google" + version = "4.83.0" + } + } + backend "gcs" { + prefix = "terraform/state" + } +} + +provider "google" { + project = var.project + region = var.location + access_token = var.access_token +} + +resource "google_project_service" "default" { + project = var.project + service = "iamcredentials.googleapis.com" +} + +resource "google_service_account" "github_actions" { + project = var.project + account_id = "github-actions" + display_name = "A service account for GitHub Actions" + description = "link to Workload Identity Pool used by github actions" +} + +resource "google_iam_workload_identity_pool" "github" { + project = var.project + workload_identity_pool_id = "github" + display_name = "github" + description = "Workload Identity Pool for GitHub Actions" +} + +resource "google_iam_workload_identity_pool_provider" "github" { + project = var.project + workload_identity_pool_id = google_iam_workload_identity_pool.github.workload_identity_pool_id + workload_identity_pool_provider_id = "github-provider" + display_name = "github actions provider" + description = "OIDC identity pool provider for execute github actions" + + attribute_mapping = { + "google.subject" = "assertion.sub" + "attribute.repository" = "assertion.repository" + "attribute.owner" = "assertion.repository_owner" + "attribute.refs" = "assertion.ref" + } + + oidc { + issuer_uri = "https://token.actions.githubusercontent.com" + } +} + +resource "google_service_account_iam_member" "github-account-iam" { + service_account_id = google_service_account.github_actions.name + role = "roles/iam.workloadIdentityUser" + member = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github.name}/attribute.repository/${var.repo_name}" +} + +resource "google_compute_network" "vpc_network" { + name = "${var.project_id}-net" + auto_create_subnetworks = false +} + +resource "google_compute_subnetwork" "vpc_subnetwork" { + name = "${var.project_id}-subnet" + region = var.location + network = google_compute_network.vpc_network.name + ip_cidr_range = "10.10.0.0/24" +} + +data "google_container_engine_versions" "gke_version" { + location = var.location + version_prefix = "1.27." +} + +resource "google_container_cluster" "primary" { + name = "pulsate-gke-cluster" + location = var.location + + # We can't create a cluster with no node pool defined, but we want to only use + # separately managed node pools. So we create the smallest possible default + # node pool and immediately delete it. + remove_default_node_pool = true + initial_node_count = 1 + + network = google_compute_network.vpc_network.name + subnetwork = google_compute_subnetwork.vpc_subnetwork.name +} + +resource "google_container_node_pool" "primary_nodes" { + name = google_container_cluster.primary.name + location = var.location + cluster = google_container_cluster.primary.name + + version = data.google_container_engine_versions.gke_version.release_channel_latest_version["STABLE"] + node_count = var.gke_num_nodes + + node_config { + oauth_scopes = [ + "https://www.googleapis.com/auth/logging.write", + "https://www.googleapis.com/auth/monitoring", + ] + + labels = { + env = var.project_id + } + + # preemptible = true + machine_type = "n1-standard-1" + tags = ["gke-node", "${var.project_id}-gke"] + metadata = { + disable-legacy-endpoints = "true" + } + } +} diff --git a/staging/outputs.tf b/staging/outputs.tf new file mode 100644 index 00000000..48a74557 --- /dev/null +++ b/staging/outputs.tf @@ -0,0 +1,9 @@ +output "service_account_github_actions_email" { + description = "github account for github actions" + value = google_service_account.github_actions.email +} + +output "google_iam_workload_identity_pool_provider_github_name" { + description = "Workload Identity Pood Provider ID" + value = google_iam_workload_identity_pool_provider.github.name +} diff --git a/staging/terraform.tfvars b/staging/terraform.tfvars new file mode 100644 index 00000000..45abec02 --- /dev/null +++ b/staging/terraform.tfvars @@ -0,0 +1,10 @@ +project = "pulsate-staging" +project_id = "pulsate-staging-400117" +repo_name = "approvers/pulsate" +location = "asia-northeast1" +container_images = [{ + name = "hello" + image = "ghcr.io/approvers/services/hello" +}] +service_account_name = "staging-deploy-from-github-act@pulsate-staging-400117.iam.gserviceaccount.com" +gke_num_nodes = 1 diff --git a/staging/variables.tf b/staging/variables.tf new file mode 100644 index 00000000..96c276f0 --- /dev/null +++ b/staging/variables.tf @@ -0,0 +1,49 @@ +variable "access_token" { + description = "An acceess token for login to GCP" + type = string + default = null +} + +variable "project" { + description = "A name of a GCP project" + type = string + default = null +} + +variable "project_id" { + description = "A project id of a GCP project" + type = string + default = null +} + +variable "repo_name" { + description = "github repository name" + default = "approvers/pulsate" +} + +variable "location" { + description = "A location of a cloud run instance" + type = string + default = "asia-northeast1" +} + +variable "container_images" { + description = "docker container images" + type = list(object({ + name = string + image = string + })) + default = [] +} + +variable "service_account_name" { + description = "Email address of the IAM service account" + type = string + default = "" +} + +variable "gke_num_nodes" { + description = "Total node count on GKE pool" + type = number + default = 1 +}