【AWS で遊ぶ】 AWS Elastic Beanstalk でインフラのことを気にせずに簡単デプロイ!
本稿の対象者
本稿の目標
はじめに
こんにちは。新卒 1 年目エンジニアのさっちゃんです。普段は Spring boot で API の設計・実装をしたり、AWS 上で Python で Lambda 書いたりしてます。使っている AWS のサービスは主に API Gateway, Lambda, Cognito, Neptune, Dynamo, S3 などで、最近は CloudFormation を使ってインフラをコード化することにハマってます。なんでもコードで管理したいお年頃。
さて、最近趣味で AWS ソリューションアーキテクト アソシエイトの勉強をしていると、さまざまなサービスが出てくるので遊んでみたくなります。その中でも「アプリケーションを実行しているインフラストラクチャについて深く意識していなくても、アプリケーションのデプロイとその管理を簡単に行うことができます。」(徹底攻略 AWS 認定ソリューションアーキテクト -アソシエイト教科書 p.90)と紹介されていた AWS Elastic Beanstalk を使えばどれくらい簡単にデプロイできるものなのか、何をしてくれるのか気になったので実際に使ってみました。
AWS Elastic Beanstalk とは
まずは AWS Elastic Beanstalk とはなんなのか、いつも通り公式サイトを見てみましょう。
AWS Elastic Beanstalk は、Java、.NET、PHP、Node.js、Python、Ruby、Go および Docker を使用して開発されたウェブアプリケーションやサービスを、Apache、Nginx、Passenger、IIS など使い慣れたサーバーでデプロイおよびスケーリングするための、使いやすいサービスです。
お客様はコードをアップロードするだけで、Elastic Beanstalk が、キャパシティのプロビジョニング、ロードバランシング、Auto Scaling からアプリケーションのヘルスモニタリングまで、デプロイを自動的に処理します。同時に、お客様のアプリケーションが稼動している AWS リソースの完全な制御を維持でき、いつでも基盤となるリソースにアクセスすることができます。
Elastic Beanstalk には追加料金はかかりません。アプリケーションを格納および実行するために必要な AWS のリソースに対してのみお支払いいただきます。
なるほど分からん。とにかく、コードをアップロードすればデプロイやロードバランシング、ヘルスモニタリングをいい感じにやってくれるらしいです。まぁとにかくやってみるかー。
まずはサンプルをデプロイ
まぁとにかく手を動かしてみるのがいいですね。コンソールから Elastic Beanstalk を選択してアプリケーションを作成します。名前は、このアプリを作成することは私にとって偉大な一歩なので GreatApplication にします。
言語は Python を選択します。
1 分くらい待つと次のような画面が出ます。なにかよくわかりませんがデプロイされたようです。
greatapplication-env.eba-ruyxxcr5.ap-northeast-1.elasticbeanstalk.com をブラウザで見ると次のように、Your first AWS Elastic Beanstalk Python Application is now running on your own dedicated environment in the AWS Cloud とデフォルト画面が出ます。
しかし自分はコードを何も見ていません。ソースコードはどこにあるんでしょうか?チュートリアルおよびサンプルの Python のサンプルをダウンロードしてみると発見できました。どうやらデフォルトではこちらが適用されるようです。
import logging.handlers # Create logger logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) # Handler LOG_FILE = '/tmp/sample-app.log' handler = logging.handlers.RotatingFileHandler(LOG_FILE, maxBytes=1048576, backupCount=5) handler.setLevel(logging.INFO) # Formatter formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') # Add Formatter to Handler handler.setFormatter(formatter) # add Handler to Logger logger.addHandler(handler) welcome = """ <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <!-- Copyright 2012 Amazon.com, Inc. or its affiliates. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at http://aws.Amazon/apache2.0/ or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Welcome</title> <style> body { color: #ffffff; background-color: #E0E0E0; font-family: Arial, sans-serif; font-size:14px; -moz-transition-property: text-shadow; -moz-transition-duration: 4s; -webkit-transition-property: text-shadow; -webkit-transition-duration: 4s; text-shadow: none; } body.blurry { -moz-transition-property: text-shadow; -moz-transition-duration: 4s; -webkit-transition-property: text-shadow; -webkit-transition-duration: 4s; text-shadow: #fff 0px 0px 25px; } a { color: #0188cc; } .textColumn, .linksColumn { padding: 2em; } .textColumn { position: absolute; top: 0px; right: 50%; bottom: 0px; left: 0px; text-align: right; padding-top: 11em; background-color: #1BA86D; background-image: -moz-radial-gradient(left top, circle, #6AF9BD 0%, #00B386 60%); background-image: -webkit-gradient(radial, 0 0, 1, 0 0, 500, from(#6AF9BD), to(#00B386)); } .textColumn p { width: 75%; float:right; } .linksColumn { position: absolute; top:0px; right: 0px; bottom: 0px; left: 50%; background-color: #E0E0E0; } h1 { font-size: 500%; font-weight: normal; margin-bottom: 0em; } h2 { font-size: 200%; font-weight: normal; margin-bottom: 0em; } ul { padding-left: 1em; margin: 0px; } li { margin: 1em 0em; } </style> </head> <body id="sample"> <div class="textColumn"> <h1>Congratulations</h1> <p>Your first AWS Elastic Beanstalk Python Application is now running on your own dedicated environment in the AWS Cloud</p> <p>This environment is launched with Elastic Beanstalk Python Platform</p> </div> <div class="linksColumn"> <h2>What's Next?</h2> <ul> <li><a href="http://docs.amazonwebservices.com/elasticbeanstalk/latest/dg/">AWS Elastic Beanstalk overview</a></li> <li><a href="http://docs.amazonwebservices.com/elasticbeanstalk/latest/dg/index.html?concepts.html">AWS Elastic Beanstalk concepts</a></li> <li><a href="http://docs.amazonwebservices.com/elasticbeanstalk/latest/dg/create_deploy_Python_django.html">Deploy a Django Application to AWS Elastic Beanstalk</a></li> <li><a href="http://docs.amazonwebservices.com/elasticbeanstalk/latest/dg/create_deploy_Python_flask.html">Deploy a Flask Application to AWS Elastic Beanstalk</a></li> <li><a href="http://docs.amazonwebservices.com/elasticbeanstalk/latest/dg/create_deploy_Python_custom_container.html">Customizing and Configuring a Python Container</a></li> <li><a href="http://docs.amazonwebservices.com/elasticbeanstalk/latest/dg/using-features.loggingS3.title.html">Working with Logs</a></li> </ul> </div> </body> </html> """ def application(environ, start_response): path = environ['PATH_INFO'] method = environ['REQUEST_METHOD'] if method == 'POST': try: if path == '/': request_body_size = int(environ['CONTENT_LENGTH']) request_body = environ['wsgi.input'].read(request_body_size) logger.info("Received message: %s" % request_body) elif path == '/scheduled': logger.info("Received task %s scheduled at %s", environ['HTTP_X_AWS_SQSD_TASKNAME'], environ['HTTP_X_AWS_SQSD_SCHEDULED_AT']) except (TypeError, ValueError): logger.warning('Error retrieving request body for async work.') response = '' else: response = welcome start_response("200 OK", [ ("Content-Type", "text/html"), ("Content-Length", str(len(response))) ]) return [bytes(response, 'utf-8')]
では template の部分を変えてみてデプロイの練習をしてみます。
import logging.handlers # Create logger logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) # Handler LOG_FILE = '/tmp/sample-app.log' handler = logging.handlers.RotatingFileHandler(LOG_FILE, maxBytes=1048576, backupCount=5) handler.setLevel(logging.INFO) # Formatter formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') # Add Formatter to Handler handler.setFormatter(formatter) # add Handler to Logger logger.addHandler(handler) welcome = """ <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Welcome</title> <style> body { color: #ffffff; background-color: #E0E0E0; font-family: Arial, sans-serif; font-size:14px; -moz-transition-property: text-shadow; -moz-transition-duration: 4s; -webkit-transition-property: text-shadow; -webkit-transition-duration: 4s; text-shadow: none; } h1 { font-size: 500%; font-weight: normal; margin-bottom: 0em; } </style> </head> <body> <h1> 猫は偉大。 </h1> </body> </html> """ def application(environ, start_response): path = environ['PATH_INFO'] method = environ['REQUEST_METHOD'] if method == 'POST': try: if path == '/': request_body_size = int(environ['CONTENT_LENGTH']) request_body = environ['wsgi.input'].read(request_body_size) logger.info("Received message: %s" % request_body) elif path == '/scheduled': logger.info("Received task %s scheduled at %s", environ['HTTP_X_AWS_SQSD_TASKNAME'], environ['HTTP_X_AWS_SQSD_SCHEDULED_AT']) except (TypeError, ValueError): logger.warning('Error retrieving request body for async work.') response = '' else: response = welcome start_response("200 OK", [ ("Content-Type", "text/html"), ("Content-Length", str(len(response))) ]) return [bytes(response, 'utf-8')]
書き換えてzip -r sample.zip ./
などで zip に固めて「アップロードとデプロイ」からファイルを選択します。そしてもう一度 Greatapplication-env.eba-ruyxxcr5.ap-northeast-1.elasticbeanstalk.com を見てみると、ちゃんと「猫は偉大。」と表示されています。なるほど、コードをアップロードするだけで勝手にデプロイされているらしい。なんかよく分からんけど動くからヨシッ!
もう満足したので環境を削除します。次のように削除は簡単です。
Flask で書かれたアプリケーションをデプロイ
Python だけだと味気ないので Flask で書かれたアプリケーションでもデプロイしてみましょうかね。Flask なんも知らんけど。先程と同様に、アプリケーション名は GreatFlaskApplication として(自分が作るものはなんでも偉大なので)、プラットフォームは Python にします。また、先ほどはアプリケーションコードとして「サンプルアプリケーション」を選択しましたが、今回は「コードのアップロード」を選択します。また、コードについては Deploying a Flask App to AWS Elastic Beanstalk を参考に、次のようなディレクトリ構成をとることにします。
. |--- .ebextensions | |--- app.config |--- application.py |--- requirements.txt
中身は次の通りです。
app.config
files: "/etc/httpd/conf.d/wsgi_custom.conf": mode: "000644" owner: root group: root content: WSGIApplicationGroup %{GLOBAL}
requirements.txt
Flask==1.1.1
application.py
from flask import Flask application = Flask(__name__) @application.route("/") def index(): return "Your Flask App Works!" @application.route("/hello") def hello(): return "Hello World!" if __name__ == "__main__": application.run(port=5000, debug=True)
これらを zip に固めて「ソースコード元」としてアップロードします。これだけでいいのですが、今回は「より多くのオプションを設定」を選んでどんな設定があるのか見てみましょう。
- プリセット:インスタンスの種類を選べます。
他にも設定がありますが、そのままで使います。
がちょんがちょん...。CloudWatch アラームだったり Auto Scaling group、EC2 インスタンス、セキュリティグループ、S3 バケットが作成されているみたいです。
できたみたいです。ブラウザで見てみます。
やったー動いた ✌️
自動で作られた AWS サービスを眺める
なるほど、確かに簡単にアプリケーションをデプロイできることは確認できました。しかし、何が作られたのか分からないままではかかる料金も分からないので気になります。新卒はお金がないので、AWS を触る時は料金に敏感です。
コンソール上から色々みると、大体「ウェブサーバー環境」のようになっているみたいです。(Route53 は作成されてません)
また、サブネットやインターネットゲートウェイのアタッチ、ルーティングなど様々なことをやってくれているみたいです。勉強になってありがたい。
最後に
今回は AWS Elastic Beanstalk について、実際にアプリケーションをデプロイする事と自動作成されたサービスを眺めることを目標に書きました。SAA の勉強で見たことが多かったので、「これ進 ◯ ゼミで見たやつ!!」という状態で楽しかったです。次回は EKS か Lambda Layer について書く予定ですが、気まぐれなので好きなことを書きます。
今回に限らず、間違った認識をしていれば指摘してください。勉強になるので。
関係ないですが、パフェが食べたいです。
参考
徹底攻略 AWS 認定ソリューションアーキテクト -アソシエイト教科書