「Pulumi Automation API」でPulumi CLIの機能をコード化しよう

2023年8月8日(火)
大関 研丞 (Kenneth Ozeki)
第6回となる今回は、Automation APIの概要を解説し、実際にAutomation APIを使ったRESTful APIの静的Webサーバーを構築するハンズオンを実践していきます。

Pulumi Program + Automation API コード編集

PythonファイルにPulumi ProgramとAutomation APIコードを記載します。

  1. 作業用ディレクトリを作成します。
    $ mkdir pulumi_over_http && cd pulumi_over_http
    
    $ pwd   
    /****/pulumi_over_http
  2. pythonファイル(app.py)を新規で作成し、以下コードを記載します。
    $ vi app.py
    import pulumi
    from pulumi import automation as auto
    from pulumi_aws import s3
    from flask import Flask, request, make_response, jsonify
    
    
    def ensure_plugins():
        ws = auto.LocalWorkspace()
        ws.install_plugin("aws", "v4.0.0")
    
    
    ensure_plugins()
    app = Flask(__name__)
    project_name = "pulumi_over_http"
    
    
    # Pulumi Program (静的Webサーバーの作成->ユーザーからのHTTPリクエストで読み込まれる)
    def create_pulumi_program(content: str):
        # S3 Bucket作成
        site_bucket = s3.Bucket(
            "s3-website-bucket",
            website=s3.BucketWebsiteArgs(index_document="index.html")
        )
        index_content = content
    
        # Bucket Public Access Block作成(Objectパブリック公開のための設定)
        s3.BucketPublicAccessBlock(
            "bucket-public-access-block",
            bucket=site_bucket.id,
            block_public_acls=False,
            block_public_policy=False,
            ignore_public_acls=False,
            restrict_public_buckets=False
        )
    
        # Bucket Object作成
        s3.BucketObject(
            "index",
            bucket=site_bucket.id,
            content=index_content,
            key="index.html",
            content_type="text/html; charset=utf-8"
        )
    
        # Bucket Policy作成
        s3.BucketPolicy(
            "bucket-policy",
            bucket=site_bucket.id,
            policy={
                "Version": "2012-10-17",
                "Statement": {
                    "Effect": "Allow",
                    "Principal": "*",
                    "Action": ["s3:GetObject","s3:PutObject"],
                    "Resource": [pulumi.Output.concat("arn:aws:s3:::", site_bucket.id, "/*")]
                }
            }
        )
    
        # WebサイトURLのエクスポート
        pulumi.export("website_url", site_bucket.website_endpoint)
    
    # コンテンツ新規作成のルーティング
    @app.route("/sites", methods=["POST"])
    def create_handler():
        """creates new sites"""
        stack_name = request.json.get('id')
        content = request.json.get('content')
        try:
            def pulumi_program():
                return create_pulumi_program(content)
            # Automation API (Stackの新規作成)
            stack = auto.create_stack(
                stack_name=stack_name,
                project_name=project_name,
                program=pulumi_program
            )
            stack.set_config("aws:region", auto.ConfigValue("ap-northeast-1"))
            # Automation API (Stackのデプロイ)
            up_res = stack.up(on_output=print)
            return jsonify(id=stack_name, url=up_res.outputs['website_url'].value)
        except auto.StackAlreadyExistsError:
            return make_response(f"stack '{stack_name}' already exists", 409)
        except Exception as exn:
            return make_response(str(exn), 500)
    
    # コンテンツ一覧取得のルーティング
    @app.route("/sites", methods=["GET"])
    def list_handler():
        """lists all sites"""
        try:
            ws = auto.LocalWorkspace(project_settings=auto.ProjectSettings(name=project_name, runtime="python"))
            stacks = ws.list_stacks()
            return jsonify(ids=[stack.name for stack in stacks])
        except Exception as exn:
            return make_response(str(exn), 500)
    
    # コンテンツURL取得のルーティング
    @app.route("/sites/", methods=["GET"])
    def get_handler(id: str):
        stack_name = id
        try:
            # Automation API (Stackの選択)
            stack = auto.select_stack(
                stack_name=stack_name,
                project_name=project_name,
                program=lambda *args: None
            )
            outs = stack.outputs()
            return jsonify(id=stack_name, url=outs["website_url"].value)
        except auto.StackNotFoundError:
            return make_response(f"stack '{stack_name}' does not exist", 404)
        except Exception as exn:
            return make_response(str(exn), 500)
    
    # コンテンツ内容更新のルーティング
    @app.route("/sites/", methods=["PUT"])
    def update_handler(id: str):
        stack_name = id
        content = request.json.get('content')
    
        try:
            def pulumi_program():
                create_pulumi_program(content)
            # Automation API (Stackの選択)
            stack = auto.select_stack(
                stack_name=stack_name,
                project_name=project_name,
                program=pulumi_program
            )
            stack.set_config("aws:region", auto.ConfigValue("ap-northeast-1"))
            # Automation API (Stackのデプロイ)
            up_res = stack.up(on_output=print)
            return jsonify(id=stack_name, url=up_res.outputs["website_url"].value)
        except auto.StackNotFoundError:
            return make_response(f"stack '{stack_name}' does not exist", 404)
        except auto.ConcurrentUpdateError:
            return make_response(f"stack '{stack_name}' already has update in progress", 409)
        except Exception as exn:
            return make_response(str(exn), 500)
    
    # コンテンツ削除のルーティング
    @app.route("/sites/", methods=["DELETE"])
    def delete_handler(id: str):
        stack_name = id
        try:
            # Automation API (Stackの選択)
            stack = auto.select_stack(
                stack_name=stack_name,
                project_name=project_name,
                program=lambda *args: None
            )
            # Automation API (Stackのリソース削除)
            stack.destroy(on_output=print)
            # Automation API (Stack自体の削除)
            stack.workspace.remove_stack(stack_name)
            return jsonify(message=f"stack '{stack_name}' successfully removed!")
        except auto.StackNotFoundError:
            return make_response(f"stack '{stack_name}' does not exist", 404)
        except auto.ConcurrentUpdateError:
            return make_response(f"stack '{stack_name}' already has update in progress", 409)
        except Exception as exn:
            return make_response(str(exn), 500)
    コードの主要な箇所について解説します。
    def create_pulumi_program(content: str):
    ~~~
      s3.Bucket(***)
    ~~~
      s3.BucketPublicAccessBlock(***)
    ~~~
      s3.BucketObject(***)
    ~~~
      s3.BucketPolicy(***)
    • Pulumi Programの内容を定義する関数です。ユーザーからのHTTPリクエストが発生した際に読み込まれます(コンテンツ新規作成及びコンテンツ内容更新時に読み込まれます)
      Programでは、S3Bucket/BucketObject/BucketPolicy/BucketPublicAccessBlockのリソースが作成されます。BucketObjectを作成するためのコンテンツの内容は、ユーザーのHTTPリクエスト(POSTメソッド)のbodyから取得します。BucketObjectに対するパブリックアクセスを可能にするため、BucketPublicAccessBlockリソースで「パブリックアクセスのブロック設定」を無効化します。
    # コンテンツ新規作成のルーティング
    @app.route("/sites", methods=["POST"])
    def create_handler():
    ~~~
    # コンテンツ一覧取得のルーティング
    @app.route("/sites", methods=["GET"])
    def list_handler():
    ~~~
    # コンテンツURL取得のルーティング
    @app.route("/sites/<string:id>", methods=["GET"])
    def get_handler(id: str):
    ~~~
    # コンテンツ内容更新のルーティング
    @app.route("/sites/<string:id>", methods=["PUT"])
    def update_handler(id: str):
    ~~~
    # コンテンツ削除のルーティング
    @app.route("/sites/<string:id>", methods=["DELETE"])
    def delete_handler(id: str):
    • ユーザーからのHTTPリクエストに応じたルーティングの作成とcaller関数の設定です。ルーティングは「コンテンツ新規作成/コンテンツ一覧取得/コンテンツ内容取得/コンテンツ内容更新/コンテンツ削除」の5つ分を作成しています。
    # Automation API (Stackの新規作成)
            stack = auto.create_stack(
                stack_name=stack_name,
                project_name=project_name,
                program=pulumi_program
            )
    ~~~
    # Automation API (Stackのデプロイ)
            up_res = stack.up(on_output=print)
    ~~~
    # Automation API (Stackの選択)
            stack = auto.select_stack(
                stack_name=stack_name,
                project_name=project_name,
                program=pulumi_program
            )
    ~~~
    # Automation API (Stackのリソース削除)
            stack.destroy(on_output=print)
    ~~~
    # Automation API (Stack自体の削除)
            stack.workspace.remove_stack(stack_name)
    • Automation APIのコードです。上記の記載により「Pulumi CLI」を介さずに、Stackの作成/デプロイ/削除などが可能になります。
  3. requirements.txtを作成して、installするPythonパッケージのバージョンを指定します。
    $ vi requirements.txt
    pulumi>=3.0.0,<4.0.0
    pulumi-aws>=4.0.0,<5.0.0
    flask>=1.1.2,<2.0.0
    markupsafe==2.0.1

Python仮想環境のセットアップ

作成したアプリケーションのPython仮想実行環境を構築します。

  1. Python 仮想環境を作成します。
    $ pwd   
    /****/pulumi_over_http 
    
    $ python3 -m venv venv
  2. Pythonパッケージインストールのユーティリティ(pip)をインストールします
    $ venv/bin/python3 -m pip install --upgrade pip
    —
    Collecting pip
      Using cached pip-23.2.1-py3-none-any.whl (2.1 MB)
    Installing collected packages: pip
      Attempting uninstall: pip
        Found existing installation: pip 20.2.3
        Uninstalling pip-20.2.3:
          Successfully uninstalled pip-20.2.3
    Successfully installed pip-23.2.1
  3. Pythonパッケージをインストールします
    $ venv/bin/pip install -r requirements.txt
    —
    Collecting pulumi<4.0.0,>=3.0.0 (from -r requirements.txt (line 1))
      Obtaining dependency information for pulumi<4.0.0,>=3.0.0 from https://files.pythonhosted.org/packages/4e/a2/a59532ee8c0e760ae99a4026e69c
    292d29c137ea0364913b211095d74200/pulumi-3.76.1-py3-none-any.whl.metadata
      Downloading pulumi-3.76.1-py3-none-any.whl.metadata (11 kB)
    ~~~
著者
大関 研丞 (Kenneth Ozeki)
クリエーションライン株式会社 Data Platform Team
前職では保険や金融エンタープライズのミッションクリティカルシステム(オンプレミス、仮想サーバー、CDN等のインフラ系業務)の設計/構築を経験。クリエーションラインに転職後はクラウドエンジニアとしてGCP関連の案件でインフラの設計/構築、IaCやCI/CDを用いたDevOpsの導入、コンテナ(Kubernetes)基盤の構築、運用自動化ツールの作成などを担当。
クリエーションラインの技術ブログをチェック

連載バックナンバー

システム運用技術解説
第10回

Pulumiの最新機能「Pulumi ESC」を使ってみよう

2023/12/26
最終回となる今回は、2023年12月時点でリリースされている新機能の紹介と、その新機能の中から「Pulumi ESC」を用いたハンズオンを実践していきます。
システム運用技術解説
第9回

TerraformからPulumiへの移行

2023/11/28
第9回となる今回は、既にTerraformでAWS環境に作成されているリソースをPulumiへ移行するケースを想定して、CoexistenceとConversionのハンズオンを実践していきます
システム運用技術解説
第8回

既に存在するリソースをPulumiで管理してみよう

2023/10/19
第8回となる今回は、既にクラウド環境にデプロイされているリソースをPulumiで管理(import)する方法について、ハンズオンで実践していきます。

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています