🙂

限定公開のGKE上でセキュアなGithub Actionsのrunnerを構築

2024/01/24に公開

モチベーション

Github Actionsのセルフホストランナーでは、long pollingによりrunner側でingressのfirewallを設定せずにrunnerをデプロイ出来るというのを最近知ったので、GKEで検証していこうと思います。

構成

ざっくりですがこんな感じ。

GKEは限定公開のクラスタとして構築し、踏み台サーバからGKEにリクエストを送ります。
Github Actionsとの通信のためにVPCにはCloud NATをアタッチします。

前提条件

terraformで構築するため、予めインストールしておくこと。(検証はv1.0.0)

構築手順

GCPサービスデプロイ

VPC

まずGKE、踏み台サーバをデプロイさせるためのVPCを準備します。

resource "google_compute_network" "private_network" {
  name                    = var.private_network_name
  project                 = var.project_id
  auto_create_subnetworks = false
}

resource "google_compute_subnetwork" "private_network_subnet" {
  name          = "${var.private_network_name}-subnet"
  ip_cidr_range = var.private_network_subnet
  region        = var.region
  network       = google_compute_network.private_network.self_link
  private_ip_google_access = true
  secondary_ip_range {
    range_name    = "services"
    ip_cidr_range = var.gke_services_subnet 
  }
  secondary_ip_range {
    range_name    = "pods"
    ip_cidr_range = var.gke_pods_subnet
  }
}

podとserviceのIP range確保のため、サブネットの中にセカンダリCIDRを設定します。

Firewall

踏み台サーバにSSH接続するため、network tagがbastionのGCEインスタンスに対し22番portでのアクセスを許可します。

resource "google_compute_firewall" "private_network_allow_ingress_my_instance" {
  name          = "${var.private_network_name}-allow-ingress-bastion-instance"
  network       = google_compute_network.private_network.self_link
  source_ranges = ["0.0.0.0/0"]


  allow {
    protocol = "icmp"
  }

  allow {
    protocol = "tcp"
    ports    = ["22"]
  }

  target_tags = ["bastion"]
}

worker nodeからcontrol planeへのアクセスを許可します。

resource "google_compute_firewall" "private_gke_network_allow_egress_masternode" {
  name               = "private-gke-network-allow-egress-masternode"
  network            = google_compute_network.private_network.self_link
  direction          = "EGRESS"
  destination_ranges = [var.gke_cluster_control_plane_ip_range]

  allow {
    protocol = "tcp"
    ports    = ["443", "10250"]
  }
}

control planeからwork nodeへのアクセスを許可します。

resource "google_compute_firewall" "private_gke_network_allow_igress_masternode" {
  name               = "private-gke-network-allow-igress-masternode"
  network            = google_compute_network.private_network.self_link
  source_ranges      = [var.gke_cluster_control_plane_ip_range]

  allow {
    protocol = "tcp"
    ports    = ["9443"]
  }
}

Cloud NAT

Action RunnerがGithubへアクセスできるようにCloud NATを作成します。

resource "google_compute_router" "nat_router" {
  name    = "${var.private_network_name}-router"
  network = google_compute_network.private_network.self_link
}

resource "google_compute_address" "nat_address" {
  name = "nat-address"
}

resource "google_compute_router_nat" "nat" {
  name                               = "${var.private_network_name}-nat"
  nat_ip_allocate_option             = "MANUAL_ONLY"
  router                             = google_compute_router.nat_router.name
  source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES"
  min_ports_per_vm                   = 64
  nat_ips                            = [google_compute_address.nat_address.self_link]
}

踏み台サーバ

パブリックIPアドレスを持つ踏み台サーバを作成します。

resource "google_compute_address" "bastion_static_ip" {
  name = "bastion-address"
}

resource "google_compute_instance" "my_pri_instance" {
  project      = var.project_id
  zone         = var.zone
  name         = "bastion"
  machine_type = "e2-micro"

  tags = ["bastion"]

  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-11"
    }
  }

  network_interface {
    subnetwork         = google_compute_subnetwork.private_network_subnet.name
    subnetwork_project = var.project_id
    access_config {
      nat_ip = google_compute_address.bastion_static_ip.address
    }
  }

  service_account {
    scopes = ["cloud-platform"]
  }

  metadata_startup_script = "apt-get install kubectl"

}

Private Google Access(Nice to Have)

Private Google Accessとは、プライベートVPC内のリソースがパブリックIPを持たずにGCPサービスにアクセスできる機能です。今回はCloud NATを構築するのでmustではないですが、セキュアな通信を実現するためは必須な機能であるためコードだけ記載しておきます。

resource "google_dns_managed_zone" "google_apis" {
  project    = var.project_id
  name       = "google-apis"
  dns_name   = "googleapis.com."
  visibility = "private"

  private_visibility_config {
    networks {
      network_url = google_compute_network.private_network.id
    }
  }
}

resource "google_dns_record_set" "google_apis_cname" {
  project      = var.project_id
  managed_zone = google_dns_managed_zone.google_apis.name
  name         = "*.${google_dns_managed_zone.google_apis.dns_name}"
  type         = "CNAME"
  ttl          = 300

  rrdatas = ["restricted.googleapis.com."]
}

resource "google_dns_record_set" "google_apis_a" {
  project      = var.project_id
  managed_zone = google_dns_managed_zone.google_apis.name
  name         = "restricted.googleapis.com."
  type         = "A"
  ttl          = 300

  rrdatas = ["199.36.153.4", "199.36.153.5", "199.36.153.6", "199.36.153.7"]
}

プライベートVPCではPGAのIPアドレスへのアクセスを許可していないため、穴あけを行います。

resource "google_compute_firewall" "private_gke_network_allow_egress_google_apis" {
  name               = "private-gke-network-allow-egress-google-apis"
  network            = google_compute_network.private_network.self_link
  direction          = "EGRESS"
  destination_ranges = ["199.36.153.4/30"]

  allow {
    protocol = "all"
  }
}

GKE

いよいよGKEの構築です。

resource "google_container_cluster" "primary" {
  name               = var.cluster_name
  location           = var.zone
  initial_node_count = 1
  network            = google_compute_network.private_network.name
  subnetwork         = google_compute_subnetwork.private_network_subnet.name
  remove_default_node_pool = true

  private_cluster_config {
    enable_private_nodes    = true # 各ノードのパブリックIPを無効化
    enable_private_endpoint = true # マスターノードのパブリックエンドポイントを無効化
    master_ipv4_cidr_block  = var.gke_cluster_control_plane_ip_range
  }

  ip_allocation_policy {
    cluster_secondary_range_name   = "pods"
    services_secondary_range_name  = "services"
  }

  master_authorized_networks_config {
    cidr_blocks {
      cidr_block   = google_compute_subnetwork.private_network_subnet.ip_cidr_range
      display_name = var.private_network_name
    }
  }
}

resource "google_service_account" "default" {
  account_id   = "service-account-id"
  display_name = "Service Account"
}

resource "google_container_node_pool" "node_pool" {
  name       = "my-node-pool"
  cluster    = google_container_cluster.primary.id
  node_count = 4
  node_config {
    machine_type = "e2-medium"
    # Google recommends custom service accounts that have cloud-platform scope and permissions granted via IAM Roles.
    service_account = google_service_account.default.email
    oauth_scopes    = [
      "https://www.googleapis.com/auth/cloud-platform"
    ]
  }
  timeouts {
    create = "30m"
    update = "20m"
  }
}

Github Actions Runnerデプロイ

Github token取得

いよいよGithub Actions RunnerをGKEにデプロイしていきます。
Github tokenが必要になるため取得します。

https://github.com/settings/tokens/new にアクセスし、repoにチェックを入れgenerete tokenをクリックします。

表示されたtokenは後で使用するので保存しておいてください。

github actions定義ファイル作成

適当なリポジトリを作成し、以下のような定義ファイルを作成しておきます。

name: github action test

on:
  push:
    branches: [ "main" ]

jobs:
  test:
    name: test
    runs-on: self-hosted
    steps:
    - id: test
      run: |
        sleep 100
        echo hello world

踏み台サーバ設定

踏み台サーバにSSH接続します。

クラスタの認証情報を取得します。

gcloud container clusters get-credentials private-gke --zone=<zone>

cert namagerデプロイ

action runnerをデプロイするため、最初にcert managerをデプロイします。

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.8.2/cert-manager.yaml

action runners controllerデプロイ

続いて、action runners controllerをデプロイします。

kubectl apply -f \
https://github.com/actions/actions-runner-controller/\
releases/download/v0.22.0/actions-runner-controller.yaml --server-side

ContainerCreatingのまま止まっているため、github tokenを登録するsecretを作成します。

kubectl create secret generic controller-manager \
    -n actions-runner-system \
    --from-literal=github_token=<token>

無事、Runningになりました。

runnerデプロイ

最後にrunnerをデプロイします。

cat << EOS > runner.yaml
apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
  name: runner
  namespace: actions-runner-system
spec:
  replicas: 3
  template:
    spec:
      repository: <ユーザ名>/<リポジトリ>
EOS

確認

デプロイに成功すると、runner podが3台立ち上がります。

この状態でgithub actionsのworkflowを起動すると、runner podがactiveになる事がわかります。

Appendix

変数は以下を使用しました。ご参考まで。

variables.tf
variable "region" {
  type    = string
  default = "us-central1"
}

variable "zone" {
  type    = string
  default = "us-central1-a"
}

variable "project_id" {
  type    = string
  default = "project-id"
}

variable "private_network_name" {
  type    = string
  default = "private-network"
}

variable "gke_cluster_control_plane_ip_range" {
  type    = string
  default = "172.16.0.0/28"
}

variable "cluster_name" {
  type    = string
  default = "private-gke"
}

variable "private_network_subnet" {
  type    = string
  default = "10.10.20.0/24"
}

variable "gke_services_subnet" {
  type    = string
  default = "10.10.21.0/24"
}

variable "gke_pods_subnet" {
  type    = string
  default = "10.20.0.0/16"
}

参考

https://qiita.com/y-uemurax/items/4376e27ccc0b2dcc85f0
https://github.com/actions/actions-runner-controller/blob/master/docs/quickstart.md

Discussion