はじめに
Streamlitで作った簡易アプリをメンバーが気軽に開発できる形で共有しようと思い、自前でデプロイパイプラインを組むことなく自動デプロイが可能なAWS App Runnerの利用を検討したのですが、どうやら現状だとApp RunnerがWebSocketに対応しておらずStreamlitが動かなそうということだったので、ひとまず勉強がてらEC2上にStreamlit実行環境を整備し、CodePipeline + CodeDeployでデプロイパイプラインを構築しました。その際の備忘録となります。
やりたいこと
- セキュアでなくてよいのでStreamlitアプリをWebに公開したい
- gitプッシュだけでデプロイできる環境を構築したい
- AWSを使いたい
準備
GitHubリポジトリ
以下のようなディレクトリ構成のStreamlitアプリがmy-app
というリポジトリ名でGitHubで管理されているとします。
my-app
├ scripts
│ ├ start_server.sh
│ └ stop_server.sh
├ main.py
├ run.sh
└ appspec.yml
main.py
は以下のようにStreamlitでHello Worldを出力するだけのコードです。
import streamlit as st
st.title('Hello, World!')
またrun.sh
はアプリの自動復旧に利用するスクリプトで、そのほかはデプロイパイプラインの実装時に作成するファイルとなります(後述)。
GitHubアクセストークン
デプロイパイプラインでGitHubからソースコードを取得する際にアクセストークンが必要となるため、GitHubのDeveloper settingsのページから取得しておきます。
AWS CLI
あとで利用するので、Homebrewなどで予めインストールしておきます。
$ brew install awscli
EC2でホスティングする
まずはEC2インスタンス上でアプリ起動してブラウザから閲覧できるようにするところまでを実装します。
インスタンスの立ち上げ
AWSコンソールからEC2インスタンスを手動で立ち上げます。今回はおおまかに以下のような設定で立ち上げました。
項目 | 内容 |
---|---|
名前とタグ |
my-app とした。CodeDeployでのデプロイ対象を指定する際に利用。 |
AMI | Amazon Linux 2 Kernel 5.10 |
インスタンスタイプ | t2.nano |
セキュリティグループ |
セキュリティグループを作成する を選択し、新規作成。 |
ボリューム | 1 ボリューム - 8 GiB |
IAMロール |
AmazonS3ReadOnlyAccess ポリシーを持つロールを新規作成し付与。あとでCodeDeployと連携する際に利用。 |
立ち上げ作業中にキーペア設定が要求され、キーペアファイルがローカルに保存されるはずですので、権限を付与(chmod 400
)した上で適切な場所に格納しておきます。あとでSSH接続する際に使います。
インバウンドルールの設定
EC2インスタンスの立ち上げ時に作成した新しいセキュリティグループに対してインバウンドルールの設定を行います。
Streamlitはデフォルトで8501番ポートを利用するため、カスタムTCPとして開けておきます。また「ソース」として送信元IPアドレスを制限することが可能なので、必要に応じて設定します。
アプリを立ち上げてみる
ローカルにあるmy-app
ディレクトリをEC2インスタンス上にコピーします。
ユーザー名はAmazon Linuxであればデフォルトでec2-user
ですので、これでログインします。xxx.xxx.xxx.xxx
にはEC2インスタンスに割り当てられたパブリックIPアドレスを指定します。
# my-appディレクトリをEC2インスタンス上にコピー
$ scp -r -i <キーペアファイルのパス> ./my-app ec2-user@xxx.xxx.xxx.xxx:/home/ec2-user/
次にEC2インスタンスにSSH接続(ssh -i <キーペアファイルのパス> ec2-user@xxx.xxx.xxx.xxx:/home/ec2-user/my-app
)した上で、以下を実行します。
# pipでStreamlitをインストール
$ pip3 install streamlit
# アプリを実行
$ cd /home/ec2-user/my-app
$ streamlit run main.py
上記実行後、パブリックIPアドレスの8501番ポートにアクセスして以下のように表示されればOKです。
自動復旧させる
万が一アプリケーションのプロセスが落ちても自動復旧できるよう、systemd
を利用した自動復旧の仕組みを構築します。
まずアプリケーション実行用のスクリプトrun.sh
をmy-appディレクトリ配下に作成し、chmod 755 run.sh
しておきます。
#!/bin/sh
~/.local/bin/streamlit run main.py # systemdから実行するため、streamlitの実行ファイルはフルパスで指定
次に、run.sh
をシステムのデーモンから実行できるようにするため、/etc/systemd/system/
配下にmy-app.service
ファイルを作成します。
[Unit]
Description=Run my-app
After=network.target
[Service]
WorkingDirectory=/home/ec2-user/my-app
ExecStart=/home/ec2-user/my-app/run.sh
Restart=always
User=ec2-user
[Install]
WantedBy=multi-user.target
最後に以下を実行してアプリ起動 & 自動実行の設定をします。
$ sudo systemctl start my-app.service # サービスを起動
$ sudo systemctl enable my-app.service # サービスの自動実行を有効化
これによりデーモンが立ち上がり、アプリが落ちていても自動復旧するようになります。
CodePipeline + CodeDeployで自動デプロイ
このままだとデプロイ作業が面倒なため、ここからはGitHubリポジトリへのプッシュをトリガーにして自動デプロイが実行されるような環境を構築していきます。今回はCodePipelineとCodeDeployを利用します。
CodeDeploy Agentのインストール
まずはCodeDeployからEC2インスタンスへのデプロイを行えるようにするため、EC2インスタンスにSSH接続してCodeDeploy Agentをインストールしておきます。
こちらを参考に、EC2インスタンス上で以下を実行します。
$ sudo yum update
$ sudo yum install ruby
$ sudo yum install aws-cli
$ cd /home/ec2-user
$ aws s3 cp s3://aws-codedeploy-ap-northeast-1/latest/install . --region ap-northeast-1 # 東京リージョンの場合
$ chmod +x ./install
$ sudo ./install auto
さらに以下を実行し、CodeDeploy Agentが起動しているかを確認します。
$ sudo service codedeploy-agent status
The AWS CodeDeploy agent is running as PID xxxx
のような応答があればOKです。
フックスクリプトとappspec.ymlを作成
CodeDeployではデプロイのタイミングで発生するイベントをトリガーとして任意のスクリプトを実行することが出来ます。これを利用して、systemdからアプリを一時停止・再開するための仕組みを作ります。
以下のようにstop_server.sh
とstart_server.sh
を作成し、my-app/scripts
ディレクトリ配下に置きます。
#!/bin/sh
sudo systemctl disable my-app.service
sudo systemctl stop my-app.service
#!/bin/sh
sudo systemctl enable my-app.service
sudo systemctl start my-app.service
さらに、appspec.yml
というCodeDeployでのデプロイ処理を制御するための設定ファイルをmy-app
ディレクトリ配下に作成します。先ほど作成したスクリプトはhooksという定義から参照されていますが、これによりデプロイ時のイベント(この例ではBeforeInstallおよびAfterInstall)をトリガーとして実行されるようになります。
デプロイ時に発生する各種イベントの詳細についてはこちらをご参照ください。
version: 0.0
os: linux
files:
- source: /
destination: /home/ec2-user/my-app
hooks:
BeforeInstall:
- location: scripts/stop_server.sh
AfterInstall:
- location: scripts/start_server.sh
file_exists_behavior: OVERWRITE
CloudFormationでパイプラインをデプロイ
ここまででCodeDeployを利用するための準備が終わったので、ここからはCodePipelineリソースとCodeDeployリソースをそれぞれ作成していきます。ただしAWSコンソールから手作業で設定するのは結構大変なため、ここではCloudFormationを利用し、スタックとしてまとめて立ち上げます。CloudFormationテンプレートは以下のようになります。
AWSTemplateFormatVersion: "2010-09-09"
Description: deploy pipeline for my-app
Parameters:
AppName:
Type: String # アプリケーション名
Branch:
Type: String # デプロイのトリガーとするブランチ名
Owner:
Type: String # リポジトリのオーナー名
Repo:
Type: String # リポジトリ名
OAuthToken:
Type: String # GitHubのDeveloper settingsから発行したアクセストークン
NoEcho: true
Resources:
# S3::Bucketの定義。CodePipelineのSourceフェーズでGitHubからダウンロードしたソースコードを保持するために利用。
ArtifactStoreBucket:
Type: AWS::S3::Bucket
Properties:
VersioningConfiguration:
Status: Enabled
# CodeDeploy::Applicationの定義
CodeDeployApplication:
Type: AWS::CodeDeploy::Application
Properties:
ApplicationName: !Ref AppName
ComputePlatform: Server
# CodeDeploy::DeploymentGroupの定義
CodeDeployDeploymentGroup:
Type: AWS::CodeDeploy::DeploymentGroup
DependsOn: CodeDeployRole
Properties:
ApplicationName: !Ref CodeDeployApplication
DeploymentGroupName: !Sub ${CodeDeployApplication}-DeployGroup
DeploymentConfigName: CodeDeployDefault.AllAtOnce
ServiceRoleArn: !Sub arn:aws:iam::${AWS::AccountId}:role/${CodeDeployApplication}-CodeDeployRole
DeploymentStyle:
DeploymentType: IN_PLACE
DeploymentOption: WITHOUT_TRAFFIC_CONTROL
Ec2TagFilters:
- Type: VALUE_ONLY
Value: !Ref AppName # デプロイ対象となるEC2インスタンスのタグ
# CodePipeline::Pipelineの定義。今回は最小構成のため、SourceフェーズとDeployフェーズの2段階のみ。
CodePipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
Name: !Sub ${CodeDeployApplication}-Pipeline
RoleArn: !GetAtt CodePipelineRole.Arn
ArtifactStore:
Location: !Ref ArtifactStoreBucket
Type: S3
Stages:
- Name: Source
Actions:
- Name: Source
ActionTypeId:
Category: Source
Owner: ThirdParty
Provider: GitHub
Version: 1
Configuration:
Branch: !Ref Branch
OAuthToken: !Ref OAuthToken
Owner: !Ref Owner
PollForSourceChanges: true
Repo: !Ref Repo
OutputArtifacts:
- Name: SourceArtifact
Region: !Ref AWS::Region
Namespace: SourceVariables
RunOrder: 1
- Name: Deploy
Actions:
- Name: Deploy
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: CodeDeploy
Version: 1
Configuration:
ApplicationName: !Ref CodeDeployApplication
DeploymentGroupName: !Sub ${CodeDeployApplication}-DeployGroup
InputArtifacts:
- Name: SourceArtifact
Region: !Ref AWS::Region
Namespace: DeployVariables
RunOrder: 1
# CodePipeline用のロール
CodePipelineRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: codepipeline.amazonaws.com
Path: /
Policies:
- PolicyName: CodePipelineAccess
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- s3:*
- iam:PassRole
- codedeploy:CreateDeployment
- codedeploy:GetApplication
- codedeploy:GetApplicationRevision
- codedeploy:GetDeployment
- codedeploy:GetDeploymentConfig
- codedeploy:RegisterApplicationRevision
Resource: "*"
# CodeDeploy用のロール
CodeDeployRole:
Type: AWS::IAM::Role
Properties:
Path: /
RoleName: !Sub ${CodeDeployApplication}-CodeDeployRole
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: codedeploy.amazonaws.com
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonS3FullAccess
- arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole
上記テンプレートをデプロイするため、以下のAWS CLIコマンドを実行します。
(今回はAPPNAME
とREPO
はともにmy-app
とし、BRANCH
はmainを指定しました)
$ aws cloudformation create-stack \
--stack-name ${APPNAME}-pipeline-stack \
--template-body file://pipeline_template.yml \
--capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM \
--parameters \
ParameterKey=OAuthToken,ParameterValue="${TOKEN}" \ # GitHubのDeveloper settingsから発行したアクセストークン
ParameterKey=Branch,ParameterValue="${BRANCH}" \ # デプロイのトリガーとなるブランチ名
ParameterKey=Owner,ParameterValue="${OWNER}" \ # リポジトリのオーナー名
ParameterKey=Repo,ParameterValue="${REPO}" \ # リポジトリ名
ParameterKey=AppName,ParameterValue="${APPNAME}" # アプリケーション名
上記実行後、CloudFormationのコンソール画面にて、作成したスタック(my-app-stack
)のステータスがCREATE_COMPLETE
になっていればOKです。
またCodePipelineのコンソール画面から、新たにmy-app-Pipeline
というパイプラインが作成されていることも確認できます。
動作確認する
パイプラインが正しく動作するか確認してみます。
main.py
を以下のように書き換えてmainブランチにpushします。
import streamlit as st
- st.title('Hello, World!')
+ st.title('Hello, World!!!!!!!!')
作成したmy-app-Pipeline
のコンソール画面からデプロイ進捗を見守り、、
Deployフェーズが成功したことを確認後にブラウザからアクセスしたところ、ちゃんと動いてそうです。
おわりに
今回は勉強を兼ねての実装でしたが、実際デプロイパイプラインの構築は多少面倒なため、可能であればStreamlitのComunity CloudやHerokuなどのサービスを使ったほうが楽なのかもしれません。またSystemdでの自動復旧、デプロイ時の実行スクリプトからサービス一時停止・再開などはかなり手探りで組んだのですが、このあたりはもっと良いやり方がありそうです。
参考