FlutterアプリをAppium × Azure Container Appsで完全自動化に挑戦!
🚀 FlutterアプリをDockerコンテナ化したAppium × Azure Container Appsで完全自動化!CDK for Terraformでインフラもコード化する最強パイプライン
こんにちは!
今回は、FlutterアプリでDockerコンテナ化したAppiumを使ったE2Eテストから、Azure Container Apps + CDK for Terraformでのインフラ構築、GitHub Actionsでの自動ビルド・デプロイまでの全工程を自動化する方法に挑戦してみたいと思います✨
「モバイルアプリのE2Eテストって面倒くさいよね」「UIテストの自動化できると労力相当減るよね」
「Appium環境の管理が大変...」「スケーラブルなテスト環境が欲しい...」「インフラもコード化したい...」
そんなあなたにぴったりの内容です!
まー、こんな記事を書きながらも老害プログラマーなおじさんが絡まない近くのチームの皆さん方は、目検と緻密な手作業で、Excelで台帳管理された冗長的かつ大量のテストケースを手作業で消化しているわけです
そんな人海戦術の作業も大事な時代もありましたが、これからは冗長的な作業を人海戦術で消化する時代から、冗長的で単調な作業はできる限り自動化を試みて、自動化の精度を上げつつ、エラー箇所や分別が困難なデータに絞り込んで人が目検で確認をする
そんな状態に持っていきたい願いを込めて、ふと思いついた構成でモバイルアプリのE2Eテストの自動化を試みてみます
これがうまく行ったら脆弱性診断とコードレビュー、コードインスペクション、テストケースの追加なんかの方法も体系立てて整理できたら良いなーと思っています
🎯 この記事で達成できること
- DockerでAppium環境をコンテナ化
- CDK for TerraformでAzure Container Appsを構築
- スケーラブルなE2Eテスト環境の実現
- GitHub Actionsで完全自動化されたCI/CDパイプライン
- インフラのコード化による再現性の確保
🛠️ 必要なツール・環境
- Flutter SDK
- Docker Desktop
- Azure CLI
- Terraform
- CDK for Terraform (cdktf)
- Node.js
- GitHub リポジトリ
🐳 1. AppiumのDockerコンテナ化
まずはAppium環境をDockerでコンテナ化しましょう!
Dockerfileの作成
docker/appium/Dockerfile
を作成:
FROM appium/appium:latest
# 必要なツールとドライバーをインストール
USER root
# Android SDKのセットアップ
RUN apt-get update && apt-get install -y \
wget \
unzip \
openjdk-11-jdk \
&& rm -rf /var/lib/apt/lists/*
ENV JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
ENV ANDROID_HOME=/opt/android-sdk
ENV PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools
# Android SDKをダウンロード
RUN wget -q https://dl.google.com/android/repository/commandlinetools-linux-8512546_latest.zip -O android-sdk.zip \
&& unzip -q android-sdk.zip -d $ANDROID_HOME \
&& rm android-sdk.zip
# Flutter ドライバーと必要なドライバーをインストール
RUN npm install -g appium@next
RUN appium driver install flutter
RUN appium driver install uiautomator2
# ポート設定
EXPOSE 4723
# Appiumサーバーを起動
CMD ["appium", "--address", "0.0.0.0", "--port", "4723", "--relaxed-security"]
Docker Composeファイル
docker-compose.yml
を作成:
version: '3.8'
services:
appium:
build:
context: ./docker/appium
dockerfile: Dockerfile
ports:
- "4723:4723"
environment:
- APPIUM_HOST=0.0.0.0
- APPIUM_PORT=4723
volumes:
- ./test_results:/opt/test_results
networks:
- appium-network
android-emulator:
image: budtmo/docker-android:emulator_11.0
privileged: true
ports:
- "6080:6080" # noVNC
- "5554:5554" # ADB
- "5555:5555" # ADB
environment:
- EMULATOR_DEVICE=Samsung Galaxy S10
- WEB_VNC=true
networks:
- appium-network
networks:
appium-network:
driver: bridge
🏗️ 2. CDK for TerraformでAzure Container Appsを構築
インフラをコード化しましょう!
CDKTFプロジェクトの初期化
# CDKTFをインストール
npm install -g cdktf-cli
# プロジェクト初期化
mkdir azure-infra && cd azure-infra
cdktf init --template=typescript --providers=azurerm
main.ts でAzure Container Appsを定義
import { Construct } from "constructs";
import { App, TerraformStack } from "cdktf";
import { AzurermProvider } from "@cdktf/provider-azurerm/lib/provider";
import { ResourceGroup } from "@cdktf/provider-azurerm/lib/resource-group";
import { ContainerApp } from "@cdktf/provider-azurerm/lib/container-app";
import { ContainerAppEnvironment } from "@cdktf/provider-azurerm/lib/container-app-environment";
import { LogAnalyticsWorkspace } from "@cdktf/provider-azurerm/lib/log-analytics-workspace";
class AppiumInfraStack extends TerraformStack {
constructor(scope: Construct, id: string) {
super(scope, id);
// Azure プロバイダーの設定
new AzurermProvider(this, "azurerm", {
features: {},
});
// リソースグループ
const rg = new ResourceGroup(this, "rg", {
name: "rg-appium-e2e",
location: "East US 2",
tags: {
environment: "test",
project: "flutter-e2e"
}
});
// Log Analytics Workspace
const logWorkspace = new LogAnalyticsWorkspace(this, "log-workspace", {
name: "log-appium-workspace",
resourceGroupName: rg.name,
location: rg.location,
sku: "PerGB2018",
retentionInDays: 30,
});
// Container App Environment
const containerAppEnv = new ContainerAppEnvironment(this, "container-app-env", {
name: "appium-environment",
resourceGroupName: rg.name,
location: rg.location,
logAnalyticsWorkspaceId: logWorkspace.id,
tags: {
environment: "test"
}
});
// Appium Container App
const appiumApp = new ContainerApp(this, "appium-app", {
name: "appium-server",
resourceGroupName: rg.name,
containerAppEnvironmentId: containerAppEnv.id,
revisionMode: "Single",
template: {
container: [{
name: "appium",
image: "your-acr.azurecr.io/appium:latest",
cpu: 1,
memory: "2Gi",
env: [{
name: "APPIUM_HOST",
value: "0.0.0.0"
}, {
name: "APPIUM_PORT",
value: "4723"
}]
}],
minReplicas: 1,
maxReplicas: 3
},
ingress: {
allowInsecureConnections: false,
externalEnabled: true,
targetPort: 4723,
traffic: [{
latestRevision: true,
percentage: 100
}]
},
tags: {
component: "appium-server"
}
});
// Android Emulator Container App
new ContainerApp(this, "emulator-app", {
name: "android-emulator",
resourceGroupName: rg.name,
containerAppEnvironmentId: containerAppEnv.id,
revisionMode: "Single",
template: {
container: [{
name: "emulator",
image: "budtmo/docker-android:emulator_11.0",
cpu: 2,
memory: "4Gi",
env: [{
name: "EMULATOR_DEVICE",
value: "Samsung Galaxy S10"
}, {
name: "WEB_VNC",
value: "true"
}]
}],
minReplicas: 1,
maxReplicas: 2
},
ingress: {
allowInsecureConnections: false,
externalEnabled: true,
targetPort: 6080,
traffic: [{
latestRevision: true,
percentage: 100
}]
}
});
}
}
const app = new App();
new AppiumInfraStack(app, "appium-infra");
app.synth();
cdktf.json の設定
{
"language": "typescript",
"app": "npm run get && npx ts-node main.ts",
"projectId": "flutter-appium-e2e",
"sendCrashReports": "false",
"terraformProviders": [
"azurerm@~>3.0"
],
"terraformModules": [],
"codeMakerOutput": "generated",
"context": {
"excludeStackIdFromLogicalIds": "true",
"allowSepCharsInLogicalIds": "true"
}
}
⚙️ 3. GitHub Actionsでインフラとアプリを自動デプロイ
.github/workflows/infrastructure.yml
を作成:
name: Deploy Infrastructure and Appium
on:
push:
branches: [ main ]
paths:
- 'azure-infra/**'
- 'docker/**'
jobs:
deploy-infrastructure:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Setup Azure CLI
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Install CDKTF CLI
run: npm install -g cdktf-cli
- name: Install dependencies
working-directory: azure-infra
run: npm install
- name: Deploy Infrastructure
working-directory: azure-infra
run: |
cdktf get
cdktf deploy --auto-approve
env:
ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
build-and-push-images:
needs: deploy-infrastructure
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Azure Container Registry Login
uses: azure/docker-login@v1
with:
login-server: ${{ secrets.ACR_LOGIN_SERVER }}
username: ${{ secrets.ACR_USERNAME }}
password: ${{ secrets.ACR_PASSWORD }}
- name: Build and push Appium image
run: |
docker build -t ${{ secrets.ACR_LOGIN_SERVER }}/appium:${{ github.sha }} ./docker/appium
docker push ${{ secrets.ACR_LOGIN_SERVER }}/appium:${{ github.sha }}
docker tag ${{ secrets.ACR_LOGIN_SERVER }}/appium:${{ github.sha }} ${{ secrets.ACR_LOGIN_SERVER }}/appium:latest
docker push ${{ secrets.ACR_LOGIN_SERVER }}/appium:latest
📱 4. 更新されたE2Eテストワークフロー
.github/workflows/e2e_tests.yml
を作成:
name: Flutter E2E Tests with Azure Container Apps
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
e2e-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.10.0'
- name: Install dependencies
run: flutter pub get
- name: Build test APK
run: flutter build apk --debug --target=test_driver/app.dart
- name: Wait for Appium service
run: |
echo "Waiting for Appium server to be ready..."
timeout 300 bash -c 'until curl -f http://${{ secrets.APPIUM_SERVER_URL }}/wd/hub/status; do sleep 5; done'
- name: Run E2E Tests
run: |
flutter drive \
--driver=test_driver/app.dart \
--target=test_driver/app_test.dart \
--dart-define=APPIUM_SERVER_URL=${{ secrets.APPIUM_SERVER_URL }}
env:
APPIUM_SERVER_URL: ${{ secrets.APPIUM_SERVER_URL }}
- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: test-results
path: test_driver/screenshots/
deploy:
needs: e2e-tests
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.10.0'
- name: Build release APK
run: |
flutter pub get
flutter build apk --release
- name: Deploy to Firebase App Distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_APP_ID }}
serviceCredentialsFileContent: ${{ secrets.CREDENTIAL_FILE_CONTENT }}
groups: testers
file: build/app/outputs/flutter-apk/app-release.apk
releaseNotes: "E2E tests passed ✅ Build: ${{ github.sha }}"
🔧 5. 更新されたテストコード
test_driver/app_test.dart
を Azure Container Apps 用に更新:
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';
void main() {
group('Flutter E2E Tests with Azure Container Apps', () {
late FlutterDriver driver;
final String appiumUrl = const String.fromEnvironment(
'APPIUM_SERVER_URL',
defaultValue: 'http://localhost:4723'
);
setUpAll(() async {
// Azure Container Apps上のAppiumサーバーに接続
driver = await FlutterDriver.connect(
dartVmServiceUrl: '$appiumUrl/wd/hub',
timeout: Duration(minutes: 2)
);
});
tearDownAll(() async {
driver?.close();
});
test('カウンターアプリの基本動作テスト', () async {
// 初期状態の確認
await driver.waitFor(find.text('0'), timeout: Duration(seconds: 10));
// プラスボタンをタップ
await driver.tap(find.byType('FloatingActionButton'));
// カウントが増加したことを確認
await driver.waitFor(find.text('1'), timeout: Duration(seconds: 5));
// スクリーンショットを撮影
await driver.screenshot().then((pixels) async {
final file = File('test_driver/screenshots/counter_test.png');
await file.writeAsBytes(pixels);
});
});
test('複数回タップのストレステスト', () async {
for (int i = 0; i < 5; i++) {
await driver.tap(find.byType('FloatingActionButton'));
await Future.delayed(Duration(milliseconds: 500));
}
await driver.waitFor(find.text('6'), timeout: Duration(seconds: 10));
});
});
}
📊 6. 監視とスケーリング設定
Azure Monitor アラートの設定
CDKTFにアラート設定を追加:
import { MonitorMetricAlert } from "@cdktf/provider-azurerm/lib/monitor-metric-alert";
// CPU使用率アラート
new MonitorMetricAlert(this, "cpu-alert", {
name: "appium-high-cpu",
resourceGroupName: rg.name,
scopes: [appiumApp.id],
criteria: [{
metricNamespace: "Microsoft.App/containerApps",
metricName: "CpuPercentage",
aggregation: "Average",
operator: "GreaterThan",
threshold: 80
}],
frequency: "PT1M",
windowSize: "PT5M",
severity: 2,
description: "Appium server high CPU usage"
});
自動スケーリングルールの追加
// Container Appの更新でスケーリングルールを追加
template: {
container: [/* existing config */],
minReplicas: 1,
maxReplicas: 5,
scale: [{
minReplicas: 1,
maxReplicas: 5,
rules: [{
name: "cpu-scaling",
custom: {
type: "cpu",
metadata: {
type: "Utilization",
value: "70"
}
}
}]
}]
}
💰 7. コスト最適化
スケジュールベースのスケーリング
// 夜間はレプリカ数を0に
template: {
scale: [{
rules: [{
name: "schedule-scaling",
custom: {
type: "cron",
metadata: {
timezone: "Asia/Tokyo",
start: "0 9 * * 1-5", // 平日9時開始
end: "0 18 * * 1-5", // 平日18時終了
desiredReplicas: "2"
}
}
}]
}]
}
🔐 8. セキュリティ強化
Key Vaultとの統合
import { KeyVault } from "@cdktf/provider-azurerm/lib/key-vault";
import { KeyVaultSecret } from "@cdktf/provider-azurerm/lib/key-vault-secret";
const keyVault = new KeyVault(this, "key-vault", {
name: "kv-appium-secrets",
resourceGroupName: rg.name,
location: rg.location,
tenantId: "your-tenant-id",
skuName: "standard"
});
new KeyVaultSecret(this, "appium-config", {
name: "appium-configuration",
keyVaultId: keyVault.id,
value: JSON.stringify({
serverUrl: "https://your-appium-server.com",
capabilities: { /* ... */ }
})
});
🎉 9. 完成!運用のベストプラクティス
デプロイメント戦略
# ブルー/グリーンデプロイメント
cdktf deploy --var="deployment_slot=green"
# カナリアリリース
cdktf deploy --var="traffic_percentage=10"
災害復旧
// マルチリージョン構成
const primaryRg = new ResourceGroup(this, "primary-rg", {
name: "rg-appium-primary",
location: "East US 2"
});
const secondaryRg = new ResourceGroup(this, "secondary-rg", {
name: "rg-appium-secondary",
location: "West US 2"
});
📈 10. パフォーマンス監視
カスタムメトリクスの追加
# GitHub Actions にメトリクス送信を追加
- name: Send metrics to Azure Monitor
run: |
az monitor metrics emit \
--resource ${{ secrets.CONTAINER_APP_ID }} \
--namespace "Custom/E2ETests" \
--metric "TestDuration" \
--value $TEST_DURATION \
--dimension "TestSuite=smoke"
💡 まとめ
Azure Container Apps + CDK for Terraformを使った最強のE2E自動化パイプラインが完成しました!🎊
得られるメリット:
- 🐳 コンテナ化: 一貫した実行環境
- ☁️ クラウドネイティブ: スケーラブルで高可用性
- 🏗️ Infrastructure as Code: 再現可能なインフラ
- 🔄 自動スケーリング: コスト効率的な運用
- 📊 包括的な監視: 問題の早期発見
- 🔐 エンタープライズレベルのセキュリティ
この構成により、開発チーム全体の生産性が飛躍的に向上し、高品質なアプリリリースが実現できます!
質問やフィードバックがあれば、コメントでお気軽にどうぞ〜 ✨
関連記事:
#Flutter #Azure #ContainerApps #CDKTerraform #Docker #Appium #E2Eテスト #InfrastructureAsCode #CloudNative
Discussion