跳至主要内容

· 閱讀時間約 8 分鐘
Justin Lin

前言

在先前的文章中,我們已經詳細介紹了如何在Google Compute Engine(GCE)上使用Container-Optimized OS和Terraform部署Elasticsearch 我們成功配置了虛擬機和相關資源,並探討了基本的設置過程 然而,實際運營中經常會遇到一些技術挑戰,尤其是與外部硬碟掛載和權限管理相關的問題

在這篇文章中,我們將深入探討在使用Container-Optimized OS進行Elasticsearch部署時遇到的一個具體問題:磁碟權限被不當覆蓋

遇到的問題與初步解決嘗試

  • 在將terraform 的設定 apply 到雲端環境後,Elasticsearch在啟動階段的最後會出現無法寫入數據的錯誤訊息導致 container 無法正確啟動
    • 既然 disk 是額外掛載的方式掛到機器上,因此高機率可以從 /mnt 找到硬碟來排查錯誤
    • 查看後知道 Container-Optmized OS 在指定外接硬碟作為 volume 的時候會將硬碟 mount 至 /mnt/disks/gce-containers-mounts/gce-persistent-disks/${disk_name}
  • 空白磁碟被掛載到VM上時是root權限
    • Elasticsearch的image已經寫死用戶是elasticsearch(UID為1000)
    • 在Container-Optimized OS中,UID 1000已被其他用戶占用,且磁碟的預設權限為755,這讓Elasticsearch無法正常寫入數據
  • 在思考解決辦法的途中,找到了GCE提供了撰寫啟動腳本的辦法
    • 首先嘗試使用GCE的startup script來進行chown,但發現這個操作會在Elasticsearch容器啟動過程中被覆蓋。於是我採用了chmod a+w來嘗試賦予更廣泛的寫入權限,但這同樣被後續操作覆蓋
    • 由於權限會被固定在 owner 為 root 且權限為 755 的情形下,無法透過將 UID 1000 的使用者加入 group 來解決

深入系統日誌與源碼分析

  • 要解決Elasticsearch的部署問題,一個關鍵步驟是理解Volume掛載過程及其權限如何被設置
    • 因此需要深入系統的日誌和相關服務的 source code,才能確認何處出了問題以及如何修復

日誌分析:確認Startup Script的執行者

  • 透過journalctl檢視系統日誌
    • 發現日誌中有名為 cloud-init 的 service unit,從名稱上來看就跟雲端系統啟動有關
    • 推測startup script應該是由cloud-init這個service執行的
    • 為進一步驗證這一點,我執行了以下命令以列出所有服務
      • sudo systemctl list-units --type="service"
cloud-config.service     loaded active exited  Apply the settings specified in cloud-config
cloud-final.service loaded active exited Execute cloud user/final scripts
cloud-init-local.service loaded active exited Initial cloud-init job (pre-networking)
cloud-init.service loaded active exited Initial cloud-init job (metadata service crawler)
containerd.service loaded active running containerd container runtime
dbus.service loaded active running D-Bus System Message Bus

找到了cloud-final.service,其狀態顯示為loaded active exited,並且描述為Execute cloud user/final scripts 這確認了我的猜測:startup script是由此service負責執行的

從日誌追蹤到Konlet的行動

接著,我仔細查看了cloud-final執行完成後的日誌記錄:

Apr 21 09:19:44 shareclass-tw-elasticsearch-dev systemd[1]: Finished cloud-config.service.
Apr 21 09:19:44 shareclass-tw-elasticsearch-dev systemd[1]: Starting cloud-final.service...
Apr 21 09:19:45 shareclass-tw-elasticsearch-dev konlet-startup[694]: d2519f41f710: Pull complete
Apr 21 09:19:45 shareclass-tw-elasticsearch-dev cloud-init[742]: Cloud-init v. 23.2.1 running 'modules:final' at Sun, 21 Apr 2024 09:19:45 +0000. Up 14.74 seconds.
Apr 21 09:19:45 shareclass-tw-elasticsearch-dev cloud-init[742]: Cloud-init v. 23.2.1 finished at Sun, 21 Apr 2024 09:19:45 +0000. Datasource DataSourceGCE. Up 15.05 seconds
# Skip...
Apr 21 09:20:23 shareclass-tw-elasticsearch-dev konlet-startup[694]: 2024/04/21 09:20:17 Creating directory /mnt/disks/gce-containers-mounts/gce-persistent-disks/volume-shareclass-tw-elasticsearc>
Apr 21 09:20:23 shareclass-tw-elasticsearch-dev konlet-startup[694]: 2024/04/21 09:20:17 Attempting to mount device /dev/disk/by-id/google-volume-shareclass-tw-elasticsearch-dev at /mnt/disks/gce>
Apr 21 09:20:23 shareclass-tw-elasticsearch-dev konlet-startup[694]: 2024/04/21 09:20:17 Created a container with name 'klt-shareclass-tw-elasticsearch-dev-bqub' and ID: c03e7f32d59e88148f0359580>
Apr 21 09:20:23 shareclass-tw-elasticsearch-dev konlet-startup[694]: 2024/04/21 09:20:17 Starting a container with ID: c03e7f32d59e88148f035958082beb25946b7c4d9a9c2c36cbdf9ff72f300902
Apr 21 09:20:23 shareclass-tw-elasticsearch-dev konlet-startup[694]: 2024/04/21 09:20:17 Saving welcome script to profile.d
  • 日誌中的Creating directory /mnt/disks/gce-containers-mounts/gce-persistent-disks/volume-shareclass-tw-elasticsearch可以看到,konlet-startup是處理掛載邏輯的主要元件
  • 透過 sudo systemctl status konlet-startup.service,追查到 service 文件位置
○ konlet-startup.service - Containers on GCE Setup
Loaded: loaded (/lib/systemd/system/konlet-startup.service; disabled; preset: disabled)
Active: inactive (dead) since Sun 2024-04-21 09:20:17 UTC; 2 days ago
Duration: 37.532s
Process: 674 ExecStart=/usr/share/gce-containers/konlet-startup (code=exited, status=0/SUCCESS)
Main PID: 674 (code=exited, status=0/SUCCESS)
CPU: 151ms
  • /usr/share/gce-containers/konlet-startup
    • 這個service會啟動 gcr.io/gce-containers/konlet 的 container
    • 啟動參數 -v=/var/run/docker.sock:/var/run/docker.sock 代表這個 service 需要在 docker 環境內取得 docker 的控制權,推測這個 service 會負責啟動 container 的所有相關事宜

查看 source code:確定掛載和權限設置

konletGitHub中,我找到了以下重要的代碼片段,這幫助我了解到Volume掛載點的創建和權限設置是如何進行的

func (env Env) createNewMountPath(volumeFamily string, volumeName string) (string, error) {
path := fmt.Sprintf("%s/%ss/%s", *mountedVolumesPathPrefixFlag, volumeFamily, volumeName)
log.Printf("Creating directory %s as a mount point for volume %s.", path, volumeName)
if err := env.OsCommandRunner.MkdirAll(path, 0755); err != nil {
return "", fmt.Errorf("Failed to create directory %s: %s", path, err)
} else {
return path, nil
}
}
  • 這段代碼揭示了Volume掛載點是如何被創建的
  • 並且確認了每次掛載時都會設置0755的權限,這是我之前設置權限被覆蓋的原因

最終解決方案

為確保權限設置不被覆蓋,我設計了一個新的service

  • 在startup script中實作
  • cloud-final 階段建立一個 service 的必要檔案,並啟動
  • 這個service會等待 volume 成功掛載後,立即調整必要的權限
信息

由於 Container-Optimized OS,會將 /usr/local 設為 read-only,因此使用 /etc作為路徑

#!/bin/bash
# 創建執行腳本
mkdir -p /etc/bin
echo "#!/bin/bash

while ! findmnt -M \"$MOUNT_PATH\" >/dev/null; do
echo \$(date) - Waiting for $MOUNT_PATH to be mounted...
sleep 2
done

echo \$(date) - Mount detected, changing permissions...
chmod a+w \"$MOUNT_PATH\"
echo \$(date) - Permissions changed.

echo \$(date) - Script completed.
exit 0
" > /etc/bin/startup_script.sh
chmod +x /etc/bin/startup_script.sh

# 創建systemd service文件
echo "[Unit]
Description=Wait for disk mount and adjust permissions
After=network-online.target

[Service]
Type=simple
ExecStart=/etc/bin/startup_script.sh
Restart=on-failure
RestartSec=30
StandardOutput=journal+console

[Install]
WantedBy=multi-user.target
" > /etc/systemd/system/wait-and-chmod.service

# 啟動並啟用service
systemctl enable wait-and-chmod.service
systemctl start wait-and-chmod.service

這樣,當磁碟被掛載且Docker容器準備就緒時,權限會被正確設定,允許Elasticsearch順利啟動並運行

結語

透過這次經驗,不僅解決了具體的技術問題,也對GCE的運作機制有了更深的理解 希望這篇分享能有所幫助,尤其是在處理雲端服務時遇到類似挑戰的情況

· 閱讀時間約 5 分鐘
Justin Lin

當需要在雲端快速部署可擴展的應用時,Google Compute Engine(GCE)配合Container-Optimized OS提供了一個高效的選擇 下面將利用Terraform來部署一個Elasticsearch容器實例,並結合外接空白硬碟以優化數據遷移和存儲

配置Terraform

定義資源:靜態內部IP

首先,定義一個內部IP地址,這個地址將被用作我們虛擬機的網絡接口 (由於這邊使用情境為純內部 service,因此不設定外部IP)

resource "google_compute_address" "internal_ip" {
name = var.service_name
address_type = "INTERNAL"
purpose = "GCE_ENDPOINT"
project = var.project_id
region = var.project_region
}

設置虛擬機實例

接下來,設定GCE虛擬機實例 使用Container-Optimized OS作為啟動盤的映像,並將Docker容器的規格通過metadata傳遞給GCE,這裡使用的是類似於Kubernetes的配置方式 也可以利用GCE的 UI 介面設定好之後畫面上會有等效的程式碼可以照搬

resource "google_compute_instance" "instance" {
boot_disk {
initialize_params {
/* 省略了部分初始化和配置代碼...*/
image = "projects/cos-cloud/global/images/cos-stable-109-17800-66-78"
}
}
/* 省略了部分初始化和配置代碼...*/

metadata = merge(
{
gce-container-declaration = var.gce_container_declaration
},
var.startup_script != null ? {
startup-script = var.startup_script
} : {}
)

network_interface {
network_ip = google_compute_address.internal_ip.address
subnetwork = "projects/shareclass-tw/regions/asia-east1/subnetworks/default"
}

dynamic "attached_disk" {
for_each = var.attached_disk
content {
source = attached_disk.value.source
device_name = attached_disk.value.device_name
mode = attached_disk.value.mode
}
}
depends_on = [google_compute_address.internal_ip]
}

在這個google_compute_instance資源定義中,我們設置了一台虛擬機來運行Elasticsearch容器。關鍵配置包括

  • 指定操作系統映像為Container-Optimized OS的最新穩定版本
  • 通過metadata傳遞Docker容器的配置(這里用了gce-container-declaration) 這種方法類似於Kubernetes的pod定義,允許細粒度地控制容器的運行環境

配置外接硬碟

resource "google_compute_disk" "elasticsearch" {
name = "volume-${var.service_name}"
type = "pd-balanced"
zone = var.project_zone
size = var.disk_size
}

這部分代碼創建了一個外接硬碟,將用於Elasticsearch數據的存儲

定義容器規格

data "template_file" "gce_container_declaration" {
template = file("${path.module}/gce_container_declaration.tpl")

vars = {
service_name = local.service_name
image = local.image
disk_name = google_compute_disk.elasticsearch.name
}
}
  • template_file數據源用於生成容器的配置
    • 讀取一個模板文件,並替換模板中的變量以生成最終的容器配置
    • 這個過程讓配置更加模塊化和可重用,便於管理大型部署
  • 若使用GCE的等效程式碼,只能看到 spec:\n containers:\n - name: instance.... 這種單行設定,不利於以後維護

定義模組

將所有配置集成到一個模組中,方便管理和重用

module "elastic_search" {
source = "../compute_container"
project_id = var.project_id
project_region = var.project_region
project_zone = var.project_zone
service_name = var.service_name
environment = var.environment
machine_type = var.machine_type
attached_disk = [
{
device_name = google_compute_disk.elasticsearch.name
mode = "READ_WRITE"
source = google_compute_disk.elasticsearch.self_link
}
]
gce_container_declaration = data.template_file.gce_container_declaration.rendered

depends_on = [
google_compute_disk.elasticsearch,
data.template_file.gce_container_declaration
]
}

最後,所有這些資源和配置被封裝進一個模組。這使得整個Elasticsearch部署可以作為一個單元被重用和管理,無論是在當前項目還是跨項目都能輕鬆配置和更新

結語

通過上述步驟,我們已經成功配置了一個在Google Compute Engine上運行Elasticsearch的環境 這個環境利用了Container-Optimized OS的穩定性和安全性,以及Terraform的自動化部署能力 通過細致的配置和模組化的設計,我們確保了系統的可擴展性和易管理性

後半段將更深入地探討在此架構中運行時遇到的特定技術問題,特別是之前提及的磁碟權限問題,這是一個在使用外接硬碟和Container-Optimized OS時常見的挑戰 將詳細討論問題的根源——如何在容器的啟動和運行過程中保護好磁碟的權限不被覆蓋,以及如何利用startup script中創建的專用service來動態調整和修正權限