現代ITインフラの王道をゆくLinuxパッケージ管理の基本構成

RedHat系におけるRPMパッケージを扱うYUM、Debian系におけるDEBパッケージを扱うAPT、これらはサーバー管理において重要なわけですが、絶妙な度合いで、おざなりに扱ってもわりとなんとか運用出来てしまう感があります。そのため今一度、こんな感じが今風のスタンダードじゃないっすかね(キリッ という構成を説明してみます。

ぶっちゃけ、たいしたことないネタの集合体なので、タイトルに下駄を履かせました。



そもそもパッケージは必要なのか

言うまでもなく必須です。理由は、インストール物のファイル管理が容易になるのと、インストール時間を短縮できるからです。既存のパッケージでconfigureオプションが物足りない時や、RPMパッケージが存在しない場合は作成することになります。

最近はプロビジョニング・ツールによって全て自動化できるので、超簡素なコンパイルのものはレシピに落とし込んで終わりにしたくなるかもですが、上記理由により悪手であると断言できます。

パッケージの作成自体はただの慣れの問題なので、サクッと修得してしまいましょう。


パッケージ管理の全体構成

まずは全体構成図になります。以降はここから切り出して説明していきます。




パッケージ作成


ここでは、パッケージ作成作業を、好きな仮想環境で行いましょう、というだけです。

普通にコンパイルからインストールして動作確認をしたり、パッケージ作成作業をしていると、ゴミが溜まっていくので、ポイ捨てできる環境でやったほうが気が楽だからです。

パッケージ作成手順そのものについては割愛!
(参考記事:CentOS7でのRPMパッケージ作成手順(sshguard編)


S3にレポジトリを作成


ApacheやNginxで独自レポジトリを運用する時代は終わりました。サーバー管理が面倒なので、S3のWeb Site Hostingを使いましょう。

S3の準備をする

バケットを作って、Web Site Hostingを有効にします。
そして、こんな感じでツリーを作成します。

S3のアクセス制限

たとえOSSのパッケージだとしても、自作パッケージを野放しに公開しておくのはよくないですから、どちらかの方法でアクセス制限をします。

ソースアドレスで制限
S3のバケットポリシーの編集にて、こんな感じで制限できます。

VPCエンドポイントで制限
外からのアクセスが不要ならば、VPCエンドポイントで制限してしまえます。


Lambdaでレポジトリ・メタデータの自動更新


S3にアップロードされたら、自動的にLambdaが起動して、レポジトリのメタデータが更新されるようにします。

IAMにロールを作成

まずは、Lambdaに割り当てるRoleを作成します。内容は大雑把にするとこんな感じ。
  • AmazonEC2FullAccess
  • AmazonS3FullAccess
  • IAMPassRole

  • レポジトリ更新用Lambdaスクリプトを作成

    UserDataにBashをぶっ込みつつEC2に処理を行わせるスクリプトです。超頑張ったらLambdaでもレポジトリのメタデータを生成できるでしょうが、どう考えても無駄なので、最安インスタンスを立ち上げて、普通にコマンドベースでやったほうがよいという判断です。

    YUM版
    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 }
    )

    APT版
    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 }
    )

    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 ファイルは無くなり、クライアントがアクセスする方のディレクトリに、最新のパッケージとメタデータが入っています。

    全体のツリー状況としてはこんな感じ。

    クライアントの設定

    これでレポジトリとしては利用できるようになったので、クライアントで利用できるかを確認します。

    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のインストール手順はよく出来ている反面、外部アクセスが必要になるものです。

    中身を見ると、GPG鍵のインポートと、レポジトリ情報を作成してから、td-agent をインストールしています。これがもし、鍵のサーバーが落ちていたら、レシピがそのままなら自分達ではどうにもできなくなる、ということになります。

    そのためレシピでは、鍵とレポジトリ情報を先にファイルとして保存し、外部アクセスは td-agent のインストールから必要とするようにしています。

    他にも、zabbix は zabbix-release というパッケージをダウンロードしてインストールすることで、鍵とレポジトリ情報を取得できますが、zabbix-release の .rpm を wget できなければ、やはりエラーになります。そのため、zabbix-release のRPMは、S3の自前レポジトリにコピーしておくという方針にしています。

    ポリシーをまとめると、

  • パッケージが無いソフトウェアはパッケージを作成してS3に置く
  • 外部から裸のパッケージを取得する必要がある場合はS3にコピーしておく
  • パッケージ以外の外部ファイルはレシピのファイルとして保存しておく
  • 外部のパッケージ・レポジトリはそのまま利用させてもらう

  • となりますが、4つ目の外部レポジトリの利用は、もし障害が発生したら解決に手間がかかってしまうため、自分達である程度のコントロールができるよう、以下のように対策をとります。

    キャッシュサーバーを経由して耐障害性を向上する

    外部レポジトリを直接見にいくと、障害の問題だけでなく、転送量的にもそれなりに迷惑をかけてしまうことになります。

    これを解決するために、クライアントにはキャッシュサーバーを経由してもらいます。yum.conf に以下のように記述することで、簡単にプロキシ経由にすることができます。

    パッケージファイルは半永久的にキャッシュできるため、こうすることで、二度目以降はキャッシュサーバーからパッケージをダウンロードすることになり、外部レポジトリにアクセス不要になります。転送量的にも障害的にも解決できて一石二鳥です。

    障害が出るとしたら、キャッシュサーバそのものの障害か、新しいパッケージの取得が必要になったと同時に、外部レポジトリに障害(またはアクセスしづらい状況)が発生した場合のみになり、かなり耐障害性が向上することが見込まれます。

    で、今回はキャッシュサーバーには squid を採用してみました。squid は多機能なので難しく見えますが、今回の部分だけならば、かなり簡単な設定で済みます。

    Squidのインストール

    インストールします。

    今回はアクセス制限をDigest認証にします。アカウント情報のテキストを作成します。

    メインの設定を追記編集します。下記設定のバージョンはver3.xです。

  • ver2.6の場合、visible_hostname unknown を追加する必要があります
  • ※2016/3/10追記
    もし、パッケージの最新情報を的確に捉えたいならば、YUMの場合は repomd.xml をキャッシュしないように設定します。

    再起動します。

    クライアントのyum設定を追記編集します。squidのポートは3128です。

  • ※CentOS6だと正しく認証処理をせず、いきなりBasic認証としてアクセスしようとするので、接続できなかったりします

  • Squidの運用

    たいしてリソースを使うシステムじゃないので、ほどほどにメモリを使って、余裕ある程度にディスク容量を使えば、問題なく運用できます。が、キャッシュヒット率などを見ると面白いので、運用コマンドについて軽く説明しておきます。

    キャッシュヒット率などの確認
    アクセス元毎の情報を表示してくれます。
  • コマンドは、ver3.xは /usr/bin、ver2.6は /usr/sbin にあります
  • キャッシュファイルの確認
    1つ1つのキャッシュオブジェクトの詳細を確認できます。
  • LV : 作成時間
  • LU : 最終アクセス時間
  • LM : Last-Modifiedヘッダ時間
  • EX : キャッシュ期間

  • EXの決定条件は色々あるっぽいですが、基本は以下。
  • Expiresヘッダをそのまま守る
  • (現在時刻 – Last-Modified) / 10
  • squid.conf の refresh_pattern で明示的に指定
  • キャッシュサーバーの耐障害性

    最後に、キャッシュサーバーが落ちたらどうするか、について考えておきます。

    利用するタイミングが、自動テストや自動イメージ作成の時だけならば、パッケージを取得できなくなっても本番サービスには影響がないため、それほど耐障害性に気を使う必要はないでしょう。せいぜい、EC2に構築して、「Amazon EC2 Auto Recovery」に任せておき、監視を設定しておけば十分です。

    もしAutoScalingのようなサーバーの自動起動において、最初にレシピを走らせてパッケージ取得をする必要があるかもしれない場合。まずはイメージ作成の時点でパッケージ操作を完了させてしまうことができないか、を最初に検討すべきでしょう。それでも必要であると判断した場合は、Squidを2台以上たてて、兄弟キャッシュの設定をしつつ、ロードバランサ経由にする、といった構成になるでしょう。


    まとめ

    仮想環境や、AWS、キャッシュサーバーを寄せ集めて、Linuxパッケージの管理をできるだけ 楽に 安全に する構成を考えてみました。それだけではなく、費用的にも極小でいけるので、良いこと盛りだくさんです。

    長く使うものですし、早い段階でこういった部分を作りこんでしまうことは、テキトーに運用している時の、どんどん汚く運用の流れがボケていくような不安を払拭できるでしょう。

    とか偉そうなこと書いている私も、最近になってようやく、それなりに十分に納得いくシステムになってきたな、という感触なんですけども:-)