ハンズオンで使うプログラム,および教科書とそのソースコードは以下のウェブページで公開している.

1. はじめに

1.1. 本講義の目的・内容

本講義は,東京大学計数工学科で2020年度S1/S2タームに開講されている"システム情報工学特論"の一部として行われるものである.

本講義(計3回)の目的は,クラウドの初心者を対象とし,クラウドの基礎的な知識・概念を解説する. また, Amazon Web Service (AWS) の提供するクラウド環境を実例として,具体的なクラウドの利用方法をハンズオンを通して学ぶ.

特に,科学・エンジニアリングの学生を対象として,研究などの目的でクラウドを利用するための実践的な手順を紹介する. 知識・理論の説明は最小限に留め,実践を行う中で必要な概念の解説を行う予定である. 受講生が今後,研究などでクラウドを利用する際の,足がかりとなることができればこの講義の目的は十分達成されたことになる.

講義スケジュールは以下の通りである.

Table 1. 講義スケジュール
講義 ハンズオン

第一回 (6/24)

クラウドの基礎

AWSに自分のサーバーを立ち上げる

第二回 (7/1)

クラウドで行う機械学習

  • AWS と Jupyter を使って始めるディープラーニング

  • AWS で自動質問回答ボットを作る

第三回 (7/8)

Serverless Architecture 入門

AWSでデータベースを作る (俳句を投稿するSNS "Bashoutter" を作る)

講義初回は,クラウドの基礎となる概念・知識を解説する. セキュリティやネットワークなど,クラウドを利用する上で最低限おさえなければいけないポイントを紹介する. ハンズオンでは,初めての仮想サーバーをAWSに立ち上げる演習を行う.

第二回では,クラウド上で科学計算(特に機械学習)を走らせるための入門となる知識・技術を解説する. 併せて, Docker とよばれる仮想計算環境に自分のプログラムをパッケージングする手順を説明する. ハンズオンでは,AWSのクラウドでJupyter notebookを使って簡単な機械学習の計算を走らせる課題を実践する. さらに,ディープラーニングを用いた自然言語処理により,質問に自動で回答を生成するボットを作成する.

第三回では,Serverless architectureと呼ばれる最新のクラウドのアーキテクチャを紹介する. これは,サーバーの処理能力を負荷に応じてより柔軟に拡大・縮小するための概念であり,それ以前 (Serverfullとしばしば呼ばれる) と質的に異なる設計思想をクラウドに導入するものである. これの実践として,ハンズオンでは簡単なデータベースをクラウド上に作成する.

1.2. 本講義のフィロソフィー

本講義のフィロソフィーを一言で表すなら, "ロケットで宇宙まで飛んでいって一度地球を眺めてみよう!" である.

どういうことか?

ここでいう"地球"とは,クラウドコンピューティングの全体像のことである. 言うまでもなく,クラウドという技術は大変奥が深く,幾多の情報技術・ハードウェア・アルゴリズムが精緻に組み合わさってできた総体である. クラウドを理解するということは,それら一つ一つの概念を熟知することが求められる.

そして,ここでいう"ロケット"とはこの講義のことである. この講義では,ロケットに乗って宇宙まで飛び立ち,地球(クラウド)の全体を自身の目で眺めてもらう. その際,ロケットの成り立ちや仕組み(クラウドを支える要素技術)は深くは問わない. 将来,自分が研究などの目的でクラウドを利用することになった時に,改めて学んでもらえば良い. 本講義の目的はむしろ,クラウドの最先端に実際に触れ,そこからどんな景色が見えるか(どんな応用が可能か)を実感してもらうことである.

そのような理由で,本講義はとても盛りだくさんなものになっている. 第一回はクラウドの基礎から始め,二回目では一気にレベルアップし機械学習(ディープラーニング)をクラウドで実行する手法を解説する. さらに第三回目では,サーバーレス・アーキテクチャというここ数年のうちに確立した全く新しいクラウドの設計について解説する. 正直に言って,一学期まるまるを費やして学んでもよいくらいの内容である.

が,このロケットにしがみついてきてもらえれば,とてもエキサイティングな景色が見られることを約束したい.

cdk output
Figure 1. 宇宙からみた地球 (Image from NASA https://www.nasa.gov/image-feature/planet-of-clouds)

1.3. AWSアカウント

今回の講義では,ハンズオン形式で AWS のクラウドを実際に触ってみる. 講義を聞きながら自分も手元でクラウドを動かしてみたい読者は,各自 AWS のアカウントの作成をしていただく. AWS のアカウントの作成の仕方は このページ を参照.

AWS はアカウント作成の際,クレジットカードの登録が求められる. そのような理由で,講義に際してアカウントの作成は強制ではない. アカウントを作成した場合,講義で紹介するチュートリアルは多くが無料利用枠分で実行できる. しかし,一部で数百円程度のコストが発生してしまうものも含まれる. したがって,これらのチュートリアルを自分で実行するかどうかは,個人の判断に任せる. 講義では,どれが無料で行え,どれがコストが発生しうるものか,明示する.

なお,課題の提出や成績の評価に際しては,AWS のアカウントの有無に関係なく評価できるように配慮する.

当初は, AWS Educate という教育向けの利用枠を使用することで,コストが発生しない予定だったのだが, 最近システムが変わったようで,AWS Educate 経由で取得したアカウントには制限が設けられ,講義で紹介するチュートリアルが実行できなくなってしまった. そのような背景で,個人でアカウントの取得をしなければならない状況になってしまった. その点,ご理解いただきたい.

なお, AWS Educate では,学生向けの様々なオンライン学習教材が無料で提供されているので,興味のある読者はぜひ活用していただきたい.

1.4. 必要な計算機環境

講義では,AWS上にクラウドを展開するハンズオンを実施する.そこで紹介するプログラムを実行するため,以下の計算機環境が必要である.

  • UNIX系コンソール: ハンズオンで紹介するコマンドを実行したり,SSHでサーバーにアクセスするため,UNIX系のコンソール環境が必要である. Mac または Linux のユーザーは,OSに標準搭載のコンソール(ターミナルとも呼ばれる)を使用すればよい. Windowsのユーザーは, Windows Subsystem for Linux (WSL) を使ってUbuntuの仮想環境をインストールすることを推奨する. WSLのインストールについては, 公式ドキュメンテーションを参照 のこと.

  • Docker: 講義ではDockerの使い方を解説する. 予め自身の計算機にDockerのインストールをしておくこと. Linux/Mac/Windowsのインストール法については 公式ドキュメンテーションを参照. 執筆時点において,Windows 10 Home へのインストールには注意が必要である.詳細は こちらのドキュメンテーションを参照 のこと.

  • Python (Version 3.6以上)

  • Node.js (version 10.0以上)

  • AWS CLI (インストールについては Section 14.2 参照)

  • AWS CDK (インストールについては Section 14.3 参照)

1.4.1. ハンズオン実行用の Docker Image

Python, Node.js, AWS CDK など,ハンズオンのプログラムを実行するために必要なプログラム/ライブラリがインストール済みの Docker image を用意した. また,ハンズオンのソースコードもクローン済みである. Docker の使い方を知っている読者は,これを使えば,諸々のインストールをする必要なく,すぐにハンズオンのプログラムを実行できる.

次のコマンドで起動する.

docker run -it registry.gitlab.com/tomomano/intro-aws:latest

Docker Image は GitLab の Container registry においてある. Docker Image のソースコードは こちら にある.

1.5. 前提知識

本講義を行うにあたり,前提知識は特に仮定しない.が,以下の前提知識があるとよりスムーズに理解をすることができるだろう.

  • Pythonの基本的な理解: 本講義ではPythonを使ってプログラムの作成を行う. 使用するライブラリは十分抽象化されており,関数の名前を見ただけで意味が明瞭なものがほとんどであるので,Pythonに詳しくなくても心配する必要はない.

  • Linuxコマンドラインの基礎的な理解: クラウドを利用する際,クラウド上に立ち上がるサーバーは基本的にLinuxである. Linuxのコマンドラインについて知識があると,トラブルシュートなどが容易になる.

1.6. 講義に関連する資料

ハンズオンで使うプログラム,および教科書とそのソースコードは以下のウェブページで公開している.

1.7. 本書で使用するノーテーションなど

  • プログラムのコードやシェルのコマンドは monospace letter で記述する.

  • シェルに入力するコマンドは,それがシェルコマンドであると明示する目的で,先頭に $ がつけてある. $ はコマンドをコピー&ペーストするときは除かなければならない. 逆に,コマンドの出力には $ はついていない点に留意する.

また,以下のような形式で注意やチップスを提供する.

追加のコメントなどを記す.
発展的な議論やアイディアなどを紹介する.
陥りやすいミスなどの注意事項を述べる.
絶対に犯してはならないミスを指摘する.

2. クラウド概論

2.1. クラウドとは?

Cloud

クラウドとはなにか? クラウドという言葉は,それ自身がとても広い意味を持つので,厳密な定義付けを行うことは難しい.

学術的な意味でのクラウドの定義づけをするとしたら,NIST(米国・国立標準技術研究所) による The NIST Definition of Cloud Computing が引用されることが多い. これによると,クラウドとは以下の要件が満たされたハードウェア/ソフトウェアの総体のことをいう.

  • On-demand self-service 利用者のリクエストに応じて計算資源が自動的に割り当てられる.

  • Broad network access 利用者はネットワークを通じてクラウドにアクセスできる.

  • Resource pooling クラウドプロバイダーは,所有する計算資源を分割することで複数の利用者に計算資源を割り当てる.

  • Rapid elasticity 利用者のリクエストに応じて,迅速に計算資源の拡大あるいは縮小を行うことができる

  • Measured service 計算資源の利用量を計測・監視することができる.

…​と,いわれても抽象的でよくわからないかもしれない.もう少し具体的な話をする.

個人が所有する計算機で,CPUの数を増やそうと思ったら,物理的に筐体を開け,CPUソケットを露出させ,新しいCPUに交換する必要があるだろう.あるいは,ストレージがいっぱいになってしまったら,古いディスクを抜き取り,新しいディスクを挿入する必要がある.新しい計算機を買ったときには,LANケーブルを差し込まないとネットワークには接続できない.

クラウドでは,これらの操作がプログラムからのコマンドによって実行できる.CPUが1000個欲しいと思ったらならば,そのようにクラウドプロバイダーにリクエストを送れば良い.すると,数分もしないうちに 1000 CPUの計算資源が割り当てられる.ストレージを1TBから10TBに拡張しようと思ったならば,そのようにコマンドを送ればよい (これは,Google Drive などのサービスなどで馴染みのある人も多いだろう).計算資源を使い終わったら,そのことをプロバイダーに伝えれば,割り当て分はすぐさま削除される.クラウドプロバイダーは,使った計算資源の量を正確にモニタリングしており,その量をもとに利用料金の計算が行われる.

このように,クラウドの本質は物理的なハードウェアの仮想化・抽象化であり,利用者はコマンドを通じて,まるでソフトウェアの一部かのように,物理的なハードウェアの管理・運用を行うことができる.もちろん,背後では,データセンターに置かれた膨大な数の計算機が大量の電力を消費しながら稼働している.クラウドプロバイダーはデータセンターの計算資源を上手にやりくりし,ソフトウェアとしてのインターフェースをユーザーに提供することで,このような仮想化・抽象化を達成しているわけである.クラウドプロバイダーの視点からすると,大勢のユーザーに計算機を貸し出し,データセンターの稼働率を常時100%に近づけることで,利益率の最大化を図っているのである.

著者の言葉で,クラウドの重要な特性を定義するならば,以下のようになる.

クラウドとは計算機ハードウェアの抽象化である.つまり,物理的なハードウェアをソフトウェアの一部かのように自在に操作・拡大・接続することを可能にする技術である.

2.2. なぜクラウドを使うのか?

上述のように,クラウドとは自由に計算資源をプログラマティックに操作することのできる計算環境である. ここでは,自前のローカル計算環境と比べて,なぜクラウドを使うと良いことがあるのかについて述べたい.

  1. 自由にサーバーのサイズをスケールできる

    なにか新しいプロジェクトを始めるとき,あらかじめ必要なサーバーのスペックを知るのは難しい.いきなり大きなサーバーを買うのはリスクが高い.一方で,小さすぎるサーバーでは,後のアップグレードが面倒である.クラウドを利用すれば,プロジェクトを進めながら,必要な分だけの計算資源を確保することができる.

  2. 自分でサーバーをメンテナンスする必要がない

    悲しいことに,コンピュータとは古くなるものである.最近の技術の進歩の速度からすると,5年も経てば,もはや当時の最新コンピューターも化石と同じである.5年ごとにサーバーを入れ替えるのは相当な手間である.またサーバーの停電や故障など不意の障害への対応も必要である.クラウドでは,そのようなインフラの整備やメンテナンスはプロバイダーが自動でやってくれるので,心配する必要がない.

  3. 初期コスト0

    自前の計算環境とクラウドの,経済的なコストのイメージを示したのが Figure 2 である.クラウドを利用する場合の初期コストは基本的に0である.その後,使った利用量に応じてコストが増大していく.一方,自前の計算環境では,大きな初期コストが生じる.その分,初期投資後のコストの増加は,電気利用料やサーバー維持費などに留まるため,クラウドを利用した場合よりも傾きは小さくなる.自前の計算機では,ある一定期間後,サーバーのアップグレードなどによる支出が生じることがある.一方,クラウドを利用する場合は,そのような非連続なコストの増大は基本的に生じない.クラウドのコストのカーブが,自前計算環境のコストのカーブの下にある範囲においては,クラウドを使うことは経済的なコスト削減につながる.

    Cost
    Figure 2. クラウドと自前計算機環境の経済的コストの比較

特に,1.の点は研究の場面では重要であると筆者は感じる.研究をやっていて,四六時中計算を走らせ続けるという場合は少ない.むしろ,新しいアルゴリズムが完成したとき・新しいデータが届いたとき,集中的・突発的に計算タスクが増大することが多いだろう.そういったときに,フレキシブルに計算力を増強させることができるのは,クラウドを使う大きなメリットである.

ここまでクラウドを使うメリットを述べたが,逆に,デメリットというのも当然存在する.

  1. クラウドは賢く使わないといけない

    Figure 2 で示したコストのカーブにあるとおり,使い方によっては自前の計算環境のほうがコスト的に有利な場面は存在しうる.クラウドを利用する際は,使い終わった計算資源はすぐに削除するなど,利用者が賢く管理を行う必要があり,これを怠ると思いもしない額の請求が届く可能性がある.

  2. セキュリティ

    クラウドは,インターネットを通じて,世界のどこからでもアクセスできる状態にあり,セキュリティ管理を怠ると簡単にハッキングの対象となりうる.ハッキングを受けると,情報流出だけでなく,経済的な損失を被る可能性がある.

  3. ラーニングカーブ

    上記のように,コスト・セキュリティなど,クラウドを利用する際に留意しなければならない点は多い.賢くクラウドを使うには,十分なクラウドの理解が必要であり,そのラーニングカーブを乗り越える必要がある.

2.3. どうやってクラウドを使うのか?

東京大学では, 情報基盤センター が大規模計算機サーバーの運用を行っている. このような,特定の組織・団体の内部のみで使用されるクラウドを,プライベートクラウド (private cloud) と呼ぶ.

一方,商用のサービスとしてのクラウドも,現在は多くの企業から提供されている. このような,一般の顧客に向けたクラウドサービスのことを,パブリッククラウド (public cloud) と呼ぶ. 有名なクラウドプラットフォームの例を挙げると, Google社が提供する Google Cloud Platform (GCP), Microsoft 社が提供する Azure, Amazon 社が提供する Amazon Web Service (AWS)

大学の所有するプライベートクラウドは,大学の構成員ならば無料もしくは最小限のコストで計算を実行できる. しかし,計算の優先度などは,研究提案の申請により決定される場合が多く,柔軟性にかける場合もある. パブリッククラウドを利用する場合は,プロバイダーの設定した利用料金を支払うことになるが,計算リソースは制限なく使用することが可能である.


小噺: Terminal の語源

Mac/Linuxなどでコマンドを入力するときに使用する,あの黒い画面のことを Terminal と呼んだりする. この言葉の語源をご存知だろうか?

Terminal

この言葉の語源は,コンピュータが誕生して間もない頃の時代に遡る.その頃のコンピュータというと,何千何万のという数の真空管が接続された,会議室一個分くらいのサイズのマシンであった.そのような高価でメンテが大変な機材であるから,当然みんなでシェアして使うことが前提となる.ユーザーがコンピュータにアクセスするため,マシンからは何本かのケーブルが伸び,それぞれにキーボードとスクリーンが接続されていた…​ これを Terminal と呼んでいたのである.人々は,代わる代わるTerminalの前に座って,計算機との対話を行っていた.

時代は流れ,WindowsやMacなどのいわゆるパーソナルコンピュータの出現により,コンピュータはみんなで共有するものではなく,個人が所有するものになった.

最近のクラウドの台頭は,みんなで大きなコンピュータをシェアするという,最初のコンピュータの使われ方に原点回帰していると捉えることもできる.一方で,スマートフォンやウェアラブルなどのエッジデバイスの普及も盛んであり,個人が複数の"小さな"コンピュータを所有する,という流れも同時に進行しているのである.

3. AWS入門

3.1. AWSとは?

本講義では,クラウドの実践を行うプラットフォームとして, AWS を用いる. 実践にあたって,最低限必要な AWS の知識を本章では解説しよう.

AWS (Amazon Web Service) はAmazon社が提供する総合的なクラウドプラットフォームである. AWSはAmazonが持つ膨大な計算リソースを貸し出すクラウドサービスとして,2006年に誕生した. 2018年では,クラウドプロバイダーとして最大のマーケットシェア(約33%)を保持している (参照). Netflix, Slackをはじめとした多くのウェブ関連のサービスで,一部または全てのサーバーリソースがAWSから提供されているとのことである. よって,知らないうちにAWSの恩恵にあずかっている人も少なくないはずだ.

最大のシェアをもつだけに,とても幅広い機能・サービスが提供されており,科学・エンジニアリングの研究用途としても頻繁に用いられるようになってきている.

3.2. AWSの機能・サービス

Figure 3 は,執筆時点においてAWSで提供されている主要な機能・サービスの一覧である.

AWS services
Figure 3. AWSで提供されている主要なサービス一覧

計算,ストレージ,データベース,ネットワーク,セキュリティなど,クラウドの構築に必要な様々な要素が独立したコンポーネントとして提供されている.基本的に,これらを組み合わせることでひとつのクラウドシステムができあがる.

また,機械学習・音声認識・AR/VRなど,特定のアプリケーションにパッケージ済みのサービスも提供されている.これらを合計すると全部で176個のサービスが提供されているとのことである (参照).

AWSの初心者は,この大量のサービスの数に圧倒され,どこから手をつけたらよいのかわからなくなる,という状況に陥りがちである.だが実のところ,基本的な構成要素はそのうちの数個のみに限られる.他の機能の多くは,基本の要素を組み合わせ,特定のアプリケーションとしてAWSがパッケージとして用意したものである.なので,基本要素となる機能の使い方を知れば,AWSのおおよそのリソースを使いこなすことが可能になる.

3.3. AWSでクラウドを作るときの基本となる部品

3.3.1. 計算

S3 EC2 (Elastic Compute Cloud) 様々なスペックの仮想マシンを作成し,計算を実行することができる. クラウドの最も基本となる構成要素である.

S3 Lambda Function as a Service (FaaS)と呼ばれる,小さな計算をサーバーなしで実行するためのサービス.Serverless architecutre の章で詳しく解説する.

3.3.2. ストレージ

S3 EBS (Elastic Block Store) EC2にアタッチすることのできる仮想データドライブ. いわゆる"普通の"(一般的なOSで使われている)ファイルシステムを思い浮かべてくれたらよい.

S3 S3 (Simple Storage Service) Object Storage と呼ばれる,APIを使ってデータの読み書きを行う,いうなれば”クラウド・ネイティブ”なデータの格納システムである. Serverless architecutre の章で詳しく解説する.

3.3.3. データベース

S3 DynamoDB NoSQL型のデータベースサービス (知っている人は mongoDB などを思い浮かべたらよい). Serverless architecutre の章で詳しく解説する.

3.3.4. ネットワーク

S3 VPC(Virtual Private Cloud) AWS上に仮想ネットワーク環境を作成し,仮想サーバー間の接続を定義したり,外部からのアクセスなどを管理する. EC2はVPCの内部に配置されなければならない.

3.4. Region と Availability Zone

AWSを使用する際の重要な概念として, RegionAvailability Zone (AZ) がある (Figure 4).

AWS regions and azs
Figure 4. AWSにおける Region と Availability Zones

Region とは,データセンターの所在地のことである. 執筆時点において,AWSは世界の24の国と地域でデータセンターを所有している. Figure 5 は2020/05時点で利用できるRegionの世界地図を示している. インターネットの接続などの観点から,地理的に一番近いRegionを使用するのが一般的によいとされる. 日本では東京にデータセンターがある.また大阪リージョンも2021年に提供開始予定とのことである. 各Regionには固有のIDがついており,例えば東京は ap-northeast-1, 米国オハイオ州は us-east-2,などと定義されている.

AWSコンソールにログインすると,画面右上のメニューバーでリージョンを選択することができる(Figure 6). EC2, S3 などのAWSのリソースは,リージョンごとに完全に独立である. したがって,リソースを新たにデプロイする時,あるいはデプロイ済みのリソースを閲覧するときは,コンソールのリージョンが正しく設定されているか,確認する必要がある. ウェブビジネスを展開する場合などは,世界の各地にクラウドを展開する必要があるが,個人的な研究用途として用いる場合は,最寄りのリージョン(i.e. 東京)を使えば基本的に問題ない.

EC2 の利用料など,リージョン間で価格設定が若干異なる場合があり,最も価格が安く設定されているリージョンを選択する,というのも重要な視点である.

AWS console select regions
Figure 6. AWSコンソールでリージョンを選択

Avaialibity Zone (AZ) とは,Region 内で地理的に隔離されたデータセンターのことである. それぞれのリージョンは2個以上のAZを有しており,もしひとつのAZで火災や停電などが起きた場合でも,他のAZがその障害をカバーすることができる. また,AZ間は高速なAWS専用ネットワーク回線で結ばれているため,AZ間のデータ転送は極めて早い. AZは,ネットのビジネスなどでサーバーダウンが許容されない場合などに注意すべき概念であり,個人的な用途で使う限りにおいてはあまり深く考慮する必要はない.言葉の意味だけ知っておけば十分である.

3.5. AWSでのクラウドの開発

AWSのクラウドの全体像がわかってきたところで,次のトピックとして,どのようにしてAWS上にクラウドの開発を行い,展開していくかについての概略を解説をしよう.

AWSのリソースを追加・編集・削除などの操作を実行するには,コンソールを用いる方法と,APIを用いる方法の,二つの経路がある.

3.5.1. コンソール画面からリソースを操作する

AWSのアカウントにログインすると,まず最初に表示されるのがAWSコンソールである (Figure 7).

AWS console
Figure 7. AWSマネージメントコンソール画面

コンソールを使うことで,EC2のインスタンスを立ち上げたり,S3のデータを追加・削除したり,ログを閲覧したりなど,あらゆるAWS上のあらゆるリソースの操作をGUI (Graphical User Interface) を使って実行することができる. 初めて触る機能をポチポチと試したり,デバッグを行うときなどにとても便利である

コンソールはさらっと機能を試したり,開発中のクラウドのデバッグをするときには便利なのであるが,実際にクラウドの開発をする場面でこれを直接いじることはあまりない.むしろ,次に紹介するAPIを使用して,プログラムとしてクラウドのリソースを記述することで開発を行うのが一般的である. そのような理由で,本講義ではAWSコンソールを使ったAWSの使い方はあまり触れない.AWSのドキュメンテーションには,たくさんの チュートリアル が用意されており,コンソール画面から様々な操作を行う方法が記述されているので,興味がある読者はそちらを参照されたい.

3.5.2. APIからリソースを操作する

API (Application Programming Interface) を使うことで,コマンドをAWSに送信し,クラウドのリソースの操作をすることができる. APIとは,簡単に言えばAWSが公開しているコマンドの一覧であり,GET, POST, DELETE などの REST API から構成されている. が,直接REST APIを入力するのは面倒であるので,その手間を解消するための様々なツールが提供されている.

AWS CLI は,UNIXのコンソールからAWS APIを送信するためのCLI (Command Line Interface) である.

CLIに加えて,いろいろなプログラミング言語での SDK (Software Development Kit) が提供されている.以下に一例を挙げる.

具体的なAPIの使用例を見てみよう.

S3に新しい保存領域(バケットと呼ばれる)を追加したいとしよう. AWS CLI を使った場合は,以下のようなコマンドを打てばよい.

$ aws s3 mb s3://my-bucket --region ap-northeast-1

上記のコマンドは, my-bucket という名前のバケットを, ap-northeast-1 のregionに作成する.

Pythonから上記と同じ操作を実行するには, boto3 ライブラリを使って,以下のようなスクリプトを実行する.

1
2
3
4
import boto3

s3_client = boto3.client("s3", region_name="ap-northeast-1")
s3_client.create_bucket(Bucket="my-bucket")

もう一つ例をあげよう.

新しいEC2のインスタンス(インスタンスとは,起動状態にある仮想サーバーの意味である)を起動するには,以下のようなコマンドを打てば良い.

$ aws ec2 run-instances --image-id ami-xxxxxxxx --count 1 --instance-type t2.micro --key-name MyKeyPair --security-group-ids sg-903004f8 --subnet-id subnet-6e7f829e

上記のコマンドにより, t2.micro というタイプ (1CPU, 1.0GB RAM) のインスタンスが起動する. ここではその他のパラメータの詳細の説明は省略する(第一回ハンズオンで詳しく解説).

Pythonから上記と同じ操作を実行するには,以下のようなスクリプトを使う.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import boto3

ec2_client = boto3.client("ec2")
ec2_client.run_instances(
    ImageId="ami-xxxxxxxxx",
    MinCount=1,
	MaxCount=1,
	KeyName="MyKeyPair",
	InstanceType="t2.micro",
    SecurityGroupIds=["sg-903004f8"],
    SubnetId="subnet-6e7f829e",
)

以上の具体例を通じて,APIによるクラウドのリソースの操作のイメージがつかめてきただろうか? コマンド一つで,新しい仮想サーバーを起動したり,データの保存領域を追加したり,任意の操作を実行することができるわけである. 基本的に,このようなコマンドを複数組み合わせていくことで,自分の望むCPU・RAM・ネットワーク・ストレージが備わった計算環境をを構築することができる.もちろん,逆の操作(リソースの削除)もAPIを使って実行できる.

3.5.3. ミニ・ハンズオン: AWS CLI を使ってみよう

ここでは,ミニ・ハンズオンとして,AWS CLI を実際に使ってみる. AWS CLI は先述の通り, AWS 上の任意のリソースの操作が可能であるが,ここでは一番シンプルな,S3を使ったファイルの読み書きを実践する (EC2の操作は少し複雑なので,第一回ハンズオンで行う). aws s3 コマンドの詳しい使い方は 公式ドキュメンテーションを参照.

AWS CLI のインストールについては, Section 14.2 を参照.

以下に紹介するハンズオンは,基本的に S3 の無料枠 の範囲内で実行することができる.

以下のコマンドを実行する前に,AWSの認証情報が正しく設定されていることを確認する. これには ~/.aws/credentials のファイルに設定が書き込まれているか,環境変数 (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION) が定義されている必要がある. 詳しくは Section 14.2 を参照.

まず最初に,S3にデータの格納領域 (Bucket と呼ばれる.一般的なOSでの"ドライブ"に相当する) を作成するところから始めよう.

$ bucketName="mybucket-$(openssl rand -hex 12)"
$ echo $bucketName
$ aws s3 mb "s3://${bucketName}"

S3のバケットの名前は,AWS全体でにユニークでなければならないことから,上ではランダムな文字列を含んだバケットの名前を生成し,bucketName という変数に格納している.

次に,バケットの一覧を取得してみよう.

$ aws s3 ls

2020-06-07 23:45:44 mybucket-c6f93855550a72b5b66f5efe

先ほど作成したバケットがリストにあることを確認できる.

本書のノーテーションとして,コマンドラインに入力するコマンドは,それがコマンドであると明示する目的で先頭に $ がつけてある. $ はコマンドをコピー&ペーストするときは除かなければならない.逆に,コマンドの出力は $ なしで表示されている.

次に,バケットにファイルをアップロードする.

$ echo "Hello world!" > hello_world.txt
$ aws s3 cp hello_world.txt "s3://${bucketName}/hello_world.txt"

上では hello_world.txt というダミーのファイルを作成して,それをアップロードした.

それでは,バケットの中にあるファイルの一覧を取得してみる.

$ aws s3 ls "s3://${bucketName}" --human-readable

2020-06-07 23:54:19   13 Bytes hello_world.txt

先ほどアップロードしたファイルがたしかに存在することがわかる.

最後に,使い終わったバケットを削除する.

$ aws s3 rb "s3://${bucketName}" --force

デフォルトでは,バケットは空でないと削除できない.空でないバケットを強制的に削除するには --force のオプションを付ける.

以上のように,AWS CLI を使って,S3のバケットの操作を実行することができた. EC2やLambda, DynamoDBなどについても同様に AWS CLI を使ってあらゆる操作を実行することができる.

3.6. CloudFormation と AWS CDK

3.6.1. CloudFormation による Infrastructure as Code (IaC)

前節で述べたように,AWS API を使うことでクラウドのあらゆるリソースの作成・管理が可能である.よって,原理上は,APIのコマンドを組み合わせていくことで,自分の作りたいクラウドを設計することができる.

しかし,ここで実用上考慮しなければならない点がひとつある.AWS API には大きく分けて,リソースを操作するコマンドと,タスクを実行するコマンドがあることである (Figure 8).

AWS console
Figure 8. AWS APIはリソースを操作するコマンドとタスクを実行するコマンドに大きく分けられる.リソースを記述・管理するのに使われるのが, CloudFormation と CDK である.

リソースを操作するとは,EC2のインスタンスを起動したり,S3の保存領域(バケット)をしたり,データベースに新たなテーブルを追加する,などの静的なリソースを準備する 操作を指す. "ハコ"を作る操作と呼んでもよいだろう. このようなコマンドは,クラウドのデプロイ時にのみ,一度だけ実行されればよい

タスクを実行するコマンド とは, EC2 のインスタンスにジョブを投入したり, S3 のバケットにデータを読み書きするなどの操作を指す. これは,EC2やS3などのリソース ("ハコ") を前提として,その内部で実行されるべき計算を記述するものである. 前者に比べてこちらは動的な操作を担当する,と捉えることもできる.

そのような観点から,インフラを記述するプログラムタスクを実行するプログラムはある程度分けて管理されるべきである.クラウドの開発は,クラウドの(静的な)リソースを記述するプログラムを作成するステップと,インフラ上で動く動的な操作を行うプログラムを作成するステップの,二段階に分けて考えることができる.

AWSでのリソースを管理するための仕組みが, CloudFormation である. CloudFormation とは,CloudFormationのシンタックスに従ったテキストにより,AWSのインフラを記述するものである. CloudFormation を使って,例えば,EC2のインスタンスをどれくらいのスペックで,何個起動するか,インスタンス間はどのようなネットワークで結び,どのようなアクセス権限を付与するか,などのリソースの定義を逐次的に記述することができる. 一度CloudFormation ファイルが出来上がれば,それにしたがったクラウド・インフラをコマンド一つでAWS上に展開することができる. また,CloudFormation ファイルを交換することで,全く同一のクラウド環境を他者が簡単に再現することも可能になる. このように,本来は物理的な実体のあるハードウェアを,プログラムによって記述し,管理するという考え方を,Infrastructure as Code (IaC)と呼ぶ.

CloudFormation を記述するには,基本的に JSON (JavaScript Object Notation) と呼ばれるフォーマットを使う.以下は,JSONで記述された CloudFormation ファイルの一例 (抜粋) である.

 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
"Resources" : {
  ...
  "WebServer": {
    "Type" : "AWS::EC2::Instance",
    "Properties": {
      "ImageId" : { "Fn::FindInMap" : [ "AWSRegionArch2AMI", { "Ref" : "AWS::Region" },
                        { "Fn::FindInMap" : [ "AWSInstanceType2Arch", { "Ref" : "InstanceType" }, "Arch" ] } ] },
      "InstanceType"   : { "Ref" : "InstanceType" },
      "SecurityGroups" : [ {"Ref" : "WebServerSecurityGroup"} ],
      "KeyName"        : { "Ref" : "KeyName" },
      "UserData" : { "Fn::Base64" : { "Fn::Join" : ["", [
                     "#!/bin/bash -xe\n",
                     "yum update -y aws-cfn-bootstrap\n",

                     "/opt/aws/bin/cfn-init -v ",
                     "         --stack ", { "Ref" : "AWS::StackName" },
                     "         --resource WebServer ",
                     "         --configsets wordpress_install ",
                     "         --region ", { "Ref" : "AWS::Region" }, "\n",

                     "/opt/aws/bin/cfn-signal -e $? ",
                     "         --stack ", { "Ref" : "AWS::StackName" },
                     "         --resource WebServer ",
                     "         --region ", { "Ref" : "AWS::Region" }, "\n"
      ]]}}
    },
    ...
  },
  ...
},

ここでは, "WebServer" という名前のつけられた EC2 インスタンスを定義している.かなり長大で複雑な記述であるが,これによって所望のスペック・OSをもつEC2インスタンスを自動的に生成することが可能になる.

3.6.2. AWS CDK

前節で紹介した CloudFormation は,見てわかるとおり大変記述が複雑であり,またそれのどれか一つにでも誤りがあってはいけない. また,基本的に"テキスト"を書いていくことになるので,プログラミング言語で使うような便利な変数やクラスといった概念が使えない (厳密には,変数に相当するような機能は存在する). また,記述の多くの部分は繰り返しが多く,自動化できる部分も多い.

そのような悩みを解決してくれるのが, AWS Cloud Development Kit (CDK) である. CDKは Python などのプログラミング言語を使って CloudFormation を自動的に生成してくれるツールである. CDK は2019年にリリースされたばかりの比較的新しいツールで,日々改良が進められている (GitHub レポジトリ のリリースを見ればその開発のスピードの速さがわかるだろう). CDK は TypeScript (JavaScript), Python, Java など複数の言語でサポートされている.

CDKを使うことで,CloudFormation に相当するクラウドリソースの記述を,より親しみのあるプログラミング言語を使って行うことができる.かつ,典型的なリソース操作に関してはパラメータの多くの部分を自動で決定してくれるので,記述しなければならない量もかなり削減される.

以下に Python を使った CDK のコードの一例 (抜粋) を示す.

 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
from aws_cdk import (
    core,
    aws_ec2 as ec2,
)

class MyFirstEc2(core.Stack):

    def __init__(self, scope, name, **kwargs):
        super().__init__(scope, name, **kwargs)

        vpc = ec2.Vpc(
            ... # some parameters
        )

        sg = ec2.SecurityGroup(
            ... # some parameters
        )

        host = ec2.Instance(
            self, "MyGreatEc2",
            instance_type=ec2.InstanceType("t2.micro"),
            machine_image=ec2.MachineImage.latest_amazon_linux(),
            vpc=vpc,
            ...
        )

上記のようなコードから,CloudFormationファイルを自動生成することができる.とても煩雑だったCloudFormationファイルに比べて,Python を使うことで格段に短く,わかりやすく記述できることができるのがわかるだろう.

本講義では,ハンズオンでCDKを使ってクラウド開発の体験をしてもらう.

Further reading
  • AWS CDK Examples: CDKのexample project が多数紹介されている.ここにある例をテンプレートに自分の開発を進めると良い.

4. Hands-on #1: 初めてのEC2インスタンスを起動する

ハンズオンの第一回では, CDK を使って EC2 のインスタンス(仮想サーバー)を作成し,SSHでサーバーにログインする,という演習を行う. このハンズオンを終えれば,あなたは自分だけのサーバーをAWS上に立ち上げ,自由に計算を走らせることができるようになるのである!

ハンズオン1のソースコードは こちらのリンク に置いてある.

ハンズオン1は,基本的に AWS の無料枠 の範囲内で実行することができる.

4.1. 準備

まずは,ハンズオンを実行するための環境を整える. これらの環境整備は,後のハンズオンでも前提となるものなので確実にミスなく行っていただきたい.

4.1.1. AWS Account

ハンズオンを実行するには個人のAWSアカウントが必要である. AWSアカウントの取得については Section 1.3 参照.

4.1.2. Python と Node.js

本ハンズオンを実行するには,Python (3.6 以上),Node.js (10.3.0 以上) がインストールされていなければならない.

4.1.3. AWS CLI

AWS CLI のインストールについては, Section 14.2 を参照.

4.1.4. AWS CDK

AWS CDK のインストールについては, Section 14.3 を参照.

4.1.5. ソースコードのダウンロード

本ハンズオンで使用するプログラムのソースコードを,以下のコマンドを使って GitLab からダウンロードする.

$ git clone https://gitlab.com/tomomano/intro-aws.git

あるいは, https://gitlab.com/tomomano/intro-aws のページに行って,右上のダウンロードボタンからダウンロードすることもできる.

4.1.6. Docker を使用する場合

Python, Node.js, AWS CDK など,ハンズオンのプログラムを実行するために必要なプログラム/ライブラリがインストール済みの Docker image を用意した. また,ハンズオンのソースコードもクローン済みである. Docker の使い方を知っている読者は,これを使えば,諸々のインストールをする必要なく,すぐにハンズオンのプログラムを実行できる.

次のコマンドでコンテナを起動する.

docker run -it registry.gitlab.com/tomomano/intro-aws:latest

4.2. SSH

SSH (secure shell) はUnix系のリモートサーバーに安全にアクセスするためのツールである. 本ハンズオンでは, SSH を使って仮想サーバーにアクセスする. SSH に慣れていない読者のため,簡単な説明をここで行う.

SSHによる通信はすべて暗号化されているので,機密情報をインターネット越しに安全に送受信することができる. 本ハンズオンで,リモートのサーバーにアクセスするためにSSHクライアントがインストールされている必要がある. SSHクライアントはLinux/Macには標準搭載されている. Windowsの場合はWSLをインストールすることでSSHクライアントを利用することを推奨する (Section 1.4 を参照).

SSH コマンドの基本的な使い方は

$ ssh <user name>@<host name>

である. <host name> はアクセスする先のサーバーのIPアドレスやDNSによるホストネームが入る. <user name> は接続する先のユーザー名である.

SSH は平文のパスワードによる認証を行うこともできるが,より強固なセキュリティを施すため,公開鍵暗号方式(Public Key Cryptography)による認証を行うことが強く推奨されており,EC2はこの方法でしかアクセスを許していない. 公開鍵暗号方式の仕組みについては各自勉強してほしい. 本ハンズオンにおいて大事なことは,EC2 インスタンスが公開鍵(Public key)を保持し,クライアントとなるコンピューター(あなた自身のコンピュータ)が秘密鍵(Private key)を保持する,という点である. EC2のインスタンスには秘密鍵を持ったコンピュータのみがアクセスすることができる.逆に言うと,秘密鍵が漏洩すると第三者もサーバーにアクセスできることになるので,秘密鍵は絶対に漏洩することのないよう注意して管理する

SSH コマンドでは,ログインのために使用する秘密鍵ファイルを -i もしくは --identity_file のオプションで指定することができる. 例えば

$ ssh -i Ec2SecretKey.pem <user name>@<host name>

のように使う.

4.3. アプリケーションの説明

このハンズオンで作成するアプリケーションの概要を Figure 9 に示す.

hands-on 01 architecture
Figure 9. ハンズオン#1で作製するアプリケーションのアーキテクチャ

このアプリケーションではまず,VPC (Virtual Private Cloud) を使ってプライベートな仮想ネットワーク環境を立ち上げている. そのVPCの public subnet の内側に,EC2 (Elatic Compute Cloud) の仮想サーバーを配置する. さらに,セキュリティのため, Security Group によるEC2インスタンスへのアクセス制限を設定している. このようにして作成された仮想サーバーに,SSHを使ってアクセスし,簡単な計算を行う.

上記のようなアプリケーションを,CDKを使って構築する.

早速ではあるが,今回のハンズオンで使用するプログラムを見てみよう (handson/01-ec2/app.py).

 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
38
39
40
class MyFirstEc2(core.Stack):

    def __init__(self, scope: core.App, name: str, key_name: str, **kwargs) -> None:
        super().__init__(scope, name, **kwargs)

        (1)
        vpc = ec2.Vpc(
            self, "MyFirstEc2-Vpc",
            max_azs=1,
            cidr="10.10.0.0/23",
            subnet_configuration=[
                ec2.SubnetConfiguration(
                    name="public",
                    subnet_type=ec2.SubnetType.PUBLIC,
                )
            ],
            nat_gateways=0,
        )

        (2)
        sg = ec2.SecurityGroup(
            self, "MyFirstEc2Vpc-Sg",
            vpc=vpc,
            allow_all_outbound=True,
        )
        sg.add_ingress_rule(
            peer=ec2.Peer.any_ipv4(),
            connection=ec2.Port.tcp(22),
        )

        (3)
        host = ec2.Instance(
            self, "MyFirstEc2Instance",
            instance_type=ec2.InstanceType("t2.micro"),
            machine_image=ec2.MachineImage.latest_amazon_linux(),
            vpc=vpc,
            vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PUBLIC),
            security_group=sg,
            key_name=key_name
        )
1 まず最初に,VPCを定義する.
2 次に,SGを定義している.ここでは,任意のIPv4のアドレスからの,ポート22 (SSHの接続に使用される)への接続を許容している.それ以外の接続は拒絶される.
3 最後に,上記で作ったVPCとSGが付与されたEC2 のインスタンスを作成している.インスタンスタイプは t2.micro を選択し, Amazon Linux をOSとして設定している.

それぞれについて,もう少し詳しく説明しよう.

4.3.1. VPC (Virtual Private Cloud)

VPC

VPCはAWS上にプライベートな仮想ネットワーク環境を構築するツールである.高度な計算システムを構築するには,複数のサーバーを連動させて計算を行う必要があるが,そのような場合に互いのアドレスなどを管理する必要があり,そのような場合にVPCは有用である.

本ハンズオンでは,サーバーは一つしか起動しないので,VPCの恩恵はよく分からないかもしれない.しかし,EC2インスタンスは必ずVPCの中に配置されなければならない,という制約があるので,このハンズオンでもミニマルなVPCを構成している.

興味のある読者のために,VPCのコードについてもう少し詳しく説明しよう.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
vpc = ec2.Vpc(
    self, "MyFirstEc2-Vpc",
    max_azs=1,
    cidr="10.10.0.0/23",
    subnet_configuration=[
        ec2.SubnetConfiguration(
            name="public",
            subnet_type=ec2.SubnetType.PUBLIC,
        )
    ],
    nat_gateways=0,
)
  • max_azs=1 : このパラメータは,前章で説明した avaialibility zone を設定している.このハンズオンでは,特にデータセンターの障害などを気にする必要はないので1にしている.

  • cidr="10.10.0.0/23" : このパラメターは,VPC内のIPv4のレンジを指定している.CIDR記法については, Wikipediaなどを参照. 10.10.0.0/2310.10.0.0 から 10.10.1.255 までの512個の連続したアドレス範囲を指している.つまり,このVPCでは最大で512個のユニークなIPv4アドレスが使えることになる.今回はサーバーは一つなので512個は明らかに多すぎるが,VPCはアドレスの数はどれだけ作成しても無料なので,多めに作成した.

  • subnet_configuration=…​ : このパラメータは,VPCにどのようなサブネットを作るか,を決めている.サブネットの種類には priavte subnetpublic subnet の二種類がある.private subnet は基本的にインターネットとは遮断されたサブネット環境である.インターネットと繋がっていないので,セキュリティは極めて高く,VPC内のサーバーとのみ通信を行えばよいEC2インスタンスは,ここに配置する.Public subnet とはインターネットに繋がったサブネットである.本ハンズオンで作成するサーバーは,外からSSHでログインを行いたいので,Public subnet 内に配置する.

  • natgateways=0 : これは少し高度な内容なので省略する (興味のある読者は 公式ドキュメンテーションを参照).が,これを0にしておかないと,NAT Gateway の利用料金が発生してしまうので,注意!

4.3.2. Security Group

Security group (SG) は,EC2インスタンスに付与することのできる仮想ファイアーウォールである.例えば,特定のIPアドレスから来た接続を許したり (インバウンド・トラフィックの制限) ,逆に特定のIPアドレスへのアクセスを禁止したり (アウトバウンド・トラフィックの制限) することができる.

コードの該当部分を見てみよう.

1
2
3
4
5
6
7
8
9
sg = ec2.SecurityGroup(
    self, "MyFirstEc2Vpc-Sg",
    vpc=vpc,
    allow_all_outbound=True,
)
sg.add_ingress_rule(
    peer=ec2.Peer.any_ipv4(),
    connection=ec2.Port.tcp(22),
)

本ハンズオンでは,SSHによる外部からの接続を許容するため, sg.add_ingress_rule(peer=ec2.Peer.any_ipv4(), connection=ec2.Port.tcp(22)) により,すべてのIPv4アドレスからのポート22番へのアクセスを許容している. また,SSHでEC2インスタンスにログインしたのち,インターネットからプログラムなどをダウンロードできるよう, allow_all_outbound=True のパラメータを設定している.

SSH はデフォルトでは22番ポートを使用するのが慣例である.

セキュリティ上の観点からは,SSHの接続は自宅や大学などの特定の地点からの接続のみを許す方が望ましい.

4.3.3. EC2 (Elastic Compute Cloud)

EC2

EC2 はAWS上に仮想サーバーを立ち上げるサービスである.個々の起動状態にある仮想サーバーのことをインスタンス (instance) と呼ぶ (しかし,コミュニケーションにおいては,サーバーとインスタンスという言葉は相互互換的に用いられることが多い).

EC2では用途に応じて様々なインスタンスタイプが提供されている. 以下に,代表的なインスタンスタイプの例を挙げる(2020/06時点での情報). EC2 のインスタンスタイプのすべてのリストは 公式ドキュメンテーションで見ることができる.

Table 2. EC2 instance types
Instance vCPU Memory (GiB) Network bandwidth (Gbps) Price per hour ($)

t2.micro

1

1

-

0.0116

t2.small

1

2

-

0.023

t2.medium

2

4

-

0.0464

c5.24xlarge

96

192

25

4.08

c5n.18xlarge

72

192

100

3.888

x1e.16xlarge

64

1952

10

13.344

このようにCPUは1コアから96コアまで,メモリーは1GBから3000GB以上まで,ネットワークは最大で100Gbpsまで,幅広く選択することができる.また,時間あたりの料金は,CPU・メモリーの占有数にほぼ比例する形で増加する. EC2 はサーバーの起動時間を秒単位で記録しており,利用料金は使用時間に比例する形で決定される. 例えば, t2.medium のインスタンスを10時間起動した場合,0.0464 * 10 = 0.464 ドルの料金が発生する.

AWS には 無料利用枠 というものがあり, t2.micro であれば月に750時間までは無料で利用することができる.

Table 2 の価格は us-east-1 のものである.地域によって多少価格設定が異なることがある.

上記で t2.micro の $0.0116 / hour という金額は,on-demandインスタンスというタイプを選択した場合の価格である. EC2 では他に,Spot instance と呼ばれるインスタンスも存在しする. Spot instance は,AWSのデータセンターの負荷が増えた場合,AWSの判断により強制シャットダウンされる可能性がある,という不便さを抱えているのだが,その分大幅に安い料金設定になっている. いうなれば,"すき間の空きCPUで計算を行う"といった格好である. 科学計算で,コストを削減する目的で,このSpot Instanceを使う事例も報告されている (Wu+, 2019).

EC2 インスタンスを定義しているコードの該当部分を見てみよう.

1
2
3
4
5
6
7
8
9
host = ec2.Instance(
    self, "MyFirstEc2Instance",
    instance_type=ec2.InstanceType("t2.micro"),
    machine_image=ec2.MachineImage.latest_amazon_linux(),
    vpc=vpc,
    vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PUBLIC),
    security_group=sg,
    key_name=key_name
)

ここでは, t2.micro というインスタンスタイプを選択している. さらに, machine_image (OSと考えてよい) として, Amazon Linux を選択している (Machine image については,第二回ハンズオンでより詳しく触れる). さらに,上で定義した VPC, SG をこのインスタンスに付与している.

以上が,今回使用するプログラムの簡単な解説であった. ミニマルな形のプログラムではあるが,仮想サーバーを作成するのに必要なステップがおわかりいただけただろうか?

4.4. プログラムを実行する

さて,ハンズオンのコードの理解ができたところで,プログラムを実際に実行してみよう.繰り返しになるが, Section 4.1 での準備ができていることが前提である.

4.4.1. Python の依存ライブラリのインストール

まずは,Python の依存ライブラリをインストールする.以下では,Python のライブラリを管理するツールとして, venv を使用する.

まずは, handson/01-ec2 のディレクトリに移動しよう.

$ cd handson/01-ec2

ディレクトリを移動したら, venv で新しい仮想環境を作成し,インストールを実行する.

$ python3 -m venv .env
$ source .env/bin/activate
$ pip install -r requirements.txt

これで Python の環境構築は完了だ.

venv の簡単な説明は Section 14.4 に記述してある.

4.4.2. AWS のシークレットキーをセットする

AWS CLI および AWS CDK を使うには,AWSのシークレットキーが設定されている必要がある.以下のようにして環境変数を設定する.

$ export AWS_ACCESS_KEY_ID=XXXXXX
$ export AWS_SECRET_ACCESS_KEY=YYYYYY
$ export AWS_DEFAULT_REGION=ap-northeast-1

上の XXXXXX, YYYYYY としたところは自分の鍵に置き換えることを忘れずに.

シークレットキーの発行については Section 14.1 を参照.

4.4.3. SSH鍵を生成

EC2 インスタンスには SSH を使ってログインする. EC2インスタンスを起動するのに先行して,今回のハンズオンで専用に使うSSHの公開鍵・秘密鍵のペアを準備する必要がある.

以下の aws-cli コマンドにより, HirakeGoma という名前のついた鍵を生成する.

$ export KEY_NAME="HirakeGoma"
$ aws ec2 create-key-pair --key-name ${KEY_NAME} --query 'KeyMaterial' --output text > ${KEY_NAME}.pem

上のコマンドを実行すると,現在のディレクトリに HirakeGoma.pem というファイルが作成される.これが,サーバーにアクセスするための秘密鍵である. SSH でこの鍵を使うため, ~/.ssh/ のディレクトリに鍵を移動する.さらに,秘密鍵が書き換えられたり第三者に閲覧されないよう,ファイルのアクセス権限を 400 に設定する.

$ mv HirakeGoma.pem ~/.ssh/
$ chmod 400 ~/.ssh/HirakeGoma.pem

4.4.4. デプロイを実行

これまでのステップで準備は整った!

早速,アプリケーションをAWSにデプロイしてみよう.

$ cdk deploy -c key_name="HirakeGoma"

-c key_name="HirakeGoma" というオプションで,先程生成した HirakeGoma という名前の鍵を使うよう指定している.

上記のコマンドを実行すると,VPC, EC2 などが実際に展開される.また,コマンドの出力の最後に Figure 10 のような出力が得られるはずである. 出力の中で InstancePublicIp に続く数字が,起動したインスタンスのパブリックIPアドレスである. IPアドレスはデプロイのごとにランダムに割り当てられる.

cdk output
Figure 10. CDKデプロイ実行後の出力

4.4.5. SSH でログイン

早速,SSHで接続してみよう.

$ ssh -i ~/.ssh/HirakeGoma.pem ec2-user@<IP address>

-i オプションで,先程生成した秘密鍵を指定している. EC2 インスタンスにはデフォルトで ec2-user という名前のユーザーが作られているので,それを使用する.最後に, <IP address> の部分は自分が作成したEC2インスタンスのIPアドレスで置き換える (54.238.112.5 など).

ログインに成功すると,以下のような画面が表示される.リモートのサーバーにログインしているので,プロンプトが [ec2-user@ip-10-10-1-217 ~]$ となっている.

ssh_login
Figure 11. SSH で EC2 インスタンスにログイン

おめでとう!これで,めでたくAWS上にEC2仮想サーバーを起動し,リモートからアクセスすることができるようになった!

4.4.6. 起動した EC2 インスタンスで遊んでみる

せっかくサーバーを起動したので,少し遊んでみよう.

ログインした EC2 インスタンスで,次のコマンドを実行してみよう. CPUの情報を取得することができる.

$ cat /proc/cpuinfo

processor	: 0
vendor_id	: GenuineIntel
cpu family	: 6
model		: 63
model name	: Intel(R) Xeon(R) CPU E5-2676 v3 @ 2.40GHz
stepping	: 2
microcode	: 0x43
cpu MHz		: 2400.096
cache size	: 30720 KB

次に,実行中のプロセスやメモリの消費を見てみよう.

$  top -n 1

top - 09:29:19 up 43 min,  1 user,  load average: 0.00, 0.00, 0.00
Tasks:  76 total,   1 running,  51 sleeping,   0 stopped,   0 zombie
Cpu(s):  0.3%us,  0.3%sy,  0.1%ni, 98.9%id,  0.2%wa,  0.0%hi,  0.0%si,  0.2%st
Mem:   1009140k total,   270760k used,   738380k free,    14340k buffers
Swap:        0k total,        0k used,        0k free,   185856k cached

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
    1 root      20   0 19696 2596 2268 S  0.0  0.3   0:01.21 init
    2 root      20   0     0    0    0 S  0.0  0.0   0:00.00 kthreadd
    3 root      20   0     0    0    0 I  0.0  0.0   0:00.00 kworker/0:0

t2.micro インスタンスなので, 1009140k = 1GB のメモリーがあることがわかる.

今回起動したインスタンスには Python 2 はインストール済みだが, Python 3 は入っていない. Python 3.6 のインストールを行ってみよう. インストールは簡単である.

$ sudo yum update -y
$ sudo yum install -y python36

インストールしたPythonを起動してみよう.

$ python3
Python 3.6.10 (default, Feb 10 2020, 19:55:14)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-28)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

Python のインタープリタが起動した! Ctrl + D あるいは exit() と入力することで,インタープリタを閉じることができる.

さて,サーバーでのお遊びはこんなところにしておこう (興味があれば各自いろいろと試してみると良い) . 次のコマンドでログアウトする.

$ exit

4.4.7. AWS コンソールから確認

これまでは,すべてコマンドラインからEC2に関連する諸々の操作を行ってきた. EC2インスタンスの状態を確認したり,サーバーをシャットダウンするなどの操作は,AWS コンソールから実行することもできる. 軽くこれを紹介しよう.

まず,AWS コンソールにログインする.

ログインしたら, Services から EC2 を検索(選択)する. 次に,左のサイドバーの Instances とページを辿る. すると, Figure 12 のような画面が得られるはずである. この画面で,自分のアカウントの管理下にあるインスタンスを確認することができる.

ec2_console
Figure 12. EC2 コンソール画面

コンソール右上で,正しいリージョン (今回の場合は ap-northeast-1, Tokyo) が選択されているか,注意する!

同様に,VPC・SG についてもコンソールから確認することができる.

前章で CloudFormation について触れたが,今回デプロイしたアプリケーションも,CloudFormation の "スタック" として管理されている. スタック (stack) とは,AWSリソースの集合のことを指す. 今回の場合は,VPC/EC2/SG などがスタックの中に含まれている.

コンソールで CloudFormation のページに行ってみよう (Figure 13).

cloudformation console
Figure 13. CloudFormation コンソール画面

"MyFirstEc2" という名前のスタックがあることが確認できる. クリックをして中身を見てみると,EC2, VPN などのリソースがこのスタックに紐付いていることがわかる.

4.4.8. スタックを削除

これにて,第一回のハンズオンで説明すべき事柄はすべて完了した. 最後に,使わなくなったスタックを削除しよう.

スタックの削除には,2つの方法がある.

1つめの方法は,前節の Cloudformation のコンソール画面で, "Delete" ボタンを押すことである (Figure 14).

cloudformation delete
Figure 14. CloudFormationコンソール画面から,スタックを削除

2つめの方法は,コマンドラインから行う方法である.

先ほど,デプロイを行ったコマンドラインに戻ろう.そうしたら,

$ cdk destroy

と実行する.すると,スタックの削除が始まる.

削除した後は,VPC, EC2 など,すべて跡形もなく消え去っている.

このように,自分の使いたいときにだけ,サーバーを立ち上げ,使い終わったら直ちに削除する,というのが現代のクラウドの正しい使い方である.

スタックの削除は各自で必ず行うこと! 行わなかった場合,EC2インスタンスの料金が発生し続けることになる!

また,本ハンズオンのために作成したSSH鍵ペアも不要なので,削除しておく.

まず,EC2側に登録してある公開鍵を削除する. これも,コンソールおよびコマンドラインの2つの方法で実行できる.

コンソールから実行するには, EC2 の画面に行き,左のサイドバーの Key Pairs を選択. 鍵の一覧が表示されるので, HirakeGoma とある鍵にチェックを入れ,画面右上の Actions から, Delete を実行 (Figure 15).

ec2_keypair_console
Figure 15. EC2でSSH鍵ペアを削除

コマンドラインから実行するには,以下のコマンドを使う.

$ aws ec2 delete-key-pair --key-name "HirakeGoma"

最後に,ローカルのコンピュータから鍵を削除する.

$ rm -f ~/.ssh/HirakeGoma.pem

これで,クラウドの片付けもすべて終了だ.

なお,頻繁にEC2インスタンスを起動したりする場合は,いちいちSSH鍵を削除する必要はない.

4.5. 講義第一回目のまとめ

ここまでが,第一回目の講義の内容である.盛りだくさんの内容であったが,ついてこれたであろうか?

第一回では,クラウドの概要と,なぜクラウドを使うのか,という点を議論した. また,クラウドを学ぶ具体的な題材としてAWSを取り上げ,AWSの概要説明を行った. さらに,ハンズオンではAWS CLI/CDK を使って,自分のマイ・サーバーをAWS上に立ち上げる演習を行った.

ハンズオンなどを通じて,いかに簡単に(たった数行のコマンドで!)仮想サーバーを立ち上げたり,削除したりすることができるか,体験することができただろう. このように,ダイナミックに計算リソースを拡大・縮小をできることが,クラウドの最も本質的な側面であると,筆者は考えている

次回以降の講義では,今回学んだクラウドの技術を基に,より現実的な問題を解くことを体験してもらう.お楽しみに!

5. クラウドで行う科学計算・機械学習

ここからが第二回目の講義の内容になる.

第二回目は,前回学んだクラウドの知識・技術を使って,現実的な問題を解くことを考える.

計算機が発達した現代では,計算機によるシミュレーションやビッグデータの解析は,科学・エンジニアリングの研究の主要な柱である. これらの大規模な計算を実行するには,クラウドは最適である. 講義第二回では,どのようにしてクラウド上で科学計算を実行するのかを,ハンズオンとともに体験してもらう. 科学計算の具体的な題材として,今回は機械学習(ディープラーニング)を取り上げる.

なお,本講義では PyTorch ライブラリを使ってディープラーニングのアルゴリズムを走らせるが,ディープラーニングおよび PyTorch の知識は不要である. 講義ではなぜ・どうやってディープラーニングをクラウドで実行するかに主眼を置いているので,実行するプログラムの詳細には立ち入らない. 将来自分でディープラーニングを使う機会が来たときに,詳しく学んでもらいたい.

5.1. なぜ機械学習をクラウドで行うのか?

2010年代に始まったAIブームのおかげで,研究だけでなく社会・ビジネスの文脈でも機械学習に高い関心が寄せられている. 特に,ディープラーニングと呼ばれる多層のレイヤーからなるニューラルネットワークを用いたアルゴリズムは,画像認識や自然言語処理などの分野で圧倒的に高い性能を実現し,革命をもたらしている.

ディープラーニングの特徴は,なんといってもそのパラメータの多さである. 層が深くなるほど,層間のニューロンを結ぶ重みパラメータの数が増大していく. 例えば,最新の言語モデルである GPT-3 には1750億個ものパラメータが含まれている! このような膨大なパラメータを有することで,ディープラーニングは高い表現力と汎化性能を実現しているのである.

cnn
Figure 16. ニューラルネットワークにおける畳み込み演算.

GPT-3 に限らず,最近の SOTA (State-of-the-art) の性能を達成するニューラルネットでは,百万から億のオーダーのパラメータを有することは頻繁になってきている.そのような巨大なニューラルネットを学習させるのは,当然のことながら巨大な計算コストがかかる.そんな巨大な計算に最適なのが,クラウドである!事実,GPT-3の学習も,詳細は明かされていないが,Microsoft社のクラウドを使って行われたと報告されている.

GPT-3 が発表された時,そのモデルがもつ表現能力には多くの人が驚嘆させられた. OpenAI のブログに,モデルが出力した翻訳や文章要約のタスクの結果が紹介されている.

5.2. GPU による機械学習の高速化

ディープラーニングの計算で欠かすことのできない技術として, GPU (Graphics Processing Unit) について少し説明する.

GPUは,その名のとおり,元々はコンピュータグラフィックスを出力するための専用計算チップである.CPU (Central Processing Unit) に対し,グラフィックスの演算に特化した設計がなされている.身近なところでは,XBoxやPS4などのゲーム機などに搭載されているし,ハイスペックなノートパソコンやデスクトップコンピュータにも搭載されていることがある.コンピュータグラフィクスでは,スクリーンにアレイ状に並んだ数百万個の画素をリアルタイムで処理する必要がある.そのため,GPUは,コアあたりの演算能力は比較的弱いかわりに,チップあたり数百から数千のコアを搭載しており (Figure 17),スクリーンの画素を並列的に処理することで,リアルタイムでの描画を実現している.

cdk output
Figure 17. GPUのアーキテクチャ.GPUには数百から数千の独立した計算コアが搭載されている. (画像出典: https://devblogs.nvidia.com/nvidia-turing-architecture-in-depth/)

このように,コンピュータグラフィクスの目的で生まれたGPUだが,2010年前後から,その高い並列計算能力をグラフィックス以外の計算(科学計算など)に用いるという流れ (General-purpose computing on GPU; GPGPU) が生まれた.GPUのコアは,その設計から,行列の計算など,単純かつ規則的な演算が得意であり,そのような演算に対しては数個程度のコアしか持たないCPUに比べて圧倒的に高い計算速度を実現することができる.現在ではGPGPUは分子動力学や気象シミュレーション,そして機械学習など多くの分野で使われている.

ディープラーニングで最も頻繁に起こる演算が,ニューロンの出力を次の層のニューロンに伝える畳み込み (Convolution) 演算である.畳み込み演算は,まさにGPUが得意とする演算であり,CPUではなくGPUを用いることで学習を飛躍的に(最大で数百倍程度!)加速させることができる.

このように GPU は機械学習の計算で欠かせないものであるが,なかなか高価である.例えば,科学計算・機械学習に専用設計されたNVIDIA社の Tesla V100 というチップは,一台で約百万円の価格が設定されている.機械学習を始めるのに,いきなり百万円の投資はなかなか大きい.だが,クラウドを使えば,初期コスト0でGPUを使用することができる.

機械学習を行うのに,V100が必ずしも必要というわけではない. むしろ,研究者などでしばしば行われるのは,コンピュータゲームに使われるグラフィックス用のGPUを買ってきて(NVIDIA GeForceシリーズなど),開発のときはをそれを用いる,というアプローチである. グラフィックス用のいわゆる"コンシューマGPU"は,市場の需要が大きいおかげで,10万円前後の価格で購入することができる. V100と比べると,コンシューマGPUはコアの数が少なかったり,メモリーが小さかったりなどで劣る点があるが,ディープラーニングの計算は特に問題なく実行することができ,開発の段階では十分な性能である.

プログラムができあがって,ビッグデータの解析や,モデルをさらに大きくしたいときなどに,クラウドは有効だろう.

クラウドでGPUを使うには,GPUが搭載されたEC2インスタンスタイプ (P3, P2, G3, G4 など) を選択しなければならない. Table 3 に,代表的なGPU搭載のインスタンスタイプを挙げる (執筆時点(2020/06)での情報).

Table 3. GPUを搭載したEC2インスタンスタイプ
Instance GPUs GPU model GPU Mem (GiB) vCPU Mem (GiB) Price per hour ($)

p3.2xlarge

1

NVIDIA V100

16

8

61

3.06

p3n.16xlarge

8

NVIDIA V100

128

64

488

24.48

p2.xlarge

1

NVIDIA K80

12

4

61

0.9

g4dn.xlarge

1

NVIDIA T4

16

4

16

0.526

Table 3 からわかるとおり,CPUのみのインスタンスと比べると少し高い価格設定になっている. また,古い世代のGPU (V100に対してのK80) はより安価な価格で提供されている. GPUの搭載数は1台から最大で8台まで選択することが可能である.

GPUを搭載した一番安いインスタンスタイプは, g2dn.xlarge であり,これには廉価かつ省エネルギー設計の NVIDIA T4 が搭載されている.今回のハンズオンでは,このインスタンスを使用して,ディープラーニングの計算を行ってみる.

Table 3 の価格は us-east-1 のもの.地域によって多少価格設定が異なることがある.

V100を一台搭載した p3.2xlarge の利用料金は一時間あたり $3.06 である.V100が約百万円で売られていることを考えると,約3000時間 (= 124日間),通算で計算を行った場合に,クラウドを使うよりもV100を自分で買ったほうがお得になる,という計算になる. (実際には,自前でV100を用意する場合は,V100だけでなく,CPUやネットワーク機器,電気使用料も必要なので,百万円よりもさらにコストがかかる.)

GPT-3 で使われた計算リソースの詳細は論文でも明かされていないのだが, Lambda社のブログで興味深い考察が行われている (Lambda社は機械学習に特化したクラウドサービスを提供している).

記事によると,1750億のパラメータを学習するには,一台のGPU (NVIDIA V100)を用いた場合,342年の月日と460万ドルのクラウド利用料が必要となる,とのことである.GPT-3のチームは,複数のGPUに処理を分散することで現実的な時間のうちに学習を完了させたのであろうが,このレベルのモデルになってくるとクラウド技術の限界を攻めないと達成できないことは確かである.

6. Hands-on #2: AWSでディープラーニングの計算を走らせる

ハンズオン第二回では,GPUを搭載したEC2インスタンスを起動し,ディープラーニングの学習と推論を実行する演習を行う.

ハンズオンのソースコードはこちらのリンクに置いてある ⇒ https://gitlab.com/tomomano/intro-aws/-/tree/master/handson/02-ec2-dnn

このハンズオンは,AWSの無料枠内では実行できない. g4dn.xlarge タイプの EC2 インスタンスを使うので, ap-northeast-1 リージョンでは 0.8 $/hour のコストが発生する.

6.1. 準備

本ハンズオンの実行には,第一回ハンズオンで説明した準備 (Section 4.1) が整っていることを前提とする.それ以外に必要な準備はない.

6.2. アプリケーションの説明

このハンズオンで作成するアプリケーションの概要を Figure 18 に示す.

hands-on 01 architecture
Figure 18. ハンズオン#2で作製するアプリケーションのアーキテクチャ

図の多くの部分が,第一回ハンズオンで作成したアプリケーションと共通していることに気がつくだろう.少しの変更で,簡単にディープラーニングを走らせる環境を構築することができるのである!主な変更点は次の3点である.

  • GPUを搭載した g4dn.xlarge インスタンスタイプを使用.

  • ディープラーニングに使うプログラムが予めインストールされたDLAMI (後述) を使用.

  • SSHにポートフォワーディングのオプションつけてサーバーに接続し,サーバーで起動しているJupyter notebook (後述) を使ってプログラムを書いたり実行したりする.

ハンズオンで使用するプログラムのコードをみてみよう (/handson/02-ec2-dnn/app.py).コードも,第一回目とほとんど共通である.変更点のみ解説を行う.

 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
38
39
class Ec2ForDl(core.Stack):

    def __init__(self, scope: core.App, name: str, key_name: str, **kwargs) -> None:
        super().__init__(scope, name, **kwargs)

        vpc = ec2.Vpc(
            self, "Ec2ForDl-Vpc",
            max_azs=1,
            cidr="10.10.0.0/23",
            subnet_configuration=[
                ec2.SubnetConfiguration(
                    name="public",
                    subnet_type=ec2.SubnetType.PUBLIC,
                )
            ],
            nat_gateways=0,
        )

        sg = ec2.SecurityGroup(
            self, "Ec2ForDl-Sg",
            vpc=vpc,
            allow_all_outbound=True,
        )
        sg.add_ingress_rule(
            peer=ec2.Peer.any_ipv4(),
            connection=ec2.Port.tcp(22),
        )

        host = ec2.Instance(
            self, "Ec2ForDl-Instance",
            instance_type=ec2.InstanceType("g4dn.xlarge"), (1)
            machine_image=ec2.MachineImage.generic_linux({
                "ap-northeast-1": "ami-09c0c16fc46a29ed9"
            }), (2)
            vpc=vpc,
            vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PUBLIC),
            security_group=sg,
            key_name=key_name
        )
1 ここで,GPUを搭載した g4dn.xlarge インスタンスタイプを選択している (第一回では,CPUのみの t2.micro だった).
2 ここで,Deep Learning 用の諸々のソフトウェアがプリンストールされたAMI (Deep Learning Amazon Machine Image; DLAMI) を選択している.(第一回では,Amazon Linux というAMIを使用していた).使用するAMIのIDは "ami-09c0c16fc46a29ed9" である.

g4dn.xlarge のインスタンスタイプについては, Section 5 ですでに触れた.DLAMI について,少し説明しよう.

6.2.1. DLAMI (Deep Learning Amazon Machine Image)

AMI (Amazon Machine Image) とは,平たく言えば OS (Operating System) のことである.当然のことながら,OSがなければコンピュータはなにもできないので,EC2インスタンスを起動する時には必ずなにかのOSを"インストール"する必要がある.AMI には,例えば UbuntuSUSE Linux などの各Linux系OSに加えて,Windows Server を選択することもできる.また,EC2での使用に最適化された Amazon Linux というAMIも提供されている.

しかしながら,AMIを単なるOSと理解するのは誤りである. AMIには,ベースとなる(空っぽの)OSを選択できることもできるが,それに加えて,各種のプログラムがインストール済みのAMIも用意されている. 必要なプログラムがインストールされているAMIを見つけることができれば,自分でインストールを行ったり環境設定をする手間が大幅に省ける. 具体例を挙げると,ハンズオン第一回では EC2 インスタンスに Python 3.6 をインストールする例を示したが,そのような操作をインスタンスを起動するたびに行うのは手間である!

AMI は,AWSや他のサードパーティーから提供されており,EC2のコンソール画面でインスタンスを起動するときに検索することができる.あるいは, AWS CLI を使って,次のコマンドでリストを取得することができる (参考).

$ aws ec2 describe-images --owners amazon

上記のコマンドにより,amazon が提供しているAMIの一覧が表示される. また,自分自身のAMIを作って登録することも可能である (参考).

ディープラーニングで頻繁に使われるプログラムが予めインストールしてあるAMIが, DLAMI (Deep Learning AMI) である. DLAMIには TensorFlow, PyTorch などの人気の高いディープラーニングのフレームワーク・ライブラリが既にインストールされているため,EC2インスタンスを起動してすぐさまディープラーニングの計算を実行できる.

本ハンズオンでは, Amazon Linux 2 をベースにした DLAMI を使用する (AMI ID = ami-09c0c16fc46a29ed9).AWS CLI を使って,このAMIの詳細情報を取得してみよう.

$ aws ec2 describe-images --owners amazon --image-ids "ami-09c0c16fc46a29ed9"
ami-info
Figure 19. AMI ID = ami-09c0c16fc46a29ed9 の詳細情報

Figure 19 のような出力が得られるはずである.得られた出力から,例えばこの DLAMI には PyTorch のバージョン1.4.0 と 1.5.0 がインストールされていることがわかる.このDLAMIを使って,早速ディープラーニングの計算を実行してみよう.

DLAMIには具体的には何がインストールされているのだろうか? 興味のある読者のために,簡単な解説をしよう (参考: 公式ドキュメンテーション).

最も low-level なレイヤーとしては, GPUドライバー がインストールされている. GPUドライバーなしにはOSはGPUにコマンドを送ることができない. 次のレイヤーが CUDAcuDNN である. CUDAは,NVIDIA社が開発した,GPU上で汎用コンピューティングを行うための言語であり,C++言語を拡張したシンタックスを備える. cuDNN は CUDA で書かれたディープラーニングのライブラリであり,n次元の畳み込みなどの演算が実装されている.

以上までが, "Base" と呼ばれるタイプの DLAMI の中身である.

これに加えて, "Conda" と呼ばれるタイプには, これらのプログラム基盤の上に, TensorFlowPyTorch などのライブラリがインストールされている. さらに, Anaconda による仮想環境を使うことによって, TensorFlow の環境, PyTorch の環境,を簡単に切り替えることができる (これについては,ハンズオンで触れる).また, Jupyter notebook もインストール済みである.

6.3. スタックのデプロイ

スタックの中身が理解できたところで,早速スタックをデプロイしてみよう.

デプロイの手順は,ハンズオン1とほとんど共通である. ここでは,コマンドのみ列挙する (# で始まる行はコメントである). それぞれのコマンドの意味を忘れてしまった場合は,ハンズオン1に戻って復習していただきたい.

# プロジェクトのディレクトリに移動
$ cd intro-aws/handson/02-ec2-dnn

# venv を作成し,依存ライブラリのインストールを行う
$ python3 -m venv .env
$ source .env/bin/activate
$ pip install -r requirements.txt

# AWS の認証情報をセットする
# 自分自身の認証情報に置き換えること!
export AWS_ACCESS_KEY_ID=XXXXXX
export AWS_SECRET_ACCESS_KEY=YYYYYY
export AWS_DEFAULT_REGION=ap-northeast-1

# SSH鍵を生成
$ export KEY_NAME="HirakeGoma"
$ aws ec2 create-key-pair --key-name ${KEY_NAME} --query 'KeyMaterial' --output text > ${KEY_NAME}.pem
$ mv HirakeGoma.pem ~/.ssh/
$ chmod 400 ~/.ssh/HirakeGoma.pem

# デプロイを実行
$ cdk deploy -c key_name="HirakeGoma"

ハンズオン1で作成したSSH鍵の削除を行わなかった場合は,SSH鍵を改めて作成する必要はない.逆に言うと,同じ名前のSSHが既に存在する場合は,鍵生成のコマンドはエラーを出力する.

デプロイのコマンドが無事に実行されれば, Figure 20 のような出力が得られるはずである.AWSにより割り振られたIPアドレス (InstancePublicIp に続く文字列) をメモしておこう.

cdk output
Figure 20. CDKデプロイ実行後の出力

6.4. ログイン

早速,デプロイしたインスタンスにSSHでログインしてみよう.

ここでは,この後で使う Jupyter notebook に接続するため,ポートフォワーディング のオプション (-L) をつけてログインする.

$ ssh -i ~/.ssh/HirakeGoma.pem -L localhost:8931:localhost:8888 ec2-user@<IP address>

ポートフォワーディングとは,クライアントマシンの特定のアドレスへの接続を,SSHの暗号化された通信を介して,リモートマシンの特定のアドレスへ転送する,という意味である. 上のコマンドの -L localhost:8931:localhost:8888 は,自分のローカルマシンの localhost:8931 へのアクセスを,リモートサーバーの localhost:8888 のアドレスに転送せよ,という意味である (: につづく数字はポート番号を意味している). リモートサーバーのポート8888には,後述する Jupyter notebook が起動している. したがって,ローカルマシンの localhost:8931 にアクセスすることで,リモートサーバーの Jupyter notebook にアクセスすることができるのである (このようなSSHによる接続方式をトンネル接続と呼ぶ).

ポートフォワーディングについて混乱した読者は,より詳しい解説が このブログ記事 にある.

ポートフォワーディングのオプションで,ポートの番号 (:8931, :8888 など) には1から65535までの任意の整数を指定できる.しかし,例えば ポート22は SSH に,ポート80は HTTP に,など,いくつか既に使われているポート番号もあることに注意する.また, Jupyter notebook デフォルトではポート8888番を使用する.したがって,リモート側のポート番号は,8888を使うのがよい.

SSH ログインコマンドの <IP address> 部分は自分のインスタンスのIPアドレスを代入することを忘れずに.

本講義の提供している Docker を使ってデプロイを実行した人へ

SSH によるログインは, Docker の外 (すなわちクライアントマシン本体) から行わなければならない. なぜなら,Jupyter を開くウェブブラウザは Docker の外にあるからである.

その際,秘密鍵を Docker の外に持ってこなければならない.手っ取り早い方法は, cat ~/.ssh/HirakeGoma と打って,出力結果をコピー&ペーストで Docker の外に持ってくる方法である.

あるいは -v オプションをつけて,ファイルシステムをマウントしてもよい (詳しくは 参照).

SSHによるログインができたら,早速,GPUの状態を確認してみよう.以下のコマンドを実行する.

$ nvidia-smi

Figure 21 のような出力が得られるはずである.出力を見ると, Tesla T4 型のGPUが1台搭載されていることが確認できる.その他,GPU Driver や CUDA のバージョンを確認することができる.

nvidia-smi
Figure 21. nvidia-smi の出力

6.5. Jupyter notebook の起動

Jupyter notebook とは,インタラクティブに Python のプログラムを書いたり実行したりするためのツールである.Jupyter は GUIとしてウェブブラウザを介してアクセスする形式をとっており,まるでノートを書くように,プロットやテーブルのデータも美しく表示することができる (Figure 22).Python に慣れている読者は,きっと一度は使ったことがあるだろう.

welcome to jupyter
Figure 22. Jupyter notebook の画面

このハンズオンでは, Jupyter notebook を使ってディープラーニングのプログラムを書いたり実行していく. DLAMI には既に Jupyter がインストールされているので,特段の設定なしに使い始めることができる.

早速, Jupyter を起動しよう. SSHでログインした先のEC2インスタンスで,次のコマンドを実行すればよい.

$ cd ~ # go to home directory
$ jupyter notebook

このコマンドを実行すると, Figure 23 のような出力が確認できるだろう. この出力から,Jupyter のサーバーが EC2 インスタンスの localhost:8888 というアドレスに起動していることがわかる. また, localhost:8888 に続く ?token=XXXXXXX は,アクセスに使うための一時的なトークンである.

jupyter launch
Figure 23. Jupyter notebook サーバーを起動

Jupyter notebook を初回に起動するときは,起動に少し時間がかかることがある. 1,2分じっと待つ.

先ほど,ポートフォワーディングのオプションをつけてSSH接続をしているので, Jupyter の起動している localhost:8888 には,ローカルマシンの localhost:8931 からアクセスすることができる.

したがって,ローカルマシンから Jupyter にアクセスするには,ウェブブラウザ (Chrome, FireFox など)から次のアドレスにアクセスすれば良い.

http://localhost:8931/?token=XXXXXXXXXX

?token=XXXXXX の部分は,上で Jupyter を起動したときに発行されたトークンの値に置き換える.

上のアドレスにアクセスすると, Jupyter のホーム画面が起動するはずである (Figure 24). これで, Jupyter の準備が整った!

jupyter home
Figure 24. Jupyter ホーム画面

DLAMI の 公式ドキュメンテーション では,自分で指定したパスワードとSSLを有効化することを推奨している. 本ハンズオンでは時間の節約のためにスキップしているが,今後個人で使うときはより強固にセキュリティを設定することを推奨する.

Jupyter notebook の使い方(超簡易版)

  • Shift + Enter: セルを実行

  • Esc: Command mode に遷移

  • メニューバーの "+" ボタン または Command mode で A : セルを追加

  • メニューバーの "ハサミ" ボタン または Command mode で X : セルを削除

ショートカットの一覧などは このブログ が参考になる.

6.6. PyTorchはじめの一歩

PyTorch は Facebook AI Research LAB (FAIR) が中心となって開発を進めている,オープンソースのディープラーニングのライブラリである. PyTorch は 有名な例で言えば Tesla 社の自動運転プロジェクトなどで使用されており,2020/06時点において最も人気の高いディープラーニングライブラリの一つである. 本ハンズオンでは, PyTorch を使ってディープラーニングの実践を行う.

PyTorch の歴史のお話

Facebook は PyTorch の他に Caffe2 と呼ばれるディープラーニングのフレームワークを開発していた (初代Caffee は UC Berkley の博士学生だった Yangqing Jia によって創られた). Caffe2 は 2018年に PyTorch プロジェクトに合併された.

また,2019年12月,日本の Preferred Networks 社が開発していた Chainer も,開発を終了し,PyTorchの開発チームと協業していくことが発表された (プレスリリース). PyTorch には,開発統合前から Chainer からインスパイアされた API がいくつもあり, Chainer の DNA は今も PyTorch に引き継がれているのである…​!

本格的なディープラーニングの計算に移る前に, PyTorch ライブラリを使って, GPU で計算を行うとはどういうものか,その入り口に触れてみよう.

まずは,新しいノートブックを作成する. Jupyterのホーム画面の右上の "New" を押し,"conda_pytorch_p36" という環境を選択した上で,新規ノートブックを作成する (Figure 25). "conda_pytorch_p36" の仮想環境には, PyTorch がインストール済みである (他にある TensorFlow なども同様).

jupyter_new
Figure 25. 新規ノートブックの作成. "conda_pytorch_p36" の環境を選択する.

次のようなプログラムを書いてみよう (Figure 26).

jupyter_pytorch
Figure 26. PyTorch始めの一歩

まずは, PyTorch をインポートする.さらに,GPUが使える環境にあるか,確認する.

1
2
import torch
print("Is CUDA ready?", torch.cuda.is_available())

出力:

Is CUDA ready? True

次に,3x3 のランダムな行列を CPU 上に作ってみよう.

1
2
x = torch.rand(3,3)
print(x)

出力:

tensor([[0.6896, 0.2428, 0.3269],
        [0.0533, 0.3594, 0.9499],
        [0.9764, 0.5881, 0.0203]])

次に,行列を GPU 上に作成する.

1
2
y = torch.ones_like(x, device="cuda")
x = x.to("cuda")

そして,行列 xy の加算を,GPU上で実行する

1
2
z = x + y
print(z)

出力:

tensor([[1.6896, 1.2428, 1.3269],
        [1.0533, 1.3594, 1.9499],
        [1.9764, 1.5881, 1.0203]], device='cuda:0')

最後に,GPU上にある行列を,CPUに戻す.

1
2
z = z.to("cpu")
print(z)

出力:

tensor([[1.6896, 1.2428, 1.3269],
        [1.0533, 1.3594, 1.9499],
        [1.9764, 1.5881, 1.0203]])

以上の例は, GPU を使った計算の初歩の初歩であるが,雰囲気はつかめただろうか? CPU と GPU で明示的にデータを交換するのが肝である.この例は,たった 3x3 の行列の足し算なので,GPUを使う意味はまったくないが,これが数千,数万のサイズの行列になった時,GPUは格段の威力を発揮する.

完成した Jupyter notebook は /handson/02-ec2-dnn/pytorch/pytorch_get_started.ipynb にある. Jupyter の画面右上の "Upload" から,このファイルをアップロードして,コードを走らせることが可能である.

しなしながら,勉強の時にはコードはすべて自分の手で打つことが,記憶に残りやすくより効果的である,というのが筆者の意見である.

6.7. CPU vs GPU の簡易ベンチマーク

実際に,ベンチマークを取ることでGPUとCPUの速度を比較をしてみよう.実行時間を計測するツールとして, Jupyter の提供する %time マジックコマンドを利用する.

まずは,CPUを使用して,10000x10000 の行列の行列積を計算した場合の速度を測ってみよう.先ほどのノートブックの続きに,次のコードを実行する.

1
2
3
4
5
6
s = 10000
device = "cpu"
x = torch.rand(s, s, device=device, dtype=torch.float32)
y = torch.rand(s, s, device=device, dtype=torch.float32)

%time z = torch.matmul(x,y)

出力は以下のようなものが得られるだろう.これは,行列積の計算に実時間で5.8秒かかったことを意味する (実行のたびに計測される時間はばらつくことに留意).

CPU times: user 11.5 s, sys: 140 ms, total: 11.6 s
Wall time: 5.8 s

次に,GPUを使用して,同じ演算を行った場合の速度を計測しよう.

1
2
3
4
5
6
7
s = 10000
device = "cuda"
x = torch.rand(s, s, device=device, dtype=torch.float32)
y = torch.rand(s, s, device=device, dtype=torch.float32)
torch.cuda.synchronize()

%time z = torch.matmul(x,y); torch.cuda.synchronize()

出力は以下のようなものになるだろう.GPUでは 553ミリ秒 で計算を終えることができた!

CPU times: user 334 ms, sys: 220 ms, total: 554 ms
Wall time: 553 ms

PyTorch において, GPU での演算は asynchronous (非同期) で実行される.その理由で,上のベンチマークコードでは, torch.cuda.synchronize() というステートメントを埋め込んである.

このベンチマークでは, dtype=torch.float32 と指定することで,32bitの浮動小数点型を用いている.ディープラーニングの学習および推論の計算には,32bit型,場合によっては16bit型が使われるのが一般的である.これの主な理由として,教師データやミニバッチに起因するノイズが,浮動小数点の精度よりも大きいことがあげられる.32bit/16bit を採用することで,メモリー消費を抑えたり,計算速度の向上が達成できる.

上記のベンチマークから,GPUを用いることで,約10倍のスピードアップを実現することができた.スピードアップの割合は,演算の種類や行列のサイズに依存する.行列積は,そのなかでも最も速度向上が見込まれる演算の一つである.

6.8. 実践ディープラーニング! MNIST手書き数字認識タスク

ここまで,AWS上でディープラーニングの計算をするための概念や前提知識をながながと説明してきたが,ついにここからディープラーニングの計算を実際に走らせてみる.

ここでは,機械学習のタスクで最も初歩的かつ有名な MNIST データセットを使った数字認識を扱う (Figure 27). 0から9までの手書きの数字の画像が与えられ,その数字が何の数字なのかを当てる,というシンプルなタスクである.

mnist_examples
Figure 27. MNIST 手書き数字データセット

今回は, MNIST 文字認識タスクを,畳み込みニューラルネットワーク (Convolutional Neural Network; CNN) を使って解く. ソースコードは /handson/02-ec2-dnn/pytorch/ にある mnist.ipynbsimple_mnist.py である. なお,このプログラムは, PyTorch の公式 Example Project 集 を参考に,多少の改変を行ったものである.

まず最初に,カスタムのクラスや関数が定義された simple_mnist.py をアップロードしよう (Figure 28). 画面右上の "Upload" ボタンをクリックし,ファイルを選択すればよい. この Python プログラムの中に,CNN のモデルや,学習の各イテレーションにおけるパラメータの更新などが記述されている. 今回は,この中身を説明することはしないが,興味のある読者は,自分でソースコードを読んでみるとよい.

jupyter upload
Figure 28. simple_mnist.py をアップロード

simple_mnist.py をアップロードできたら,次に新しい notebook を作成しよう. "conda_pytorch_p36" の環境を選択することを忘れずに.

新しいノートブックが起動したら,まず最初に,必要なライブラリをインポートしよう.

1
2
3
4
5
6
7
8
import torch
import torch.optim as optim
import torchvision
from torchvision import datasets, transforms
from matplotlib import pyplot as plt

# custom functions and classes
from simple_mnist import Model, train, test

torchvision パッケージには,MNIST データセットをロードするなどの便利な関数が含まれている. また,今回のハンズオンで使うカスタムのクラス・関数 (Model, train, test) のインポートを行っている.

次に,MNIST テストデータをダウンロードしよう. 同時に,画像データの輝度の正規化も行っている.

1
2
3
4
5
6
7
8
transf = transforms.Compose([transforms.ToTensor(),
                             transforms.Normalize((0.1307,), (0.3081,))])

trainset = datasets.MNIST(root='./data', train=True, download=True, transform=transf)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)

testset = datasets.MNIST(root='./data', train=False, download=True, transform=transf)
testloader = torch.utils.data.DataLoader(trainset, batch_size=1000, shuffle=True)

今回扱う MNIST データは 28x28 ピクセルの正方形の画像(モノクロ)と,それぞれのラベル(0 - 9 の数字)の組で構成されている. いくつかのデータを抽出して,可視化してみよう. Figure 29 のような出力が得られるはずである.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
examples = iter(testloader)
example_data, example_targets = examples.next()

print("Example data size:", example_data.shape)

fig = plt.figure(figsize=(10,4))
for i in range(10):
    plt.subplot(2,5,i+1)
    plt.tight_layout()
    plt.imshow(example_data[i][0], cmap='gray', interpolation='none')
    plt.title("Ground Truth: {}".format(example_targets[i]))
    plt.xticks([])
    plt.yticks([])
plt.show()
mnist_ground_truth
Figure 29. MNIST の手書き数字画像とその教師ラベル

次に, CNN のモデルを定義する.

1
2
model = Model()
model.to("cuda") # load to GPU

今回使う Modelsimple_mnist.py の中で定義されている. このモデルは,Figure 30 に示したような,2層の畳み込み層と2層の全結合層からなるネットワークである. 出力層 (output layer) には Softmax 関数を使用し,損失関数 (Loss function) には 負の対数尤度関数 (Negative log likelyhood; NLL) を使用している.

cnn architecture
Figure 30. 本ハンズオンで使用するニューラルネットの構造.

続いて, CNN のパラメータを更新する最適化アルゴリズムを定義する. ここでは, Stochastic Gradient Descent (SGD) を使用している.

1
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)

これで,準備が整った. CNN の学習ループを開始しよう!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
train_losses = []
for epoch in range(5):
    losses = train(model, trainloader, optimizer, epoch)
    train_losses = train_losses + losses
    test(model, testloader)

plt.figure(figsize=(7,5))
plt.plot(train_losses)
plt.xlabel("Iterations")
plt.ylabel("Train loss")

ここでは5エポック分学習を行っている. GPU を使えば,これくらいの計算であれば1分程度で完了するだろう.

出力として, Figure 31 のようなプロットが得られるはずである. イテレーションを重ねるにつれて,損失関数 (Loss function) の値が減少している (=精度が向上している) ことがわかる.

出力には各エポック終了後のテストデータに対する精度も表示されている. 最終的には 99% 程度の極めて高い精度を実現できていることが確認できるだろう (Figure 32).

train_loss
Figure 31. 学習の進行に対する Train loss の変化
mnist_final_score
Figure 32. 学習したCNNのテストデータに対するスコア (5エポック後)

最後に,学習した CNN の推論結果を可視化してみよう. 次のコードを実行することで, Figure 33 のような出力が得られるだろう. この図で,下段右下などは,"1"に近い見た目をしているが,きちんと"9"と推論できている. なかなか賢い CNN を作り出すことができたようだ!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
model.eval()

with torch.no_grad():
    output = model(example_data.to("cuda"))

fig = plt.figure(figsize=(10,4))
for i in range(10):
    plt.subplot(2,5,i+1)
    plt.tight_layout()
    plt.imshow(example_data[i][0], cmap='gray', interpolation='none')
    plt.title("Prediction: {}".format(output.data.max(1, keepdim=True)[1][i].item()))
    plt.xticks([])
    plt.yticks([])
plt.show()
mnist_prediction
Figure 33. 学習した CNN による,MNIST画像の推論結果

おめでとう! これで,めでたくあなたは AWS クラウドの仮想サーバーを使って,最初のディープラーニングの計算を行うことができた! MNIST 文字認識のタスクを行うニューラルネットを,GPUを使って高速に学習させ,現実的な問題を一つ解くことができたのである.

興味のある読者は,今回のハンズオンを雛形に,自分で他のディープラーニングの計算を走らせてみるとよいだろう.

6.9. スタックの削除

これにて,ハンズオン第二回の内容はすべて説明した. クラウドの利用料金を最小化するため,使い終わったEC2インスタンスはすぐさま削除しよう.

ハンズオン第一回と同様に, AWS の CloudFormation コンソールか, AWS CLI により削除を実行する (詳細は Section 4.4.8 参照).

$ cdk destroy

スタックの削除は各自で必ず行うこと! 行わなかった場合,EC2インスタンスの料金が発生し続けることになる! g4dn.xlarge は $0.526 / hour の料金設定なので,一日起動しつづけると約$12の請求が発生することになる!

AWS で, GPU 搭載型のインスタンスは高めの料金設定がされている. したがって,プログラムの開発やデバッグはローカルマシンの GPU で行い,大規模なデータを処理するときや,複数の GPU を並列に使って高速にモデルの学習を行いたい場合などにクラウドを利用する,と使い分けるのがよいと筆者は考えている.

7. Docker を用いた大規模機械学習システムの構築

Section 5Section 6 で, AWS 上に仮想サーバーを立ち上げ,ディープラーニングの計算を走らせる方法を紹介してきた. ここまでは,単一のサーバーを立ち上げ,それにマニュアルでログインをして,コマンドを叩くことで計算を行ってきた. いわば,パーソナルコンピュータの延長のような形でクラウドを使ってきたわけである.

このようなクラウドの使い方も,もちろん便利であるし,応用の可能性を広げるものである. しかし,これだけではクラウドの本当の価値は十分に発揮されていない. Section 2 で述べたように,現代的なクラウドの一番の強みは自由に計算機の規模を拡大できることにある. すなわち,多数のサーバーを同時に起動し,複数のジョブを分散並列的に実行させることで大量のデータを処理してこそ,クラウドの本領が発揮されるのである.

この章では,クラウドを使うことで,どのようにビッグデータの解析に立ち向かうのか,その片鱗をお見せしたい. 特に,前章で扱ったディープラーニングの計算を,どのようにビッグデータに適用していくかという点に焦点を絞って,議論していきたい.

7.1. 機械学習の大規模化

Section 5 で紹介した GPT-3 のような,超巨大なディープラーニングのモデルを学習させたいとしよう. そのような計算を行いたい場合,一つのサーバーでは計算力が到底足りない. したがって,典型的には Figure 34 に示すような計算システムの設計がなされる. すなわち,大量の教師データを,小さなチャンクとして複数のマシンに分散し,並列的にニューラルネットのパラメータを最適化していくという構造である.

big_dnn_training
Figure 34. 複数の計算機を使った大規模なディープラーニングモデルの学習

あるいは,学習済みのモデルを大量にあるデータに適用し,解析を行いたいとしよう. 例えば, SNS のプラットフォームで大量の画像が与えられて,それぞれの写真に何が写っているのかをラベルづけする,などのアプリケーションを想定できる. そのような場合は, Figure 35 のようなアーキテクチャが考えられるだろう. すなわち,大量のデータを複数のマシンで分割し,それぞれのマシンで推論の計算を行うというような構造である.

big_dnn_inference
Figure 35. 複数の計算機を使った大規模なディープラーニングモデルの学習

このような複数の計算機を同時に走らせるようなアプリケーションをクラウド上で実現するには,どのようにすればよいのだろうか?

ひとつ重要なポイントとして, Figure 34Figure 35 で起動している複数のマシンは,基本的に全く同一のOS・計算環境を有している点である. ここで,個人のコンピュータでやるようなインストールの操作を,各マシンで行うこともできるが,それは大変な手間であるし,メンテナンスも面倒だろう. すなわち,大規模な計算システムを構築するには,簡単に計算環境を複製できるような仕組みが必要であるということがわかる.

そのような目的を実現するために使われるのが, Docker と呼ばれるソフトウェアである.

7.2. Docker 概論

docker
Figure 36. Docker のロゴ

Docker とは, コンテナ (Container) と呼ばれる仮想環境下で,ホストOSとは独立した別の計算環境を走らせるためのソフトウェアである. Docker を使うことで, OS を含めた全てのプログラムをコンパクトにパッケージングすることが可能になる (パッケージされたひとつの計算環境のことを イメージ (Image) と呼ぶ). Dockerを使うことで,クラウドのサーバー上に瞬時に計算環境を複製することが可能になり,上で見たような複数の計算機を同時に走らせるためのシステムが実現できる.

Docker は2013年に Solomon Hykes らを中心に開発され,以降爆発的に普及し,クラウドコンピューティングだけでなく,機械学習・科学計算の文脈などで,欠かすことのできないソフトウェアとなった. 概念としては, Docker は仮想マシン (Virtual machine; VM) にとても近い. ここでは, VM との対比をしながら,Docker とはなにかを簡単に説明しよう.

仮想マシン(VM) とは,ホストとなるマシンの上に,仮想化されたOSを走らせる技術である (Figure 37). VM には ハイパーバイザー (Hypervisor) と呼ばれるレイヤーが存在する. Hypervisor はまず,物理的な計算機リソース (CPU, RAM, network など) を分割し,仮想化する. 例えば, 物理的にCPUが4コアあるとして,ハイパーバイザーはそれを (2,2) 個の組に仮想的に分割することができる. VM 上で起動する OS には,ハイパーバイザーによって仮想化されたハードウェアが割り当てられる. VM 上で起動する OS は基本的に完全に独立であり,例えば OS-A は OS-B に割り当てられたCPUやメモリー領域にアクセスすることはできない. VM を作成するための有名なソフトウェアとしては, VMwareVirtualBoxXen などがある. また,これまで触ってきた EC2 も,基本的に VM 技術によって実現されている.

Docker も, VM と同様に,仮想化された OS をホストのOS上に走らせるための技術である. VM に対し, Docker ではハードウェアレベルの仮想化は行われておらず,すべての仮想化はソフトウェアレベルで実現されている (Figure 37). Docker で走る仮想 OS は,多くの部分をホストのOSに依存しており,結果として非常にコンパクトである. その結果, Docker で仮想 OS を起動するために要する時間は, VM に比べて圧倒的に早い. また, image のサイズも完全なOSよりも圧倒的に小さくなるので,ネットワークを通じたやり取りが非常に高速化される点も重要である. 加えて, VM のいくつかの実装では,メタル (仮想化マシンに対して,物理的なハードウェアを使用した場合のこと) と比べ,ハイパーバイザーレイヤでのオーバーヘッドなどにより性能が低下することが知られているが, Docker ではメタルとほぼ同様の性能を引き出すことができるとされている.

その他, VM との相違点などはたくさんあるのだが,ここではこれ以上詳細には立ち入らない. 大事なのは, Docker とはとても軽くてコンパクトな仮想計算環境を作るツールである,という点である. その手軽さゆえに,2013年の登場以降,クラウドシステムでの利用が急速に増加し,現代のクラウドでは欠くことのできない中心的な技術になっている.

docker_vs_vm
Figure 37. Docker と VM の比較 (画像出典: https://www.docker.com/blog/containers-replacing-virtual-machines/)

7.3. Docker チュートリアル

Docker とはなにかを理解するためには,実際に触って動かしてみるのが一番有効な手立てである. ここでは, Docker の簡単なチュートリアルを行う.

Docker のインストールについては, Section 1.4 および 公式のドキュメンテーション を参照してもらいたい. Docker のインストールが完了している前提で,以下は話を進めるものとする.

7.3.1. Image をダウンロード

パッケージ化された Docker の仮想環境 (Image と呼ぶ) は, Docker Hub からダウンロードできる. Docker Hub には,個人や会社・機関が作成した Docker Image が集められており, GitHub などと同じ感覚で,オープンな形で公開されている.

例えば, Ubuntu 18.04 の Image は このリンク で公開されており, pull コマンドを使うことでローカルにダウンロードすることができる.

$ docker pull ubuntu:18.04

Docker image を公開するためのデータベース (registry と呼ぶ) は Docker Hub だけではない. 例えば,GitLab は独自の registry 機能を提供しているし,個人のサーバーで registry を立ち上げることも可能である.

7.3.2. Image を起動

Pull してきた Image を起動するには, run コマンドを使う.

$ docker run -it ubuntu:18.04

ここで, -it とは,インタラクティブな shell のセッションを開始するために必要なオプションである.

上のコマンドを実行すると,仮想化された Ubuntu が起動され,コマンドラインからコマンドが打ち込めるようになる (Figure 38).

docker_shell
Figure 38. Docker を使って ubuntu:18.04 イメージを起動

上で使った ubuntu:18.04 のイメージは は,空の Ubuntu OS だが,既にプログラムがインストール済みのものもある. これは, Section 6 でみた DLAMI と概念として似ている. たとえば, pytorch がインストール済みの Image は こちら で公開されている.

これを起動してみよう.

$ docker run -it pytorch/pytorch

docker run をしたとき,ローカルに該当する Image がない場合は,自動的に Docker Hub からダウンロードがされる

pytorch の container が起動したら, Python のシェルを立ち上げて, pytorch をインポートしてみよう.

$ python3
Python 3.7.7 (default, May  7 2020, 21:25:33)
[GCC 7.3.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import torch
>>> torch.cuda.is_available()
False

このように, Docker を使うことで簡単に特定のOS・プログラムの入った計算環境を再現することが可能になる.

7.3.3. 自分だけの Image を作る

自分の使うソフトウェア・ライブラリがインストールされた,自分だけの Image を作ることも可能である.

例えば, 本講義で提供している docker image には, Python, Node.js, AWS CLI, AWS CDK などのソフトウェアがインストール済みであり,ダウンロードしてくるだけですぐにハンズオンのプログラムが実行できるようになっている.

カスタムの docker image を作るには, Dockerfile と呼ばれるファイルを用意し,その中にどんなプログラムをインストールするかなどを記述していく.

具体例として,本講義で提供している Docker image のレシピを見てみよう (/docker/Dockerfile).

FROM node:12

(1)
RUN cd /opt \
    && curl -q "https://www.python.org/ftp/python/3.7.6/Python-3.7.6.tgz" -o Python-3.7.6.tgz \
    && tar -xzf Python-3.7.6.tgz \
    && cd Python-3.7.6 \
    && ./configure --enable-optimizations \
    && make install

RUN cd /opt \
    && curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" \
    && unzip awscliv2.zip \
    && ./aws/install

(2)
RUN npm install -g aws-cdk@1.30

# Make command line prettier...
RUN echo "alias ls='ls --color=auto'" >> /root/.bashrc
RUN echo "PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@aws-handson\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '" >> /root/.bashrc

RUN mkdir -p /root/.ssh
WORKDIR /root
ENTRYPOINT ["/bin/bash"]

Dockerfile の中身の説明は特に行わないが,例えば上のコードで <1> で示したところは, Python 3.7 のインストールを実行している. また, <2> で示したところは, AWS CDK のインストールを行っていることがわかるだろう. このように,インストールのコマンドを逐一記述していくことで,自分だけの Docker image を作成することができる. 一度 image を作成すれば,それを他人に渡すことで,他者も同一の計算環境を簡単に再構成することができる.

"ぼくの環境ではそのプログラム走ったのにな…​" というのは,プログラム初心者ではよく見かける会話だが, docker を使いこなせばそのような心配とは無縁である. そのような意味で,クラウド以外の場面でも, Docker の有用性・汎用性は極めて高い.

7.4. Elastic Container Service (ECS)

ECS

以上で説明したように, Docker を使うことで仮想計算環境を簡単に複製・起動することが可能になる. 本章の最後の話題として, AWS 上で Docker を使った計算システムを構築する方法を解説しよう.

Elastic Container Service (ECS) とは, Docker を使った計算機クラスターを AWS 上に作成するためのツールである. ECS の概要を示したのが Figure 39 である.

ECS は,タスク (Task) と呼ばれる単位で管理された計算ジョブを受け付ける. システムにタスクが投下されると,ECS はまず最初にタスクで指定された Docker イメージを外部レジストリからダウンロードしてくる. 外部レジストリとしては, DockerHub や AWS 独自の Docker レジストリである ECR (Elastic Container Registry) を指定することができる.

次に, ECS はクラスターのスケーリングを行う. スケーリングとは,クラスター内の仮想インスタンスの計算負荷をモニタリングし,計算負荷が指定された閾値 (例えば80%の稼働率) を超えていた場合,新たな仮想インスタンスをクラスター内に立ち上げる,というような操作のことを言う. また,計算が終了した後,不要になったインスタンスをシャットダウンする操作も, スケーリングの重要な役割である. クラスターの中に配置できる仮想インスタンスは, EC2 に加えて, Fargate と呼ばれる ECS での利用に特化した仮想インスタンスを選択することができる. Fargate については Section 8 で解説する.

最後に, ECS はタスクの配置を行う. クラスター内で,計算負荷が小さい仮想インスタンスを選び出し,そこに Docker イメージを配置することで指定された計算タスクが開始される.

これら一連のタスクの管理・クラスターのスケーリングを, ECS はほとんど自動でやってくれる. ユーザーは,クラスターのスケーリングやタスクの配置に関してのパラメータを指定するだけでよい. パラメータには,目標とする計算負荷 (例えば80%の負荷を維持する,など) などが含まれる.

ecs
Figure 39. ECS の概要

7.5. Fargate

Fargate

Fargate は, EC2 と同様に,仮想サーバー上で計算を走らせるためのサービスであるが,特に ECS での利用に特化されたものである. EC2 に比べると,計算上のいろいろな制約があるのだが, ECS に特化した結果,利用者の側で設定しなければならないパラメータが圧倒的に少なく便利である.

Fargate では, EC2 と同様に CPUの数・RAM のサイズを必要な分だけ指定できる. 執筆時点 (2020/06) では, CPU は 0.25 - 4 コア, RAM は 0.5 - 30 GB の間で選択することができる (参照).

Fargate による仮想インスタンスは, ECS によって動的に管理することができる. すなわち, ECS はタスクに応じて動的に Fargate インスタンスを立ち上げ,タスクの完了を検知して動的にインスタンスをシャットダウンすることができる. EC2 を使っても,同様のことは実現できるのだが,利用者の側で設定しなければならないことが多く,少しハードルが高い. また, インスタンスの起動時間で見ても,EC2 が2-3分程度の時間を要するのに対し,Fargate は典型的には30-60秒程度の時間で済む. したがって, Fargate を用いることでより俊敏にクラスターのスケーリングをすることが可能になる.

8. Hands-on #3: AWSで自動質問回答ボットを走らせる

ハンズオン第三回では,前章で学んだ Docker と ECS を使うことで,大規模な機械学習システムの最もシンプルなものを実装する.

具体的には, Transformer と呼ばれるディープラーニングのモデルを使った自然言語処理を利用することで, 英語で与えられた質問への回答を自動で生成するボットを作成してみる. 特に,何百何千もの質問に同時に対応できるように,単一のサーバーに展開するのではなく,リクエストに応じて複数のサーバーを自動的に起動し,並列でジョブを実行させるシステムを設計する. まさに,初歩的ながら, Siri/Alexa/Google assistant のようなシステムを作り上げるのである!

ハンズオンのソースコードはこちらのリンクに置いてある ⇒ https://gitlab.com/tomomano/intro-aws/-/tree/master/handson/03-qa-bot

このハンズオンでは 1CPU/4GB RAM の Fargate インスタンスを使用する. 計算の実行には 0.025 $/hour のコストが発生することに注意.

8.1. 準備

本ハンズオンの実行には,第一回ハンズオンで説明した準備 (Section 4.1) が整っていることを前提とする.それ以外に必要な準備はない.

8.2. Transformer を用いた question-answering プログラム

このハンズオンで解きたい,自動質問回答の問題をより具体的に定義しよう. 次のような文脈 (context) と質問 (question) が与えられた状況を想定する.

context: Albert Einstein (14 March 1879 – 18 April 1955) was a German-born theoretical physicist who developed the theory of relativity, one of the two pillars of modern physics (alongside quantum mechanics). His work is also known for its influence on the philosophy of science. He is best known to the general public for his mass–energy equivalence formula E = mc2, which has been dubbed \"the world's most famous equation\". He received the 1921 Nobel Prize in Physics \"for his services to theoretical physics, and especially for his discovery of the law of the photoelectric effect\", a pivotal step in the development of quantum theory.

question: In what year did Einstein win the Nobel prize?

今回作成する自動回答システムは,このような問題に対して, context に含まれる文字列から正解となる言葉を見つけ出すものとする. 上の問題では,次のような回答を返すべきである.

answer: 1921

人間にとっては,このような文章を理解することは容易であるが,コンピュータにそれをやらせることはなかなか難しいことは,想像ができるだろう. しかし,近年のディープラーニングを使った自然言語処理の進歩は著しく,上で示したような例題などは,かなり高い正答率で回答できるモデルを作ることができる.

今回は, huggingface/transformers で公開されている学習済みの言語モデルを利用することで,上で定義した問題を解く Q&A ボットを作る. この Q&A ボットは Transformer と呼ばれるモデルを使った自然言語処理に支えられえている (Figure 40). このプログラムを, Docker にパッケージしたものが https://gitlab.com/tomomano/intro-aws/container_registry/handson03 という名前で用意してある. クラウドの設計に入る前に,まずはこのプログラムを単体で動かしてみよう.

なお,今回は学習済みのモデルを用いているので,私達が行うのは,与えられた入力をモデルに投げて予測を行う (推論) のみである. 推論の演算は, CPU だけでも十分高速に行うことができるので,コストの削減と,よりシンプルにシステムを設計をする目的で,このハンズオンでは GPU は利用しない. 一般的に, ニューラルネットは学習のほうが圧倒的に計算コストが大きく,そのような場合に GPU はより威力を発揮する.

transformer
Figure 40. Transformer モデルアーキテクチャ (画像出典: Vaswani+ 2017)

次のコマンドで,今回使う Docker image を ローカルにダウンロード (pull) してこよう.

$ docker pull registry.gitlab.com/tomomano/intro-aws/handson03:latest

pull できたら,早速この Docker に質問を投げかけてみよう.

$ context="Albert Einstein (14 March 1879 – 18 April 1955) was a German-born theoretical physicist who developed the theory of relativity, one of the two pillars of modern physics (alongside quantum mechanics). His work is also known for its influence on the philosophy of science. He is best known to the general public for his mass–energy equivalence formula E = mc2, which has been dubbed \"the world's most famous equation\". He received the 1921 Nobel Prize in Physics \"for his services to theoretical physics, and especially for his discovery of the law of the photoelectric effect\", a pivotal step in the development of quantum theory."
$ question="In what year did Einstein win the Nobel prize ?"
$ docker run registry.gitlab.com/tomomano/intro-aws/handson03:latest "${context}" "${question}" foo --no_save

今回用意した Docker image は,第一引数に context となる文字列を,第二引数に question に相当する文字列を受けつける. 第三引数,第四引数については,クラウドに展開するときの実装上の都合なので,今は気にしなくてよい.

上のコマンドを実行すると,以下のような出力が得られるはずである.

{'score': 0.9881729286683587, 'start': 437, 'end': 441, 'answer': '1921'}

"score" は正解の自信度を表す数字で, [0,1] の範囲で与えられる. "start", "end" は, context 中の何文字目が正解に相当するかを示しており, "answer" が正解と予測された文字列である.

1921 年という,正しい答えが返ってきていることに注目してほしい.

もう少し難しい質問を投げかけてみよう.

$ question="Why did Einstein win the Nobel prize ?"
$ docker run registry.gitlab.com/tomomano/intro-aws/handson03:latest "${context}" "${question}" foo --no_save

出力:

{'score': 0.5235594527494207, 'start': 470, 'end': 506, 'answer': 'his services to theoretical physics,'}

今度は, score が 0.52 と,少し自信がないようだが,それでも正しい答えにたどりつけていることがわかる.

このように, ディープラーニングに支えられた言語モデルを用いることで,なかなかに賢い Q-A ボットを実現できていることがわかる. 以降では,このプログラムをクラウドに展開することで,大量の質問に自動で対応できるようなシステムを設計していく.

今回使用する Question & Answering システムには, DistilBERT という Transformer を基にした言語モデルが用いられている. 興味のある読者は, 原著論文 を参照してもらいたい. また, huggingface/transformers の DistilBert についてのドキュメンテーションは こちら

huggingface/transformers には,様々な最新の言語モデルが実装されている. 解けるタスクも, question-answering だけでなく,翻訳や要約など複数用意されている. 興味のある読者は, ドキュメンテーション を参照.

今回提供する Docker のソースコードは https://gitlab.com/tomomano/intro-aws/handson/03-qa-bot/docker においてある.

8.3. アプリケーションの説明

このハンズオンで作成するアプリケーションの概要を Figure 41 に示す.

hands-on 03 architecture
Figure 41. ハンズオン#2で作製するアプリケーションのアーキテクチャ

簡単にまとめると,以下のような設計である.

  • クライアントは,質問を AWS 上のアプリケーションに送信する.

  • 質問のタスクは ECS によって処理される.

  • ECS は, GitLab container registry から, Docker image をダウンロードする.

  • 次に,ECS はクラスター内に新たな仮想インスタンスを立ち上げ,ダウンロードされた Docker image をこの新規インスタンスに配置する.

    • このとき,ひとつの質問に対し一つの仮想インスタンスを立ち上げることで,複数の質問を並列的に処理できるようにする.

  • ジョブが実行される.

  • ジョブの実行結果 (質問への回答) は, データベース (DynamoDB) に書き込まれる.

  • 最後に,クライアントは DynamoDB から質問への回答を読み取る.

それでは,プログラムのソースコードを見てみよう (/handson/03-qa-bot/app.py).

 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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class EcsClusterQaBot(core.Stack):

    def __init__(self, scope: core.App, name: str, **kwargs) -> None:
        super().__init__(scope, name, **kwargs)

        (1)
        # dynamoDB table to store questions and answers
        table = dynamodb.Table(
            self, "EcsClusterQaBot-Table",
            partition_key=dynamodb.Attribute(
                name="item_id", type=dynamodb.AttributeType.STRING
            ),
            billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST,
            removal_policy=core.RemovalPolicy.DESTROY
        )

        (2)
        vpc = ec2.Vpc(
            self, "EcsClusterQaBot-Vpc",
            max_azs=1,
        )

        (3)
        cluster = ecs.Cluster(
            self, "EcsClusterQaBot-Cluster",
            vpc=vpc,
        )

        (4)
        taskdef = ecs.FargateTaskDefinition(
            self, "EcsClusterQaBot-TaskDef",
            cpu=1024, # 1 CPU
            memory_limit_mib=4096, # 4GB RAM
        )

        # grant permissions
        table.grant_read_write_data(taskdef.task_role)
        taskdef.add_to_task_role_policy(
            iam.PolicyStatement(
                effect=iam.Effect.ALLOW,
                resources=["*"],
                actions=["ssm:GetParameter"]
            )
        )

        (5)
        container = taskdef.add_container(
            "EcsClusterQaBot-Container",
            image=ecs.ContainerImage.from_registry(
                "registry.gitlab.com/tomomano/intro-aws/handson03:latest"
            ),
        )
1 ここでは,回答の結果を書き込むためのデータベースを用意している. DynamoDB については, Serverless architecture の章で扱うので,今は気にしなくてよい.
2 ここでは,ハンズオン #1, #2 で行ったのと同様に, VPC を定義している.
3 ここで, ECS のクラスター (cluster) を定義している. クラスターとは,仮想サーバーのプールのことであり,クラスターの中に複数の仮想インスタンスを配置する.
4 ここで,実行するタスクを定義している (task definition).
5 ここで, タスクの実行で使用する Docker image を定義している.

8.3.1. ECS と Fargate

ECS と Fargate の部分について,コードをくわしく見てみてみよう.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
cluster = ecs.Cluster(
    self, "EcsClusterQaBot-Cluster",
    vpc=vpc,
)

taskdef = ecs.FargateTaskDefinition(
    self, "EcsClusterQaBot-TaskDef",
    cpu=1024, # 1 CPU
    memory_limit_mib=4096, # 4GB RAM
)

container = taskdef.add_container(
    "EcsClusterQaBot-Container",
    image=ecs.ContainerImage.from_registry(
        "registry.gitlab.com/tomomano/intro-aws/handson03:latest"
    ),
)

cluster = の箇所で,空の ECS クラスターを定義している.

次に, taskdef=ecs.FargateTaskDefinition の箇所で, Fargate インスタンスを使ったタスクを定義しており,特にここでは 1 CPU, 4GB RAM というマシンスペックを指定している. また,このようにして定義されたタスクは,デフォルトで1タスクにつき1インスタンスが使用される.

最後に, container = の箇所で,タスクの実行でで使用する Docker image を定義している. ここでは, GitLab container registry に置いてある image をダウンロードしてくるよう指定している.

このようにわずか数行のコードであるが,これだけで上で説明したような,タスクのスケジューリングなどが自動で実行される.

8.4. スタックのデプロイ

スタックの中身が理解できたところで,早速スタックをデプロイしてみよう.

デプロイの手順は,これまでのハンズオンとほとんど共通である. SSH によるログインの必要がないので,むしろ単純なくらいである. ここでは,コマンドのみ列挙する (# で始まる行はコメントである). それぞれの意味を忘れてしまった場合は,ハンズオン1, 2に戻って復習していただきたい.

# プロジェクトのディレクトリに移動
$ cd intro-aws/handson/03-qa-bot

# venv を作成し,依存ライブラリのインストールを行う
$ python3 -m venv .env
$ source .env/bin/activate
$ pip install -r requirements.txt

# AWS の認証情報をセットする
# 自分自身の認証情報に置き換えること!
export AWS_ACCESS_KEY_ID=XXXXXX
export AWS_SECRET_ACCESS_KEY=YYYYYY
export AWS_DEFAULT_REGION=ap-northeast-1

# デプロイを実行
$ cdk deploy

デプロイのコマンドが無事に実行されれば, Figure 42 のような出力が得られるはずである.

cdk output
Figure 42. CDKデプロイ実行後の出力

AWS コンソールにログインして,デプロイされたスタックを確認してみよう. コンソールから,ECS のページに行くと Figure 43 のような画面が表示されるはずである.

Cluster というのが,先ほど説明したとおり,複数の仮想インスタンスを束ねる一つの単位である. この時点ではひとつもタスクが走っていないので,タスクの数字はすべて0になっている. この画面にはまたすぐ戻ってくるので,開いたままにしておこう.

ecs_console
Figure 43. ECS コンソール画面

8.5. タスクの実行

それでは,早速,質問を実行してみよう.

ECS にタスクを投入するのはやや複雑なので,タスクの投入を簡単にするプログラム (run_task.py) を用意した (/handson/03-qa-bot/run_task.py).

次のようなコマンドで,ECSクラスターに新しい質問を投入することができる.

$ python run_task.py ask "A giant peach was flowing in the river. She picked it up and brought it home. Later, a healthy baby was born from the peach. She named the baby Momotaro." "What is the name of the baby?"

run_task.py を実行するには, 環境変数によって AWS の認証情報が設定されていることが前提である.

"ask" の引数に続き,文脈 (context) と質問を引数として渡している.

上のコマンドを実行すると, "Waiting for the task to finish…​" と出力が表示され,回答を得るまでしばらく待たされることになる. この間, AWS では, ECS がタスクを受理し,新しい Fargate のインスタンスを起動し, Docker image をそのインスタンスに配置する,という一連の処理がなされている. AWS コンソールから,この一連の様子をモニタリングしてみよう.

先ほどの ECS コンソール画面にもどり,クラスターの名前をクリックすることで,クラスターの詳細画面を開く. 次に, "Tasks" という名前のタブがあるので,それを開く (Figure 44). すると,実行中のタスクの一覧が表示されるだろう.

ecs_task_monitoring
Figure 44. ECS のタスクの実行状況をモニタリング

Figure 44 で見て取れるように, "Desired status = RUNNING", "Last status = PENDING" となっていることから,この時点では,タスクを実行するための準備している段階である,ということがわかる. Fargate のインスタンスを起動し, Docker image を配置するまでおよそ1-2分の時間がかかる.

しばらく待つうちに, Status が "RUNNING" に遷移し,計算が始まる. 計算が終わると, Status は "STOPPED" に遷移し, ECS によって Fargate インスタンスは自動的にシャットダウンされる.

Figure 44 の画面から, "Task" の列にあるタスクIDクリックすることで,タスクの詳細画面を開いてみよう (Figure 45). "Launch type = FARGATE", "Last status = STOPPED" など,タスクの情報が表示されている. また, "Logs" のタブを開くことで, container の吐き出した実行ログを閲覧することができる.

ecs_task_detail
Figure 45. 質問タスクの実行結果

さて, run_task.py を実行したコマンドラインに戻ってきてみると, Figure 46 のような出力が得られているはずである. "Momotaro" という正しい回答が返ってきている!

ask_question_output
Figure 46. 質問タスクの実行結果

8.6. タスクの同時実行

さて,先ほどはたった一つの質問を投入したわけだが,今回設計したアプリケーションは, ECS と Fargate を使うことで同時にたくさんの質問を処理することができる. 実際に,たくさんの質問を一度に投入してみよう.

run_task.pyask_many というオプションを付けることで,複数の質問を一度に送信できる. 質問の内容は /handson/03-qa-bot/problems.json に定義されている.

次のようなコマンドを実行しよう.

$ python run_task.py ask_many

このコマンドを実行した後で,先ほどの ECS コンソールに行き,タスクの一覧を見てみよう (Figure 47). 複数の Fargate インスタンスが起動され,タスクが並列に実行されているのがわかる.

ecs_many_tasks
Figure 47. 複数の質問タスクを同時に投入する

すべてのタスクのステータスが "STOPPED" になったことを確認した上で,質問への回答を取得しよう. それには,次のコマンドを実行すれば良い.

$ python run_task.py list_answers

結果として, Figure 48 のような出力が得られるだろう. それなりに複雑な文章問題に対し,高い正答率で回答できていることがわかるだろう.

ask_many_output
Figure 48. $ python run_task.py list_answers の実行結果

おめでとう! ここまでついてこれた読者は,とても初歩的ながらも,ディープラーニングによる言語モデルを使って自動で質問への回答を生成するシステムを創り上げることができた! それも,数百の質問にも同時に対応できるような,とても高いスケーラビリティーを持ったシステムである!

run_task.py で質問を投入し続けると,回答を記録しているデータベースにどんどんエントリーが溜まっていく. これらのエントリーをすべて消去するには,次のコマンドを使う.

$ python run_task.py clear

8.7. スタックの削除

これにて,第三回ハンズオンは終了である.最後にスタックを削除しよう.

スタックを削除するには,次のコマンドを実行すればよい.

$ cdk destroy

8.8. 講義第二回目のまとめ

ここまでが,第二回目の講義の内容である.第一回に引き続き盛りだくさんの内容であったが,ついてこれたであろうか?

第二回では,ディープラーニングの計算をクラウドで実行するため, GPU 搭載型の EC2 インスタンスの起動について解説した. その際, CUDA や PyTorch などのディープラーニング使うソフトウェアのインストールの手間を省くため, DLAMI を利用した. さらに,ハンズオン第二回では,クラウドで起動した仮想サーバーを使って, MNIST 文字認識タスクを解くニューラルネットを学習させた.

また,より大規模な機械学習アプリケーションを作るための手段として, Docker と ECS による動的に計算リソースが管理されるクラスターの作り方の初歩を説明した. その応用として,英語で与えられた文章問題への回答を自動で生成するボットをクラウドに展開した.

もちろん,この講義で紹介したプログラムはごく初歩的なものなので,現実的な問題を解くためにはプログラムのいろいろな側面を精緻化していく必要がある. しかしながら,このような技術を応用することでどのようにして現実世界の問題を解くのか,なんとなくイメージが伝わっただろうか?

第三回では,さらにレベルアップし, Serverless architecture という最新のクラウドの設計方法について解説する. その応用として,簡単な SNS サービスをゼロから実装する予定である. お楽しみに!

9. Web サービスの作り方

ここからが,第三回目の講義の内容になる.

これまでの講義では,仮想サーバーをクラウド上に起動し,そこで計算を走らせる方法について解説をしてきた. 最初に,EC2上に個人で使用するためのサーバーを立ち上げ,機械学習の計算を実践した. 第二回の最後では,大規模な機械学習システムのもっとも初歩的なものとして, ECS と Docker を使ったクラスターを作成する方法を解説した. ここまで紹介したクラウドの計算の利用は,個人的な用途に限られていたが,クラウドの重要な側面のひとつとして,広く一般に使ってもらえるような計算サービス・データベースを提供する,というものが挙げられるだろう.

今回の講義は,前回までとは少し方向性を変え,どのようにしてクラウド上にアプリケーションを展開し,広く一般の人に使ってもらうか,という点を講義したいと思う. 講義を通じて,どのように世の中のウェブサービスが出来上がっているのかを知り,さらにどうやって自分でそのようなアプリケーションを作るのか,という点を学んでもらう. その過程で, Serverless アーキテクチャという最新のクラウド設計手法を解説する.

9.1. ウェブサービスの仕組み — Twitter を例に

あなたがパソコンやスマートフォンから Twitter, Facebook, YouTube などのウェブサービスにアクセスしたとき, 実際にどのようなことが行われ,ページがロードされているのだろうか?

ここは知っている人も多いと思うので簡潔な説明にとどめるが, Twitter を具体例として,背後にあるサーバーとクライアントの間の通信を概説しよう. 概念図としては Figure 49 のような通信がクライアントとサーバーの間で行われていることになる.

web_server
Figure 49. クライアントと Web サーバーの通信の概念図

前提として,クライアントとサーバーの通信は HTTP (Hypertext Transfer Protocol) を使って行われる. また,最近では,暗号化された HTTP である HTTPS を用いることがスタンダードになってきている. 第一のステップとして,クライアントは HTTP(S) 通信によってサーバーから静的なコンテンツを取得する. 静的なコンテンツとは, HTML (Hyptertext Markup Language) で記述されたウェブページの文書本体, CSS (Cascading Style Sheets) で記述されたページのデザインやレイアウトファイル,そして JavaScript (JS) で記述されたページの動的な挙動を定義したプログラム,が含まれる. Twitter を含む現代的なウェブアプリケーションの設計では,この静的なファイル群はページの”枠”を定義するだけで,中身となるコンテンツ (e.g. ツイートの一覧) は別途 API (Application Programming Interface) によって取得されなければならない. そこで,クライアントは先ほど取得された JavaScript で定義されたプログラムに従って,サーバーに API を送信し,ツイートや画像データを取得する. この際,テキストデータのやり取りには JSON (JavaScript Object Notation) というフォーマットが用いられることが多い. 画像や動画などのコンテンツも同様にAPIにより取得される. このようにして取得されたテキストや画像が,HTMLの文書に埋め込まれることで,最終的にユーザーに提示されるページが完成するのである. また,新しいツイートを投稿するときにも,クライアントから API を通じてサーバーのデータベースにデータが書き込まれる.

9.2. REST API

API (Application Programming Interface) とはこれまで何度も出てきた言葉であるが,ここではよりフォーマルな定義付けを行う. API とはあるソフトウェア・アプリケーションが,外部のソフトウェアに対しコマンドやデータをやりするための"媒介"の一般的総称である. とくに,ウェブサービスの文脈では,サーバーが外界に対して提示しているコマンドの一覧のことを意味する. クライアントは,提示されている API から適切なコマンドを使うことによって,所望のデータを取得したり,あるいはサーバーにデータを送信したりする.

特に, REST (Representational State Transfer) とよばれる設計思想に基づいた API が現在では最も一般的に使われている. REST に従った API のことを REST API あるいは RESTful API と呼んだりする.

REST API は, Figure 50 に示したような MethodURI (Universal Resource Identifier) の組からなる.

rest_api
Figure 50. REST API

Method (メソッド) とは,"どのような操作を行いたいか"を抽象的に表す ("動詞"と捉えてもよい). REST API では典型的には Table 4 に示したメソッドが用いられる.

一方, URI は,操作が行われる対象 (リソースとも呼ばれる) を表す. メソッドが動詞であることに対して, URI は"目的語"であると捉えても良い. Figure 50 の例で言えば, /status/home_timeline というリソース (ホームタイムラインのツイートの一覧) を取得せよ,という意味になる.

Table 4. REST API Methods
メソッド 動作

GET

要素を取得する

POST

新しい要素を作成する

PUT

既存の要素を新しい要素と置き換える

PATCH

既存の要素の一部を更新する

DELETE

要素を削除する

REST API のメソッドには, Table 4 で挙げたもの以外に, HTTP プロトコルで定義されている他のメソッド (OPTIONS, TRACE など) を用いることもできるが,あまり一般的ではない.

また,これらのメソッドだけでは動詞として表現し切れないこともあるが,URIのパスなどでより意味を明確にすることもある. メソッドの使い方も,要素を削除する際は必ず DELETE を使わなければならない,という決まりもなく,例えば, Twitter API でツイートを消す API は POST statuses/destroy/:id で定義されている.

最終的には,各ウェブサービスが公開している API ドキュメンテーションを読んで,それぞれの API がどんな操作をするのかをしっかりと調べる必要がある.

9.2.1. Twitter API

もう少し具体的にウェブサービスのAPIを体験する目的で,ここでは Twitter のAPIを見てみよう.

Twitter が提供している API の一覧は このページ で見ることができる. いくつかの代表的な API を Table 5 にまとめた.

Table 5. Twitter API

GET /statuses/home_time_line

ホームのタイムラインのツイートの一覧を取得する.

GET statuses/show/:id

:id で指定されたツイートの詳細情報を取得する.

POST statuses/update

新しいツイートを投稿する.

POST statuses/retweet/:id

:id で指定されたツイートをリツイートする.

POST favorites/create/:id

:id で指定されたツイートを"いいね"する.

POST statuses/destroy/:id

:id で指定されたツイートを削除する.

Twitter のアプリまたはウェブサイトを開くと,背後では上記のような API が実行され,結果としてGUIのページがレンダリングされている. また, Twitter 上でボット (bot) を作るときは,開発者がこれらのAPIを自動で呼ぶようなプログラムを記述することで出来上がっている.

このように, API はあらゆるウェブサービスを作る上で一番基礎となる要素である. 次からの章では,どのようにしてクラウド上に API を構築していくかを解説しよう.

10. Serverless architecture

Serverless Architecture あるいは Serverless Computing とは, 従来とは全くアプローチの異なるクラウドシステムの設計方法である. 歴史的には, AWS が2014年に発表した Lamba がサーバーレスアーキテクチャの最初の先駆けとされている. その後, Google や Microsoft などのクラウドプラットフォームも同様の機能の提供を開始している. サーバーレスアーキテクチャの利点は,スケーラブルなクラウドシステムを安価かつ簡易に作成できる点であり,近年いたるところで導入が進んでいる.

Serverless とは,文字通りの意味としてはサーバーなしで計算をするということになるが,それは一体どういう意味だろうか? サーバーレスについて説明するためには,まずは従来的な, "serverful" と呼ばれるようなシステムについて解説しなければならない.

10.1. Serverful クラウド (従来型)

従来的なクラウドシステムのスケッチを Figure 51 に示す. クライアントから送信されたリクエストは,まず最初にAPIサーバーに送られる. API サーバーでは,リクエストの内容に応じてタスクが実行される. タスクには,APIサーバーだけで完結できるものもあるが,多くの場合,データベースの読み書きが必要である. データベースには,データベース専用の独立したサーバーマシンが用いられることが一般的である. また,画像や動画などもデータは,また別のストレージサーバーに保存されることが一般的である. これらの APIサーバー,データベースサーバー,ストレージサーバーはそれぞれ独立したサーバーマシンであり, AWS では EC2 を使った仮想インスタンスを想定してもらったら良い.

多くのウェブサービスでは,多数のクライアントからのリクエストを処理するため,複数のサーバーマシンがクラウド内で起動し,負荷を分散するような設計がなされている. クライアントから来たリクエストを計算容量に余裕のあるサーバーに振り分けるような操作を Load balancing とよび,そのような操作を担当するマシンのことを Load balancer という.

Load balancing の目的でたくさんのインスタンスを起動するのはよいのだが,それぞれがなんの計算もせず,ただ新しいタスクが来るのを待っているようではコストと電力の無駄遣いである. したがって,全てのサーバーが常に目標とする計算負荷を維持するよう,計算の負荷に応じてクラスター内の仮想サーバーの数を動的に増減させるような仕組みが必要である. そのような仕組みをクラスターのスケーリングとよび,負荷の増大に応答して新しい仮想インスタンスをクラスターに追加する操作を scale-out,負荷の減少に応答してインスタンスをシャットダウンする操作を scale-in と呼ぶ. クラスターのスケーリングは,各インスタンスを監視・統括するようなひとつ階層が上のサーバーを配置することで自動的に実行されるような設計がなされる. クラスターのスケーリングは, API サーバーではもちろんのこと,データベースサーバー・ストレージサーバーでも必要になることが多い. クラウドシステム内すべてのインスタンスで,負荷が均一になるような調整が必要なのである.

serverful
Figure 51. Serverful なクラウドシステム

10.2. Serverless クラウドへ

上述したように,従来のクラウドシステムの設計で非常に重要なのが,クラスターのスケーリングである. コストパフォーマンスを最大化するには,各サーバーの稼働率を100%に近づけるようなスケーリングのパラメータの調整が必要である. しかしながら,クラスターのスケーリングの最適化はかなり手間のかかる作業である.

さらに問題を複雑にするのは,APIサーバーで処理されるべきタスクが,非一様である点である. 非一様であるとは,例えばタスクAは3000ミリ秒の実行時間と 512MB のメモリーを消費し,別のタスクBは1000ミリ秒の実行時間と 128MB のメモリーを消費する,というような状況を差している. 一つのサーバーマシンが計算負荷が異なる複数のタスクを処理する場合,クラスターのスケーリングはより複雑になる. この状況をシンプルにするために,1サーバーで実行するタスクは1種類に限る,という設計も可能であるが,そうするとで生まれる弊害も多い (ほとんど使われないタスクに対してもサーバー一台をまるまる割り当てなければならない = ほとんどアイドリング状態になってしまう,など).

もっとシンプルで見通しの良いクラウドシステムのスケーリングの仕組みはないだろうか?

従来の serverful なシステムでの最大の問題点は,サーバーをまるまる占有してしまうという点にある. すなわち, EC2 インスタンスを起動したとき,そのインスタンスは起動したユーザーだけが使えるものであり,計算のリソース (CPUやRAM) が独占的に割り当てられた状態になる. 固定した計算資源の割り当てがされてしまっているので,インスタンスの計算負荷が0%であろうが100%であろうが,均一の使用料金が起動時間に比例して発生する.

サーバーレスアーキテクチャは,このような 独占的に割り当てられた計算リソースというものを完全に廃止する. サーバーレスアーキテクチャでは,計算のリソースは,クラウドプロバイダーが全て管理する. クライアントは,仮想インスタンスを一台まるごと借りるのではなく,実行したいプログラムをクラウドに提出する. クラウドプロバイダーは,自身の持つ巨大な計算リソースから空きを探し,提出されたプログラムを実行し,実行結果をクライアントに返す. 以上を図示すると, Figure 52 のようになる.

serverless
Figure 52. 従来のクラウドと Serverless クラウドの比較

サーバーレスクラウドを利用することで,クラウドのコストは実際に使用した計算の総量 (CPU稼働時間) で決定されることになる. これは,計算の実行総量に関わらずインスタンスの起動時間で料金が決定されていた従来のシステムと比べて大きな違いである. 一方で,クライアントが同時に大量のタスクを送信した場合でも,クラウドプロバイダー側はその需要に応えることのできるような計算リソースを瞬時に割り当てることができるので,非常に高いスケーラビリティを実現することができる.

従来型の(仮想インスタンスをたくさん起動するような)クラウドシステムは,賃貸と似ているかもしれない. 部屋を借りるというのは,その部屋でどれだけの時間を過ごそうが,月々の家賃は一定である. 同様に,仮想サーバーも,それがどれほどの計算を行っているかに関わらず,一定の料金が時間ごとに発生する.

一方で,サーバーレスクラウドは,電気・水道・ガス料金 と似ている. こちらは, (ある程度の基本料金はあるかもしれないが) 実際に使用した分で料金が決定されている. サーバーレスクラウドも,実際に計算を行った総時間で料金が決まる仕組みになっている.

10.3. Lambda

Lambda

AWS でサーバーレスコンピューティングの中心を担うのが, Lambda である.

Lambda の使い方を Figure 53 に図示している. Lambda の仕組みはシンプルで,まずユーザーは実行したいプログラムを予め登録しておく. プログラムは, Python, Node.js, ruby などの主要な言語がサポートされている. そして,プログラムを実行したいときに,そのプログラムを実行 (invoke する)コマンドを Lambda に送信する. Lambda では, invoke のリクエストを受け取ると直ちに (数ミリセカンドから数百ミリセカンドのレイテンシーで) プログラムの実行を開始する. そして,実行結果をクライアントやその他の計算機に返す.

lambda_workflow
Figure 53. AWS Lambda

このように,Lambda は仮想インスタンスを専有することはない. invoke のリクエストが来たときにのみ,動的に起動し,実行の終了とともに速やかにシャットダウンされる. また,同時に複数のリクエストが来た場合でも, AWS はそれらを実行するための計算リソースを割り当て,並列的に処理を行ってくれる. 原理上は,数千から数万のリクエストが同時に来たとしても, Lambda はそれらを同時に実行することができる. このような,占有された仮想サーバーの存在なしに,動的に関数を実行するサービスを FaaS (Function as a Service) と呼ぶ.

Lambda では 128MB から 3008MB のメモリーを使用することができる (2020/06時点). 実行時間は100ミリ秒の単位で記録され,実行時間に比例して料金が決定される. Table 6 は Lambda の利用料金の利用料金表である.

Table 6. Lambda の料金表
Memory (MB) Price per 100ms

128

$0.0000002083

512

$0.0000008333

1024

$0.0000016667

3008

$0.0000048958

例えば, 128MB のメモリーを使用する関数を,それぞれ200ミリ秒,合計で100万回実行した場合, 0.0000002083 * 2 * 10^6 = $0.4 の料金となる. ウェブサーバーのデータベースの更新など簡単な計算であれば,200ミリ秒程度で実行できる関数も多いことから,100万回データベースの更新を行ったとしても,たった $0.4 しかコストが発生しないことになる.

10.4. サーバーレスストレージ: S3

S3

サーバーレスの概念は,ストレージにも拡張されている.

従来的なストレージ (ファイルシステム) では,必ずホストとなるマシンと OS が存在しなければならない. 従って,それほどパワーは必要ないまでも,ある程度の CPU リソースを割かなければならない. また,従来的なファイルシステムでは,データ領域のサイズは最初に作成するときに決めなければならず,後から容量を増加させることはしばしば困難である (ZFS などのファイルシステムを使えばある程度は自由にファイルシステムのサイズを増減できるが). よって,従来的なクラウドでは,ストレージを借りるときには予めディスクのサイズを指定せねばならず,ディスクの容量が空であろうと満杯であろうと,同じ利用料金が発生することになる.

Simple Storage Service (S3) は,サーバーレスなストレージシステムを提供する. S3 では,予めデータ保存領域の上限は定められていない. データを入れれば入れた分だけ,保存領域は拡大していく (仕様上はペタバイトスケールのデータを保存することが可能である). ストレージにかかる料金も,保存してあるデータの総容量で決定される.

その他,データの冗長化やバックアップなど,通常ならば CPU が介在しなければならない操作も, API を通じて行うことができる. これらの観点から, S3 も サーバーレスクラウドの一部として取り扱われることが一般的である.

s3_vs_filesystem
Figure 54. S3 と従来的なファイルシステムの比較

S3 の料金は,保存してあるデータの総容量と,外部へのデータ転送の総量で決定される (参考). 執筆時点では,データの保存には $0.025 per GB per month のコストが発生する. 従って,1000GB のデータを S3 に一ヶ月保存した場合, $25 の料金が発生することになる. また, S3 はデータを外に取り出す際の通信にもコストが発生する. 執筆時点では,S3 からインターネットを通じて外部にデータを転送すると $0.114 per GB のコストが発生する. データを S3 に入れる (data-in) 通信は無料で行える. また, AWS の 同じ Region 内のサービス (Lambda など) にデータを転送するのは無料である. AWS の Region をまたいだデータの転送には, $0.09 per GB のコストが発生する.

10.5. サーバーレスデータベース: DynamoDB

S3

サーバーレスの概念は,データベースにも適用することができる.

ここでいうデータベースとは, Web サービスなどにおけるユーザー情報を記録しておくための保存領域のことを指している. 従来的に有名なデータベースとしては MySQL, PostgreSQL, MongoDB などが挙げられる. データベースと普通のストレージの違いは,データの検索機能にある. 普通のストレージではデータは単純にディスクに書き込まれるだけだが, データベースでは検索がより効率的になるようなデータの配置がされたり, 頻繁にアクセスされるデータはメモリーにキャッシュされるなどの機能が備わっている. これにより,巨大なデータの中から,興味のある要素を高速に取得することができる.

このような検索機能を実現するには,当然 CPU の存在が必須である. 従って,従来的なデータベースを構築する際は,ストレージ領域に加えて,たくさんのCPUを搭載したマシンが用いられることが多い. また,格納するデータが巨大な場合は複数マシンにまたがった分散型のシステムが設計される. 分散型システムの場合は, Section 10.1 で議論したようにデータベースへのアクセス負荷に応じて適切なスケーリングがなされる必要がある.

DynamoDB は,サーバーレスなデータベースである.

DynamoDB は分散型のデータベースであるが,データベースのスケーリングは AWS によって行われる. ユーザーとしては,特になにも考えずに,送りたいだけのリクエストをデータベースに送信すればよい. データベースへの負荷が増減したときのスケーリングは, DynamoDB が自動で行ってくれる.

10.6. その他のサーバーレスクラウドの構成要素

その他,サーバーレスクラウドを構成するための構成要素を以下にあげる. API Gateway については,ハンズオン#5 で触れる.

  • API Gateway: API を構築する際のルーティングを担う.

  • Fargate: ハンズオン第三回で触れた Fargate も,サーバーレスクラウドの要素の一部である. Lambda では実行できないような,メモリーや複数CPUを要するような計算などを行うために用いる.

  • Simple Notification Service (SNS): サーバーレスのサービス間 (Lambda と DynamoDB など) でイベントをやり取りするためのサービス.

  • Step Functions: サーバーレスのサービス間のオーケストレーションを担う.

サーバーレスアーキテクチャは万能か?

この問への答えは,筆者は NO であると考える.

ここまで,サーバーレスの利点を強調して説明をしてきたが,まだまだ新しい技術なだけに,欠点,あるいはサーバーフルなシステムに劣る点は,数多くある.

ひとつ大きな欠点をあげるとすれば,サーバーレスのシステムは各クラウドプラットフォームに固有なものなので,特定のプラットフォームでしか運用できないシステムになってしまう点であろう. AWS で作成したサーバーレスのシステムを, Google のクラウドに移植するには,かなり大掛かりなプログラムの書き換えが必要になる. 一方, serverful なシステムであれば,プラットフォーム間のマイグレーションは比較的簡単に行うことができる. クラウドプロバイダーとしては,自社のシステムへの依存度を強めることで,顧客を離さないようにするという狙いがあるのだろう…​

その他,サーバーレスコンピューティングの欠点や今後の課題などは,次の論文で詳しく議論されている. 興味のある読者は読んでみると良い.

11. Hands-on #4: サーバーレス入門

第四回目のハンズオンでは,サーバーレスクラウドを構成する主要な構成要素について,実際に動かしながら学んでもらう.

今回のハンズオンで触れるのは以下の3つである.

  • Lambda: サーバーレスの計算エンジン

  • DynamoDB: サーバーレス・データベース

ハンズオンのソースコードはこちらのリンクに置いてある ⇒ https://gitlab.com/tomomano/intro-aws/-/tree/master/handson/04-serverless

11.1. Lambda ハンズオン

ここでは,ショートハンズオンとして,実際に AWS 上に Lambda を使った関数を定義し,計算を実行してみよう. ここでは, AWS CDK を利用してとてもシンプルな Lambda の関数を作成する.

ハンズオンのソースコードはこちらに置いてある ⇒ https://gitlab.com/tomomano/intro-aws/-/tree/master/handson/04-serverless/lambda

このハンズオンは,基本的に AWS Lambda の無料枠 の範囲内で実行することができる.

app.py にデプロイするプログラムが書かれている. 中身を見てみよう.

 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
(1)
FUNC = """
import time
from random import choice, randint
def handler(event, context):
    time.sleep(randint(2,5))
    pokemon = ["Charmander", "Bulbasaur", "Squirtle"]
    message = "Congratulations! You are given " + choice(pokemon)
    print(message)
    return message
"""

class SimpleLambda(core.Stack):

    def __init__(self, scope: core.App, name: str, **kwargs) -> None:
        super().__init__(scope, name, **kwargs)

        (2)
        handler = _lambda.Function(
            self, 'LambdaHandler',
            runtime=_lambda.Runtime.PYTHON_3_7,
            code=_lambda.Code.from_inline(FUNC),
            handler="index.handler",
            memory_size=128,
            timeout=core.Duration.seconds(10),
            dead_letter_queue_enabled=True,
        )
1 ここで, Lambda で実行されるべき関数を定義している. これは非常に単純な関数で,2-5秒のランダムな時間スリープした後,["Charmander", "Bulbasaur", "Squirtle"] のいずれかの文字列をランダムに返す (これらは初代ポケットモンスターのゲームでオーキド博士にもらうヒトカゲ・フシギダネ・ゼニガメのことだ)・
2 次に, Lambda の関数の諸々のパラメータを設定している. それぞれのパラメータの意味は,文字通りの意味なので明瞭であるが,以下に解説する.
  • runtime=_lambda.Runtime.PYTHON_3_7: ここでは, Python3.7 を使って上記で定義された関数を実行せよ,と指定している. Python3.7 の他に, Node.js, Java, Ruby, Go などの言語を指定することが可能である.

  • code=_lambda.Code.from_inline(FUNC): 実行されるべき関数が書かれたコードを指定する. ここでは, FUNC=…​ で定義した文字列を渡しているが,文字列以外にもファイルのパスを渡すことも可能である.

  • handler="index.handler": これは,コードの中にいくつかのサブ関数が含まれているときに,メインとサブを区別するためのパラメータである. handler という名前の関数をメイン関数として実行せよ,という意味である.

  • memory_size=128: メモリーは 128MB を最大で使用することを指定している. メモリーオーバーした場合は

  • timeout=core.Duration.seconds(10) タイムアウト時間を10秒に設定している. 10秒以内に関数の実行が終了しなかった場合,エラーが返される.

  • dead_letter_queue_enabled=True: アドバンストな設定なので説明は省略する.

上記のプログラムを実行することで, Lambda 関数がクラウド上に作成される. 早速デプロイしてみよう.

11.1.1. デプロイ

デプロイの手順は,これまでのハンズオンとほとんど共通である. ここでは,コマンドのみ列挙する (# で始まる行はコメントである). それぞれの意味を忘れてしまった場合は,ハンズオン1, 2に戻って復習していただきたい.

# プロジェクトのディレクトリに移動
$ cd intro-aws/handson/04-serverless/lambda

# venv を作成し,依存ライブラリのインストールを行う
$ python3 -m venv .env
$ source .env/bin/activate
$ pip install -r requirements.txt

# AWS の認証情報をセットする
# 自分自身の認証情報に置き換えること!
export AWS_ACCESS_KEY_ID=XXXXXX
export AWS_SECRET_ACCESS_KEY=YYYYYY
export AWS_DEFAULT_REGION=ap-northeast-1

# デプロイを実行
$ cdk deploy

デプロイのコマンドが無事に実行されれば, Figure 55 のような出力が得られるはずである. ここで表示されている SimpleLambda.FunctionName = XXXX の XXX の文字列は後で使うのでメモしておこう.

cdk output
Figure 55. CDKデプロイ実行後の出力

AWS コンソールにログインして,デプロイされたスタックを確認してみよう. コンソールから,Lambda のページに行くと Figure 56 のような画面から Lambda の関数の一覧が確認できる.

cdk output
Figure 56. Lambda コンソール - 関数の一覧

今回のアプリケーションで作成したのが SimpleLambda-XXXX という名前のついた関数だ. 関数の名前をクリックして,詳細を見てみる. すると Figure 57 のような画面が表示されるはずだ. 先ほどプログラムの中で定義したPythonの関数がエディターから確認することができる. また,下の方にスクロールすると,関数の各種設定も確認することができる.

lambda_console_func_detail
Figure 57. Lambda コンソール - 関数の詳細

11.1.2. Lambda 関数の実行

それでは,作成した Lambda 関数を実際に実行 (invoke) してみよう. AWS の API を使うことで,関数の実行をスタートすることができる. 今回は, invoke_one.py に関数を実行するための簡単なプログラムを提供している. 興味のある読者はコードを読んでもらいたい.

以下のコマンドで,Lambda の関数を実行する. コマンドの XXXX の部分はは,先ほどデプロイしたときに SimpleLambda.FunctionName = XXXX で得られた XXXX の文字列で置換する.

$ python invoke_one.py XXXX

すると, "Congratulations! You are given Squirtle" という出力が得られるはずだ. とてもシンプルではあるが,クラウド上で先ほどの関数が走り,乱数が生成された上で,ポケモンが選択されて出力が返されている. 上のコマンドを何度か打ってみて,実行のごとに違うポケモンが返されることを確認しよう.

さて,上のコマンドは,一度につき一回の関数を実行したわけであるが, Lambda の本領は一度に大量のタスクを同時に実行できる点である. そこで,今度は一度に100個のタスクを同時に送信してみよう.

以下のコマンドを実行する. XXXX の部分は上と同様に置き換える. 第二引数の 100 は 100個のタスクを投入せよ,という意味である.

$ python invoke_many.py XXXX 100

すると以下のような出力が得られるはずだ.

....................................................................................................
Submitted 100 tasks to Lambda!

実際に,100 個のタスクが同時に実行されていることを確認しよう. Figure 57 の画面に戻り, "Monitoring" というタブがあるので,それをクリックする. すると, Figure 58 のようなグラフが表示されるだろう.

lambda_console_monitoring
Figure 58. Lambda コンソール - 関数の実行のモニタリング

Figure 58 のグラフの更新には数分かかることがあるので,なにも表示されない場合は少し待つ.

Figure 58 で "Invocations" が関数が何度実行されたかを意味している. たしかに100回実行されていることがわかる. さらに, "Concurrent executions" が何個のタスクが同時に行われたかを示している. ここでは 96 となっていることから,96個のタスクが並列的に実行されたことを意味している. (これが,100とならないのは,タスクの開始のコマンドが送られたのが完全には同タイミングではないことに起因する)

このように,非常にシンプルではあるが, Lambda を使うことで,同時並列的に処理を実行することのできるクラウドシステムを簡単に作ることができた.

もしこのようなことを従来的な serverful なクラウドで行おうとした場合,クラスターのスケーリングなど多くのコードを書くことに加えて,いろいろなパラメータを調節する必要がある.

興味がある人は,一気に1000個などのジョブを投入してみると良い. が,あまりやりすぎると Lambda の無料利用枠を超えて料金が発生してしまうので注意.

11.1.3. スタックの削除

最後にスタックを削除しよう.

スタックを削除するには,次のコマンドを実行すればよい.

$ cdk destroy

11.2. DynamoDB ハンズオン

ここでは,ショートハンズオンとして,新しい DynamoDB のテーブルを作成する. そして実際にそこにデータの読み書きを行ってみる.

ハンズオンのソースコードはこちらに置いてある ⇒ https://gitlab.com/tomomano/intro-aws/-/tree/master/handson/04-serverless/lambda

このハンズオンは,基本的に AWS Lambda の無料枠 の範囲内で実行することができる.

app.py にデプロイするプログラムが書かれている. 中身を見てみよう.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class SimpleDynamoDb(core.Stack):
    def __init__(self, scope: core.App, name: str, **kwargs) -> None:
        super().__init__(scope, name, **kwargs)

        table = ddb.Table(
            self, "SimpleTable",
            partition_key=ddb.Attribute(
                name="item_id",
                type=ddb.AttributeType.STRING
            ),
            billing_mode=ddb.BillingMode.PAY_PER_REQUEST,
            removal_policy=core.RemovalPolicy.DESTROY
        )

以上のコードで,最低限の設定がなされた空の DynamoDB テーブルを作成することができる. それぞれのパラメータの意味を簡単に解説しよう.

  • partition_key: 全ての DynamoDB テーブルには Partition Key が定義されていなければならない. Partition key とは,テーブル内のレコードごとに固有のIDのことである. 同一の Partition key を持った要素はテーブルの中に一つしか存在することはできない. また, Partition key が定義されていない要素はテーブルの中に存在することはできない. ここでは,Partition key に item_id という名前をつけている.

  • billing_mode: ddb.BillingMode.PAY_PER_REQUEST を基本的に選択しておけばよい

  • removal_policy: 省略

11.2.1. デプロイ

デプロイの手順は,これまでのハンズオンとほとんど共通である. ここでは,コマンドのみ列挙する (# で始まる行はコメントである). それぞれの意味を忘れてしまった場合は,ハンズオン1, 2に戻って復習していただきたい.

# プロジェクトのディレクトリに移動
$ cd intro-aws/handson/04-serverless/dynamodb

# venv を作成し,依存ライブラリのインストールを行う
$ python3 -m venv .env
$ source .env/bin/activate
$ pip install -r requirements.txt

# AWS の認証情報をセットする
# 自分自身の認証情報に置き換えること!
export AWS_ACCESS_KEY_ID=XXXXXX
export AWS_SECRET_ACCESS_KEY=YYYYYY
export AWS_DEFAULT_REGION=ap-northeast-1

# デプロイを実行
$ cdk deploy

デプロイのコマンドが無事に実行されれば, Figure 59 のような出力が得られるはずである. ここで表示されている SimpleDynamoDb.TableName = XXXX の XXX の文字列は後で使うのでメモしておこう.

cdk output
Figure 59. CDKデプロイ実行後の出力

AWS コンソールにログインして,デプロイされたスタックを確認してみよう. コンソールから, DynamoDB のページに行き,左のメニューバーから "Tables" を選択する. すると, Figure 60 のような画面からテーブルの一覧が確認できる.

cdk output
Figure 60. CDKデプロイ実行後の出力

今回のアプリケーションで作成したのが SimpleDynamoDb-XXXX という名前のついたテーブルだ. テーブルの名前をクリックして,詳細を見てみる. すると Figure 61 のような画面が表示されるはずだ. "Items" のタブをクリックすると,テーブルの中のレコードを確認することができる. 現時点ではなにもデータを書き込んでいないので,空である.

cdk output
Figure 61. CDKデプロイ実行後の出力

11.2.2. データの読み書き

それでは,上で作ったテーブルを使ってデータの読み書きを実践してみよう. ここでは Python と boto3 ライブラリを用いた方法を紹介する.

まず最初に, boto3 ライブラリを用意する. 次に,テーブルの名前から Table オブジェクトを作成する. "XXXX" の部分を自分がデプロイしたテーブルの名前 (Figure 59) に置き換えた上で,以下のコードを実行しよう.

1
2
3
4
import boto3
ddb = boto3.resource('dynamodb')

table = ddb.Table("XXXX")

新しいデータを書き込むには次のコードを実行する.

1
2
3
4
5
6
7
8
table.put_item(
   Item={
       'item_id': 'bec7c265-46e2-4065-91d8-80b2e8dcc9c2',
       'first_name': 'John',
       'last_name': 'Doe',
       'age': 25,
    }
)

テーブルの中のデータを,そのデータの Partition key を使って読み出すには,次のコードを実行する.

1
2
3
table.get_item(
   Key={"item_id": 'bec7c265-46e2-4065-91d8-80b2e8dcc9c2'}
).get("Item")

テーブルの中にあるデータを全て読み出したければ以下のコードを実行する.

1
table.scan().get("Items")

11.2.3. 大量のデータの読み書き

DynamoDB の利点は,最初に述べた通り,負荷に応じて自在にその処理能力を拡大できる点である.

そこで,ここでは一度に大量のデータを書き込む場合をシミュレートしてみよう. batch_rw.py に,一度に大量の書き込みを実行するためのプログラムが書いてある.

次のコマンドを実行してみよう (XXXX は自分のテーブルの名前に置き換える).

$ python batch_rw.py XXXX write 1000

このコマンドを実行することで,ランダムなデータが1000個データベースに書き込まれる.

さらに,データベースの検索をかけてみよう. 今回書き込んだデータには age という属性に1から50のランダムな整数が割り当てられている. age が2以下であるような要素だけを拾ってくるには,以下のコマンドを実行すればよい.

$ python batch_rw.py XXXX search_under_age 2

12. Hands-on #5: Bashoutter

さて,最終回となるハンズオン第五回では,これまで学んできたサーバーレスクラウドの技術を使って,簡単なウェブサービスを作ってみよう. 具体的には,人々が自分の作った俳句を投稿するSNSサービス (Bashoutter と名付ける) を作成してみよう. これまでの講義の集大成として,コードの長さとしては最も長くなっているが,頑張ってついてきてもらいたい. 最終的には, Figure 62 のような,ミニマルではあるがとても現代風な SNS サイトが完成する!

ハンズオンのソースコードはこちらのリンクに置いてある ⇒ https://gitlab.com/tomomano/intro-aws/-/tree/master/handson/05-bashoutter

このハンズオンは,基本的に AWS の無料枠 の範囲内で実行することができる.

bashoutter
Figure 62. ハンズオン#5で作製する SNS アプリケーション "Bashoutter"

12.1. 準備

本ハンズオンの実行には,第一回ハンズオンで説明した準備 (Section 4.1) が整っていることを前提とする.それ以外に必要な準備はない.

12.2. アプリケーションの説明

12.2.1. API

今回のアプリケーションでは,人々からの俳句の投稿を受け付けたり,投稿された俳句の一覧を取得する,といった機能を実装したい. そこで, Table 7 に示すような4つの API を今回は実装する.

Table 7. Hands-on #5 で実装するAPI

GET /haiku

俳句の一覧を取得する

POST /haiku

新しい俳句を投稿する

PATCH /haiku/{item_id}

{item_id} で指定された俳句にお気に入り票を一つ入れる

DELETE /haiku/{item_id}

{item_id} で指定された俳句を削除する

それぞれのAPIのパラメータおよび返り値の詳細は, https://gitlab.com/tomomano/intro-aws/-/blob/master/handson/05-bashoutter/specs/swagger.yml に定義してある.

Open API Specification (OAS; 少し以前は Swagger Specification と呼ばれていた) は, REST API のための記述フォーマットである. OAS に従って API の仕様が記述されていると,簡単にドキュメンテーションを生成したり,クライアントアプリケーションを自動生成することができる. 今回用意したAPI仕様 も, OAS に従って書いてあるので,非常に見やすいドキュメンテーションを瞬時に生成することができる. 詳しくは このページ などを参照.

12.2.2. アプリケーションアーキテクチャ

このハンズオンで作成するアプリケーションの概要を Figure 63 に示す.

hands-on 05 architecture
Figure 63. ハンズオン#5で作製するアプリケーションのアーキテクチャ

簡単にまとめると,以下のような設計である.

  • クライアントからの API リクエストは, API Gateway (後述)にまず送信され, API の URI に従って指定された Lambda 関数へ転送される.

  • それぞれの API のパスごとに独立した Lambda が用意されている.

  • 俳句の情報 (作者,俳句本体,投稿日時など) を記録するためのデータベース (DynamoDB) を用意する.

  • 各 Lambda 関数には, DynamoDB へのアクセス権を付与する.

  • 最後に,ウェブブラウザからコンテンツを表示できるよう, ウェブページの静的コンテンツを配信するための S3 バケットを用意する.クライアントはこの S3 バケットにアクセスすることで HTML/CSS/JS などのコンテンツを取得する.

それでは,プログラムのソースコードを見てみよう (/handson/05-bashoutter/app.py).

  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
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
class Bashoutter(core.Stack):

    def __init__(self, scope: core.App, name: str, **kwargs) -> None:
        super().__init__(scope, name, **kwargs)

        (1)
        # dynamoDB table to store haiku
        table = ddb.Table(
            self, "Bashoutter-Table",
            partition_key=ddb.Attribute(
                name="item_id",
                type=ddb.AttributeType.STRING
            ),
            billing_mode=ddb.BillingMode.PAY_PER_REQUEST,
            removal_policy=core.RemovalPolicy.DESTROY
        )

        (2)
        bucket = s3.Bucket(
            self, "Bashoutter-Bucket",
            website_index_document="index.html",
            public_read_access=True,
            removal_policy=core.RemovalPolicy.DESTROY
        )
        s3_deploy.BucketDeployment(
            self, "BucketDeployment",
            destination_bucket=bucket,
            sources=[s3_deploy.Source.asset("./gui/dist")],
            retain_on_delete=False,
        )

        common_params = {
            "runtime": _lambda.Runtime.PYTHON_3_7,
            "environment": {
                "TABLE_NAME": table.table_name
            }
        }

        (3)
        # define Lambda functions
        get_haiku_lambda = _lambda.Function(
            self, "GetHaiku",
            code=_lambda.Code.from_asset("api"),
            handler="api.get_haiku",
            memory_size=512,
            **common_params,
        )
        post_haiku_lambda = _lambda.Function(
            self, "PostHaiku",
            code=_lambda.Code.from_asset("api"),
            handler="api.post_haiku",
            **common_params,
        )
        patch_haiku_lambda = _lambda.Function(
            self, "PatchHaiku",
            code=_lambda.Code.from_asset("api"),
            handler="api.patch_haiku",
            **common_params,
        )
        delete_haiku_lambda = _lambda.Function(
            self, "DeleteHaiku",
            code=_lambda.Code.from_asset("api"),
            handler="api.delete_haiku",
            **common_params,
        )

        (4)
        # grant permissions
        table.grant_read_data(get_haiku_lambda)
        table.grant_read_write_data(post_haiku_lambda)
        table.grant_read_write_data(patch_haiku_lambda)
        table.grant_read_write_data(delete_haiku_lambda)

        (5)
        # define API Gateway
        api = apigw.RestApi(
            self, "BashoutterApi",
            default_cors_preflight_options=apigw.CorsOptions(
                allow_origins=apigw.Cors.ALL_ORIGINS,
                allow_methods=apigw.Cors.ALL_METHODS,
            )
        )

        haiku = api.root.add_resource("haiku")
        haiku.add_method(
            "GET",
            apigw.LambdaIntegration(get_haiku_lambda)
        )
        haiku.add_method(
            "POST",
            apigw.LambdaIntegration(post_haiku_lambda)
        )

        haiku_item_id = haiku.add_resource("{item_id}")
        haiku_item_id.add_method(
            "PATCH",
            apigw.LambdaIntegration(patch_haiku_lambda)
        )
        haiku_item_id.add_method(
            "DELETE",
            apigw.LambdaIntegration(delete_haiku_lambda)
        )
1 ここで,俳句の情報を記録しておくための DynamoDB テーブルを定義している.
2 続いて,静的コンテンツを配信するための S3 バケットを用意している. また,スタックのデプロイ時に,必要なファイル群を自動的にアップロードするような設定を行っている.
3 続いて,それぞれの API で実行される Lambda 関数を定義している. 関数は Python3.7 で書かれており,コードは /handson/05-bashoutter/api/api.py にある.
4 次に,2で定義された Lambda 関数に対し,データベースへの読み書きのアクセス権限を付与している.
5 ここで,API Gateway により,各APIパスとそこで実行されるべき Lambda 関数を紐付けている.

それぞれについて,もう少し詳しく説明しよう.

12.2.3. Public access mode の S3 バケット

S3 のバケットを作成しているコードを見てみよう.

1
2
3
4
5
6
bucket = s3.Bucket(
    self, "Bashoutter-Bucket",
    website_index_document="index.html",
    public_read_access=True,
    removal_policy=core.RemovalPolicy.DESTROY
)

ここで注目してほしいのは public_read_access=True の部分だ.

前章で, S3 について説明を行った時には触れなかったが, S3 には Public access mode という機能がある. Public access mode をオンにしておくと,バケットの中のファイルは基本的にすべて認証無しで (i.e. インターネット上の誰でも) 閲覧できるようになる. この設定は,ウェブサイトの静的なコンテンツを置いておくのに最適であり,多くのサーバーレスによるウェブサービスでこのような設計が行われる. public access mode を設定しておくと, http://XXXX.s3-website-ap-northeast-1.amazonaws.com/ のような固有の URL がバケットに対して付与される. そして,クライアントがこの URL にアクセスをすると,バケットの中にある index.html がクライアントに返され,ページがロードされる. (どのページがサーブされるかは, website_index_document="index.html" の部分で設定している.)

より本格的なウェブページを運用する際には, public access mode の S3 バケットに, CloudFront という機能を追加することが一般的である.

CloudFront はいくつかの役割を担っているのだが,最も重要な機能が Content Delivery Nework (CDN) である. CDN とは,頻繁にアクセスされるデータをメモリーなどの高速記録媒体にキャッシュしておくことで,クライアントがより高速にデータをダウンロードすることを可能にする仕組みである. また,世界各地のデータセンターにそのようなキャッシュを配置することで,クライアントと地理的に最も近いデータセンターからデータが配信する,というような設定も可能である.

また,CloudFront を配置することで, HTTPS 通信を設定することができる. (逆に言うと, S3 単体では HTTP 通信しか行うことができない.) 現代的なウェブサービスでは,秘匿情報を扱う扱わないに関わらず, HTTPS を用いることが標準となっている.

今回のハンズオンでは説明の簡略化のため CloudFront の設定を行わなかったが,興味のある読者は以下のリンクのプログラムが参考になるだろう.

今回の S3 バケットには, AWS によって付与されたランダムな URL がついている. これを. example.com のような自分のドメインでホストしたければ, AWS によって付与された URL を自分のドメインの DNS レコードに追加すればよい.

Public access mode の S3 バケットを作成した後,バケットの中に配置するウェブサイトコンテンツを,以下のコードによりアップロードしている.

1
2
3
4
5
6
s3_deploy.BucketDeployment(
    self, "BucketDeployment",
    destination_bucket=bucket,
    sources=[s3_deploy.Source.asset("./gui/dist")],
    retain_on_delete=False,
)

ウェブサイトのコンテンツは /handson/05-bashoutter/gui/ にある (特に,ビルド済みのものが /dist/ 以下にある). 興味のある読者は中身を確認してみるとよい.

今回のウェブサイトは Vue.jsVuetify という UI フレームワークを使って作成した. ソースコードは /handson/05-bashoutter/gui/src/ にあるので,見てみるとよい.

12.2.4. API のハンドラ関数

API リクエストが来たときに,リクエストされた処理を行う関数のことを特にハンドラ (handler) 関数と呼ぶ. Lambda を使って GET /haiku の API に対してのハンドラ関数を定義している部分を見てみよう.

1
2
3
4
5
6
get_haiku_lambda = _lambda.Function(
    self, "GetHaiku",
    code=_lambda.Code.from_asset("api"),
    handler="api.get_haiku",
    ...
)

code=_lambda.Code.from_asset("api"), handler="api.get_haiku" のところで,外部のディレクトリ (api/) にある api.py というファイルの, get_haiku() という関数をハンドラ関数として実行せよ,と指定している. この get_haiku() のコードを見てみよう (/handson/05-bashoutter/api/api.py).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
ddb = boto3.resource("dynamodb")
table = ddb.Table(os.environ["TABLE_NAME"])

def get_haiku(event, context):
    """
    handler for GET /haiku
    """
    try:
        response = table.scan()

        status_code = 200
        resp = response.get("Items")
    except Exception as e:
        status_code = 500
        resp = {"description": f"Internal server error. {str(e)}"}
    return {
        "statusCode": status_code,
        "headers": HEADERS,
        "body": json.dumps(resp, cls=DecimalEncoder)
    }

response = table.scan() で,俳句の格納された DynamoDB テーブルから,全ての要素を取り出している. もしなにもエラーが起きなければステータスコード200が返され,もしなにかエラーが起こればステータスコード500が返されるようになっている.

上記のような操作を,他の API についても繰り返すことで,すべての API のハンドラ関数が定義されている.

GET /haiku のハンドラ関数で, response = table.scan() という部分があるが,実はこれは最善の書き方ではない. DynamoDB の scan() メソッドは,最大で 1MB までのデータしか返さない. データベースのサイズが大きく, 1MB 以上のデータがある場合には,再帰的に scan() メソッドを呼ぶ必要がある. 詳しくは boto3 ドキュメンテーション を参照.

12.2.5. AWS における権限の管理 (IAM)

以下の部分のコードに注目してほしい.

1
2
3
4
table.grant_read_data(get_haiku_lambda)
table.grant_read_write_data(post_haiku_lambda)
table.grant_read_write_data(patch_haiku_lambda)
table.grant_read_write_data(delete_haiku_lambda)

これまでは説明の簡略化のため敢えて触れてこなかったが, AWS には IAM (Identity and Access Management) という重要な概念がある. IAM は基本的に,あるリソースが他のリソースに対してどのような権限を持っているか,を規定するものである. Lambdaは,デフォルトの状態では他のリソースにアクセスする権限をなにも有していない. したがって, Lambda 関数が DynamoDB のデータを読み書きするためには,それを許可するような IAM が Lambda 関数に付与されていなければならない.

CDK による dynamodb.Table オブジェクトには grant_read_write_data() という便利なメソッドが備わっており,アクセスを許可したい Lambda 関数を引数としてこのメソッドを呼ぶことで,データベースへの読み書きを許可する IAM を付与することができる.

各リソースに付与する IAM は,必要最低限の権限を与えるにとどめるというのが基本方針である. これにより,セキュリティを向上させるだけでなく,意図していないプログラムからのデータベースへの読み書きを防止するという点で,バグを未然に防ぐことができる.

そのような理由により,上のコードでは GET のハンドラー関数に対しては grant_read_data() によって, read 権限のみを付与している.

12.2.6. API Gateway

API Gateway とは, API の"入り口"として,APIのリクエストパスに従って Lambda 関数などに接続を行うという機能を担う. このような API のリソースパスに応じて接続先を振り分けるようなサーバーをルーターと呼んだりする. 従来的には,ルーターにはそれ専用の仮想サーバーが置かれることが一般的であったが, API Gateway はその機能をサーバーレスで担ってくれる. すなわち, API のリクエストが来たときのみ起動し,API が来ていない間は完全にシャットダウンしている. 一方で,アクセスが大量に来た場合はそれに比例してルーティングの処理能力を増大してくれる.

API Gateway を配置することで,大量 (1秒間に数千から数万件) の API リクエストに対応することのできるシステムを容易に構築することができる. API Gateway の料金は Table 8 のように設定されている. また,無料利用枠により,月ごとに100万件までのリクエストは0円で使用できる.

Table 8. API Gateway の利用料金設定 (参照)
Number of Requests (per month) Price (per million)

First 333 million

$4.25

Next 667 million

$3.53

Next 19 billion

$3.00

Over 20 billion

$1.91

ソースコードの該当箇所を見てみよう.

 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
api = apigw.RestApi(
    self, "BashoutterApi",
    default_cors_preflight_options=apigw.CorsOptions(
        allow_origins=apigw.Cors.ALL_ORIGINS,
        allow_methods=apigw.Cors.ALL_METHODS,
    )
)

haiku = api.root.add_resource("haiku")
haiku.add_method(
    "GET",
    apigw.LambdaIntegration(get_haiku_lambda)
)
haiku.add_method(
    "POST",
    apigw.LambdaIntegration(post_haiku_lambda)
)

haiku_item_id = haiku.add_resource("{item_id}")
haiku_item_id.add_method(
    "PATCH",
    apigw.LambdaIntegration(patch_haiku_lambda)
)
haiku_item_id.add_method(
    "DELETE",
    apigw.LambdaIntegration(delete_haiku_lambda)
)
  • api = apigw.RestApi() により,空の API Gateway を作成している.

  • 次に, api.root.add_resource() のメソッドを呼ぶことで, /haiku という API パスを追加している.

  • 続いて, add_method() を呼ぶことで, GET, POST のメソッドを /haiku のパスに定義している.

  • さらに, haiku.add_resource("{item_id}") により, /haiku/{item_id} という API パスを追加している.

  • 最後に, add_method() を呼ぶことにより, PATCH, DELETE のメソッドを /haiku/{item_id} のパスに定義している.

このように, 逐次的に API パスとそこで実行されるメソッド・Lambda を記述していくだけでよい.

上記のプログラムで 新規 API を作成すると, AWS からランダムな URL がその API のエンドポイントとして割り当てられる. これを. api.example.com のような自分のドメインでホストしたければ, AWS によって付与された URL を自分のドメインの DNS レコードに追加すればよい.

API Gateway で新規 API を作成したとき, default_cors_preflight_options= というパラメータで Cross Origin Resource Sharing (CORS) の設定を行っている. これは,ブラウザで走る Web アプリケーションと API を接続する際に必要な設定である. 興味のある読者は各自 CORS について調べてもらいたい.

12.3. アプリケーションのデプロイ

アプリケーションの中身が理解できたところで,早速デプロイを行ってみよう.

デプロイの手順は,これまでのハンズオンとほとんど共通である. ここでは,コマンドのみ列挙する (# で始まる行はコメントである). それぞれの意味を忘れてしまった場合は,ハンズオン1, 2に戻って復習していただきたい.

# プロジェクトのディレクトリに移動
$ cd intro-aws/handson/05-bashoutter

# venv を作成し,依存ライブラリのインストールを行う
$ python3 -m venv .env
$ source .env/bin/activate
$ pip install -r requirements.txt

# AWS の認証情報をセットする
# 自分自身の認証情報に置き換えること!
export AWS_ACCESS_KEY_ID=XXXXXX
export AWS_SECRET_ACCESS_KEY=YYYYYY
export AWS_DEFAULT_REGION=ap-northeast-1

# デプロイを実行
$ cdk deploy

デプロイのコマンドが無事に実行されれば, Figure 64 のような出力が得られるはずである. ここで表示されている Bashoutter.BashoutterApiEndpoint = XXXX, Bashoutter.BucketUrl = YYYY の二つ文字列は次に使うのでメモしておこう.

cdk output
Figure 64. CDKデプロイ実行後の出力

$ cdk bootstrap のコマンドを実行していないと,上記のデプロイはエラーを出力する.

bootstrap のコマンドは1アカウントにつき1度実行されていればよい. Section 14.3 も参照のこと.

上記のデプロイで得られた API のエンドポイントは API Gateway によりランダムに作成されたアドレスである. このアドレスを DNS に登録することで,自分の好きなドメイン名 (例: api.example.com) と結びつけることが可能である.

AWS コンソールにログインして,デプロイされたスタックを確認してみよう. コンソールから, API Gateway のページに行くと, Figure 65 のような画面が表示され,デプロイ済みの API エンドポイントの一覧が確認できる.

apigw_console_list
Figure 65. API Gateway コンソール画面 (1)

今回デプロイした "BashoutterApi" という名前の API をクリックすることで Figure 66 のような画面に遷移し,詳細情報を閲覧できる. GET /haiku, POST /haiku などが定義されていることが確認できる.

それぞれのメソッドをクリックすると,そのメソッドの詳細情報を確認できる. API Gateway は,上で説明したルーティングの機能だけでなく,認証機能などを追加することも可能であり,そのような理由で Figure 66 で画面右端赤色で囲った部分に,この API で呼ばれる Lambda 関数が指定されている. 関数名をクリックすることで,関数の中身を閲覧することが可能である.

apigw_console_detail
Figure 66. API Gateway コンソール画面 (2)

次に, S3 のコンソール画面に移ってみよう. "bashouter-XXXXX" という名前のバケットが見つかるはずである (Figure 67).

s3_console
Figure 67. S3 コンソール画面

バケットの名前をクリックすることで,バケットの中身を確認してみよう. index.html のほか, css/, js/ などのディレクトリがあるのが確認できるだろう (Figure 68). これらが,ウェブページの"枠"を定義している静的コンテンツである.

s3_contents
Figure 68. S3 バケットの中身

12.4. API を送信する

それでは,デプロイしたアプリケーションに対し,実際に API を送信してみよう (ひとまずは, S3 にあるGUIの方はおいておく.今回のアプリケーションでより本質的なのは API の方だからである).

ここではコマンドラインから API を送信するためのシンプルなHTTPクライアントである HTTPie を使ってみよう. HTTPie は,スタックをデプロイするときに Python 仮想環境を作成した際,一緒にインストールした. コマンドラインに http と打ってみて,コマンドの使い方が出力されることを確認しよう.

まず最初に,先ほどデプロイを実行した際に得られた API のエンドポイントの URL (Bashoutter.BashoutterApiEndpoint = XXXX で得られた XXXX の文字列) をコマンドラインの変数に設定しておく.

$ export ENDPOINT_URL="https://OOOO.execute-api.ap-northeast-1.amazonaws.com/prod/"

上のコマンドで,URLは自分のデプロイしたスタックのURLに置き換える.

次に,俳句の一覧を取得するため, GET /haiku の API を送信してみよう.

$ http GET "${ENDPOINT_URL}/haiku"

現時点では,まだだれも俳句を投稿していないので,空の配列 ([]) が返ってくる.

それでは次に,俳句を投稿してみよう.

$ http POST "${ENDPOINT_URL}/haiku" \
username="松尾芭蕉" \
first="閑さや" \
second="岩にしみ入る" \
third="蝉の声"

以下のような出力が得られるだろう.

HTTP/1.1 201 Created
Connection: keep-alive
Content-Length: 49
Content-Type: application/json
....
{
    "description": "Successfully added a new haiku"
}

新しい俳句を投稿することに成功したようである. 本当に俳句が追加されたか,再び GET リクエストを呼ぶことで確認してみよう.

$ http GET "${ENDPOINT_URL}/haiku"

HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 258
Content-Type: application/json
...
[
    {
        "created_at": "2020-07-06T02:46:04+00:00",
        "first": "閑さや",
        "item_id": "7e91c5e4d7ad47909e0ac14c8bbab05b",
        "likes": 0.0,
        "second": "岩にしみ入る",
        "third": "蝉の声",
        "username": "松尾芭蕉"
    }
]

素晴らしい!

次に, PATCH /haiku/{item_id} を呼ぶことでこの俳句にいいねを追加してみよう. 上のコマンドで取得した俳句の item_id を,下のコマンドの XXXX

$ http PATCH "${ENDPOINT_URL}/haiku/XXXX"

再び GET リクエストを送ることで,いいね (likes) が1増えたことを確認しよう.

$ http GET "${ENDPOINT_URL}/haiku"
...
[
    {
        ...
        "likes": 1.0,
        ...
    }
]

最後に, DELETE リクエストを送ることで俳句をデータベースから削除しよう. XXXXitem_id の値で置き換えた上で以下のコマンドを実行する.

$ http DELETE "${ENDPOINT_URL}/haiku/XXXX"

再び GET リクエストを送ることで,返り値が空 ([]) になっていることを確認しよう.

以上のように, SNS に必要な基本的な API がきちんと動作していることが確認できた.

12.5. 大量の API リクエストをシミュレートする

さて,前節ではマニュアルでひとつづづ俳句を投稿した. 多数のユーザーがいるような SNS では,一秒間に数千件以上の投稿がされている. 今回はサーバーレスアーキテクチャを採用したことで,そのような瞬間的な大量アクセスにも容易に対応できるようなシステムが構築できている!

その点をデモンストレートするため,ここでは大量の API が送信された状況をシミュレートしてみよう.

/handson/05-bashoutter/client.py に,大量のAPIリクエストをシミュレートするためのプログラムが書かれている. このプログラムは基本的に POST /haiku の API リクエストを指定された回数だけ実行する.

テストとして, API を300回送ってみよう. 以下のコマンドを実行する.

$ python client.py $ENDPOINT_URL post_many 300

数秒のうちに実行が完了するだろう. これがもし,単一のサーバーからなる API だったとしたら,このような大量のリクエストの処理にはもっと時間がかかっただろう. 最悪の場合には,サーバーダウンにもつながっていたかもしれない. 従って,今回作成したサーバーレスアプリケーションは,とてもシンプルながらも一秒間に数百件の処理を行えるような,スケーラブルなクラウドシステムであることがわかる. サーバーレスでクラウドを設計することの利点を垣間見ることができただろうか?

上記のコマンドにより,大量の俳句を投稿するとデータベースに無駄なデータがどんどん溜まってしまう. データベースを完全に空にするには,以下のコマンドを使用する.

$ python client.py $ENDPOINT_URL clear_database

12.6. Bashoutter GUI を動かしてみる

前節ではコマンドラインから API を送信する演習を行った. ウェブアプリケーションでは,これらの API はウェブブラウザ上のウェブページから送信され,コンテンツが表示されている (Figure 49 参照). 最後に, API が GUI と統合されるとどうなるのか,見てみよう.

デプロイを実行したときにコマンドラインで出力された, ashoutter.BucketUrl= に続く URL を確認しよう (Figure 64). これは,先述したとおり, Public access mode の S3 バケットの URL である.

ウェブブラウザを開き,そのURLへアクセスをしてみよう. すると, Figure 69 のようなページが表示されるはずである.

bashoutter
Figure 69. "Bashoutter" の GUI 画面

ページが表示されたら,一番上の "API Endpoint URL" と書いてあるテキストボックスに,今回デプロイした API Gateway の URL を入力する (今回のアプリケーションでは,API Gateway の URL はランダムに割り当てられるのでこのような仕様になっている). そうしたら,画面の "REFRESH" と書いてあるボタンを押してみよう. データベースに俳句が登録済みであれば,俳句の一覧が表示されるはずである. 各俳句の左下にあるハートのアイコンをクリックすることで, "like" の票を入れることができる.

新しい俳句を投稿するには,五七五と投稿者の名前を入力して, "POST" を押す. "POST" を押した後は,再び "REFRESH" ボタンを押すことで最新の俳句のリストをデータベースから取得する.

今回は,どうやって GUI を作成したかは触れないが,基本的にページの背後では GET /haiku, POST /haiku などの API がクラウドに送信されることで,コンテンツが表示されている. 興味のある読者は GUI のソースコードも読んでみるとよい (/handson/05-bashoutter/gui/).

12.7. アプリケーションの削除

これにて,第五回ハンズオンは終了である.最後にスタックを削除しよう.

スタックを削除するには,まず最初に S3 バケットの中身をすべて削除しなければならない. コンソールから実行するには, S3 コンソールに行き,バケットの中身を開いた上で,全てのファイルを選択し, "Actions" → "Delete" を実行すれば良い.

コマンドラインから実行するには, 次のコマンドを使う. <BUCKET NAME> のところは,自分の バケットの名前 ("BashoutterBucketXXXX" というパターンの名前がついているはずである) に置き換えることを忘れずに.

$ aws s3 rm <BUCKET NAME> --recursive

S3 バケットを空にしたら,次のコマンドを実行してスタックを削除する.

$ cdk destroy

12.8. 講義第三回目のまとめ

ここまでが,講義第三回目の内容である.

今回は,クラウドの応用として,一般の人に使ってもらうようなウェブアプリケーション・データベースをどのようにして作るのか,という点に焦点を当てて,講義を行った. その中で,従来的なクラウドシステムの設計と,ここ数年の最新の設計方法であるサーバーレスアーキテクチャについて解説した. 特に, AWS でのサーバーレスの実践として, Lambda, S3, DynamoDB のハンズオンを行った. 最後に,ハンズオン第五回目では,これらの技術を統合することで,完全サーバーレスなウェブアプリケーション "Bashoutter" を作成した.

今回の講義を通じて,世の中のウェブサービスがどのようにして出来上がっているのか,少し理解が深まっただろうか? また,そのようなウェブアプリケーションを自分が作りたいと思ったときの,出発点となることができたならば幸いである.

13. まとめ

14. Appendix

14.1. AWS のシークレットキーの作成

AWS シークレットキーとは, AWS CLI や AWS CDK から AWS の API を操作する際に,ユーザー認証を行うための鍵のことである. AWS CLI/CDK を使うには,最初にシークレットキーを発行する必要がある. AWS シークレットキーの詳細は 公式ドキュメンテーション を参照.

  1. AWS コンソールにログインする.

  2. 画面右上のアカウント名をクリックし,表示されるプルダウンメニューから "My Security Credentials" を選択 (Figure 70)

  3. "Access keys for CLI, SDK, & API access" の下にある "Create accesss key" のボタンをクリックする (Figure 70)

  4. 表示された Access key ID, Secret access key を記録しておく (画面を閉じると以降は表示されない).

  5. 鍵を忘れてしまった場合などは,何度でも発行が可能である.

  6. 発行したシークレットキーは, ~/.aws/credentials のファイルに書き込むか,環境変数に設定するなどして使う (詳しくは Section 14.2).

aws_secret_key_1
Figure 70. AWS シークレットキーの発行1
aws_secret_key_2
Figure 71. AWS シークレットキーの発行2

14.2. AWS CLI のインストール

公式のドキュメンテーションに従い,インストールを行う.

Linuxマシンならば,以下のコマンドを実行すれば良い.

$ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
$ unzip awscliv2.zip
$ sudo ./aws/install

インストールできたか確認するため,以下のコマンドを打って正しくバージョンが表示されることを確認する.

$ aws --version

インストールができたら,以下のコマンドにより初期設定を行う (参照).

$ aws configure

コマンドを実行すると, AWS Access Key ID, AWS Secret Access Key を入力するよう指示される. シークレットキーの発行については Section 14.1 を参照. コマンドは加えて,Default region name を訊いてくる. ここには ap-northeast-1 (東京リージョン)を指定するのがよい. 最後の Default output formatJSON としておくとよい.

上記のコマンドを完了すると, ~/.aws/credentials~/.aws/config という名前のファイルに設定が保存されているはずである.念の為,中身をしてみるとよい.

$ cat ~/.aws/credentials
[default]
aws_access_key_id = XXXXXXXXXXXXXXXXXX
aws_secret_access_key = YYYYYYYYYYYYYYYYYYY

$ cat ~/.aws/config
[profile default]
region = ap-northeast-1
output = json

上記のような出力が得られるはずである.

~/.aws/credentials には認証鍵の情報が, ~/.aws/config には各設定が記録されている.

デフォルトでは, [default] という名前でプロファイルが保存される. いくつかのプロファイルを使い分けたければ, default の例に従って,例えば [myprofile] という名前でプロファイルを追加すればよい.

AWS CLI でコマンドを打つときに,プロファイルを使い分けるには,

$ aws s3 ls --profile myprofile

のように, --profile というオプションをつけてコマンドを実行する.

いちいち --profile オプションをつけるのが面倒だと感じる場合は,以下のように環境変数を設定することもできる.

export AWS_ACCESS_KEY_ID=XXXXXX
export AWS_SECRET_ACCESS_KEY=YYYYYY
export AWS_DEFAULT_REGION=ap-northeast-1

上の環境変数は, ~/.aws/credentials よりも高い優先度を持つので,環境変数が設定されていればそちらの情報が使用される (参照).

14.3. AWS CDK のインストール

公式ドキュメントに従いインストールを行う.

Node.js がインストールされていれば,基本的に以下のコマンドを実行すれば良い.

$ npm install -g aws-cdk

本書のハンズオンはAWS CDK version 1.30.0 で開発した.CDK は開発途上のライブラリなので,将来的にAPIが変更される可能性がある.APIの変更によりエラーが生じた場合は, version 1.30.0 を使用することを推奨する.

$ npm install -g aws-cdk@1.30

インストールできたか確認するため,以下のコマンドを打って正しくバージョンが表示されることを確認する.

$ cdk --version

インストールができたら,以下のコマンドによりAWS側の初期設定を行う.これは一度実行すればOK.

$ cdk bootstrap

cdk bootstrap を実行するときは,AWSの認証情報とリージョンが正しく設定されていることを確認する.デフォルトでは ~/.aws/config にあるデフォルトのプロファイルが使用される.デフォルト以外のプロファイルを用いるときは AWS_ACCESS_KEY_ID などの環境変数を設定する (参照).

AWS CDK の認証情報の設定は AWS CLI と基本的に同じである.詳しくは Section 14.2 を参照.

14.4. Python venv クイックガイド

他人からもらったプログラムで, numpy や scipy のバージョンが違う!などの理由で,プログラムが動かない,という経験をしたことがある人は多いのではないだろうか. もし,自分のPCの中に一つしかPython環境がないとすると,プロジェクトを切り替えるごとに正しいバージョンをインストールし直さなければならず,これは大変な手間である.

コードのシェアをよりスムーズにするためには,ライブラリのバージョンはプロジェクトごとに管理されるべきである. それを可能にするのが Python 仮想環境と呼ばれるツールであり, venv, pyenv, conda などがよく使われる.

そのなかでも, venv は Python に標準搭載されているので,とても便利である. pyenvconda は,別途インストールの必要があるが,それぞれの長所もある.

$ python -m venv .env

というコマンドを実行することで,venv モジュールにより .env/ というディレクトリが作られる.

この仮想環境を起動するには

$ source .env/bin/activate

と実行する.

シェルのプロンプトに (.env) という文字が追加されていることを確認しよう. これが, "いまあなたは venv の中にいますよ" というしるしになる.

venv shell
Figure 72. venv を起動したときのプロンプト

仮想環境を起動すると,それ以降実行する pip コマンドは, .env/ 以下にインストールされる.このようにして,プロジェクトごとに使うライブラリのバージョンを切り分けることができる.

Python では requirements.txt というファイルにに依存ライブラリを記述するのが一般的な慣例である.他人からもらったプログラムに, requirements.txt が定義されていれば,

$ pip install -r requirements.txt

と実行することで,必要なライブラリをインストールし,瞬時にPython環境を再現することができる.

15. 謝辞

本原稿の執筆にあたり,以下の方々からの協力を得た.この場を借りて,感謝を表したい.

  • 勝俣敬寛氏 - Docker イメージの作成

  • 香取真知子氏 - ハンズオンプログラムの動作確認

  • @shuuji3 - MR !15

  • @takatama_jp - MR !14

本書の執筆には Asciidoctor を使用した.

16. 著者紹介

真野 智之 (Tomoyuki Mano) 東京大学大学院情報理工学系研究科博士課程

研究分野 神経科学・神経情報学

現在の研究テーマ クラウドを用いた脳画像解析・データベース構築

趣味 料理・ランニング・鉄道・アニメ・村上春樹

17. ライセンス

本教科書およびハンズオンのソースコードは CC BY-NC-ND 4.0 に従うライセンスで公開しています.

教育など非商用の目的での本教科書の使用や再配布は自由に行うことが可能です. 商用目的で本書の全体またはその一部を無断で転載する行為は,これを固く禁じます.

cc_by_nc_nd