By

Object指向CloudFormationとしてのBarcelona

はじめに

前の記事に書いたように、当社のインフラは Barcelona に移行しています。今回は、これを実際に使っている一ユーザの立場からこれを紹介してみたいと思います。

Barcelonaとは、簡単に言えば、AWSのスターターキットです。AWSにはたくさんのサービスがありますが、数が多すぎて初心者には、何をどう使ったらいいか判断できません。特に、単体では意味不明だけど、これとこれをこう組み合せると、ちょっとした手間でセキュリティとスケーラビリティの面でいいことが多いパターン、いわゆるグッドプラクティスがたくさんあります。Barcelonaを使うと、グッドプラクティスを詰めこんだインフラを簡単に構築できます。

スターターキットと言っても、適用範囲が単純なアプリケーションに限定されているわけではありません。私の担当は、当社のeCommerceプラットフォームで、これは、周辺のサブシステムも含めると、かなり複雑な構成のアプリケーションなのですが、先日、これも無事に Barcelona ベースのインフラに移行しました。これが移行できたということは Barcelona の実用性と柔軟性が証明されたと考えています。

Barcelonaのいい所は、機能や守備範囲を欲張ってないことです。ランタイムとしてはほぼ ECS そのものだし、権限の管理については IAM に依存しています。AWSに依存しないで Barcelona 単独で行なうことはほとんどないのですが、ECSやIAM を生で使うよりはるかに簡単かつ安全に行なえます。

それでは、AWSの各機能に対するラッパーの寄せ集めかというとそうではなくて、統一感を持ってインフラを構築、運用できます。また、自然な形でグッドプラクティスに誘導されていきます。

バンドで言えばベース、サッカーで言うと中盤の底、野球ならキャッチャーという感じで、目を引くような目立つ働きはしないのですが、ゲームの組み立てを裏から支配しているという感じです。

これは、ぜひみなさんにも体験していただいて、この良さをシェアしたいと思うのですが、一方で、この控え目な所が、Barcelonaのわかりにくさにもなります。それが何をするツールかわからないので、その良さも見えないのです。

良いベースプレイヤーやボランチは、玄人筋からは全体の要として評価されますが、一般の人から見ると「そんな人いたっけ」「何をしてたの?」と言われてしまいます。それと同じようなものです。Barcelonaは視野は広いのですが、動きは地味なのです。ベースがうまいバンドは、「音がいい」「ノリがいい」と言われてバンドの音全体が評価されることは多いのですが、ベースプレイヤー個人がほめられることはあまりありません。

Barcelonaを使うようになって、インフラの構築運用がスッキリして楽になったのは確かですが、その中のどの部分にBarcelonaが貢献しているのかを説明するのは難しいです。

そこで、この記事では「オブジェクト指向CloudFormation」という、ちょっと変わった切り口でこれを紹介してみたいと思います。

現在のインフラの課題

インフラ回りでの今ホットな話題は「クラウド」と「Docker」だと思います。Barcelonaはこの二つを一番いい形で結合します。まず、これらがなぜ注目されているか考えてみましょう。

クラウドになって一番便利なことは、APIでサーバを用意できることです。APIがあると、スクリプトでインフラの構築作業を記述できるので、作業をコード化できます。コードになるということは、アプリケーションのコードと同じようにレポジトリに履歴を保存したり差分をレビューしたりできます。これによって、構築作業が再現可能、検証可能になることが一番の大きな違いです。

AWSには CloudFormation という独自のインフラ記述言語があって、これを使うことで、「作業」をほとんどなくしてインフラを「記述」することができます。

一方、Dockerの意味は、コンテナという形で、アプリケーションとインフラの接点が標準化されたことだと思います。

インフラと言っても、CPU(コア)の台数やメモリ容量などのハードウェアに近い部分から、gem のようにアプリケーションに近い部分まで多くのレイヤーがあって、それぞれにレイヤーが入り組んでいて互いに依存している要素もたくさんあります。これが全部一枚岩になっている所にインフラの難しさというか、一筋縄ではいかない所があります。

Dockerが広まったことで一番いいのは、コンテナの中(アプリケーション+Dockerfile)を作る人と、それを配置して実際に走らせる人が分業しやすくなったことです。つまり、インフラの中に混ざりようがないスッキリとした境界線が引けたことが重要だと思います。

ですから、今一番イケてるやり方は、CloudFormationでECSクラスタを構築して、その上で Docker コンテナを走らせることです。この組み合わせで、2、3年前には考えられなかったような高度なレベルで、多様なアプリケーションを走らせる検証可能なインフラを構築することができます。

ただ、唯一の問題は、この CloudFormation を書くのがやたら難しいことです。特に、ECSを走らせるには多くのリソースが必要なので、CloudFormation の記述量も多くなるし、要素間の関連も複雑になります。

単純なマクロ化、テンプレート化では解決しない問題

こういう問題に対する一番単純な解決方法は「分割統治」です。CloudFormation が長くなるなら、機能別に分けて少しづつ管理していけばいいでしょう。

分割することで、再利用もしやすくなります。小さい検証済みのパーツを組合せて少しづつ大きな機能を作っていくこともできます。

CloudFormation そのものにもパラメータ展開の機能があるし、それが機能不足だと思うなら、それをラップするツールを作ればいいでしょう。

当社もその方向を模索しましたが、すぐに壁にぶちあたりました。テンプレートを分解していくと、指定すべきパラメータがどんどん増えていくのです。

この様子を Ruby風の疑似コードで示すと次のようになります。

def create_cluster(aws_access_key_id, aws_secret_access_key, region, instance_type)
  # VPC を構築
  vpc_id = create_vpc(aws_access_key_id, aws_secret_access_key, region)
  # VPC 内に subnet を構築
  subnet_ids = create_subnets(aws_access_key_id, aws_secret_access_key, region, vpc_id)
  # subnet内に EC2 instance を構築 
  subnet_ids.each do |subnet_id|
    create_instance(aws_access_key_id, aws_secret_access_key, region, vpc_id, subnet_id, instance_type)
  end
end

# staging test 用の環境構築
create_cluster(<ACCESS_KEY>, <SECRET_KEY>, "ap-northeast-1", "t2.micro")

# 本番用の環境構築
create_cluster(<ACCESS_KEY>, <SECRET_KEY>, "ap-northeast-1", "m4.large")

VPCの中にsubnetを作るには、そのVPCのIDを与える必要があるし、インスタンスを作るには、subnetとVPCのIDを与えなくてはいけません。リソースを作る手続きはどれもたくさんのパラメータを必要とします。

実際には、EC2インスタンスを構築するには、もっとたくさんのパラメータが必要になります。そして、その多くは、本番用の環境とテスト用の環境では異なるので、もっとたくさんのパラメータをスクリプト(テンプレート)の外から与える必要があります。

かといって、全部一括で作ろうとすると、リソース間の関連が固定されて適用範囲が狭くなります。 production: true のようなフラグを渡して分岐しようとすると、テスト環境ではうまくいったのに本番環境では(検証されてないコードが走るので)失敗してしまうことになります。

インフラのコード化の恐しい所は、そういうちょっとしたうっかりミスが直接サービスダウンにつながる所です。

再利用を意識してテンプレートを分割していくと、このようなパラメータやオプションがとめどもなく増えて、結局それを呼びだす側、使う側に複雑さを押し付けるだけになってしまいます。

オブジェクト指向による整理

これは、プログラミングの問題と考えると、古典的な問題であるとも言えます。つまり、相互関連するオブジェクトを手続き的にシミュレートすることに無理があるのです。

そこで、これをオブジェクト指向的に解決するとどうなるか、やはり Ruby風の疑似コードで示してみます。


class VPC
  def initialize(aws_access_key_id, aws_secret_access_key, region)
    @aws_access_key_id = aws_access_key_id
    @aws_secret_access_key = aws_secret_access_key
    @region = region
    @vpc_id = create_vpc()
    create_subents()
  end
  
  def create_vpc
    ...
  end
  
  def create_subnets
    ...
  end
  
  def create_instance(instance_type)
    ...
  end
end

# staging test 用の環境構築
staging_vpc = VPC.new(<ACCESS_KEY>, <SECRET_KEY>, "ap-northeast-1")
staging_vpc.create_instance("t2.micro")

# 本番用の環境構築
production_vpc = VPC.new(<ACCESS_KEY>, <SECRET_KEY>, "ap-northeast-1")
production_vpc.create_instance("m4.large")
    

このように、テンプレートをクラス化し、テンプレートのユーザが関知しない内部的な情報(regionなど)をオブジェクトの内部にカプセル化することで、テンプレートを外側から見た時の見通しがよくなります。

たとえば、インスタンス生成のメソッドに渡すパラメータは、instance_typeのように、インスタンス生成にどうしても必要なものに限定することができます。

もちろん、単純に Ruby のスクリプトを書くだけでは、そのスクリプトの見通しはよくなるとしても、内部で生成された subnet などのリソースの情報は、スクリプト終了時に消えてしまいます。

ですから、何らかのオブジェクトの内部に情報をカプセル化する、という方向性は正しいとしても、それを単なるライブラリ(クラス)として実装したのでは目的を果たせません。データを永続化するのはデータベースで、それを管理するエージェントが必要になります。

Barcelona=抽象的なリソース+デーモン+API(CLI)

Barcelonaは、このようなオブジェクト指向的な発想で、複雑な関連のあるリソースを管理するツールです。

ただし、そのオブジェクトの状態は、メモリ上の変数でなくデータベースなどの永続的な記憶媒体に保存する必要があります。従って、Barcelonaは、単純なライブラリではなく、データベースとそれを管理するエージェントによって、リソースの情報を管理します。

そのエージェントは、REST的なHTTP APIを持っていて、その API に要求を送信するためのコマンドラインツールも付属しています。

また、ECSに必要なリソースは、VPCのようにネットワークに関連するものばかりではありません。そのため、VPCをそのままモデル化せずに、それを抽象化して「District」とというモデルを持っています。

「District」とは、VPCを中心とした、アプリケーションを動かす場を抽象化したモデルです。「本番環境」「テスト環境」という時の「環境」を指していると言った方がいいかもしれません。

「District」に配置されるのは、アプリケーションあるいは、Docker Image ですが、これも「Heritage」というモデルとして抽象化されています。「District」が配置する場の抽象モデルだとしたら、「Heritage」はそこに配置されるものの抽象モデルです。

Barcelonaでは、運用時に直接意識するのはこの「District」と「Heritage」という二つの抽象モデルで、この二つのモデルを操作することで、背後で実際のAWSリソースが生成されていくという仕組みになっています。この二つのモデルの抽象度がいい感じなので、Barcelonaの操作体系がスッキリしているのだと思います。

時系列に沿って言うと、以下のようになります。

  1. 本番用、テスト用の二つの「District」を作成する
  2. アプリケーションを Docker Image の形で作成する
  3. テスト用「District」の中に、そのアプリケーションの「Heritage」を作成する(=ステージングテスト)
  4. 本番用「District」の中に、そのアプリケーションの「Heritage」を作成する(=サービスイン)
  5. アプリケションを変更時は、Docker Imageを更新してから各「Heritage」に更新指示を出す

これをアプリケーションごとに繰り返すことになります。

これらの作業ステップは、bcn という付属の cli のコマンドとして投入します。bcn は、デーモンとして動いている Barcelona 本体に、HTTP でリクエストを出し、Barcelona本体が、AWS の API を呼び出して指示を実行すると同時に、状態の変化をデータベースに記録します。

この bcn の指示が、前の疑似コードでのメソッド呼び出しに相当します。これらの指示は論理的には「District」や「Heritage」のメソッド呼び出しになります。これらのオブジェクトは、関連するリソースの情報を持っているので、ほとんどの場合、冗長な情報を与える必要はなく、与えるパラメータは一つか二つです。

たとえば、「District」の作成コマンドは、District名の他にはリージョンの指定だけです。

$ bcn district create --region=ap-northeast-1 <district name>

これによって、以下のようなリソースが作成されます。

  • VPC
  • Subnet(publicとprivateをそれぞれ二つづつ)
  • Security Group
  • Nat Gateway
  • Auto Scaling
  • ECS Cluster
  • S3 Bucket (for ECS)

これらのリソースのIDは Barcelona 内部に保存されますので、運用において意識する必要はありません。

つまり、抽象化されたモデルにコマンドを発行することで、冗長なオプションなしで、複雑なインフラを構築、運用することが可能になるわけです。

Barcelonaは、CloudFormation が目指した理想を、ちょっと違う方向から別のアプローチで実現したものととらえるのが、一番わかりやすいような気がします。

インフラツール独自のセキュリティの問題

さて、このようなツールには、独特の問題が一つあります。

それは、デーモンの権限が強すぎることと、セキュリティ上重要な情報をたくさん管理する必要があることです。実際、上記の疑似コードでは、AWSのアクセスキーとシークレットを保存しているのですが、このアクセスキーに与えるべき権限はたくさんあり、この情報が漏れたら大変なことになります。

Barcelonaは、この問題に対しても IAM を使用したエレガントで安全な方法で対応しているのですが、この詳細については、別の記事で説明したいと思います。

簡単に言うと、一番セキュリティ上の問題になりそうな所は、AWS標準の IAM にまかせるという方法です。アクセスキー(+シークレット)は保存せず、代わりに IAM Role を作成して、その Role の使用を IAM 標準の機能によって制限するという方法を使っています。ですから、Barcelona のデータベースは仮に漏洩したとしても内部の構成図を知られるだけで、この情報だけから不正アクセスを行うことはできません。

グッドプラクティスをデフォルトにする

さて、インフラの詳細をエージェントの中にカプセル化することにはもう一つ利点があります。それは、エージェントにノウハウを内包できることです。

たとえば上記の例(疑似コード)では、create_instance というメソッドを例にして説明しましたが、実際の Barcelona には、そのようなコマンドはありません。Docker(ECS)ベースで運用する以上、インスタンスを個別に作成する必要はないからです。

実際には、各Districtが cluster_size という属性を持っていて、この数に従って、Auto Scaling Group (ASG) を作成します。インスタンスは、この ASG が作成します。

ASGの配下でインスタンスを作成することには、いくつかの利点があります。たとえば、仮にハードウェアの故障でインスタンスが意図せず停止した場合には、ASGが自動的に代わりのインスタンスを起動してくれます。また、運用中のインスタンスの入れ替え作業(Rolling Update)も簡単になります。

つまり、「インスタンスを個別に作成するよりASG配下で自動作成した方がいい」というノウハウが、Barcelonaの内部に含まれているのです。

あるいは、「アプリケーションサーバは、外部(インターネット)に直結せず、private なネットワークに接続した方がいい」という定石があります。これはほとんどの場合に守るべきグッドプラクティスですが、これを守るには、private subnet を作るだけでなく、別にpublic な subnet を作成し、外向きの接続経路として、Nat gateway を作成する必要があります。踏み台サーバも用意する必要があります。つまり、構築すべきリソースが格段に多くなるのです。

Barcelonaは、これについても、特に指示しなくても、そのようなネットワーク構成を作成します。そして、そのリソースの情報を内部に保持しているので、複数の subnet の存在を特に意識することなく、その後の作業を進めることができます。

これは単なるテンプレートやマクロには不可能なことです。高度な機能やオプションを持つマクロを作ることはできますが、マクロの場合は、その機能が複雑になると、それに比例して使う側に要求される知識のレベルも高くなります。

エージェントにすることで、エージェントに対する指示はシンプルなものであっても、内部で行なう動作は複雑で高度なものにすることが可能になります。これが Barcelona が実現していることです。

essential devops?

このように、情報を内部に持つエージェントを置くことで、インフラの構築、運用はずっと見通しがよくなるのですが、ひとつだけ弱点があります。

chef のように、全てのインフラの状態がなんらかのソースコードとして記述されていれば、それをリポジトリで管理し、レビューやCIなど、アプリケーションコードで行なわれてきたグッドプラクティスを全て適用することができます。いわゆる DevOps です。

この記事では、Barcelonaは、CloudFormation の進化形だと説明してきましたが、DevOpsという観点からは、一歩後退と言えなくもありません。

ただし、個々のアプリケーションに関わる部分については、アプリケーション別に、barcelona.yml というソースに記述して、これを bcn コマンドでエージェントに与えるようになっています。このファイルと Dockerfile で、アプリケーションに近い部分のインフラについては、DevOps的運用ができます。

District が保持する情報については、コマンドによる運用になるので、DevOps的ではありません。ただ、この部分は、最初に設定したらめったに変更、再構築をしない変化の少ない情報が集約されているので、履歴管理などの必要性は低い部分になるかと思います。

よく言えば、BarcelonaでのDevOpsは、比較的変化の激しいアプリケーション寄りの部分に限定される、いわば、essential DevOps と言うべきかもしれません。

まとめ

Barcelonaは、CloudFormationの延長線にある、インフラ構築支援ツールです。ただし、オブジェクト指向的な発想により内部の複雑さを意識せず、簡単に使えるようになっています。

  • セキュリティを考慮した上で、インフラの詳細を保持するレポジトリ(DB)を用意する
  • レポジトリに対するアクセスをエージェント(デーモン+API)でラップする
  • インフラの構築作業をエージェントに対する指示にすることで単純化する
  • エージェント内部に内包されたノウハウによって、グッドプラクティスを適用した一式のリソースが作成される
  • エージェントの実装においては、IAM,ASG,CloudFormation といったAWS標準の機能を活用し、独自実装はなるべく避ける

これにより、大変見通しのよい、理想的なインフラ構築、運用ができるようになりました。

一緒にユニークな決済サービスを作ってくれる Rails エンジニアを募集中です!
多国籍なメンバーと一緒に仕事をしてみませんか?詳細はこちらのリンクです:D