こんにちは。
今回は下の記事の続編的な立ち位置に当たります。
前回から変更した点なども踏まえて紹介していこうと思います。
文量が増えてみにくくなると思ったので、詳細なTerraformの内容は載せてないです。
もし気になる点あればご質問いただけると幸いです。
また、間違いなどあればご指摘ください。
⚠️注意
前回から引き続きSingle ノードでの構築になります。
Multiノードではケアするべきポイント(ノード間通信など)が増えますのでこちらの構成を参考にしないでください。
環境
インスタンス: e2-medium
ソースイメージ: "debian-cloud/debian-11"
起動フロー
タイトルにもある通りGCPで完結しています。
GCEのVMインスタンスにElasticsearchとKibanaのコンテナを立てています。
リクエストが来たらIAP(Identity-Aware Proxy)が待ち構えており、Google認証を行います。
社内で使うことがメインなので、社内アカウント以外は完全にシャットアウトしてます。
あとはロードバランサでElasticsearchか、Kibanaに繋ぐ形です。
Dockerコンテナの起動時には、GCS(Google Cloud Storage)から必要なファイルを持ってきて使用します。
前回からの変更点
- パスワードはランダムに生成 -> 初期パスワードを設定することにした
- IAP周りでコマンドが叩けなくなってしまったため
- 証明書はlet's encryptで作成
- 証明書と秘密鍵が必要、また証明書が信頼されていて欲しいため
-
.env
をなくした- Terraformだけで管理できそうだったため
書くまえはもうちょいありそうな気がしましたが意外と少ないですね
Terraformでよかったとこ!
何よりもテンプレートファイルが超便利です!
元々、環境変数を使って変数的に管理していたものたちをterraform側で吸収できるのはでかいです。
また、テンプレートファイルを元にterraformで作成した各種起動用ファイルを、GCSにアップロードするようにしていて、変更があるたびに自動更新されるところも運用面から楽です。
詰まったところ
IAP
ロードバランサ
検証をVMインスタンス単体でやっていたこととロードバランサに対する知識が無さすぎてどう接続していいかわかんなかったです。
結論、url_mapと呼ばれるものを作れば良いです。
指定したパスやサブドメインに対して、ロードバランサがバックエンドサービスを紐づけてくれます。
ざっくりとモジュール化したterraformで説明します。
今回はサブドメインを使った構成にしていて、指定したホストとpathmatcherで指定したバックエンドサービスを紐づけてます。
resource "google_compute_url_map" "main" {
name = var.name
default_service = var.default_service
dynamic "host_rule" {
for_each = var.host_rules
content {
hosts = host_rule.value.hosts
path_matcher = host_rule.value.path_matcher
}
}
dynamic "path_matcher" {
for_each = var.path_matchers
content {
name = path_matcher.value.name
default_service = path_matcher.value.default_service
}
}
}
IAPを導入するにあたって、インスタンスグループとロードバランサは必須のようなので要チェックです。
パスワード設定
鬼門でした。今まではdocker execでコンテナからelasticsearch標準コマンドを使ってパスワードのリセットなどを行なっていましたが、IAPを導入するとできなくなります。
理由は、elasticsearch標準コマンドを使う際にヘルスチェックが行われるのそうなのですが、このヘルスチェックにおいてcurlっぽい処理を行っているからです。
そのため外部接続扱いになり、IAPで弾かれてしまいます。
これを回避するにはAuthorizationヘッダにトークンを入れたいところですが、どうも設定でき無さそうだったのでcurlを使った手法に切り替えてます。
また、curlを使った手法にすることで、elasticsearch側でも認証が必要となるため、初期パスワードを設定することにしました。
具体的な処理については下の方で紹介してます。
2段階認証
セキュアな状態を目指すと、IAPを導入し、elasticsearchでも認証を入れることになると思います。
しかし、curlコマンドを使うとAuthorizationの情報が一つまでしか入れられないので、どちらかの認証情報が認識されなくなるのです。
でも大丈夫。Proxy-Authorization
を使えば、IAPの認証情報を分離できるので、2段階の認証も怖くありません。
同様にしたで紹介します。
証明書が作れない!
私の短いエンジニア人生では、certbot
のHTTP-01チャレンジで事足りていました。
これはインスタンス側で受信用サーバーを立てて、あるパスにトークンを設定できるかチェックすることでドメインの所有者であることを確認する方法です。
しかし、IAPを導入することによりAuthorization Tokenがなければサーバーにアクセスができなくなってしまうのです。
でも大丈夫。DNS-01チャレンジを使えば問題ありません。
DNS-01チャレンジは、指定されたサブドメインのTXTレコードにトークンを設定できるかを見ることでドメインの所有者であることを確認する方法です。
起動用ファイルの一例(テンプレート)
start.sh
補足事項を挙げると、container側とhost側でファイルに対する権限が変わってくるのでelasticsearchのユーザーに対して、権限を与えています。1000
がelasticsearchのユーザーidにあたります。
#!/bin/bash
set -euo pipefail
DOMAIN="${es_host}"
EMAIL="${email}"
WORKDIR="/opt/serviceplace-search"
CERTSDIR="$WORKDIR/certs"
SECRET_NAME="certbot-dns-credentials"
GOOGLE_CREDENTIALS_PATH="/root/gcloud-dns-creds.json"
echo "🛠️ Installing dependencies..."
apt-get update
apt-get install -y --only-upgrade google-cloud-sdk
apt-get install -y docker.io curl jq docker-compose
apt-get install -y certbot python3-certbot-dns-google # ← GCP用プラグイン
echo "📁 Creating working directories..."
mkdir -p "$CERTSDIR"
cd "$WORKDIR"
echo "📦 Fetching docker-compose file from GCS..."
gsutil -m cp -r gs://${bucket_name}/serviceplace-search/* .
sudo chown 1000:1000 "$WORKDIR/healthcheck_es.sh"
sudo chown 1000:1000 "$WORKDIR/healthcheck_kib.sh"
sudo chmod +x "$WORKDIR/healthcheck_es.sh"
sudo chmod +x "$WORKDIR/healthcheck_kib.sh"
echo "🌐 Creating docker network..."
docker network create elastic || true
echo "🔐 Fetching service account key from Secret Manager..."
gcloud secrets versions access latest --secret="$SECRET_NAME" > "$GOOGLE_CREDENTIALS_PATH"
chmod 600 "$GOOGLE_CREDENTIALS_PATH"
echo "🌍 Getting Let's Encrypt certificate using Cloud DNS..."
certbot certonly --dns-google \
--dns-google-credentials "$GOOGLE_CREDENTIALS_PATH" \
--dns-google-propagation-seconds 60 \
--non-interactive --agree-tos \
--email "$EMAIL" -d "$DOMAIN" -d "es.$DOMAIN" -d "kib.$DOMAIN"
# Copy certs to Docker-readable path
cp /etc/letsencrypt/live/"$DOMAIN"/fullchain.pem "$CERTSDIR/elasticsearch.pem"
cp /etc/letsencrypt/live/"$DOMAIN"/privkey.pem "$CERTSDIR/elasticsearch-key.pem"
chown 1000:1000 "$CERTSDIR/elasticsearch.pem"
chown 1000:1000 "$CERTSDIR/elasticsearch-key.pem"
chmod 644 "$CERTSDIR/elasticsearch.pem"
chmod 644 "$CERTSDIR/elasticsearch-key.pem"
echo "🚀 Starting Elasticsearch container..."
docker-compose -f compose.prod.yml up -d es01
echo "⏳ Waiting for Elasticsearch to be ready..."
sleep 15
echo "🔧 (Optional) Running password setup script..."
if [[ -f "$WORKDIR/set_password.sh" ]]; then
chmod +x ./set_password.sh
source ./set_password.sh
fi
echo "🚀 Starting Kibana container..."
docker-compose -f compose.prod.yml up -d kib01
echo "📝 Writing cert auto-renewal script..."
cat <<EOF > /opt/serviceplace-search/renew_certs.sh
#!/bin/bash
set -euo pipefail
DOMAIN="${es_host}"
CERTSDIR="/opt/serviceplace-search/certs"
GOOGLE_CREDENTIALS_PATH="/root/gcloud-dns-creds.json"
echo "🔐 Refreshing service account key from Secret Manager..."
gcloud secrets versions access latest --secret="$SECRET_NAME" > "\$GOOGLE_CREDENTIALS_PATH"
chmod 600 "\$GOOGLE_CREDENTIALS_PATH"
echo "🔄 Renewing Let's Encrypt certificate..."
certbot renew --quiet \
--dns-google --dns-google-credentials "\$GOOGLE_CREDENTIALS_PATH" \
--dns-google-propagation-seconds 60 \
--deploy-hook "
echo '✅ Certificate renewed. Copying to certs dir...';
cp /etc/letsencrypt/live/\$DOMAIN/fullchain.pem \$CERTSDIR/elasticsearch.pem;
cp /etc/letsencrypt/live/\$DOMAIN/privkey.pem \$CERTSDIR/elasticsearch-key.pem;
echo '🔁 Restarting containers...';
docker-compose -f /opt/serviceplace-search/compose.prod.yml restart es01 kib01
"
EOF
chmod +x /opt/serviceplace-search/renew_certs.sh
echo "📆 Setting up cron job for cert renewal..."
cat <<EOF > /etc/cron.d/cert_renew
TZ=Asia/Tokyo
0 3 * * * root /opt/serviceplace-search/renew_certs.sh >> /var/log/renew_certs.log 2>&1
EOF
chmod 644 /etc/cron.d/cert_renew
touch /var/log/renew_certs.log
echo "✅ Deployment Complete"
docker-compose.yml
GCE内のaptでdockerのインストールを完結しようとすると、いわゆるv1系のdockerしかインストールできないっぽいです。(調査不足かもしれない)
なのでところどころ古めの記述が見られると思います。(versionあたりとか)
version: "3.3"
services:
es01:
image: docker.elastic.co/elasticsearch/elasticsearch:${es_version}
container_name: es01
environment:
- network.host=0.0.0.0
- node.name=es01
- discovery.type=single-node
- bootstrap.memory_lock=true
- ELASTIC_PASSWORD=${es_password}
- ES_JAVA_OPTS=-Xms${heap_min} -Xmx${heap_max}
- xpack.security.enabled=true
- xpack.security.http.ssl.enabled=true
- xpack.security.http.ssl.key=${es_path_conf}/${certs_dir}/${certs_file_key}
- xpack.security.http.ssl.certificate=${es_path_conf}/${certs_dir}/${certs_file}
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- data01:/usr/share/elasticsearch/data
- ./${certs_dir}:${es_path_conf}/${certs_dir}:ro
- /opt/serviceplace-search:/opt/serviceplace-search
ports:
- 9200:9200
networks:
- elastic
healthcheck:
test: ["CMD", "bash", "/opt/serviceplace-search/healthcheck_es.sh"]
interval: 30s
timeout: 30s
retries: 5
kib01:
image: docker.elastic.co/kibana/kibana:${es_version}
container_name: kib01
depends_on:
- es01
environment:
- ELASTICSEARCH_USERNAME=kibana_system
- ELASTICSEARCH_PASSWORD=${kib_password}
- ELASTICSEARCH_HOSTS=https://es01:9200
- ELASTICSEARCH_SSL_VERIFICATIONMODE=certificate
- csp.strict=true
- SERVER_HOST=0.0.0.0
- SERVER_SSL_ENABLED=true
- SERVER_SSL_CERTIFICATE=${kib_path_conf}/${certs_dir}/${certs_file}
- SERVER_SSL_KEY=${kib_path_conf}/${certs_dir}/${certs_file_key}
- server.securityResponseHeaders.disableEmbedding=true
- server.securityResponseHeaders.strictTransportSecurity=max-age=31536000
volumes:
- kibanadata:/usr/share/kibana/data
- ./${certs_dir}:${kib_path_conf}/${certs_dir}:ro
- /opt/serviceplace-search:/opt/serviceplace-search
ports:
- 5601:5601
networks:
- elastic
healthcheck:
test: ["CMD", "bash", "/opt/serviceplace-search/healthcheck_kib.sh"]
interval: 30s
timeout: 30s
retries: 5
volumes:
data01:
driver: local
kibanadata:
driver: local
networks:
elastic:
driver: bridge
ヘルスチェック
IAP突破のために、最新トークンをとってきている点がポイントです。
Authorizationヘッダにぶち込みましょう。
#!/bin/bash
set -euo pipefail
# メタデータサーバーから最新トークン取得
TOKEN=$(curl -s -H 'Metadata-Flavor: Google' \
"http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=${iap_client_id}&format=full")
# IAP 越しにヘルスチェック
curl -s -H "Authorization: Bearer $${TOKEN}" \
"https://${host}" > /dev/null
kibana_systemのパスワード設定
最後にパスワード設定のところです。
IAPのトークン作成とその使い方に注目ですね。
パスワード設定のcurlではProxy-Authorizationを使っています!
#!/bin/bash
set -euo pipefail
WORKDIR="/opt/serviceplace-search"
CERTSDIR="$WORKDIR/certs"
echo "🔐 Fetching secrets from Secret Manager..."
ACCESS_TOKEN=$(curl -s -H 'Metadata-Flavor: Google' \
"http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=${iap_client_id}&format=full")
echo "Start Creating Containers...";
echo "Waiting for Elasticsearch availability";
until curl -s https://${es_host} -H "Authorization: Bearer $ACCESS_TOKEN" >/dev/null; do
sleep 5;
echo "Waiting for Elasticsearch...";
done;
# Elasticsearchのユーザー名と現在の管理者パス(ELASTIC_PASSWORD)を.envなどから取得
ES_USER="elastic"
ES_PASSWORD=${es_password}
# kibana_system のパスワードを変更
echo "🔐 Setting new password for kibana_system via Basic Auth..."
curl -s -o /dev/null \
-u "$ES_USER:$ES_PASSWORD" \
-X POST "https://${es_host}/_security/user/kibana_system/_password" \
-H "Content-Type: application/json" \
-H "Proxy-Authorization: Bearer $ACCESS_TOKEN" \
-d "{\"password\": \"${kib_password}\"}"
echo "Finish Creating Containers...";
思いつく改善点と感想
terraformで初期パスワードを設定するわけですが、ファイルに変更がないとパスワードが更新されないっぽいので、どうにかしたいです。
あと、コンテナの更新が今の形だとサービスダウンに繋がるので、サービスを継続可能な状態でコンテナを切り替えられるようにしたいです。
マルチノード環境ではTCPの話も入ってきますし、何よりクラスタで管理しないとなのでより難しそうです。
無事に動くものができてよかったです。
ありがとうございました。
参考