Cloud-init初回boot直後にapt-get updateすると失敗した原因と対処
こんにちは。MedOps Technologiesの杉田です。主にインフラを担当しています。「原理に立ち返るインフラ管理」を心がけて一つ一つの課題を解決しています。
弊社ではProxmoxを採用しており、VMの構成をTerraformで記述しています。Telmate/proxmoxのproxmox_vm_qemuリソースのCloud-initによるProvisionを使っているのですが、remote-execでapt-get update
しようとしたら以下のようなエラーが出たため、原因を特定しました。
E: Could not get lock /var/lib/apt/lists/lock. It is held by process 576 (apt-get)
N: Be aware that removing the lock file is not a solution and may break your system.
E: Unable to lock directory /var/lib/apt/lists/
記事を書き終えてから気づいたのですが、そもそもTerraformでProvisionした先はAnsible使いましょう、という話ですね。書いてしまったものを捨てるのも悲しいですし、一定の知見は得られたので、そのまま公開することにしました。
問題のプログラム
今回の問題のスクリプトです。
一部だけ取り出してますので、変数宣言等は適宜補ってください。
resource "proxmox_vm_qemu" "debian-vm" {
vmid = var.vmid
name = "debian-vm-${var.username}"
clone = "debian-12-generic-amd64" # 社内のDebian VMテンプレート
os_type = "cloud-init"
.
.
ipconfig0 = "ip=${var.ip_address}/24,gw=${var.gateway}"
ciuser = "${var.username}"
sshkeys = "${data.local_file.ssh-public.content}"
provisioner "remote-exec" {
connection {
type = "ssh"
host = var.ip_address
user = "${var.username}"
private_key = "${data.local_file.ssh-private.content}"
}
inline = [
"sudo apt-get update", # 問題の箇所 実際はシェルスクリプトをscriptsで私ているが、簡単のためinlineで提示
]
}
}
これでterraform apply
すると以下のようになります。
proxmox_vm_qemu.debian-vm (remote-exec): Connected!
proxmox_vm_qemu.debian-vm (remote-exec): Reading package lists... 0%
proxmox_vm_qemu.debian-vm (remote-exec): Reading package lists... 0%
proxmox_vm_qemu.debian-vm (remote-exec): Reading package lists... 34%
proxmox_vm_qemu.debian-vm (remote-exec): Reading package lists... Done
proxmox_vm_qemu.debian-vm (remote-exec): E: Could not get lock /var/lib/apt/lists/lock. It is held by process 576 (apt-get)
proxmox_vm_qemu.debian-vm (remote-exec): N: Be aware that removing the lock file is not a solution and may break your system.
proxmox_vm_qemu.debian-vm (remote-exec): E: Unable to lock directory /var/lib/apt/lists/
エラーメッセージの分析
3つのメッセージを意訳すると以下のようになります。
- プロセス576(apt-get)が使用しているため、
/var/lib/apt/lists/lock
をロックできない - ロックファイルを消去することは解決策ではなくシステムを壊す可能性があることに気をつけなさい
-
/var/lib/apt/lists
ディレクトリをロックできない
恐らくapt-get update
はパッケージリストの破損を防ぐために排他制御が実装されているのだとは思いますが、具体的にはどういうことなのでしょうか。
ソースコードを読む
debian/aptのソースを読んでみましょう。
apt-get update
コマンドを実行すると、比較的初期にprivate-update.cc
のDoUpdate()
がコールされます。そこから、update.cc
のListUpdate(..)
が呼ばれ、ソースリストのアップデートが行われます。
1 bool ListUpdate(pkgAcquireStatus &Stat,
2 pkgSourceList &List,
3 int PulseInterval)
4 {
5 pkgAcquire Fetcher(&Stat);
6 if (Fetcher.GetLock(_config->FindDir("Dir::State::Lists")) == false)
7 return false;
8
9 // Populate it with the source selection
10 if (List.GetIndexes(&Fetcher) == false)
11 return false;
12
13 return AcquireUpdate(Fetcher, PulseInterval, true);
14 }
5行目でFetcher.GetLock()
が呼ばれており、これはaquire.cc
のpkgAquire::GetLock
に実装されています。ここで/var/apt/list/lock
の排他制御がコードされていると解釈して良さそうです。
1 bool pkgAcquire::GetLock(std::string const &Lock)
2 {
3 if (Lock.empty() == true)
4 return false;
5
6 // check for existence and possibly create auxiliary directories
7 string const listDir = _config->FindDir("Dir::State::lists");
8 string const archivesDir = _config->FindDir("Dir::Cache::Archives");
9
10 if (Lock == listDir)
11 {
12 if (SetupAPTPartialDirectory(_config->FindDir("Dir::State"), listDir, "partial", 0700) == false)
13 return _error->Errno("Acquire", _("List directory %s is missing."), (listDir + "partial").c_str());
14 }
15 if (Lock == archivesDir)
16 {
17 if (SetupAPTPartialDirectory(_config->FindDir("Dir::Cache"), archivesDir, "partial", 0700) == false)
18 return _error->Errno("Acquire", _("Archives directory %s is missing."), (archivesDir + "partial").c_str());
19 }
20 if (Lock == listDir || Lock == archivesDir)
21 {
22 if (SetupAPTPartialDirectory(_config->FindDir("Dir::State"), listDir, "auxfiles", 0755) == false)
23 {
24 // not being able to create lists/auxfiles isn't critical as we will use a tmpdir then
25 }
26 }
27
28 if (_config->FindB("Debug::NoLocking", false) == true)
29 return true;
30
31 // Lock the directory this acquire object will work in
32 if (LockFD != -1)
33 close(LockFD);
34 LockFD = ::GetLock(flCombine(Lock, "lock"));
35 if (LockFD == -1)
36 return _error->Error(_("Unable to lock directory %s"), Lock.c_str());
37
38 return true;
39 }
36行目に"Unable to lock directory %s"とあり、今回の3番目のメッセージはここから出力されているのがわかります。%s
は今回は/var/lib/apt/list
のことかと思います。
34行目にはさらにGetLock
関数があり、flCombine(Lock, "lock")
ファイルを引数に渡していることから、/var/lib/apt/list/lock
ファイルの排他制御をもって、/var/lib/apt/list
ディレクトリ全体の排他制御を実装していることがわかります。
このGetLock
はfileutl.cc
にコードされています。
以下、必要な部分以外を削って引用します。
1 int GetLock(string File,bool Errors)
2 {
3 int FD = open(File.c_str(),O_RDWR | O_CREAT | O_NOFOLLOW,0640);
4
5 // (中略: ファイルオープンのエラーハンドリング)
6
7 struct flock fl;
8 fl.l_type = F_WRLCK;
9 fl.l_whence = SEEK_SET;
10 fl.l_start = 0;
11 fl.l_len = 0;
12 if (fcntl(FD,F_SETLK,&fl) == -1)
13 {
14 int Tmp = errno;
15
16 if ((errno == EACCES || errno == EAGAIN))
17 {
18 fl.l_type = F_WRLCK;
19 fl.l_whence = SEEK_SET;
20 fl.l_start = 0;
21 fl.l_len = 0;
22 fl.l_pid = -1;
23 fcntl(FD, F_GETLK, &fl);
24 }
25 else
26 {
27 fl.l_pid = -1;
28 }
29 close(FD);
30 errno = Tmp;
31
32 // (中略: NFSマウントされたファイルへの対応)
33
34 if (Errors == true)
35 {
36 if (fl.l_pid != -1)
37 {
38 auto name = GetProcessName(fl.l_pid);
39 if (name.empty())
40 _error->Error(_("Could not get lock %s. It is held by process %d"), File.c_str(), fl.l_pid);
41 else
42 _error->Error(_("Could not get lock %s. It is held by process %d (%s)"), File.c_str(), fl.l_pid, name.c_str());
43 }
44 else
45 _error->Errno("open", _("Could not get lock %s"), File.c_str());
46
47 _error->Notice(_("Be aware that removing the lock file is not a solution and may break your system."));
48 }
49
50 return -1;
51 }
52
53 return FD;
54 }
- 3行目でlockファイルを開いています。
O_CREAT
を渡しており、ファイルがなければ生成されます。 - 7-11行目でflockの構造体を構成しています。
fl.l_type = F_WRLCK
と指定していることから、12行目で排他ロックを要求していることがわかります。 -
fcntl(FD,F_SETLK,&fl)
が失敗し、errnoがEACCES
またはEAGAIN
の場合、他のプロセスがファイルをロックしているという意味です(参考)。ちなみにPython等他の環境ではEACCES
はPermission Deny
を表すことが多く、注意が必要です(参考)。 - なので、22行目で
fl.l_pid = -1
とプロセスIDを-1
に仮設定しておいて、23行目のfcntl(FD, F_GETLK, &fl)
で現在ロックしているプロセス情報を取りに行きます。 -
EACCES
,EAGAIN
以外の場合はfl.l_pid = -1
が設定され、以下この値で固定されます。 -
EACCES
/EAGAIN
の場合、fl.l_PID
は-1
以外の値となるはずなので、36行目のif文でmatchします。 - そして42行目で本件の1つ目のエラーメッセージが出力されます。
- さらに47行目で本件の2つ目のメッセージが出力されます。
ここまで来れば、冒頭の
恐らく
apt-get update
はパッケージリストの破損を防ぐために排他制御が実装されているのだとは思いますが、具体的にはどういうことなのでしょうか。
という仮説が裏付けられました。
では何が起こっているのか
Terraformのremote-execがapt-get update
を実行しようとして時点で、既にprocess 576(apt-get)が走っていると考えられます。
ゆえに**「Cloud-initがVM立ち上げ時にapt-get
を実行しているのではないか」という仮説が立ちます**。
Cloud-initのDocumentを読めば一目瞭然で、Cloud-initはデフォルトでは初回動作時にapt-get update
とapt-get upgrade
を実行していることがわかりました。
対処方法
Ansibleを使わない対処法を2つ考えました。良い子は真似しないでね!
その1: Cloud-initが終わるまで待つ
Cloud-initが終了すると/var/lib/cloud/instance/boot-finished
ファイルが作成されるので、これを検出してからapt-get
を実行するというソリューションが成立します(参考)。
#!/bin/bash
until [ -f /var/lib/cloud/instance/boot-finished ]; do
sleep 1
done
sudo apt-get update
ワンライナーバージョン↓
$ until [ -f /var/lib/cloud/instance/boot-finished ]; do; sleep 1; done && sudo apt-get update
その2: Cloud-initでapt-getを実行しない
そもそもCloud-initの初期設定でapt-get
を実行しなければ良い、というソリューションです。
以下のようなcloud-config.ymlを用意します。
#cloud-config
users:
- name: username
sudo: ALL=(ALL) NOPASSWD:ALL
ssh_authorized_keys: ssh-ed25519 XXXX
package_update: false
package_upgrade: false
proxmox_vm_qemu
の場合、cloud-config.yml
を/var/lib/vz/snippets/
以下にコピーして、cicustom
に指定します。
resource "proxmox_vm_qemu" "debian-vm" {
vmid = var.vmid
name = "debian-vm-${var.username}"
clone = "debian-12-generic-amd64" # 社内のDebian VMテンプレート
os_type = "cloud-init"
.
.
ipconfig0 = "ip=${var.ip_address}/24,gw=${var.gateway}"
# 以下のように書き換える
cicustom = "user=local:snippets/debian-cloudinit.yml"
provisioner "remote-exec" {
connection {
type = "ssh"
host = var.ip_address
user = "${var.username}"
private_key = "${data.local_file.ssh-private.content}"
}
inline = [
"sudo apt-get update",
]
}
}
まとめ
今回はCloud-initとapt-getの排他制御についてまとめました。
長くなってしまいましたが、ここまで読んでいただきありがとうございます。この記事が皆さんのお役に立てば嬉しいです。
Discussion