現代のソフトウェア開発において、マイクロサービスは極めて重要なアーキテクチャです。複雑なシステムのスケーラビリティ、柔軟性の確保、効率的な管理を可能にしています。

マイクロサービスは、特定のタスクを実行する小規模で独立したアプリケーションであり、柔軟なデプロイとスケーリングができます。ソフトウェア設計におけるこのようなモジュラー型アプローチは、コンポーネント間の結合を緩め、開発全体を通じて柔軟性と管理性を高めます。

この記事では、マイクロサービスの概要、その機能、Pythonを使った構築方法についてご説明します。また、Dockerfileを使用してKinstaにマイクロサービスをデプロイする流れも実演したいと思います。

マイクロサービスとは

マイクロサービスはアプリケーション内の独立した自律的なサービスで、それぞれが特定のビジネスニーズに対応します。これが軽量なAPIやメッセージブローカーを通じて通信し、包括的なシステムを形成します。

完全に需要に基づいてスケールするモノリシックなシステムとは異なり、マイクロサービスはトラフィックの多い個々のコンポーネントのスケーリングを可能にします。モノリシックな構造とは正反対に、このようなアーキテクチャでは、障害管理や機能アップデートを容易に行うことができます。

マイクロサービスの利用には、以下のようなメリットがあります。

  • 柔軟性とスケーラビリティ:個々のサービスを切り離すことで、トラフィックが多い特定のサービスのインスタンスを実行するノード数を増やすことができる
  • コードのモジュール性:各サービスが個別のテクノロジースタックを使用でき、それぞれに最適な開発ツールを選択できる

しかし、マイクロサービスアーキテクチャには課題もあります。

  • 複数のサービスの監視:特定のサービスのインスタンスが複数のノードにデプロイされ分散しているため、システム内の個々のサービスを監視することが困難(この難しさは、ネットワーク障害やその他のシステム問題の発生時に特に顕著になります)
  • コスト:マイクロサービスアプリケーションの開発は、複数のサービスを管理するためのコストがかかるため、モノリシックなシステムを構築するよりも大幅にコストがかかる可能性がある(各サービスに独自のインフラとリソースが必要なため、特にシステムをスケールアップする際にコストがかかる可能性あり)

Pythonを使ってマイクロサービスを設計する方法

マイクロサービスアーキテクチャの利点がわかったところで、いよいよPythonを使ってマイクロサービスを構築してみましょう。

この例では、ECウェブアプリケーションを構築したいとします。ウェブサイトには、商品カタログ、注文リスト、支払い処理システムとログなど、いくつかのコンポーネントがあり、それぞれを独立したサービスとして実装します。さらに、HTTPのような、これらのサービス間でデータを効率的に転送するためのサービス間通信メソッドを確立する必要があります。

Pythonを使って、商品カタログを管理するマイクロサービスを構築してみましょう。このマイクロサービスでは、指定したソースから商品データを取得し、JSON形式でデータを返すようにします。

前提条件

この説明を読み進めるには、以下のものが必要です。

1. プロジェクトの作成

  1. まず、flask-microserviceというプロジェクト用のフォルダを作成し、カレントディレクトリをプロジェクトのディレクトリに作成します。
  2. 次に、python3 --versionを実行して、Pythonがコンピュータに正しくインストールされていることを確認します。
  3. 以下のコマンドを実行して、virtualenvをインストールし、Flaskマイクロサービス用の隔離された開発環境を作成します。
    pip3 install virtualenv
  4. 以下のコマンドを実行して仮想環境を作成します。
    virtualenv venv
  5. 最後に、お使いのコンピュータのオペレーティングシステムに応じて、以下のコマンドのいずれかを使用して仮想環境を有効にします。
    # Windows: 
    .\venv\Scripts\activate
    # Unix or macOS:
    source venv/bin/activate

2. Flaskサーバーのセットアップ

ルートディレクトリにrequirements.txtファイルを作成し、以下の依存関係を追加します。

flask
requests

pip3コマンドを実行して依存関係をインストールします。

pip install -r requirements.txt

次に、ルートディレクトリに新しいフォルダを作成し、名前をservicesとします。このフォルダの中にproducts.pyという新しいファイルを作成し、Flaskサーバーをセットアップするために以下のコードを追加します。

import requests
import os
from flask import Flask, jsonify
app = Flask(__name__)
port = int(os.environ.get('PORT', 5000))

@app.route("/")
def home():
    return "Hello, this is a Flask Microservice"
if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0", port=port)

上のコードでは、基本的なFlaskサーバーをセットアップしています。Flaskアプリを初期化し、ルートURL("/")のルートを1つ定義し、アクセスされると"Hello, this is a Flask Microservice" というメッセージを表示します。サーバーは、環境変数から取得するかデフォルトのポート5000から指定されたポートで実行され、デバッグモードで開始し、受信するリクエストを処理できるようにします。

3. APIエンドポイントの定義

サーバーが構成された状態で、一般にアクセス可能なAPIから商品データを取得するマイクロサービスのAPIエンドポイントを作成します。以下のコードをproducts.pyファイルに追加します。

BASE_URL = "https://dummyjson.com"
@app.route('/products', methods=['GET'])
def get_products():
    response = requests.get(f"{BASE_URL}/products")
    if response.status_code != 200:
        return jsonify({'error': response.json()['message']}), response.status_code
    products = []
    for product in response.json()['products']:
        product_data = {
            'id': product['id'],
            'title': product['title'],
            'brand': product['brand'],
            'price': product['price'],
            'description': product['description']
        }
        products.append(product_data)
    return jsonify({'data': products}), 200 if products else 204

上記のコードは、Flaskサーバーに/productsエンドポイントを作成するものです。GETリクエストでアクセスされると、ダミーのAPIから商品データを取得します。成功すれば、取得したデータを処理し、商品の詳細情報を抽出し、JSON形式で情報を返します。エラーや利用可能なデータがない場合は、関連するエラーメッセージとステータスコードで応答します。

4. マイクロサービスのテスト

ここまでで、シンプルなマイクロサービスのセットアップが完了しました。サービスを開始するには、開発サーバーを用意します。開発サーバーはhttp://localhost:5000で起動します。

flask --app services/products run

Postmanクライアントを使って/productsエンドポイントにGETリクエストを送ると、下のスクリーンショットと同じようなレスポンスが返ってくるはずです。

PostmanのダミーAPI商品エンドポイントへのGETリクエスト成功
PostmanでHTTP API GET リクエストをテストする

Pythonマイクロサービスで認証と認可を実装する方法

マイクロサービスを構築する際には、認証や認可といった堅牢なセキュリティ強化策を実装することが重要です。マイクロサービスのセキュリティを確保することで、許可されたユーザーだけがサービスにアクセスして使用できるようになり、機密データを保護して悪意のある攻撃を防ぐことができます。

マイクロサービスでセキュアな認証と認可を実装する効果的な方法のひとつに、JSON Webトークン(JWT)があります。

JWTは広く使われているオープンスタンダードで、クライアントとサーバー間で認証情報を送信する安全で効率的な方法です。JWTはコンパクトで、暗号化され、デジタル署名されたトークンで、HTTPリクエストと一緒に渡すことになります。各リクエストにJWTを含めると、サーバーはユーザーの身元と権限を素早く確認可能です。

マイクロサービスにJWT認証を実装するには、次のようにします。

  1. Pythonのpyjwtパッケージをrequirements.txtファイルに追加し、pip install -r requirements.txtを使って依存関係を再インストールします。
  2. サービスには専用のデータベースがないので、プロジェクトのルートディレクトリにusers.jsonファイルを作成し、認証されたユーザー一覧を保存します。そのファイルに以下のコードを貼り付けます。
    [
        {   
            "id": 1,
            "username": "admin",
            "password": "admin"
        
        }
    ]

  3. そして、services/products.pyファイルのimport文を以下のように置き換えてください。
    import requests 
    from flask import Flask, jsonify, request, make_response
    import jwt
    from functools import wraps
    import json
    import os
    from jwt.exceptions import DecodeError

    HTTPリクエストの処理、Flaskアプリの作成、JSONデータの管理、JWT ベースの認証の実装、例外処理のためにこれらのモジュールをインポートし、Flaskサーバー内で幅広い機能を使えるようにします。

  4. Flaskアプリのインスタンス生成の下に以下のコードを追加して、JWTトークンの署名に使用する秘密鍵を生成します。
    app.config['SECRET_KEY'] = os.urandom(24)
  5. JWTを検証するには、デコレータ関数を作成し、FlaskサーバーコードのAPIルートの上に以下のコードを追加します。このデコレータ関数は、ユーザーが保護されたルートにアクセスする前に、ユーザーを認証・検証するためのものです。
    def token_required(f):
        @wraps(f)
        def decorated(*args, **kwargs):
            token = request.cookies.get('token')
            if not token:
                return jsonify({'error': 'Authorization token is missing'}), 401
            try:
                data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=["HS256"])
                current_user_id = data['user_id']
            except DecodeError:
                return jsonify({'error': 'Authorization token is invalid'}), 401
            return f(current_user_id, *args, **kwargs)
        return decorated

    このデコレータ関数は、リクエストヘッダまたはクッキーにあるはずの JWT 認証トークンがあるかどうか、HTTPリクエストをチェックします。トークンが見つからないか無効な場合には、デコレータはレスポンスとしてunauthorized status codeメッセージを送信します。

    逆に、有効なトークンが存在する場合、デコレータはそれをデコードした後にユーザーIDを抽出します。この処理により、認可されたユーザーのみにアクセスを許可することで、APIエンドポイントを保護します。

  6. 以下のコードを使用して、ユーザー認証用のAPIエンドポイントを定義します。
    with open('users.json', 'r') as f:
        users = json.load(f)
    @app.route('/auth', methods=['POST'])
    def authenticate_user():
        if request.headers['Content-Type'] != 'application/json':
            return jsonify({'error': 'Unsupported Media Type'}), 415
        username = request.json.get('username')
        password = request.json.get('password')
        for user in users:
            if user['username'] == username and user['password'] == password:
                token = jwt.encode({'user_id': user['id']}, app.config['SECRET_KEY'],algorithm="HS256")
                response = make_response(jsonify({'message': 'Authentication successful'}))
                response.set_cookie('token', token)
                return response, 200
        return jsonify({'error': 'Invalid username or password'}), 401

    ユーザーを認証・認可するために、/auth API エンドポイントが、POSTリクエストのJSONペイロード内の認証情報を、許可されたユーザーの一覧と照合します。情報が有効であると判断された場合には、ユーザーのIDとアプリの秘密鍵を使用してJWTトークンを生成し、トークンをレスポンスのクッキーとして設定します。ユーザーはこのトークンを使って、以降のAPIリクエストを行うことができます。

    /authエンドポイントを作成したら、Postmanを使ってhttp://localhost:5000/authに HTTP POSTリクエストを送信します。リクエストボディには、作成したサンプル管理ユーザーの認証情報を記載します。

    リクエストボディを表示(Postmanを使ったリクエスト)
    リクエストボディを表示(Postmanを使ったリクエスト)

    リクエストが成功すると、APIはJWTトークンを生成し、Postmanのクッキーに設定し、認証後の成功レスポンスを送信します。

  7. 最後に、GET APIエンドポイントを更新して、以下のコードを使用してJWTトークンをチェックし、検証します。
    @app.route('/products', methods=['GET'])
    @token_required
    def get_products(current_user_id):
        headers = {'Authorization': f'Bearer {request.cookies.get("token")}'}    
        response = requests.get(f"{BASE_URL}/products", headers=headers)
        if response.status_code != 200:
            return jsonify({'error': response.json()['message']}), response.status_code
        products = []
        for product in response.json()['products']:
            product_data = {
                'id': product['id'],
                'title': product['title'],
                'brand': product['brand'],
                'price': product['price'],
                'description': product['description']
            }
            products.append(product_data)
        return jsonify({'data': products}), 200 if products else 204

DockerでPythonマイクロサービスをコンテナ化する方法

Dockerは、アプリケーションとその依存関係を分離された開発環境でパッケージ化するプラットフォームです。マイクロサービスをコンテナにパッケージ化することで、各サービスがコンテナ内で独立して実行され、サーバーへのデプロイと管理プロセスを効率化できます。

マイクロサービスをコンテナ化するには、コンテナ内でアプリケーションを実行するために必要な依存関係を指定したDockerfileからDockerイメージを作成する必要があります。プロジェクトのルートディレクトリにDockerfileを作成し、以下の指示を追加します。

FROM python:3.9-alpine
WORKDIR /app
COPY requirements.txt ./
RUN pip install -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "./services/products.py"]

イメージをビルドする前に、以下のコマンドを確認してください。

  • FROM:Dockerに使用するベースイメージを指示します。ベースイメージは、コンテナ内でFlaskアプリケーションを実行するためのソフトウェアと依存関係を含むビルド済みのインスタンスです。
  • WORKDIR:コンテナ内の指定ディレクトリを作業ディレクトリとして設定します。
  • COPY requirements.txt ./requirements.txtファイル内の依存関係をコンテナのrequirements.txtファイルにコピーします。
  • RUN:指定されたコマンドを実行し、イメージが必要とする依存関係をインストールします。
  • COPY . .:プロジェクトのルートディレクトリからコンテナ内の作業ディレクトリにすべてのファイルをコピーします。
  • EXPOSE:コンテナがリクエストをリッスンするポートを指定します。ただし、Dockerはこのポートをホストマシンに公開しません。
  • CMD:コンテナ起動時に実行するデフォルトコマンドを指定します。

次に、プロジェクトのルートディレクトリに.dockerignoreファイルを追加し、Dockerイメージが除外すべきファイルを指定します。イメージの内容を制限することで、最終的なサイズと関連するビルド時間が短縮できます。

/venv
/services/__pycache__/
.gitignore

次に、以下のコマンドを実行してDockerイメージをビルドします。

docker build -t flask-microservice .

最後に、イメージがビルドできたら、以下のコマンドを使ってDockerコンテナでマイクロサービスを実行することができます。

docker run -p 5000:5000 flask-microservice

このコマンドが、マイクロサービスを実行するDockerコンテナを起動し、コンテナ上のポート5000をホストマシン上のポート5000に公開します。これにより、ウェブブラウザまたはPostmanからURL http://localhost:5000を使用してHTTPリクエストを行うことができます。

KinstaにPythonマイクロサービスをデプロイする

Kinstaは、ウェブアプリケーション、データベース用ホスティングソリューションを提供しています。本番環境でPythonマイクロサービスとバックエンドAPIをシームレスにデプロイして管理することが可能です。

以下の手順に従って、FlaskマイクロサービスをMyKinstaでデプロイすることができます。

  1. まず、ルートディレクトリに新しいProcfileを作成し、以下のコードを追加します。これが、Pythonアプリケーション用のKinstaのGunicorn WSGI HTTP Server上でFlaskマイクロサービスを実行するコマンドを指定します。
    web: gunicorn services.wsgi
  2. requirements.txtファイルにGunicornの依存関係を追加します。
    gunicorn==20.1.*
  3. 次に、新しいservices/wsgi.pyファイルを作成し、以下のコードを追加します。
    from services.products import app as application
    if __name__ == "__main__":
                    application.run()
  4. プロジェクトのルートフォルダに.gitignoreファイルを作成し、以下を追加します。
    services/__pycache__
    venv
  5. 最後に、GitHubに新しいリポジトリを作成してプロジェクトファイルをプッシュします。

リポジトリの準備ができたら、以下の手順に従ってFlaskマイクロサービスをKinstaにデプロイします。

  1. ログインするか、アカウントを作成してコントロールパネル「MyKinsta」に移動します。
  2. Gitサービス(BitbucketGitHub、またはGitLab)でKinstaを認証します。
  3. 左サイドバーの「アプリケーション」をクリックし、「アプリケーションの作成」をクリックします。
  4. ダッシュボードからであれば、「サービスを追加」をクリックし、「アプリケーション」を選択してください。
  5. デプロイするリポジトリとブランチを選択します。
  6. アプリに一意の名前を割り当て、「データセンターの所在地」を選択します。
  7. ビルド」環境を設定するには、「Dockerfileを使用してコンテナイメージを設定」を選択します。
  8. Dockerfileへのパスとコンテキストを指定します。
  9. 最後に、その他の情報を確認し「アプリケーションを作成」をクリックします。

マイクロサービスのテスト

デプロイプロセスが完了したら、提供されたURLをクリックし、PostmanでHTTPリクエストを行ってマイクロサービスをテストします。rootエンドポイントにGETリクエストを実行してみましょう。

Homeルートのデプロイ済みマイクロサービスURLへのGETリクエスト成功
マイクロサービスのproduct エンドポイントに HTTP API GET リクエストを行う

認証してJWTトークンを生成するには、/auth APIエンドポイントにPOSTリクエストを送信し、リクエスト本文に管理者認証情報を渡します。

Postmanでの認証のためのMicroservice Auth EndpointへのPOSTリクエスト
HTTP API POST リクエストをマイクロサービスのauthエンドポイントに送信する

最後に、認証に成功したら、/productsエンドポイントにGETリクエストを送り、データを取得します。

PostmanのproductsエンドポイントへのHTTP API GETリクエスト成功
HTTP API GETリクエストをマイクロサービスproductsエンドポイントへ

まとめ

アプリケーションのサイズと複雑さが増すにつれて、利用可能なリソースに負担をかけることなくソフトウェアシステムを拡張できるアーキテクチャパターンを採用することが非常に重要です。

マイクロサービスアーキテクチャは、スケーラビリティ、開発の柔軟性、保守性を高め、複雑なアプリケーションの管理を容易にします。

Kinstaはマイクロサービスのホスティングプロセスを簡素化する優れた選択肢です。Kinstaでは、お好みのデータベースを簡単に使用でき、一つのコントロールパネルを使ってアプリケーションデータベースの両方を手間なく管理することができます。

Jeremy Holcombe Kinsta

Kinstaのコンテンツ&マーケティングエディター、WordPress開発者、コンテンツライター。WordPress以外の趣味は、ビーチでのんびりすること、ゴルフ、映画。高身長が特徴。