7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Streamlitで作ったアプリのEC2ホスティング&デプロイ自動化

Last updated at Posted at 2023-02-16

はじめに

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を出力するだけのコードです。

main.py
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アドレスを制限することが可能なので、必要に応じて設定します。
スクリーンショット 2023-02-16 14.53.39.png

アプリを立ち上げてみる

ローカルにある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しておきます。

run.sh
#!/bin/sh
~/.local/bin/streamlit run main.py  # systemdから実行するため、streamlitの実行ファイルはフルパスで指定

次に、run.shをシステムのデーモンから実行できるようにするため、/etc/systemd/system/配下にmy-app.serviceファイルを作成します。

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.shstart_server.shを作成し、my-app/scriptsディレクトリ配下に置きます。

stop_server.sh
#!/bin/sh

sudo systemctl disable my-app.service
sudo systemctl stop my-app.service
start_server.sh
#!/bin/sh

sudo systemctl enable my-app.service
sudo systemctl start my-app.service

さらに、appspec.ymlというCodeDeployでのデプロイ処理を制御するための設定ファイルをmy-appディレクトリ配下に作成します。先ほど作成したスクリプトはhooksという定義から参照されていますが、これによりデプロイ時のイベント(この例ではBeforeInstallおよびAfterInstall)をトリガーとして実行されるようになります。
デプロイ時に発生する各種イベントの詳細についてはこちらをご参照ください。

appspec.yml
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テンプレートは以下のようになります。

pipeline_template.yml
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コマンドを実行します。
(今回はAPPNAMEREPOはともに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です。
スクリーンショット 2023-02-16 11.58.48.png

またCodePipelineのコンソール画面から、新たにmy-app-Pipelineというパイプラインが作成されていることも確認できます。
スクリーンショット 2023-02-16 12.19.51.png

動作確認する

パイプラインが正しく動作するか確認してみます。
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での自動復旧、デプロイ時の実行スクリプトからサービス一時停止・再開などはかなり手探りで組んだのですが、このあたりはもっと良いやり方がありそうです。

参考

7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?