다음을 통해 공유


Python 애플리케이션에서 변형 기능 플래그 사용

이 자습서에서는 변형 기능 플래그를 사용하여 예제 애플리케이션 인 오늘의 견적에서 다양한 사용자 세그먼트에 대한 환경을 관리합니다. 기능 플래그를 변형하는 방법에서 만든 변형 기능 플래그를 활용합니다. 계속하기 전에 App Configuration 저장소에서 Greeting이라는 변형 기능 플래그를 만들어야 합니다.

필수 조건

Python Flask 웹앱 설정

Python Flask 웹앱이 이미 있는 경우 변형 기능 플래그 사용 섹션으로 건너뛸 수 있습니다.

  1. QuoteOfTheDay라는 새 프로젝트 폴더를 만듭니다.

  2. QuoteOfTheDay 폴더에 가상 환경을 만듭니다 .

    python -m venv venv
    
  3. 가상 환경을 활성화합니다.

    .\venv\Scripts\Activate
    
  4. 다음 패키지의 최신 버전을 설치합니다.

    pip install flask
    pip install flask-login
    pip install flask_sqlalchemy
    pip install flask_bcrypt
    

오늘의 견적 앱 만들기

  1. 다음 내용을 사용하여 폴더에 QuoteOfTheDay 명명된 app.py 새 파일을 만듭니다. 사용자 인증을 사용하여 기본 Flask 웹 애플리케이션을 설정합니다.

    from flask_bcrypt import Bcrypt
    from flask_sqlalchemy import SQLAlchemy
    from flask_login import LoginManager
    from flask import Flask
    
    app = Flask(__name__, template_folder="../templates", static_folder="../static")
    bcrypt = Bcrypt(app)
    
    db = SQLAlchemy()
    db.init_app(app)
    
    login_manager = LoginManager()
    login_manager.init_app(app)
    
    from .model import Users
    
    @login_manager.user_loader
    def loader_user(user_id):
        return Users.query.get(user_id)
    
    with app.app_context():
        db.create_all()
    
    if __name__ == "__main__":
        app.run(debug=True)
    
    from . import routes
    app.register_blueprint(routes.bp)
    
  2. 다음 콘텐츠를 사용하여 QuoteOfTheDay 폴더에 model.py파일을 만듭니다. Flask Quote 웹 애플리케이션에 대한 데이터 클래스 및 사용자 모델을 정의합니다.

    from dataclasses import dataclass
    from flask_login import UserMixin
    from . import db
    
    @dataclass
    class Quote:
        message: str
        author: str
    
    # Create user model
    class Users(UserMixin, db.Model):
    
        id = db.Column(db.Integer, primary_key=True)
        username = db.Column(db.String(250), unique=True, nullable=False)
        password_hash = db.Column(db.String(250), nullable=False)
    
        def __init__(self, username, password):
            self.username = username
            self.password_hash = password
    
  3. 다음 콘텐츠를 사용하여 QuoteOfTheDay 폴더에 routes.py파일을 만듭니다. Flask 웹 애플리케이션에 대한 경로를 정의하여 사용자 인증을 처리하고 임의의 따옴표로 홈페이지를 표시합니다.

    import random
    
    from flask import Blueprint, render_template, request, flash, redirect, url_for
    from flask_login import current_user, login_user, logout_user
    from . import db, bcrypt
    from .model import Quote, Users
    
    bp = Blueprint("pages", __name__)
    
    @bp.route("/", methods=["GET", "POST"])
    def index():
        context = {}
        user = ""
        if current_user.is_authenticated:
            user = current_user.username
            context["user"] = user
        else:
            context["user"] = "Guest"
        if request.method == "POST":
            return redirect(url_for("pages.index"))
    
        quotes = [
            Quote("You cannot change what you are, only what you do.", "Philip Pullman"),
        ]
    
        greeting_message = "Hi"
    
        context["model"] = {}
        context["model"]["greeting_message"] = greeting_message
        context["model"]["quote"] = {}
        context["model"]["quote"] = random.choice(quotes)
        context["isAuthenticated"] = current_user.is_authenticated
    
        return render_template("index.html", **context)
    
    @bp.route("/register", methods=["GET", "POST"])
    def register():
        if request.method == "POST":
            password = request.form.get("password")
            hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')
            user = Users(request.form.get("username"), hashed_password)
            try:
                db.session.add(user)
                db.session.commit()
            except Exception as e:
                flash("Username already exists")
                return redirect(url_for("pages.register"))
            login_user(user)
    
            return redirect(url_for("pages.index"))
        return render_template("sign_up.html")
    
    
    @bp.route("/login", methods=["GET", "POST"])
    def login():
        if request.method == "POST":
            user = Users.query.filter_by(username=request.form.get("username")).first()
            password = request.form.get("password")
            if user and bcrypt.check_password_hash(user.password_hash, password):
                login_user(user)
                return redirect(url_for("pages.index"))
        return render_template("login.html")
    
    @bp.route("/logout")
    def logout():
        logout_user()
        return redirect(url_for("pages.index"))
    
  4. QuoteOfTheDay 폴더에 템플릿이라는 새 폴더를 만들고 다음 콘텐츠와 함께 base.html파일을 추가합니다. 웹 애플리케이션의 레이아웃 페이지를 정의합니다.

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>QuoteOfTheDay</title>
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
        <link rel="stylesheet" href="{{ url_for('static', filename='site.css') }}">
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
    </head>
    <body>
        <header>
            <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
                <div class="container">
                    <a class="navbar-brand"  href="/">QuoteOfTheDay</a>
                    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                            aria-expanded="false" aria-label="Toggle navigation">
                        <span class="navbar-toggler-icon"></span>
                    </button>
                    <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
    
                        <ul class="navbar-nav flex-grow-1">
                            <li class="nav-item">
                                <a class="nav-link text-dark" href="/">Home</a>
                            </li>
                        </ul>
                        {% block login_partial %}
                        <ul class="navbar-nav">
                        {% if isAuthenticated %}
                            <li class="nav-item">
                                <a  class="nav-link text-dark">Hello {{user}}!</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link text-dark"  href="/logout">Logout</a>
                            </li>
                        {% else %}
                            <li class="nav-item">
                                <a class="nav-link text-dark"  href="/register">Register</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link text-dark"  href="/login">Login</a>
                            </li>
                        {% endif %}
                        </ul>
                        {% endblock %}
                    </div>
                </div>
            </nav>
        </header>
        <div class="container">
            <main role="main" class="pb-3">
                {% block content %}
                {% endblock %}
            </main>
        </div>
    </body>
    
    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2024 - QuoteOfTheDay
        </div>
    </footer>
    
    <script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
    </body>
    </html>
    
  5. 다음 내용이 포함된 템플릿 폴더에 index.html이라는 새 파일을 만듭니다. 기본 템플릿을 확장하고 콘텐츠 블록을 추가합니다.

    {% extends 'base.html' %}
    
    {% block content %}
    <div class="quote-container">
        <div class="quote-content">
           {% if model.greeting_message %}
                <h3 class="greeting-content">{{model.greeting_message}}</h3>
            {% endif %}
            <br />
            <p class="quote">“{{model.quote.message}}”</p>
            <p>- <b>{{model.quote.author}}</b></p>
        </div>
    
        <div class="vote-container">
            <button class="btn btn-primary" onclick="heartClicked(this)">
                <i class="far fa-heart"></i> <!-- Heart icon -->
            </button>
        </div>
    
        <form action="/" method="post">
        </form>
    </div>
    <script>
        function heartClicked(button) {
            var icon = button.querySelector('i');
            icon.classList.toggle('far');
            icon.classList.toggle('fas');
        }
    </script>
    {% endblock %}
    
  6. 다음 내용을 사용하여 템플릿 폴더에 sign_up.html파일을 만듭니다. 사용자 등록 페이지의 템플릿을 정의합니다.

    {% extends 'base.html' %}
    
    {% block content %}
    <div class="login-container">
      <h1>Create an account</h1>
      <form action="#" method="post">
        <label for="username">Username:</label>
        <input type="text" name="username" />
        <label for="password">Password:</label>
        <input type="password" name="password" />
        <button type="submit">Submit</button>
      </form>
    </div>
    {% endblock %}
    
  7. 다음 콘텐츠를 사용하여 템플릿 폴더에 login.html파일을 만듭니다. 사용자 로그인 페이지의 템플릿을 정의합니다.

    {% extends 'base.html' %}
    
    {% block content %}
    <div class="login-container">
      <h1>Login to your account</h1>
      <form action="#" method="post">
        <label for="username">Username:</label>
        <input type="text" name="username" />
        <label for="password">Password:</label>
        <input type="password" name="password" />
        <button type="submit">Submit</button>
      </form>
    </div>
    {% endblock %}
    
  8. QuoteOfTheDay 폴더에 static이라는 새 폴더를 만들고 다음 콘텐츠와 함께 site.css파일을 추가합니다. 웹 애플리케이션에 대한 CSS 스타일을 추가합니다.

    html {
        font-size: 14px;
      }
    
      @media (min-width: 768px) {
        html {
          font-size: 16px;
        }
      }
    
      .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
        box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
      }
    
      html {
        position: relative;
        min-height: 100%;
      }
    
      body {
        margin-bottom: 60px;
      }
    
      body {
        font-family: Arial, sans-serif;
        background-color: #f4f4f4;
        color: #333;
    }
    
    .quote-container {
        background-color: #fff;
        margin: 2em auto;
        padding: 2em;
        border-radius: 8px;
        max-width: 750px;
        box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2);
        display: flex;
        justify-content: space-between;
        align-items: start;
        position: relative;
    }
    
    .login-container {
      background-color: #fff;
      margin: 2em auto;
      padding: 2em;
      border-radius: 8px;
      max-width: 750px;
      box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2);
      justify-content: space-between;
      align-items: start;
      position: relative;
    }
    
    .vote-container {
        position: absolute;
        top: 10px;
        right: 10px;
        display: flex;
        gap: 0em;
    }
    
        .vote-container .btn {
            background-color: #ffffff; /* White background */
            border-color: #ffffff; /* Light blue border */
            color: #333
        }
    
            .vote-container .btn:focus {
                outline: none;
                box-shadow: none;
            }
    
            .vote-container .btn:hover {
                background-color: #F0F0F0; /* Light gray background */
            }
    
    .greeting-content {
        font-family: 'Georgia', serif; /* More artistic font */
    }
    
    .quote-content p.quote {
        font-size: 2em; /* Bigger font size */
        font-family: 'Georgia', serif; /* More artistic font */
        font-style: italic; /* Italic font */
        color: #4EC2F7; /* Medium-light blue color */
    }
    

변형 기능 플래그 사용

  1. 다음 패키지의 최신 버전을 설치합니다.

    pip install azure-identity 
    pip install azure-appconfiguration-provider
    pip install featuremanagement[AzureMonitor]
    
  2. app.py 파일을 열고 파일 끝에 다음 코드를 추가합니다. App Configuration에 연결하고 기능 관리를 설정합니다.

    App Configuration 저장소에 인증하는 데 사용합니다 DefaultAzureCredential . 지침따라 자격 증명에 App Configuration 데이터 판독역할을 할당합니다. 애플리케이션을 실행하기 전에 권한이 전파될 수 있는 충분한 시간을 허용해야 합니다.

    import os
    from azure.appconfiguration.provider import load
    from featuremanagement import FeatureManager
    from azure.identity import DefaultAzureCredential
    
    ENDPOINT = os.getenv("AzureAppConfigurationEndpoint")
    
    # Updates the flask app configuration with the Azure App Configuration settings whenever a refresh happens
    def callback():
        app.config.update(azure_app_config)
    
    # Connect to App Configuration
    global azure_app_config
    azure_app_config = load(
        endpoint=ENDPOINT,
        credential=DefaultAzureCredential(),
        on_refresh_success=callback,
        feature_flag_enabled=True,
        feature_flag_refresh_enabled=True,
    )
    app.config.update(azure_app_config)
    
    # Create a FeatureManager
    feature_manager = FeatureManager(azure_app_config)
    
  3. routes.py 다음 코드를 열고 끝에 추가하여 구성을 새로 고치고 기능 변형을 가져옵니다.

    from featuremanagement.azuremonitor import track_event
    from . import azure_app_config, feature_manager
    
    ...
    # Update the post request to track liked events
    if request.method == "POST":
        track_event("Liked", user)
        return redirect(url_for("pages.index"))
    
    ...
    # Update greeting_message to variant
    greeting = feature_manager.get_variant("Greeting", user)
    greeting_message = ""
    if greeting:
        greeting_message = greeting.configuration
    

앱 빌드 및 실행

  1. AzureAppConfigurationEndpoint라는 환경 변수를 Azure Portal의 저장소 개요 아래에 있는 App Configuration 저장소의 엔드포인트로 설정합니다.

    Windows 명령 프롬프트를 사용하는 경우 다음 명령을 실행하고, 명령 프롬프트를 다시 시작하여 변경 내용을 적용합니다.

    setx AzureAppConfigurationEndpoint "<endpoint-of-your-app-configuration-store>"
    

    PowerShell을 사용하는 경우 다음 명령을 실행합니다.

    $Env:AzureAppConfigurationEndpoint = "<endpoint-of-your-app-configuration-store>"
    

    macOS 또는 Linux를 사용하는 경우 다음 명령을 실행합니다.

    export AzureAppConfigurationEndpoint='<endpoint-of-your-app-configuration-store'
    
  2. 명령 프롬프트의 QuoteOfTheDay 폴더에서 flask run를 실행합니다.

  3. 앱이 시작될 때까지 기다린 다음 브라우저를 열고 .으로 이동합니다 http://localhost:5000/.

  4. 실행 중인 애플리케이션을 확인한 후 오른쪽 상단의 등록을 선택하여 새 사용자를 등록합니다.

    등록을 보여 주는 Quote of the Day 앱 스크린샷.

  5. usera@contoso.com이라는 새 사용자를 등록합니다.

    참고 항목

    이 자습서에서는 이러한 이름을 정확하게 사용해야 합니다. 기능이 예상대로 구성되었으면 두 사용자에게 서로 다른 변형이 표시되어야 합니다.

  6. 사용자 정보를 입력한 후 제출 단추를 선택합니다.

  7. 자동으로 로그인됩니다. 앱을 볼 usera@contoso.com 때 긴 메시지가 표시됩니다.

    사용자에게 특별한 메시지를 표시하는 Quote of the Day 앱 스크린샷.

  8. 오른쪽 위에 있는 로그아웃 단추를 사용하여 로그아웃 합니다.

  9. 라는 userb@contoso.com두 번째 사용자를 등록합니다.

  10. 자동으로 로그인됩니다. 앱을 볼 userb@contoso.com 때 짧은 메시지가 표시됩니다.

    사용자에 대한 메시지를 보여 주는 날짜 앱의 견적 스크린샷

다음 단계

Python 기능 관리 라이브러리의 전체 기능 개요는 다음 문서를 참조하세요.