Azure Pipelines を使用してAzure App Service にアプリケーションをデプロイしてみた
はじめに
コンサル部の神野です。皆さんはAzure Pipelinesをご存知ですか。
Azure DevOps Services(以下Azure DevOps)の1機能で、CI / CDパイプラインを構築することが可能になります。
今回は実際にAzure App Service(以下App Service)に対してアプリケーションをデプロイを試してみて、どう言ったやり方でデプロイが実施できるのか理解したく記事にしてみました。
Azure DevOpsについて
Azure DevOpsとは、Microsoftが提供する統合型のDevOpsツールセットで、ソフトウェア開発の全ライフサイクルをサポートします。コードのバージョン管理、CI/CDパイプラインの自動化、作業管理、テスト、さらにはパッケージ管理といった機能を一つのプラットフォームで提供しているサービスとなります。
具体的には、以下の主要機能が内包されています。
- Azure Boards
タスク、フィーチャー、バグなどの作業項目を管理するツール。スクラムやカンバンボードによるアジャイルなワークフローで使用 - Azure Repos
GitやTFVCを利用したソースコード管理を提供 - Azure Pipelines
ビルド、テスト、デプロイといったCI/CDパイプラインの自動化を実現 - Azure Test Plans
手動および自動テストの設計や実行、結果の管理を行うツール。 - Azure Artifacts
NuGet、npm、Mavenなどのパッケージ管理システムを統合。
これらの機能が一体となることで、Azure DevOpsはコードの開発からテスト、デプロイに至るまで、エンドツーエンドのプロセス管理を可能にしています。さらに、クラウドベースのサービスとして提供されるため、柔軟なスケーリングや迅速な環境構築が行える点も魅力の1つです。
本記事では、そんなAzure DevOpsの中でも特にAzure Pipelinesに注目し、App Serviceへのデプロイを通じて色々と触っていきたいと思います。
今回作成するシステム構成図
今回作成する環境は下記となります。
作成するリソース
App Serviceでアプリケーションをホストして、その環境にデプロイするようAzure Pipelinesの設定を入れていきます。App Serviceの1機能として提供されているデプロイスロットについては補足をご参照ください。
- Azure
- App Service
- アプリケーションサーバーとして使用。プランはS1(Standard)を想定。
- デプロイスロットはBlue/Greenデプロイを実施出来るよう2つ用意
- アプリケーションサーバーとして使用。プランはS1(Standard)を想定。
- Azure Container Registry(以下ACR)
- コンテナイメージを格納
- App Service
- Azure DevOps
- Azure Repos
- アプリケーションのソースコードを格納
- Azure Pipelines
- デプロイ用のパイプラインを構築
- Azure Reposの
master
ブランチにソースコードがPushされたら、パイプラインが起動して下記ステップで起動- コンテナイメージをビルド
- コンテナイメージをAzure Container Registryへプッシュ
- Azure Reposの
- デプロイ用のパイプラインを構築
- Azure Repos
デプロイの流れ
以下のステップでデプロイを進めていきます。
- 開発者がソースコードをAzure Reposにプッシュ
- Azure Reposの変更を検知し、Azure Pipelinesが起動
- Azure Pipelinesでコンテナイメージをビルド & Azure Container Registryへプッシュ
- Azure Container Registryにコンテナイメージがプッシュされる
- Azure App ServiceのGreenスロットへコンテナイメージをデプロイ
- Greenスロットで動作確認を実施
- 動作確認が完了したら、BlueスロットとGreenスロットをスワップし変更を本番環境へ適応
Azure 環境の作成
前提
今回はTerraformを使用するため事前にインストールが必要になります。
使用したバージョンは下記となります。
- Terraform・・・v1.9.4(provider registry.terraform.io/hashicorp/azurerm v4.4.0)
またAzure環境は既に存在し、サブスクリプションおよびリソースグループに対して自身の権限はあるものとします。
環境作成
今回Terraformを使用して環境を作成します。
コードは下記となります。
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "=4.4.0"
}
}
}
provider "azurerm" {
subscription_id = var.subscription_id
resource_provider_registrations = "none"
features {}
}
# 既存のリソースグループを参照
data "azurerm_resource_group" "rg" {
name = var.resource_group_name
}
# ACRの作成
resource "azurerm_container_registry" "acr" {
name = "applicationregistry"
resource_group_name = data.azurerm_resource_group.rg.name
location = data.azurerm_resource_group.rg.location
sku = "Basic"
admin_enabled = true
}
# App Service Planの作成
resource "azurerm_service_plan" "app_plan" {
name = "app-service-plan"
resource_group_name = data.azurerm_resource_group.rg.name
location = data.azurerm_resource_group.rg.location
os_type = "Linux"
sku_name = "S1"
}
# App Serviceの作成
resource "azurerm_linux_web_app" "app" {
name = "sample-app-yjinno-test"
resource_group_name = data.azurerm_resource_group.rg.name
location = data.azurerm_resource_group.rg.location
service_plan_id = azurerm_service_plan.app_plan.id
site_config {
always_on = false
application_stack {
docker_image_name = "${azurerm_container_registry.acr.login_server}/myapp:latest"
docker_registry_url = "https://${azurerm_container_registry.acr.login_server}"
docker_registry_username = azurerm_container_registry.acr.admin_username
docker_registry_password = azurerm_container_registry.acr.admin_password
}
}
app_settings = {
"WEBSITES_ENABLE_APP_SERVICE_STORAGE" = "false"
}
}
resource "azurerm_linux_web_app_slot" "green" {
name = "green-slot"
app_service_id = azurerm_linux_web_app.app.id
site_config {
always_on = false
application_stack {
docker_image_name = "${azurerm_container_registry.acr.login_server}/myapp:latest"
docker_registry_url = "https://${azurerm_container_registry.acr.login_server}"
docker_registry_username = azurerm_container_registry.acr.admin_username
docker_registry_password = azurerm_container_registry.acr.admin_password
}
}
}
既にサブスクリプション及びリソースグループは作成済みで環境変数(terraform.tfvars
)からそれぞれのIDや名称を取得する作りとしています。
試しにplanコマンドを実行して、問題がないか確認します。
planコマンド実行結果
azure-terraform-test % terraform plan
data.azurerm_resource_group.rg: Reading...
data.azurerm_resource_group.rg: Read complete after 1s [id=/subscriptions/xxx/resourceGroups/yyy]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following
symbols:
+ create
Terraform will perform the following actions:
# azurerm_container_registry.acr will be created
+ resource "azurerm_container_registry" "acr" {
+ admin_enabled = true
+ admin_password = (sensitive value)
+ admin_username = (known after apply)
+ encryption = (known after apply)
+ export_policy_enabled = true
+ id = (known after apply)
+ location = "japaneast"
+ login_server = (known after apply)
+ name = "applicationregistry"
+ network_rule_bypass_option = "AzureServices"
+ network_rule_set = (known after apply)
+ public_network_access_enabled = true
+ resource_group_name = "yyy"
+ sku = "Basic"
+ trust_policy_enabled = false
+ zone_redundancy_enabled = false
}
# azurerm_linux_web_app.app will be created
+ resource "azurerm_linux_web_app" "app" {
+ app_settings = {
+ "WEBSITES_ENABLE_APP_SERVICE_STORAGE" = "false"
}
+ client_affinity_enabled = false
+ client_certificate_enabled = false
+ client_certificate_mode = "Required"
+ custom_domain_verification_id = (sensitive value)
+ default_hostname = (known after apply)
+ enabled = true
+ ftp_publish_basic_authentication_enabled = true
+ hosting_environment_id = (known after apply)
+ https_only = false
+ id = (known after apply)
+ key_vault_reference_identity_id = (known after apply)
+ kind = (known after apply)
+ location = "japaneast"
+ name = "sample-app-yjinno-test"
+ outbound_ip_address_list = (known after apply)
+ outbound_ip_addresses = (known after apply)
+ possible_outbound_ip_address_list = (known after apply)
+ possible_outbound_ip_addresses = (known after apply)
+ public_network_access_enabled = true
+ resource_group_name = "yyy"
+ service_plan_id = (known after apply)
+ site_credential = (sensitive value)
+ webdeploy_publish_basic_authentication_enabled = true
+ zip_deploy_file = (known after apply)
+ site_config {
+ always_on = false
+ container_registry_use_managed_identity = false
+ default_documents = (known after apply)
+ detailed_error_logging_enabled = (known after apply)
+ ftps_state = "Disabled"
+ http2_enabled = false
+ ip_restriction_default_action = "Allow"
+ linux_fx_version = (known after apply)
+ load_balancing_mode = "LeastRequests"
+ local_mysql_enabled = false
+ managed_pipeline_mode = "Integrated"
+ minimum_tls_version = "1.2"
+ remote_debugging_enabled = false
+ remote_debugging_version = (known after apply)
+ scm_ip_restriction_default_action = "Allow"
+ scm_minimum_tls_version = "1.2"
+ scm_type = (known after apply)
+ scm_use_main_ip_restriction = false
+ use_32_bit_worker = true
+ vnet_route_all_enabled = false
+ websockets_enabled = false
+ worker_count = (known after apply)
+ application_stack {
+ docker_image_name = (known after apply)
+ docker_registry_password = (sensitive value)
+ docker_registry_url = (known after apply)
+ docker_registry_username = (known after apply)
}
}
}
# azurerm_linux_web_app_slot.green will be created
+ resource "azurerm_linux_web_app_slot" "green" {
+ app_metadata = (known after apply)
+ app_service_id = (known after apply)
+ client_affinity_enabled = false
+ client_certificate_enabled = false
+ client_certificate_mode = "Required"
+ custom_domain_verification_id = (sensitive value)
+ default_hostname = (known after apply)
+ enabled = true
+ ftp_publish_basic_authentication_enabled = true
+ hosting_environment_id = (known after apply)
+ https_only = false
+ id = (known after apply)
+ key_vault_reference_identity_id = (known after apply)
+ kind = (known after apply)
+ name = "green-slot"
+ outbound_ip_address_list = (known after apply)
+ outbound_ip_addresses = (known after apply)
+ possible_outbound_ip_address_list = (known after apply)
+ possible_outbound_ip_addresses = (known after apply)
+ public_network_access_enabled = true
+ site_credential = (sensitive value)
+ webdeploy_publish_basic_authentication_enabled = true
+ zip_deploy_file = (known after apply)
+ site_config {
+ always_on = false
+ container_registry_use_managed_identity = false
+ default_documents = (known after apply)
+ detailed_error_logging_enabled = (known after apply)
+ ftps_state = "Disabled"
+ http2_enabled = false
+ ip_restriction_default_action = "Allow"
+ linux_fx_version = (known after apply)
+ load_balancing_mode = "LeastRequests"
+ local_mysql_enabled = false
+ managed_pipeline_mode = "Integrated"
+ minimum_tls_version = "1.2"
+ remote_debugging_enabled = false
+ remote_debugging_version = (known after apply)
+ scm_ip_restriction_default_action = "Allow"
+ scm_minimum_tls_version = "1.2"
+ scm_type = (known after apply)
+ scm_use_main_ip_restriction = false
+ use_32_bit_worker = true
+ vnet_route_all_enabled = false
+ websockets_enabled = false
+ worker_count = (known after apply)
+ application_stack {
+ docker_image_name = (known after apply)
+ docker_registry_password = (sensitive value)
+ docker_registry_url = (known after apply)
+ docker_registry_username = (known after apply)
}
}
}
# azurerm_service_plan.app_plan will be created
+ resource "azurerm_service_plan" "app_plan" {
+ id = (known after apply)
+ kind = (known after apply)
+ location = "japaneast"
+ maximum_elastic_worker_count = (known after apply)
+ name = "app-service-plan"
+ os_type = "Linux"
+ per_site_scaling_enabled = false
+ reserved = (known after apply)
+ resource_group_name = "yyy"
+ sku_name = "S1"
+ worker_count = (known after apply)
}
Plan: 4 to add, 0 to change, 0 to destroy.
実行結果も問題ないですね!問題ないので、apply
コマンドを実行して、リソースを作成します。
terraform apply
アプリケーションのソースコード
今回はWebフレームワークにFastAPIを使った簡単なアプリケーションを作成します。
リソースはapp
フォルダを作成してその中に配置していきます。
まずは必要なライブラリはrequirements.txt
に記載します。
fastapi
uvicorn
メインの処理はapp.py
に記載します。
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# CORSミドルウェアの設定
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
async def root():
return {"message": "Hello World"}
@app.get("/health")
async def health_check():
return {"status": "healthy"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
コンテナイメージを起動するために、Dockerfile
も作成します。
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
# App ServiceはPORT環境変数を提供します
CMD uvicorn app:app --host 0.0.0.0 --port ${PORT:-8000}
試しにローカルでも起動するか確認してみます。
コンテナイメージを立ち上げてレスポンスが下記のように返却されればOKです。
# イメージのビルド
docker build -t container-app .
# イメージを起動
docker run -it container-app
INFO: Started server process [7]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
# curlコマンドで起動しているか確認
curl http://localhost:8000
# レスポンス
{"message":"Hello World"}
Azure Pipelinesで実行するyamlファイル
Pipelineで実行する処理をyamlファイル上に記載します。ファイル名はazure-pipeline.yml
として下記定義を記載します。
trigger:
- master
variables:
acrName: 'applicationregistry'
appName: 'sample-app-yjinno-test'
imageRepository: 'fastapi-app'
tag: '$(Build.BuildId)'
stagingSlotName: 'green-slot' # ステージングスロットの名前を定義
stages:
- stage: Build
jobs:
- job: BuildAndTest
pool:
vmImage: 'ubuntu-latest'
steps:
# Dockerビルドとプッシュ
- task: Docker@2
inputs:
containerRegistry: 'container-connection'
repository: '$(imageRepository)'
command: 'buildAndPush'
Dockerfile: 'app/Dockerfile'
buildContext: 'app'
tags: |
$(tag)
latest
- stage: Deploy
jobs:
- job: DeployToStaging
pool:
vmImage: 'ubuntu-latest'
steps:
# ステージングスロットへのデプロイ
- task: AzureWebAppContainer@1
inputs:
azureSubscription: 'service-connection-sample'
appName: $(appName)
slotName: $(stagingSlotName) # ステージングスロットを指定
imageName: '$(acrName).azurecr.io/$(imageRepository):$(tag)'
このパイプライン定義を詳しく見ていきます。
まず、trigger
セクションでは、masterブランチへの変更があった際に自動的にパイプラインが実行されるよう設定しています。
variables
セクションでは、パイプライン全体で使用する変数を定義しています。
- ACR名、App Service名、イメージリポジトリ名などの基本情報
- ビルドIDをタグとして使用することで、各デプロイを一意に識別
- デプロイ先のステージングスロット名(Blue/Greenデプロイ用)
パイプラインは2つの主要ステージで構成されています。
-
Buildステージ
- Ubuntu環境でDockerイメージをビルド
Docker@2
タスクを使用して、アプリケーションのDockerfileからイメージを作成- ビルドしたイメージに現在のビルドIDと
latest
の2つのタグを付与 - 事前設定した
container-connection
を使用してACRにイメージをプッシュ- この名前は後ほど作成するDocker Registryサービス接続の名前と一致させる必要があります。異なる名前で作成した場合は、このyamlファイルの値を変更してください。
-
Deployステージ
AzureWebAppContainer@1
タスクを使用- 事前設定した
service-connection-sample
接続を使用- この名前は後ほど作成するAzure Resource Managerサービス接続の名前と一致させる必要があります。異なる名前で作成した場合は、このyamlファイルの値を変更してください。
- App Serviceの
green-slot
(ステージングスロット)にビルドステージで作成したコンテナイメージをデプロイ
このパイプラインは、コードの変更がmasterブランチにプッシュされるたびに、自動的にDockerイメージをビルド・プッシュし、そのイメージをステージング環境にデプロイする一連の流れを自動化しています。本番環境へのスワップ(Blue/Greenデプロイの完了)は、動作確認後に手動で行うことを前提としています。
サービス接続の名前は、Azure DevOpsの「Project Settings」→「Service connections」で作成・確認できます。接続名はプロジェクト内で一意である必要があり、yamlファイル内の参照名と一致させる必要があります。
App Serviceの作成とローカルでのアプリケーションの確認および、パイプラインのテンプレートファイルは作成完了したので、次はAzure DevOpsの作成を進めていきます。
Azure DevOpsの環境作成
プロジェクト作成 ~ レポジトリへソースコードをPush
-
まずはAzure DevOpsでプロジェクトを作成します。任意のプロジェクト名を入れて
Create Project
ボタンを押下します。
-
プロジェクトを作成したら、
Repos
タブを押下します。
-
コピーリンクを押下して、レポジトリのURLをコピー
-
ローカルのアプリケーションソースコードをAzure ReposにPush
#アプリケーションのディレクトリへ移動
cd ./app
#Gitリポジトリを初期化
git init
#ファイルをステージングエリアに追加
git add .
#コミットを作成
git commit -m "Initial commit"
#リモートリポジトリを追加
git remote add origin <コピーしたURL>
#masterブランチにプッシュ
git push -u origin master
補足ですが、認証情報のポップアップなどが求められた場合はログインして認証を実施してください。
Azure リソースとの接続設定
下記2点の設定を行います。これはAzureリソースにAzure DevOpsが問題なくアクセスできるようの認証設定みたいなものを行う必要があります。
- Azure Resource Manager
- Azureリソースとの接続設定
- Container registry
- Azure Container Registryとの接続設定
service connection
-
左下の歯車マークを押下
-
Pipelines > Service Connection
タブをクリック
-
New Service Connection
ボタンを押下
-
Azure Resource Managerを選択
-
連携したいAzureのサブスクリプションおよびリソースグループを選択し、任意の
Service connection name
を入力し(今回はservice-connection-sample
としてyamlファイルで設定する値と合わせています)、Grant access permission to all pipelines
にチェックを入れてSave
ボタンを押下
-
作成が完了したら、 一覧に作成したService connectionが追加されます
-
再度、
New service connection
を押下して、Docker Registry
を選択してNext
ボタンを押下
-
Azure Container Registry
を選択して、使用しているSubscriptionおよび、Terraformで作成したACR名(今回ならapplicationregistry
)を選択して、任意のService Connection Name
(今回はcontainer-connection
としてyamlファイルで設定する値と合わせています)を入力後、Grant access permission to all pipelines
にチェックを入れてSave
ボタンを押下
Pipelineの作成 & 実行
Pipelines
タブを押下
Create Pipeline
ボタンを押下
Azure Repos Git
を選択
- 該当のレポジトリを選択
Existing Azure Pipelines YAML file
を選択
/azure-pipeline.yml
ファイルを選択して、Continue
ボタンを押下
Save
ボタンを押下
Run Pipeline
ボタンを押下
Run
ボタンを押下
作成したパイプラインが実行されます!しばらくすると成功するかと思います。
こんな形でパイプラインの実行結果詳細を確認可能です。
また、Build
やDeploy
などのカードを押下いただくと各ブロックで実行結果なども見れます。
今度はデプロイが上手くいっているかApp Serviceの動作確認をしてみます。
App Serviceの動作確認
まずはAzureポータル上から、Terraformで作成したApp Serviceの画面を開きます。
規定のドメインにApp Serviceで自動生成されたURLがあるので、こちらにアクセスしてみます。
まだ本番環境用デプロイスロットにはコンテナイメージを反映していないためErrorとなります。
一方でTerraformで作成した検証環境用のデプロイスロットgreen-slot(以下green-slot)にはAzure Pipelinesからコンテナイメージをデプロイしているのでアプリケーションが実行されるか確認してみます。
規定のドメインに記載があるリンクを押下してアクセスしてみます。
green-slotはアプリケーションがデプロイされているのでレスポンスが期待通り返却されましたね!
ここでgreen-slotと本番環境用のデプロイスロットをスワップしてみます。
App Serviceにはデプロイスロットのスワップ機能が存在し、green-slotと本番環境用のデプロイスロットの中身を簡単に入れ替えてデプロイができるので試してみます。
green-slotの画面でスワップボタンを押下します。
Sourceがgreen-slotとなっていて、Targetが本番環境用のデプロイスロットになっているのでスワップしてみます。
スワップが完了したら再度両環境にアクセスしてみます。
本番環境の動作
green-slotの動作
無事切り替わりました!!本番環境はデプロイしたアプリケーションが、green-slotはアプリケーションがデプロイされていない状態となりました。お手軽に切り替えすることができて面白いですね!!
今度はソースコードの一部修正を行ってコミット & プッシュし、パイプラインが自動で実行されて、green-slotで変更が適応されているか確認し、スワップして本番適応のステップで試してみます。
@app.get("/")
async def root():
# re-delopyを追加
return {"message": "Hello World re-deploy"}
re-deploy
を追加してソースコードをコミット&プッシュします。
プッシュがmaster
ブランチに行われると、パイプラインが変更を検知して自動で開始されます。
しばらく待ってパイプラインが下記のように成功したら再度green-slotの環境を確認してみます。
green-slotのURLにアクセスします。
変更が適当されていますね!!一方で本番環境は未だスワップしていないので変わらずかも確認してみます。
本番環境は変更されていないですね。再度スワップしてみます。
スワップ後、再度両環境を確認してみます。
Swap後の本番環境
Swap後のgreen-slot
無事切り替わっていますね!!簡単にテストして環境を切り替えられるのはAデプロイスロット機能の魅力の1つに感じました。
おわりに
Azure Pipelinesを使ったデプロイはいかがだったでしょうか。直感的に使えるような印象です。また、App Serviceのデプロイスロット機能が面白く、検証↔︎本番のスワップがボタン1つでできるのでテスト→デプロイが容易に行えますね!
本記事が少しでも役に立ちましたら幸いです。最後までご覧いただきありがとうございました!!
補足:App Serviceのデプロイスロットについて
App Serviceのデプロイスロットは、Standard以上のプランでのみ利用可能な機能です。本番環境に影響を与えずに新バージョンをテストでき、スワップ操作で瞬時に切り替えられます。また、トラフィック分割機能を使えばカナリアリリースやA/Bテストも実現可能な機能となります。
環境変数などは環境個別の値としたい場合は、デプロイ スロットの設定
にチェックを入れることでスワップされないように個別に設定することも可能です。
詳細は公式ドキュメントをご参照ください。