RedHat系におけるRPMパッケージを扱うYUM、Debian系におけるDEBパッケージを扱うAPT、これらはサーバー管理において重要なわけですが、絶妙な度合いで、おざなりに扱ってもわりとなんとか運用出来てしまう感があります。そのため今一度、こんな感じが今風のスタンダードじゃないっすかね(キリッ という構成を説明してみます。
ぶっちゃけ、たいしたことないネタの集合体なので、タイトルに下駄を履かせました。
そもそもパッケージは必要なのか
言うまでもなく必須です。理由は、インストール物のファイル管理が容易になるのと、インストール時間を短縮できるからです。既存のパッケージでconfigureオプションが物足りない時や、RPMパッケージが存在しない場合は作成することになります。
最近はプロビジョニング・ツールによって全て自動化できるので、超簡素なコンパイルのものはレシピに落とし込んで終わりにしたくなるかもですが、上記理由により悪手であると断言できます。
パッケージの作成自体はただの慣れの問題なので、サクッと修得してしまいましょう。
パッケージ管理の全体構成
まずは全体構成図になります。以降はここから切り出して説明していきます。
パッケージ作成
ここでは、パッケージ作成作業を、好きな仮想環境で行いましょう、というだけです。
普通にコンパイルからインストールして動作確認をしたり、パッケージ作成作業をしていると、ゴミが溜まっていくので、ポイ捨てできる環境でやったほうが気が楽だからです。
パッケージ作成手順そのものについては割愛!
(参考記事:CentOS7でのRPMパッケージ作成手順(sshguard編) )
S3にレポジトリを作成
ApacheやNginxで独自レポジトリを運用する時代は終わりました。サーバー管理が面倒なので、S3のWeb Site Hostingを使いましょう。
S3の準備をする
バケットを作って、Web Site Hostingを有効にします。
そして、こんな感じでツリーを作成します。
.
├── centos # クライアントがHTTPアクセスしにくるところ
├── centos - packages # 作成者がアップロードするところ
│ └── 7
│ ├── SRPM
│ └── x86 _ 64 # .rpm置き場
├── debian # クライアントがHTTPアクセスしにくるところ
└── debian - packages # 作成者がアップロードするところ
└── jessie # .deb置き場
S3のアクセス制限
たとえOSSのパッケージだとしても、自作パッケージを野放しに公開しておくのはよくないですから、どちらかの方法でアクセス制限をします。
ソースアドレスで制限
S3のバケットポリシーの編集にて、こんな感じで制限できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"Id" : "Policy1456810921862" ,
"Version" : "2012-10-17" ,
"Statement" : [
{
. . . ,
"Condition" : {
"IpAddress" : {
"aws:SourceIp" : [
"1.2.3.4" ,
"3.2.1.0/24"
]
}
}
}
]
}
VPCエンドポイントで制限
外からのアクセスが不要ならば、VPCエンドポイントで制限してしまえます。
{
"Id" : "Policy1456810921862" ,
"Version" : "2012-10-17" ,
"Statement" : [
{
. . . ,
"Condition" : {
"StringNotEquals" : {
"aws:sourceVpce" : "vpce-1234abcd"
}
}
}
]
}
Lambdaでレポジトリ・メタデータの自動更新
S3にアップロードされたら、自動的にLambdaが起動して、レポジトリのメタデータが更新されるようにします。
IAMにロールを作成
まずは、Lambdaに割り当てるRoleを作成します。内容は大雑把にするとこんな感じ。
AmazonEC2FullAccess
AmazonS3FullAccess
IAMPassRole
レポジトリ更新用Lambdaスクリプトを作成
UserDataにBashをぶっ込みつつEC2に処理を行わせるスクリプトです。超頑張ったらLambdaでもレポジトリのメタデータを生成できるでしょうが、どう考えても無駄なので、最安インスタンスを立ち上げて、普通にコマンドベースでやったほうがよいという判断です。
YUM版
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import json
import urllib
import boto3
import os
import re
# constants
image_id = 'ami-59bdb937' # Amazon Linux AMI 2015.09.2 x86_64 HVM GP2
subnet_id = 'subnet-example'
instance_profile_name = 'InstanceProfileName'
s3 = boto3.client('s3')
ec2 = boto3.client('ec2')
def lambda_handler(event, context):
# Get the object from the event and show its content type
bucket = event['Records'][0]['s3']['bucket']['name']
key = urllib.unquote_plus(event['Records'][0]['s3']['object']['key']).decode('utf8')
s3.delete_object(Bucket=bucket, Key=key)
# key: centos-packages/7/x86_64/re-index
# target_path: s3://yum-repo-bucket/centos/7/x86_64/
# sync_path: s3://yum-repo-bucket/centos-packages/7/x86_64/
target_key = re.sub(r'([a-zA-Z]+)-packages/([^/]+)/([^/]+)/re-index', r'\1/\2/\3', key)
target_path = "s3://%s/%s/" % (bucket, target_key)
sync_path = "s3://%s/%s/" % (bucket, os.path.dirname(key))
# Create UserData
user_data = """\
#!/bin/bash
yum install -y createrepo
aws s3 sync %s /repo --region ap-northeast-1
createrepo --checksum sha /repo
aws s3 sync --exact-timestamps --delete /repo %s --region ap-northeast-1
shutdown -h now
""" % (sync_path, target_path)
# Create Instance
ec2.run_instances(
ImageId = image_id,
MinCount = 1,
MaxCount = 1,
InstanceType = 't2.nano',
UserData = user_data,
InstanceInitiatedShutdownBehavior='terminate',
SubnetId = subnet_id,
IamInstanceProfile = { 'Name': instance_profile_name }
)
View the code on Gist .
APT版
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import json
import urllib
import boto3
import os
import re
# constants
image_id = 'ami-a21529cc' # ubuntu-trusty-14.04-amd64-server-20160114.5
subnet_id = 'subnet-example'
instance_profile_name = 'InstanceProfileName'
s3 = boto3.client('s3')
ec2 = boto3.client('ec2')
def lambda_handler(event, context):
# Get the object from the event and show its content type
bucket = event['Records'][0]['s3']['bucket']['name']
key = urllib.unquote_plus(event['Records'][0]['s3']['object']['key']).decode('utf8')
s3.delete_object(Bucket=bucket, Key=key)
# key: debian-packages/jessie/re-index
# target_path: /debian/jessie/
# sync_path: s3://yum-repo-bucket/debian-packages/jessie/
# distribution: jessie
target_path = re.sub(r'([a-zA-Z]+)-packages/([^/]+)/re-index', r'/\1/\2/', key)
sync_path = "s3://%s/%s/" % (bucket, os.path.dirname(key))
codename = key.split('/')[1]
# Create UserData
user_data = """\
#!/bin/bash
apt-get update
apt-get install awscli bzip2 -y
mkdir /aptly
wget -q -O - https://bintray.com/artifact/download/smira/aptly/aptly_0.9.6_linux_amd64.tar.gz | sudo tar xz -C /aptly --strip 1
mkdir /deb
aws s3 sync %s /deb --region ap-northeast-1
codename="%s"
cat <<EOF > /etc/aptly.conf
{
"gpgDisableSign": true,
"gpgDisableVerify": true,
"S3PublishEndpoints": {
"drecom": {
"region": "ap-northeast-1",
"bucket": "%s"
}
}
}
EOF
/aptly/aptly repo create --distribution="$codename" --component=main "$codename"
/aptly/aptly repo add "$codename" /deb
/aptly/aptly snapshot create snap from repo "$codename"
/aptly/aptly publish snapshot snap s3:drecom:%s
shutdown -h now
""" % (sync_path, codename, bucket, target_path)
# Create Instance
ec2.run_instances(
ImageId = image_id,
MinCount = 1,
MaxCount = 1,
InstanceType = 't2.nano',
UserData = user_data,
InstanceInitiatedShutdownBehavior='terminate',
SubnetId = subnet_id,
IamInstanceProfile = { 'Name': instance_profile_name }
)
View the code on Gist .
LambdaのEvent Sourcesを登録
YUMとAPTにそれぞれ、パッケージファイルと同階層に “re-index” というファイルが置かれたら発動するよう、Event Sourcesを設定します。
YUM : ObjectCreated | centos-packages, re-index
APT : ObjectCreated | debian-packages, re-index
パッケージは複数一気にアップロードすることがあるので、.rpm をトリガーにしてしまうと、複数処理が走って色々面倒になるので、独自ルールを設けて運用することにしました。空ファイルをピロっとアップするだけなので、特に苦はないです。
パッケージファイルをアップロード
.rpm や .deb を所定の位置にアップロードし、re-index ファイルを置くと Lambda が走ります。完了すると、re-index ファイルは無くなり、クライアントがアクセスする方のディレクトリに、最新のパッケージとメタデータが入っています。
全体のツリー状況としてはこんな感じ。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
. /
├── centos
│ └── 7
│ └── x86 _ 64
│ ├── repodata
│ │ ├── 3e24f101c94d - filelists . xml . gz
│ │ ├── 807c8cf5dbf9 - other . sqlite . bz2
│ │ ├── 9b1b2a31078b - filelists . sqlite . bz2
│ │ ├── 9fbc896c2ad9 - primary . xml . gz
│ │ ├── b89fbda6cdd2 - other . xml . gz
│ │ ├── d6cb4b85a394 - primary . sqlite . bz2
│ │ └── repomd . xml
│ └── nginx - 1.8.1 - 1.el7.ngx.x86_64.rpm
├── centos - packages
│ └── 7
│ └── x86 _ 64
│ └── nginx - 1.8.1 - 1.el7.ngx.x86_64.rpm
├── debian
│ └── jessie
│ ├── dists
│ │ └── jessie
│ │ ├── main
│ │ │ ├── binary - amd64
│ │ │ │ ├── Packages
│ │ │ │ ├── Packages . bz2
│ │ │ │ ├── Packages . gz
│ │ │ │ └── Release
│ │ │ └── Contents - amd64 . gz
│ │ └── Release
│ └── pool
│ └── main
│ └── n
│ └── nginx
│ └── nginx - dbg_1 . 8.0 - 1 ~ jessie_amd64 . deb
└── debian - packages
└── jessie
└── nginx - dbg_1 . 8.0 - 1 ~ jessie_amd64 . deb
クライアントの設定
これでレポジトリとしては利用できるようになったので、クライアントで利用できるかを確認します。
YUMの場合はbaseurlに
http://example.s3-website-ap-northeast-1.amazonaws.com/centos/$releasever/$basearch/
APTの場合は一行
deb http://example.s3-website-ap-northeast-1.amazonaws.com/debian/jessie jessie main
を書きます。あとはそれぞれのパッケージ検索などを実行して表示されればOKで、出てこなければ yum clear all や apt-get update してみるなど。それでダメなら、何かエラーが出てると思います。
キャッシュサーバーの構築
プロビジョニング・ツールのレシピをGitで管理して、PullRequestのたびにCIテストを走らせていると、結構な頻度でどこかのパッケージ・レポジトリでエラーになって停止したりします。
テストだけならまだしも、他にイメージ自動作成や、インスタンス起動時などにもレシピを実行するタイミングがある場合、「レポジトリエラーの回避」は実は結構高い重要度となります。
パッケージ以外にも、wget などで外部アクセスをレシピに盛り込むと、そこがエラーになって止まる可能性はそれなりにあり、外部サイトの利用については予めポリシーを決めておく必要があります。
パッケージと外部サイト利用のポリシー
非常に良い例として、fluentd があります。fluentdのインストール手順はよく出来ている反面、外部アクセスが必要になるものです。
curl - L https : / / toolbelt .treasuredata .com / sh / install - redhat - td - agent2 .sh | sh
中身を見ると、GPG鍵のインポートと、レポジトリ情報を作成してから、td-agent をインストールしています。これがもし、鍵のサーバーが落ちていたら、レシピがそのままなら自分達ではどうにもできなくなる、ということになります。
そのためレシピでは、鍵とレポジトリ情報を先にファイルとして保存し、外部アクセスは td-agent のインストールから必要とするようにしています。
他にも、zabbix は zabbix-release というパッケージをダウンロードしてインストールすることで、鍵とレポジトリ情報を取得できますが、zabbix-release の .rpm を wget できなければ、やはりエラーになります。そのため、zabbix-release のRPMは、S3の自前レポジトリにコピーしておくという方針にしています。
ポリシーをまとめると、
パッケージが無いソフトウェアはパッケージを作成してS3に置く
外部から裸のパッケージを取得する必要がある場合はS3にコピーしておく
パッケージ以外の外部ファイルはレシピのファイルとして保存しておく
外部のパッケージ・レポジトリはそのまま利用させてもらう
となりますが、4つ目の外部レポジトリの利用は、もし障害が発生したら解決に手間がかかってしまうため、自分達である程度のコントロールができるよう、以下のように対策をとります。
キャッシュサーバーを経由して耐障害性を向上する
外部レポジトリを直接見にいくと、障害の問題だけでなく、転送量的にもそれなりに迷惑をかけてしまうことになります。
これを解決するために、クライアントにはキャッシュサーバーを経由してもらいます。yum.conf に以下のように記述することで、簡単にプロキシ経由にすることができます。
proxy = http : //package-cache.example.com/
パッケージファイルは半永久的にキャッシュできるため、こうすることで、二度目以降はキャッシュサーバーからパッケージをダウンロードすることになり、外部レポジトリにアクセス不要になります。転送量的にも障害的にも解決できて一石二鳥です。
障害が出るとしたら、キャッシュサーバそのものの障害か、新しいパッケージの取得が必要になったと同時に、外部レポジトリに障害(またはアクセスしづらい状況)が発生した場合のみになり、かなり耐障害性が向上することが見込まれます。
で、今回はキャッシュサーバーには squid を採用してみました。squid は多機能なので難しく見えますが、今回の部分だけならば、かなり簡単な設定で済みます。
Squidのインストール
インストールします。
今回はアクセス制限をDigest認証にします。アカウント情報のテキストを作成します。
メインの設定を追記編集します。下記設定のバージョンはver3.xです。
ver2.6の場合、visible_hostname unknown を追加する必要があります
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
auth_param digest program / usr / lib64 / squid / digest_pw_auth / etc / squid / digest . auth
auth_param digest children 3
auth_param digest realm Yum Proxy
auth_param digest nonce_garbage _ interval 5 minutes
auth_param digest nonce_max _ duration 30 minutes
auth_param digest nonce_max _ count 50
acl password proxy_auth REQUIRED
http_access allow localnet
http_access allow localhost
http_access allow password
http_access deny all
cache_dir ufs / var / spool / squid 4192 16 256
cache _ mem 128 MB
maximum_object _ size 128 MB
maximum_object_size_in _ memory 16 MB
※2016/3/10追記
もし、パッケージの最新情報を的確に捉えたいならば、YUMの場合は repomd.xml をキャッシュしないように設定します。
acl REPOMD urlpath _ regex . * repomd \ . xml $
cache deny REPOMD
cache allow all
再起動します。
クライアントのyum設定を追記編集します。squidのポートは3128です。
※CentOS6だと正しく認証処理をせず、いきなりBasic認証としてアクセスしようとするので、接続できなかったりします
proxy = http : //UserName:SquidPassword@package-cache.example.com:3128/
Squidの運用
たいしてリソースを使うシステムじゃないので、ほどほどにメモリを使って、余裕ある程度にディスク容量を使えば、問題なく運用できます。が、キャッシュヒット率などを見ると面白いので、運用コマンドについて軽く説明しておきます。
キャッシュヒット率などの確認
アクセス元毎の情報を表示してくれます。
コマンドは、ver3.xは /usr/bin、ver2.6は /usr/sbin にあります
$ squidclient mgr : client_list
Address : 1.2.3.4
Name : 1.2.3.4
Currently established connections : 0
ICP Requests 0
HTTP Requests 21376
TCP _ HIT 13456 63 %
TCP _ MISS 236 1 %
TCP_REFRESH _ HIT 700 3 %
TCP_REFRESH _ MISS 25 0 %
TCP_MEM _ HIT 697 3 %
TCP _ DENIED 6262 29 %
キャッシュファイルの確認
1つ1つのキャッシュオブジェクトの詳細を確認できます。
LV : 作成時間
LU : 最終アクセス時間
LM : Last-Modifiedヘッダ時間
EX : キャッシュ期間
EXの決定条件は色々あるっぽいですが、基本は以下。
Expiresヘッダをそのまま守る
(現在時刻 – Last-Modified) / 10
squid.conf の refresh_pattern で明示的に指定
$ squidclient mgr : objects
. . .
KEY BD85377090C9084486227A67EF70E8C6
GET http : / / ftp .riken .jp / Linux / centos / 7 / os / x86_64 / Packages / NetworkManager - 1.0.6 - 27.el7.x86_64.rpm
STORE_OK IN_MEMORY SWAPOUT_DONE PING_DONE
CACHABLE , DISPATCHED , VALIDATED
LV : 1456812660 LU : 1456812660 LM : 1448464812 EX : - 1
0 locks , 0 clients , 1 refs
Swap Dir 0 , File 0X00016E
inmem_lo : 0
inmem_hi : 2131034
swapout : 2131034 bytes queued
. . .
キャッシュサーバーの耐障害性
最後に、キャッシュサーバーが落ちたらどうするか、について考えておきます。
利用するタイミングが、自動テストや自動イメージ作成の時だけならば、パッケージを取得できなくなっても本番サービスには影響がないため、それほど耐障害性に気を使う必要はないでしょう。せいぜい、EC2に構築して、「Amazon EC2 Auto Recovery」に任せておき、監視を設定しておけば十分です。
もしAutoScalingのようなサーバーの自動起動において、最初にレシピを走らせてパッケージ取得をする必要があるかもしれない場合。まずはイメージ作成の時点でパッケージ操作を完了させてしまうことができないか、を最初に検討すべきでしょう。それでも必要であると判断した場合は、Squidを2台以上たてて、兄弟キャッシュの設定をしつつ、ロードバランサ経由にする、といった構成になるでしょう。
まとめ
仮想環境や、AWS、キャッシュサーバーを寄せ集めて、Linuxパッケージの管理をできるだけ 楽に 安全に する構成を考えてみました。それだけではなく、費用的にも極小でいけるので、良いこと盛りだくさんです。
長く使うものですし、早い段階でこういった部分を作りこんでしまうことは、テキトーに運用している時の、どんどん汚く運用の流れがボケていくような不安を払拭できるでしょう。
とか偉そうなこと書いている私も、最近になってようやく、それなりに十分に納得いくシステムになってきたな、という感触なんですけども:-)