S3+Lambda+CloudFormationでサーバレスyumリポジトリ

先日の記事で、大きめの処理をLambda+CloudFormationで実行するめどがついたので、S3+Lambda+CloudFormationでサーバレスyumリポジトリを作ってみた。

処理の概要

  1. S3にrpmを追加・更新・削除
  2. Lambdaがイベントをフック→cfnスタックを作成
  3. cfnスタックがEC2インスタンスを起動→S3リポジトリをダウンロード
  4. createrepoを実行して、インデックスをS3にアップロード
  5. cfnスタックは処理完了後に自動的に削除

Lambda Function

こんな感じ。

var Promise = require('bluebird');

var AWS = require("aws-sdk");
AWS.config.update({region: 'ap-northeast-1'});

var cloudformation = Promise.promisifyAll(new AWS.CloudFormation());

var fs = Promise.promisifyAll(require('fs'));

exports.handler = function(event, context) {
  var stackName = 'my-stack-' + new Date().getTime();

  fs.readFileAsync('template.json', 'utf8').then(function(data) {
    return cloudformation.createStackAsync({
      StackName: stackName,
      Parameters: [
        {
          ParameterKey: 'S3Bucket',
          ParameterValue: 'my-yum-repo'
        }
      ],
      TemplateBody: data
    });
  }).then(function(data) {
    console.log(data);
  }).then(function() {
    context.succeed('OK');
  }).catch(function(err) {
    context.fail(err);
  });
};

roleにはcfnスタックを作成できる適切な権限を付与しておく。

また、S3のイベントソースをPost/Put/Deleteで追加する。

f:id:winebarrel:20150808160127p:plain

なんか、イベントソースの情報取得まわりがぶっ壊れているような気が… 一応、動作はしました。

cfn template

こんな感じ。

{
  "Parameters": {
    "S3Bucket": {
      "Type": "String"
    }
  },
  "Resources": {
    "MyInstance": {
      "Type": "AWS::EC2::Instance",
      "Properties": {
        "ImageId": "ami-cbf90ecb",
        "InstanceType": "t2.micro",
        "SubnetId": "subnet-XXXXXXXX",
        "IamInstanceProfile": "my_role",
        "Tags": [
          {
            "Key": "Name",
            "Value": {"Ref": "AWS::StackId"}
          }
        ],
        "UserData": {
          "Fn::Base64": {
            "Fn::Join": [
              "",
              [
                "#!/bin/bash\n",
                "yum install -y createrepo\n",
                "aws s3 sync s3://", {"Ref": "S3Bucket"}, " myrepo --delete\n",
                "createrepo myrepo\n",
                "aws s3 sync myrepo s3://", {"Ref": "S3Bucket"}, " --delete\n",
                "aws cloudformation delete-stack --stack-name ", {"Ref": "AWS::StackName"}, " --region ", {"Ref": "AWS::Region"}, "\n"
              ]
            ]
          }
        }
      }
    }
  }
}

やってることは、まあ見たとおりです。

S3のバケットの構成

s3://my-yum-repo/
├── noarch
│   └── hello-0.1.0-1.noarch.rpm
└── repodata
    ├── ...
    └── repomd.xml

yumの設定ファイル

/etc/yum.repos.d/my.repo

[my]
name=my
baseurl=http://my-yum-repo.s3-website-ap-northeast-1.amazonaws.com/
gpgcheck=0
enabled=1

使ってみる

初期状態でhello2というrpmはなし。

$ sudo yum clean all; sudo yum search hello2
...
警告: 一致するものが見つかりません: hello2
見つかりませんでした

rpmファイルをS3においてみる。

$ aws s3 cp hello2-0.1.0-1.noarch.rpm s3://my-yum-repo/noarch/
upload: rpmbuild/RPMS/noarch/hello2-0.1.0-1.noarch.rpm to s3://my-yum-repo/noarch/hello2-0.1.0-1.noarch.rpm

すると、Lambdaが起動。

START RequestId: 3043f34d-3d9c-11e5-8a8b-ebe5957bb289
2015-08-08T07:07:55.968Z    3043f34d-3d9c-11e5-8a8b-ebe5957bb289    { ResponseMetadata: { RequestId: '30fc94cc-3d9c-11e5-8ca5-3128034c20e8' },
  StackId: 'arn:aws:cloudformation:ap-northeast-1:123456789012:stack/my-stack-1439017674468/31109130-3d9c-11e5-8e97-5001aba754a8' }
END RequestId: 3043f34d-3d9c-11e5-8a8b-ebe5957bb289
REPORT RequestId: 3043f34d-3d9c-11e5-8a8b-ebe5957bb289  Duration: 1558.67 ms    Billed Duration: 1600 ms    Memory Size: 128 MB Max Memory Used: 16 MB

Lambdaがcfnスタックを作成→EC2インスタンス起動。

f:id:winebarrel:20150808160949p:plain

EC2インスタンスリポジトリのインデックスを更新→S3にアップロード。

f:id:winebarrel:20150808161221p:plain

EC2インスタンスは自動的に削除。

f:id:winebarrel:20150808161303p:plain

yumリポジトリにhello2が追加される。

$ sudo yum clean all; sudo yum search hello2読み込んだプラグイン:fastestmirror
...
======================================= N/S Matched: hello2 ========================================
hello2.noarch : hello2