3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【GitLab×GKE×SOPS】 SecretマニフェストのSOPSを用いた暗号化・GitLab CI/CDでセキュアに自動デプロイする方法

Posted at

1.はじめに: Kubernetes Secretが抱える問題

  • SecretはKubernetesにおいて機密情報(DBパスワードやAPIキー)を扱うためのリソース。

  • しかし、base64エンコードで格納されるだけで暗号化はされていない。

  • Gitでのバージョン管理やCI/CDツールとの連携時に、平文のSecretが漏洩するリスクがある。

  • 解決策として、SOPS(Secrets OPerationS)を使ってマニフェストそのものを暗号化し、安全に管理・運用する方法を紹介。

これらの問題を解決するために、本記事では以下の方法を紹介します:

✅ SOPS(Secrets OPerationS)を活用し、secretマニフェスト全体を暗号化
✅ GitLabに安全にpushし、CI/CD経由で復号 → 運用までを自動化

2. 🎯 この記事のゴール

SecretをGitLabに安全に組み込み、CI/CDパイプラインでGKEに自動デプロイするまでの一連のセキュアなワークフローを構築。

具体的には以下の構成でクラウドネイティブなToDoアプリを実装・運用。

[GitLab]
    |
 ー[GKE]ーーーーーーーーーーーーーーーーーーーーーーーーー
|                                    |
|[Internet]                        |
|     |                                              |
|[ LoadBalancer Service ]                            |
|     |                                              |
|[ Node.js アプリ ]                                 |
|     |                                              |
|[ Kubernetes Secret ] (DB接続情報など)               |
|     |                                              |
|[ PostgreSQL StatefulSet ] ── [ PersistentVolume ]  |
|     |                                              |
|[ ConfigMap (SQL定義) ]                           |
 ーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

各コンポーネント技術:

コンポーネント 技術
クラスタ GKE(Google Kubernetes Engine)
アプリケーション Node.js(ExpressベースREST API)
データベース PostgreSQL(StatefulSet+PV)
Secret暗号化 SOPS(GCP KMSと統合)
CI/CDパイプライン GitLab CI/CD(マニフェストapply)

3. ⚙️ 実装

 本記事では、クラウドネイティブなToDoアプリケーションを「アプリ開発」「Kubernetesマニフェスト作成」「SOPSによるSecretの安全管理」「GitLab CI/CDによるGKEデプロイ」という本番環境に即した構成で一貫して構築していきます。
(GitHubに必要なディレクトリとファイルは公開してあります。)
https://github.com/Elie314159265/secure_secret_with_sops

3.1 必要なディレクトリとファイルを作成

今回のディレクトリ構成は以下のようになります。GCPで以下のファイルとディレクトリを作成してください。(ディレクトリやファイル名は適切に変更してOK)

k8s_secure_secret_resource_with_sops
|
|ーnodejs-app/
|  |ーDockerfile           # アプリをDockerイメージとしてコンテナ化するための定義
|  |ーindex.js             # Expressで構築されたメインのAPIロジック
|  |ーdb.js                # PostgreSQL接続設定
|  |ーpackage.json         # Node.jsプロジェクトの依存管理と実行定義
|  |ー.env.sample          # 環境変数の定義ファイル
| 
|ーk8s/
|   ├── deployment.yaml              # Node.jsアプリのDeployment
|   ├── postgres-init-sql.yaml      # 初期SQLのConfigMap
|   ├── (postgres-secret.enc.yaml)    # 暗号化されたSecret。後に作成します
|   ├── postgres-secret.yaml        # 復号後の平文Secret(実際にはGitに含めな|い)
|   ├── postgres-service.yaml       # PostgreSQL用のService(Headless)
|   ├── postgres-statefulset.yaml   # PostgreSQLのStatefulSet
|   └── service.yaml                # Node.jsアプリ用のService
|
|ー.gitignore    # コミットしないファイル名を追加
|ー.sops.yaml    #
|ー.gitlab-ci.yml # GitLab CI ファイル。自動デプロイ情報定義

上記ファイルのそれぞれのテンプレートを示します。

🐳 Dockerfile:アプリをDockerで実行するための定義

FROM node:20-alpine

WORKDIR /usr/src/app

COPY package*.json ./
RUN npm install --production

COPY . .

EXPOSE 3000

CMD ["npm", "start"]

✅ 解説:

  • ベースイメージ:node:20-alpine は軽量でセキュアなNode.jsランタイム。

  • 作業ディレクトリ:/usr/src/app にコードを配置。

  • 依存解決:npm install --production により、本番用の依存のみをインストール。

  • CMD:Node.js アプリを npm start で起動(これは後述の package.json に定義されている)。


:page_facing_up: index.js:ExpressベースのREST APIロジック

const express = require('express');
const bodyParser = require('body-parser');
const db = require('./db');
const app = express();
const port = 3000;

✅ エンドポイント機能:

  • GET /todos:全ToDo取得

  • POST /todos:新規ToDo登録

  • DELETE /todos/:id:ToDo削除

✅ 特徴:

  • body-parserでJSONのPOSTボディを受け取れるようにしている。

  • pgのラッパーであるdb.jsを使って、SQLを実行。

  • 簡潔な構成だが、REST API設計・Kubernetesデプロイにも適した構造。


:page_facing_up: db.js:PostgreSQLとの接続モジュール

const { Pool } = require('pg');

const pool = new Pool({
  host: process.env.DB_HOST || 'localhost',
  port: process.env.DB_PORT || 5432,
  user: process.env.DB_USER || 'node_user',
  password: process.env.DB_PASSWORD || 'password',
  database: process.env.DB_NAME || 'node_app_db',
});

module.exports = {
  query: (text, params) => pool.query(text, params),
};

✅ 解説:

  • pg モジュールの Pool クラスを使って、PostgreSQLへのコネクションプールを構築。

  • .env や Kubernetesの Secret 経由で環境変数から設定値を取得。

  • query() メソッドを提供し、SQLを実行可能なシンプルなDBインターフェースとして外部ファイルで使えるように設計。


:page_facing_up: package.json:Node.jsアプリのメタ情報と依存定義

{
  "name": "todo-api",
  "version": "1.0.0",
  "description": "Simple ToDo API with Express and PostgreSQL",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "body-parser": "^1.20.2",
    "express": "^4.18.2",
    "pg": "^8.11.3"
  }
}

✅ 解説:

  • name, version, description:プロジェクトの基本情報

  • main:アプリのエントリポイント

  • scripts.start:npm start で index.js を実行

  • dependencies:使用しているnpmパッケージ


:page_facing_up: .env.sample: 環境変数構成例を示すテンプレート

# PostgreSQL settings
DB_HOST=your-db-host
DB_PORT=1000
DB_USER=your-username
DB_PASSWORD=your-password
DB_NAME=your-database

✅ 解説:

  • この .env.sample は 環境変数のテンプレートファイル
  • Git管理対象にしておき、実際の .env は .gitignore で除外しておくのがベストプラクティス
  • あくまで雛形、参考用なので実際にこの変数が使われるわけではない。
  • どの環境変数を定義すべきかを明示するためのファイル

✅ 各変数の説明

変数名 用途
DB_HOST PostgreSQL サーバのホスト名。K8s内では Servicepostgres を参照。
DB_PORT PostgreSQL のポート番号。デフォルトは 5432
DB_USER DB接続に使うユーザー名。例:node_user
DB_PASSWORD DB接続パスワード。セキュアに管理する必要あり
DB_NAME 使用するデータベース名。例:node_app_db

:page_facing_up: deployment.yaml:Node.jsアプリ用 Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nodejs-app
  labels:
    app: nodejs-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nodejs-app
  template:
    metadata:
      labels:
        app: nodejs-app
    spec:
      containers:
        - name: nodejs
          image: <registoryユーザ名>/todo-app:latest #DockerHubのユーザ名に置き換える
          ports:
            - containerPort: 3000
          envFrom:
            - secretRef:
                name: postgres-secret

🔍 ポイント

  • Node.js アプリのPodを2つ起動

  • PostgreSQL接続情報を Secret から注入

  • LoadBalancer Serviceとセットで外部公開が可能


:page_facing_up:postgres-init-sql.yaml:初期SQLを提供する ConfigMap

apiVersion: v1
kind: ConfigMap
metadata:
  name: postgres-init-sql
data:
  init.sql: |
    CREATE TABLE IF NOT EXISTS todos (
      id SERIAL PRIMARY KEY,
      title TEXT NOT NULL,
      done BOOLEAN NOT NULL DEFAULT false,
      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );

🔍 ポイント

  • PostgreSQL起動時に実行される初期化SQL(/docker-entrypoint-initdb.d/init.sql)を定義

  • StatefulSet の initContainers によりPod起動時にコピーされる


:page_facing_up:postgres-secret.yaml:暗号化前のK8s Secret(Git非公開)

apiVersion: v1
kind: Secret
metadata:
  name: postgres-secret
type: Opaque
stringData:
  DB_HOST: postgres
  DB_PORT: "5432"
  DB_USER: node_user
  DB_PASSWORD: password
  DB_NAME: node_app_db
  POSTGRES_USER: node_user
  POSTGRES_PASSWORD: password
  POSTGRES_DB: node_app_db

🔍 ポイント

  • sops -d postgres-secret.enc.yaml > postgres-secret.yaml によって生成される一時ファイル(復号化)

  • Gitには含めない(.gitignore)

  • kubectl apply -f によって Deployment や StatefulSet で参照される

  • Gitにそのまま Secret マニフェストをpushすると機密情報漏洩リスクがあるのでセキュアなCI/CDパイプラインでもSecretの復号処理が必要

🔐 SOPS(Secrets OPerationS) の導入により以下が可能になります:

項目 内容
暗号化対象 stringDatadata フィールドの値のみ
対応KMS GCP KMS / AWS KMS / PGP などに対応(今回はGCP KMSを利用)
Git管理 .enc.yamlとして安全にリポジトリ管理可能
CI/CD対応 GitLabやGitHub Actionsで復号→applyできる

:page_facing_up:postgres-service.yaml:PostgreSQL用 Headless Service
yaml

apiVersion: v1
kind: Service
metadata:
  name: postgres
spec:
  clusterIP: None  # <- Headlessにする
  selector:
    app: postgres
  ports:
    - port: 5432

🔍 ポイント

  • StatefulSet のためのHeadless Service

  • DNSレコード形式が podname.service.namespace.svc.cluster.local に

  • Podごとに異なるストレージやSQL初期化を行うために必要


:page_facing_up:postgres-statefulset.yaml:PostgreSQLのStatefulSet

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:15-alpine
          ports:
            - containerPort: 5432
          env:
            - name: PGDATA
              value: /var/lib/postgresql/data/pgdata
          envFrom:
            - secretRef:
                name: postgres-secret
          volumeMounts:
            - name: pgdata
              mountPath: /var/lib/postgresql/data
            - name: init-sql
              mountPath: /docker-entrypoint-initdb.d

      initContainers:
        - name: init-sql-loader
          image: busybox
          command: [sh, -c, 'cp /init.sql /docker-entrypoint-initdb.d/init.sql']
          volumeMounts:
            - name: init-sql-config
              mountPath: /init.sql
              subPath: init.sql
            - name: init-sql
              mountPath: /docker-entrypoint-initdb.d

      volumes:
        - name: init-sql
          emptyDir: {}
        - name: init-sql-config
          configMap:
            name: postgres-init-sql

  volumeClaimTemplates:
    - metadata:
        name: pgdata
      spec:
        accessModes: [ReadWriteOnce]
        resources:
          requests:
            storage: 5Gi

🔍 ポイント

  • 永続ボリューム(PV)付きPostgreSQLインスタンス

  • initContainers により init.sql を /docker-entrypoint-initdb.d/ に配置

  • データは永続化され、Pod再起動時も保持される

  • StatefulSet+PVで永続化かつ信頼性のあるDB
    (通常のDeploymentはPodが再作成されるたびにストレージは破棄されるがStatefulSetを使うことでデータを永続化することができ、またpostgres-0, postgres-1, ... のように一意な名前が割り当てることができる。)


:page_facing_up:service.yaml:Node.jsアプリ用のService

apiVersion: v1
kind: Service
metadata:
  name: nodejs-service
spec:
  type: LoadBalancer
  selector:
    app: nodejs-app
  ports:
    - port: 80
      targetPort: 3000

🔍 ポイント

  • 外部向けアクセス用のエントリーポイント

  • GKEでクラウドのLB(例: GCLB)をプロビジョニング

  • 内部の 3000 ポートを外部の 80 にマッピング


✅ k8s/のファイルのまとめ

ファイル名 目的 ポイント
deployment.yaml Node.jsアプリのデプロイ Secretと連携、レプリカ数管理
postgres-init-sql.yaml SQL初期化 ConfigMap + InitContainer
postgres-secret.enc.yaml 暗号化Secret(SOPS) Git管理OK、CI/CDで復号
postgres-secret.yaml 平文Secret Git管理NG、CI/CD生成用
postgres-service.yaml Headless Service StatefulSet用のDNS解決
postgres-statefulset.yaml PostgreSQL本体 永続化、初期化処理付き
service.yaml 外部公開Service LoadBalancer(またはNodePort)

:page_facing_up: .sops.yaml:SOPS による 自動暗号化ルール を定義するための設定ファイル

creation_rules:
  - kms: projects/${PROJECT_ID}/locations/global/keyRings/secret_key/cryptoKeys/secret_key
    encrypted_regex: '^(data|stringData)$'
    path_regex: '.*\.yaml$'

✅ 各項目の解説

項目 意味
creation_rules SOPSが新しく暗号化する際に適用するルールのリスト(配列)
kms GCPのKMSキーを使うことを指定。
これによりGCPプロジェクト内のKMS鍵で暗号化/復号される。
encrypted_regex マニフェスト中のどのキーの値を暗号化するかを正規表現で指定。ここでは data または stringData の中身だけ暗号化対象。
path_regex どのファイル名にこのルールを適用するか。ここでは .yaml 拡張子の全ファイル。

:page_facing_up:.gitignore: Gitで追跡させたくないファイルやディレクトリを明示的に除外するためのファイル

.env
k8s/postgres-secret.yaml

:page_facing_up: .gitlab-ci.yml: GitLab CI/CD を使って GKE(Google Kubernetes Engine)に安全かつ自動的にデプロイするためのパイプライン定義ファイル

stages:
  - deploy

variables:       # ここはGitLab CIの変数で設定推奨
  REGISTRY_HOST: "your-name" #今回はDockerHubを用いるためDockerHubのユーザ名
  DEFAULT_CI_IMAGE: "your-name/kubernetes:latest"  #後ほどDockerfileからビルドするイメージを使います。
  GCP_KMS_KEY: projects/${GCP_PROJECT_ID}/locations/global/keyRings/secret_keyring/cryptoKeys/secret_key

before_script:
  # サービスアカウントキーで認証
  - echo "$GCP_KEY" > ${CI_PROJECT_DIR}/gcp-key.json
  - export GOOGLE_APPLICATION_CREDENTIALS="${CI_PROJECT_DIR}/gcp-key.json"
  - gcloud auth activate-service-account --key-file=${CI_PROJECT_DIR}/gcp-key.json
  - gcloud config set project ${GCP_PROJECT_ID}
  # GKE クラスタ認証情報取得
  - gcloud container clusters get-credentials ${CLUSTER_NAME} --zone ${CLUSTER_ZONE}
  - export KUBECONFIG=~/.kube/config
  # SOPSインストール
  - curl -LO https://github.com/getsops/sops/releases/download/v3.10.2/sops-v3.10.2.linux.amd64
  - chmod +x sops-v3.10.2.linux.amd64
  - mv sops-v3.10.2.linux.amd64 ./sops
  - ./sops -d k8s/postgres-secret.enc.yaml > k8s/postgres-secret.yaml
  # 復号されたファイルは .gitignore 対象にすることで公開リポジトリへの漏洩を防止
deploy:
  stage: deploy  
  script:
    #暗号化マニフェストを除外してデプロイ
    - find k8s -type f ! -name "*.enc.yaml" -exec kubectl apply -f {} \;
    - kubectl get all
default:
  image: $DEFAULT_CI_IMAGE
  timeout: 90m

以下のページでSOPSの最新版を確認してください。
https://github.com/getsops/sops (SOPS公式ページ)
今回は.gitlab-ci.ymlの記述方法や文法は省略します。以下のページを参考にしてください。
https://gitlab-docs.creationline.com/ee/ci/quick_start/ (公式GitLab日本版)
https://archives.docs.gitlab.com/16.0/ee/ci/yaml/gitlab_ci_yaml.html (公式GitLab)
:triangular_flag_on_post:全体の目的

  • SOPS で暗号化された Secret を復号

  • GCP 認証と GKE クラスタ接続を行い

  • kubectl apply で K8s マニフェストを GKE にデプロイする

🧱 stages

stages:
  - deploy
  • ジョブの実行ステージを定義(ここでは1つだけ: deploy)

  • 必要があれば build, test, lint, release なども追加可能

⚙️ variables

variables:       # ここはGitLab CIの変数で設定推奨
  REGISTRY_HOST: "your-name" #今回はDockerHubを用いるためDockerHubのユーザ名
  DEFAULT_CI_IMAGE: "your-name/kubernetes:latest"  #後ほどDockerfileからビルドするイメージを使います。
  GCP_KMS_KEY: projects/${GCP_PROJECT_ID}/locations/global/keyRings/secret_key/cryptoKeys/secret_key
  • DEFAULT_CI_IMAGE は CI 実行用の Docker イメージ(例: kubectl, gcloud,が入った独自イメージ)

  • CLUSTER_NAME, CLUSTER_ZONE などは GitLab の CI/CD variables に登録推奨(セキュリティ・再利用性向上)

  • GCP_KMS_KEY は SOPS による暗号化に使用する GCP KMS キーのパス(復号のため必要)

🛠 before_script
GitLab CI での事前処理。認証・準備がメイン

- echo "$GCP_KEY" > ${CI_PROJECT_DIR}/gcp-key.json
  • GitLab に登録した CI/CD 変数 GCP_KEY(GCPサービスアカウントのJSONキー)をファイルに保存
 - gcloud auth activate-service-account --key-file=${CI_PROJECT_DIR}/gcp-key.json
  - gcloud config set project ${GCP_PROJECT_ID}
   # GKE クラスタ認証情報取得
  - gcloud container clusters get-credentials ${CLUSTER_NAME} --zone ${CLUSTER_ZONE}
  • GCP認証 & GKEクラスタの kubeconfig を取得(kubectl を使う準備)
- sops -d k8s/postgres-secret.enc.yaml > k8s/postgres-secret.yaml
  • Gitに含めた暗号化ファイルを復号し、Kubernetesが読める形式に変換(ポイント:k8s/postgres-secret.yaml は .gitignore 済)

🚀 deploy ジョブ

deploy:
  stage: deploy
  script:
    - find k8s -type f ! -name "*.enc.yaml" -exec kubectl apply -f {} \;
   - kubectl get all
  • デプロイ実行本体

  • kubectl apply でマニフェストを GKE に反映

  • kubectl get all で状態確認(Pod や Service の展開確認)

:gear:defalutセクション

default:
  image: $DEFAULT_CI_IMAGE
  timeout: 90m
  • 使用する Docker イメージとタイムアウト時間の設定

  • イメージはvariablesセクションで定義したイメージを使用


🚀 ここまでで、セキュアなKubernetesマニフェストとCI/CDパイプラインの基礎準備が整いました。次章では、GitLabにおけるCI変数の設定とパイプラインの実行手順に進みます。

3.2 GitLabプロジェクトの作成とCI/CD変数の定義

もしGitLabのアカウント登録をしていない方は以下のページを参考に作成できます。
https://gitlab.com/users/sign_up (GitLabアカウント登録サイト)
https://mashimashi.net/skill/git/515/ (わかりやすいGitLabアカウント登録手順サイト)

まずGitLabでProjectを作成します。
Project名はお好きな名前を設定してください。
Projectが作成できましたら左側の設定から[Setting] > [CI/CD]に移り変数の定義をします。

📌 登録するCI/CD変数一覧(GitLabの [Settings] > [CI/CD] > Variables で設定)

Key Type 説明
CLUSTER_NAME Variable GKEクラスタの名前
CLUSTER_ZONE Variable GKEクラスタのゾーン(例: asia-northeast1-a)
GCP_PROJECT_ID Variable GCPのプロジェクトID(ID欄に表示される文字列)
GCP_KEY Variable サービスアカウントのJSONキー内容(ファイル中身をコピー)
KUBECONFIG_DATA File ~/.kube/config の中身(クラスタ認証情報)

それぞれ変数の内容を解説していきます。

GKEでkubernetesクラスターを作成していなければ作成してください。作成の仕方は以下のコマンドで作成できます。これには5分ほどかかる場合があります。
詳しくは以下のページを参考にしてください
https://qiita.com/Elie1729/items/ad527d8617ac95bab751 (GKEクラスタ構築例)

$ gcloud container clusters create <cluster-name> --zone=asia-northeast1-a
# クラスタ名は任意でOKです(例: `gitlab-gke`)。必要に応じて `--num-nodes` を指定することで初期ノード数も調整できます。

🔐 GitLab に ~/.kube/config を登録
まず~/.kube/configを生成。クラスタの認証情報を取得。.kube/以下にconfigファイルが存在するか確認

$ gcloud container clusters get-credentials gitlab-gke --zone asia-northeast1-a
$ ls .kube

✅ サービスアカウントとGCP認証の準備
サービスアカウント作成(IAM)

$ gcloud iam service-accounts create gitlab-deployer --display-name "GitLab Deployer"

Kubernetesアクセス権限付与。
YOUR_PROJECT_IDはGCPで表示されているプロジェクトIDに置き換えてください。
番号: ・・・・ ID: ・・・・ のIDの方です。

$ gcloud projects add-iam-policy-binding YOUR_PROJECT_ID --member="serviceAccount:gitlab-deployer@YOUR_PROJECT_ID.iam.gserviceaccount.com" --role="roles/container.admin"

JSONキーを生成して保存、key.jsonが生成されたかを確認

$ gcloud iam service-accounts keys create key.json --iam-account=gitlab-deployer@YOUR_PROJECT_ID.iam.gserviceaccount.com
$ ls 

CLUSTER_NAME: GKEのクラスター名
先ほど作成したGKEクラスタ名を定義します。例えばgke-clusterと名前を設定していたらKEYはCLUSTER_NAMEでVALUEはgke-clusterと入力して保存します。

CLUSTER_ZONE: GKEクラスターのゾーン
先ほどの例ではasia-northeast1-aで設定したのでKEYをCLUSTER_ZONE、VALUEをasia-northeast1-aと入力して保存します。

GCP_KEY: GCPにGitLabからアクセスするための情報
JSONキーをGitLab CI/CDのvariableにGCP_KEYとして登録
先ほどのkey.jsonコピー( { から } までをすべてコピー)

$ cat key.json
{
・・・・・・・・
・・・・・・・・
}

KEY:GCP_KEY
Value:コピーしたJSON内容

GCP_PROJECT_ID: GCPのプロジェクトID
これはGCPでプロジェクトを作成した際に付与される一意のIDです。番号: ・・・・ ID: ・・・・ のIDの方です。
KEY: GCP_PROJECT_ID
VALUE: ID

KUBECONDIG_DATA: クラスタの認証情報
~/.kube/configはGCPコンソールからcatなどで表示し、コピーしてCI/CDのVariablesにペーストしてください。ファイル名ではなく中身であることに注意。
⚠ 注意:これは機密情報のため、外部に公開しないこと!セキュリティ上、絶対に公開リポジトリやIssue等に貼らないよう注意しましょう。
ここでこれまでは変数のTypeはVariableでしたがこの変数のみFileとして扱います。
KEY:KUBECONFIG_DATA
Type:File
Value:~/.kube/config の内容

🧩 これで、GitLab側で必要なすべてのCI/CD変数が定義できました。

3.3 GCP KMSキーの作成とSOPSによるSecret暗号化

今回SOPSで暗号化するために用いる暗号化キーはGCP KMS(Google Cloud KMS)キーを利用します。
以下のコマンドでキーを作成します。

# KMSキーリングの作成(グローバルに作成)
$ gcloud kms keyrings create secret_keyring --location global
# KMS暗号化キーの作成
$ gcloud kms keys create secret_key --keyring secret_keyring --location global --purpose encryption

このキーは、SOPSでKubernetes Secretを安全に暗号化・復号化するために使用します。

キーが作成できたら実際にSOPSでsecretマニフェストを暗号化します。
暗号化するファイル: postgres-secret.yaml
YOUR_PROJECT_IDはGCPのプロジェクトIDに変更してください。
また、sops は gcloud CLI の認証情報を使うので認証を済ませておく

# 認証情報を取得(SOPSはgcloudの認証情報を利用)
$ gcloud auth application-default login
# postgres-secret.yaml を GCP KMS で暗号化
$ sops -e --gcp-kms projects/YOUR_PROJECT_ID/locations/global/keyRings/secret_keyring/cryptoKeys/secret_key postgres-secret.yaml > postgres-secret.enc.yaml
$ cat postgres-secret.enc.yaml

暗号化ができたら次にGitLabがGCP KMSキーを復号化用で参照できるように権限を付与します。

🔐 GitLabデプロイ用サービスアカウントにKMS復号権限を付与

  1. GCPコンソール左メニュー → [IAMと管理] → [サービスアカウント]
  2. gitlab-deployer を選択
  3. 「権限を編集」をクリック
  4. ロールを追加:Cloud KMS 暗号鍵復号者(roles/cloudkms.cryptoKeyDecrypter)
  5. 保存

これでGitLab CIパイプラインでpostgres-secret.enc.yamlを復号化してデプロイすることが可能となりました。

3.4 DockerfileでイメージをビルドとDockerHubへのプッシュ

次にnodejs-app/ディレクトリにあるDockerfileでイメージをビルドしてDockerHubにプッシュします。
DockerHubのアカウントを登録してない方は以下のページを参考に作成できます
https://hub.docker.com/ (DockerHub公式ページ)
https://docs.docker.jp/docker-hub/accounts.html (DockerHubアカウント作成手順)

# nodejs-app ディレクトリに移動
$ cd nodejs-app

# Dockerイメージをビルド
$ docker build -t <your-username>/todo-app:latest .

# イメージがビルドできたかを確認
$ docker images

# DockerHub にログイン
$ docker login 

# イメージをDockerHubにプッシュ
$ docker push <your-username>/todo-app:latest
  • <your-dockerhub-username> にはご自身のDockerHubアカウント名を入力してください。
  • ビルド・プッシュが成功すれば、後ほどDeploymentのimageでこのイメージを参照できます。

イメージがビルドできたかを確認するにはDockerHubの公式ページでログインすることで確認できます。
https://hub.docker.com/ (DockerHub)
後ほどkubectl apply -f k8s/でnode.jsをデプロイしますがこのイメージをpullして使います。

🛠 もしビルドやプッシュが難しい場合:
以下の私のパブリックイメージを代用できます。
イメージ名: elie3141/todo-app:latest


🔧 これで、アプリケーションをDockerイメージとしてビルド・登録する準備が整いました。

4. GitLabからGKEへの自動デプロイ

手順は長かったですがGitLabに先ほどのファイルやディレクトリをpushするとGitLabが.gitlab-ci.ymlを読み込んでパイプラインが動きます。

🔐 SSHキーの生成とGitLabへの登録(初回のみ)

GitLabにリポジトリをpushするためにはSSHキーが必要です。
以下のコマンドでキーを作成します:

$ ssh-keygen -t rsa

実行後現在のディレクトリに.sshというディレクトリが作成されます。
このディレクトリ内のid_rsa.pub(公開鍵)をGitLabに登録します。GitLabプロジェクト作成後、上部にSSHキーを追加してくださいというメッセージが表示されていると思うのでそこから今回のid_rsa.pubを登録してください。id_rsa.pubともう一つid_rsaが.ssh/にありますが、これは秘密鍵なので外部に公開しないようにしましょう。
詳しくは以下のページは詳しくSSHキー作成手順を解説しています。必要に応じて参考にしてください。
https://qiita.com/to3izo/items/9b5b80430e43cd3c4e3c

次に作成したファイルをコミットしてGitLabにプッシュしましょう。
ディレクトリは
k8s_secure_secret_resource_with_sopsに移動して以下のコマンドを実行

# 作業ディレクトリに移動
$ cd k8s_secure_secret_resource_with_sops

# Gitにファイルを登録してコミット
$ git add .
$ git commit -m "add all files"

# GitLabにpush(mainブランチ)
$ git push -u origin main

成功したらGitLabに移動してプロジェクトを見るとファイルがportgres-secret.yaml以外追加されていると思います。そして自動でgitlab-ci.ymlを読み込んでパイプラインが発していると思います。
[CI/CD] > [Pipelines] タブからジョブの進行状況を確認できます。

  • 各ジョブが passed と表示されたら成功です。
  • 実行には1〜5分ほどかかることがあります。

GCPでkubectl get allで確認できます。
これでGitLab×GKEのパイプライン構築が完了しました。

5. ToDoアプリの動作確認(登録&取得)

先ほどのkubectl get allですべてのリソースが正常にデプロイが確認出来たらserviceのEXTERNAL-IPのIPアドレスをメモしてください。これは外部からToDoアプリにアクセスする際のIPアドレスです。そのIPアドレスを以下ではxx.xx.xx.xxとします。

$ kubectl get svc

# 出力例
NAME               TYPE           CLUSTER-IP      EXTERNAL-IP      PORT        AGE
nodejs-service     LoadBalancer   10.0.0.42       xx.xx.xx.xx    80:32462/TCP   5m

以下のコマンドでToDoを1件登録してみましょう。

# データを1件登録
$ curl -X POST http://xx.xx.xx.xx/todos -H "Content-Type: application/json" -d '{"title": "Kubernetes学習"}'

次にデータを取り出します。

# データを取得
$ curl http://34.146.172.56/todos

[
  {
    "id": 1,
    "title": "Kubernetes学習",
    "done": false,
    "created_at": "2025-06-27T00:00:00.000Z"
  }
]

このようなJSONレスポンスが返ってくれば、ToDoアプリは正常に動作しています。
Node.jsアプリ、PostgreSQL、Secret の連携が正しく構成されている証拠です。
非常に長かったと思いますが、セキュアなsecretリソースの運用方法を実践的なアプリケーションを例に学んだことで技術力が確実に上がったと思います:fire:

🎉 お疲れさまでした
ここまでで、GitLab CI/CD × GKE × SOPS によるセキュアなアプリケーション運用フローを一通り終えました。

ToDoアプリという実用例を通して、以下の技術が確実に身についたはずです:

  • GCP KMS と SOPS を用いた Secret の暗号/復号化・管理
  • GitLab CI/CD による自動ビルド・デプロイ
  • Kubernetes 上でのアプリケーション構築・公開

この経験は、今後のクラウド開発・DevOps案件で大きな強みになると思います🔥

✅ 6. まとめと今後の展望

お疲れ様でした!

このプロジェクトでは、「Node.js + PostgreSQL」をバックエンドに採用したToDoアプリAPIを構築し、Kubernetes + GitLab CI/CD + SOPS によってセキュアかつ自動化されたデプロイを実現しました。

現時点で、以下の要素が動作しています:

  • RESTful APIによるToDoデータの操作(取得・追加・削除)

  • PostgreSQLとの永続的なデータ連携

  • Kubernetes上でのアプリケーション実行

  • SOPSとGCP KMSによるSecret管理の自動化

✅ この構成の実用性
このようなNode.js + PostgreSQLの構成は、WebアプリのAPIサーバーの基本形であり、以下のようなサービスに応用可能です:

  • タスク管理(今回のようなToDo)

  • ユーザー登録・ログインAPI

  • ショッピングカートの中身管理

  • 簡易CMSバックエンド


🚀 今後の展望
このToDoアプリAPIは、以下のように拡張・発展させることで実用的なWebアプリケーションへと成長させることができます。

  1. フロントエンドの追加
  • React や Vue.js などのモダンなJavaScriptフレームワークを導入し、

  • 直感的で操作性の高いGUI を持ったフロントエンドを構築

  1. SPA(Single Page Application)の構築
  • クライアント側でルーティングを制御することで、ページ遷移のないスムーズなUIを実現

  • axios や fetch を用いて、Node.js APIと連携し、ToDoの状態をリアルタイムに反映

  1. 認証・認可の導入
  • JWTを用いたユーザー認証機能

  • ユーザーごとのToDo管理など、実サービスに近い要件への対応

  1. 本番環境への公開
  • GCPのCloud Load BalancingやCloud SQLとの連携

  • Let's Encrypt + Ingress + TLS によるHTTPS対応

  • ドメイン設定を含めた本番運用の準備


💡最後に
本記事では、「Kubernetesを活用した実践的なWebアプリ開発・運用の第一歩」を踏み出すことができました。

ここからさらに、DevOpsスキルの強化やクラウドインフラ設計の理解へとつなげていくことで、より実務的なエンジニアリング力が身についていきます。

今後のステップアップの一助となれば幸いです。お疲れ様でした!🚀

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?