TECH PLAY

NTTドコモビジネス

NTTドコモビジネス の技術ブログ

613

はじめに こんにちは、イノベーションセンターの福田です。 今回、開発環境改善の取り組みとして GitHub Actions の self-hosted runners を AWS 上に構築しました。 この構築で得られた知見について共有します。 概要 GitHub Actions は GitHub で CI/CD を手軽に実現する機能です。 GitHub が提供している環境を利用して、 CI/CD のジョブを実行できます 1 。 一方で、ハードウェア等をカスタマイズできないため、例えば容量が大きくより速度の早いストレージを利用したい場合や、より多くのメモリを利用したい場合に対応ができません。 そこで、GitHub Actions には self-hosted runners という機能があり、自身の環境で GitHub Actions の CI/CD ジョブを走らせる環境を用意できます。 今回はより柔軟に利用しやすく、保守しやすいように AWS Cloud Development Kit (以下CDK)を用いて AWS 上に self-hosted runners 環境を構築したので、その構築手順等について解説していきたいと思います。 アーキテクチャ 今回構築するシステムのアーキテクチャについて解説します。 構築するインフラの構成図は次の通りです。 図の中で点線によってグループ化されていますが、本システムは大きく次の2つの部分からなっています。 Webhook イベントの受信部分 self-hosted runners 環境提供部分 GitHub では GitHub Actions の CI/CD ジョブが走り始めたときに Webhook イベントを飛ばすことができ、これを利用して CI/CD ジョブの開始と同時にその CI/CD ジョブを走らせる self-hosted runners を起動するというシステムになっています。 Webhook イベントの受信 まず、 self-hosted runners で走らせたい CI/CD ジョブが開始されたのかを知る必要があります。 GitHub には workflow_job というイベントが Webhook イベントとして出ており、このイベントのペイロードに以下の情報が含まれています。 ジョブの実行環境の指定 GitHub-hosted runners での実行か、 self-hosted runners での実行かも指定されています。 ジョブのステータス ジョブの開始が要求されたのか、現在ジョブが走っているのか、ジョブが終了したのかのステータスがわかります。 この workflow_job イベントを Webhook イベントとして受信することで self-hosted runners で走らせたい CI/CD ジョブが開始されたのかを知ることができます。 したがって、最初に workflow_job イベントを受信する Webhook の受信先を AWS に用意します。 AWS には API Gateway というサービスがあり、このサービスを使って容易に Web API を作成できます。 なので、 workflow_job イベントの受信部分に関してはこの API Gateway を使って構築します。 しかし、 workflow_job は GitHub-hosted runners で走らせたい CI/CD ジョブが開始されたときにも発行されるイベントです。 そのため、 API Gateway で作成した Web API でこの workflow_job イベントを受信し self-hosted runners を起動するという単純な実装にしてしまうと self-hosted runners で走らせたいジョブ数よりも大くの self-hosted runners が起動し、予定よりも多くの課金がなされてしまいます。 これを避けるため、作成した Web API が webhook イベントを受信したら self-hosted runners で走らせたい CI/CD ジョブの開始が要求されたイベントだけを抽出し、それ以外のイベントは無視するような仕組みを導入します。 今回使用する API Gateway は Lambda 関数 の HTTP エンポイントを提供できるので、この機能を利用し、作成した Web API のバックエンドで Lambda 関数によるイベントフィルタリングの機能を追加します。 しかし、この Lambda 関数の HTTP エンドポイントの機能には Lambda 関数の最大実行時間が29秒までであるという制約が存在します。 そのまま self-hosted runners を動かそうとするとこの制約にひっかかってしまい、 self-hosted runners が起動できなくなる可能性があります。 これを回避するために、 Lambda 関数はフィルターを行った後に self-hosted runners を起動するようなことはせず、一旦 SQS にフィルターしたイベントを送信します。 こうすれば SQS からあとの処理は独立して行えるため、先述した制限を回避できます。 2 。 以上をまとめると、 Webhook の受信部分は次のようなインフラストラクチャーを構築することになります。 self-hosted runners の提供 ここからは受信した Webhook イベントを元に、適切な self-hosted runners を走らせる VM (以下、 self-hosted runners ノード) を立ち上げるためのインフラ構成について解説します。 SQS には Webhook イベントが送信されているので、まずはその SQS からメッセージを受信する部分を構築します。 SQS がメッセージを受信したとき、Lambda 関数へメッセージの受信イベントを通知できます 3 。 この機能を利用して、 SQS から Webhook イベントを受信次第 self-hosted runners ノードとなる VM である EC2 インスタンス を起動する Lambda 関数を用意します。 この際、 EC2 インスタンスを EC2 インスタンスの起動を行う API を通して起動することになるのですが、ここで Launch Template という EC2 インスタンスの起動パラメータのテンプレートを利用して起動するようにします。 Launch Template は CDK で簡単に用意することができる ため、こちらの機能を通して起動する方が Lambda 関数に EC2 インスタンスの起動パラメータを全て渡すようなやり方よりも楽に保守・管理・運用できます 4 。 このように、 SQS から Webhook イベントを受信してから Lambda 関数を起動し、その Lambda 関数は self-hosted runners ノードを起動することで self-hosted runners を必要な分だけ提供できます。 起動した self-hosted runners ノードは CI/CD ジョブを実行し、その CI/CD ジョブが終了次第シャットダウンするようにします。 EC2 インスタンスにはシャットダウン終了後に自動で VM の削除するように設定でき、この設定もしておきます。 こうすることでインスタンスやそのストレージへの課金額を抑えられると同時に、前のジョブの情報がインスタンス内に残らないようにできます。 つまり、毎回クリーンな環境でインスタンスを起動でき、各ジョブに依存関係が発生するのを防ぐことができます。 最後に、 self-hosted runners を起動する際には以下の2つの処理を self-hosted runners の起動前に行う必要があるので、これらを EC2 インスタンスの 起動時にスクリプトを走らせる機能 を使っておこないます。 最新版の self hosted runners のダウンロード self-hosted runners を登録 このとき、シェルクリプトでやるには複雑すぎることがあったり、やりとりされるパケットの量を抑えたりするために、いくつかの処理を Lambda 関数を通して行うようにします。 これらをまとめると、以下のような構成図を構築することになります。 図中には S3 バケットが出現していますが、こちらは 1. のダウンロードしてきた self-hosted runners をキャッシュするために利用しています。 CDK によるインフラ構築 先述したシステムを Web コンソールや CLI だけで構築し、適切に保守・運用していくのは難しいため CDK を利用します。 self-hosted runners 環境 最初に self-hosted runners ノードを起動する仕組みを構築します。 self-hosted runners ノード自体は Public な場所に置いておく必要はなく、 GitHub との接続ができればよいため Private な サブネット へと設置するようにし、 Public なサブネットに NAT ゲートウェイ を設置して Private サブネットの通信をそこへ流すようにします。 まずは self-hosted runners ノード起動時に走らせるスクリプト内で呼ばれる補助 Lambda 関数を用意します。 self-hosted runners を提供するプログラムをダウンロードする関数 ダウンロードするバイナリーにバージョン番号がついているため、先に最新版のバージョン番号を GitHub API から取得しておく必要があり、スクリプトの複雑度や制御難易度を上げてしまうので肩代わりさせる 関数を呼び出せば最新版が常に手に入り、テストも用意になる 走らせる self-hosted runners を GitHub に登録するためのトークンをつくる関数 この部分は JWT をつくったりするなど多少複雑なロジックを実装する必要があり、 bash スクリプトでやるよりも Lambda 関数に処理を移譲してしまった方が良さそうです 実は 1. の場合は Lambda 関数の制限として 6MB までのレスポンスしか返せません 。 self-hosted runners はこの制限である 6MB を越えています 。 そのため、Lambda 関数としては self-hosted runners をそのまま返すことはせずに、 S3 バケット に一旦ダウンロードしてきたものを配置し、その配置先をレスポンスに載せます。 ダウンロードは VPC に S3 のエンドポイントを設置して、 Lambda 関数のレスポンスから配置先をよみとって S3 バケットからダウンロードしてきます。 こうすることで、副次的に NAT ゲートウェイ を通るパケットの量を減らすことができるので、 NAT ゲートウェイの料金を抑える効果もあります。 さて、この2つの関数を CDK に載せていきます。 なお、ここでは用意する関数はすべて Go 言語 で記載しています。 また、 Go 言語の module 機能を利用するので go v1.11 以上が必要です。 さらに、 CDK では Python や Java 等様々な言語が使えますが、ここでは TypeScript を用いて CDK コードを記述していきます。 最初に 1. の関数を載せます。 やりたいこととしては以下の4つになります。 ダウンロードしたい self-hosted runners のバージョンを取得 先程取得したバージョンを指定して self-hosted runners をダウンロード ダウンロードしたものを S3 バケットに設置 設置先を関数の返り値として返す これを動かすコードは以下のようになります。 package main import ( "context" "encoding/json" "fmt" "io/ioutil" "net/http" "os" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/s3" ) type Response struct { Bucket string `json:"bucket"` Key string `json:"key"` } type PackageMetadata struct { TagName string `json:"tag_name"` } func Fetch(url string ) ([] byte , error ) { resp, err := http.Get(url) if err != nil { return nil , err } defer resp.Body.Close() return ioutil.ReadAll(resp.Body) } func HandleRequest(ctx context.Context) (Response, error ) { // self-hosted runners のキャッシュ先 S3 バケット名は // 環境変数として設定する bucket, has := os.LookupEnv( "BUCKET" ) if !has { return Response{}, fmt.Errorf( "No such environement: BUCKET" ) } cfg, err := config.LoadDefaultConfig(ctx) if err != nil { return Response{}, err } client := s3.NewFromConfig(cfg) // 1. ダウンロードしたい self-hosted runners のバージョンを取得 // // ここでは最新版をダウンロード b, err := Fetch( "https://api.github.com/repos/actions/runner/releases/latest" ) if err != nil { return Response{}, err } m := &PackageMetadata{} if err := json.Unmarshal(b, m); err != nil { return Response{}, err } // 先頭に `v` がプリフィックスとしてついてるので、 // そこだけ無視してバージョンを取得 ver := m.TagName[ 1 :] // 2. 先程取得したバージョンを指定して self-hosted runners をダウンロード b, err = Fetch( fmt.Sprintf( "https://github.com/actions/runner/releases/download/v%s/actions-runner-linux-x64-%s.tar.gz" , ver, ver) ) if err != nil { return Response{}, err } // 3. self-hosted runners を S3 に設置する param := &s3.PutObjectInput{ Bucket: aws.String(bucket), // 決め打ちで runner.tar.gz というキーで // self-hosted runners のバイナリーを保存しておく Key: aws.String( "runner.tar.gz" ), Body: b, } if _, err := client.PutObject(ctx, param); err != nil { return Response{}, err } // 4. 設置先を関数の返却値として返す return Response{Bucket: bucket, Key: "runner.tar.gz" }, nil } func main() { lambda.Start(HandleRequest) } Lambda は Docker コンテナを動かせる ので、 そのための Dockerfile を書きます。 FROM --platform=$TARGETPLATFORM golang:1 as build WORKDIR /usr/src/app ARG TARGETOS ARG TARGETARCH COPY go.mod go.sum ./ RUN go mod download && go mod tidy COPY . . RUN env CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -v -o /main ./... FROM --platform=$TARGETPLATFORM public.ecr.aws/lambda/provided:al2 COPY --from=build /main ${LAMBDA_RUNTIME_DIR}/bootstrap CMD [ "main" ] 同じように 2. の関数も用意します。 注意点として、 事前に GitHub Apps として self-hosted runners を登録する Lambda 関数を登録する必要があります。 GitHub Apps として登録する際には以下2点を設定しておかなければならないことに注意してください。 self-hosted runners を登録する Lambda 関数が self-hosted runners まわりの操作を Read/Write できるよう権限を付与しておく GitHub Organizations の全てのリポジトリで動作するようにさせる場合、 Repository Access を All Repositories に設定しておく この GitHub Apps への登録ができたら、起動させる self-hosted runners を GitHub へ登録する関数を書いていきます。 package main import ( "context" "fmt" "os" "strconv" "github.com/aws/aws-lambda-go/lambda" "github.com/bradleyfalzon/ghinstallation/v2" "github.com/google/go-github/v45/github" ) func InstallationId(org string ) ( int64 , error ) { itr, err := ghinstallation.NewAppsTransport( https.DefaultTransport, appId, privateKey, ) if err != nil { return 0 , err } client := github.NewCient(&http.Client{Transport: itr}) listOpt := &github.ListOptions{} for { is, resp, err := client.Apps.ListInstallations(ctx, listOpt) if err != nil { return 0 , err } // GitHub Apps の全てのインストール先から、 // 指定された Organization にインストールされているものを見つけて // そのインストールにつけられた ID を取得する。 for _, inst := range insts { if inst.GetAccount().GetLogin() == org { return inst.GetID(), nil } } if resp.NextPage == 0 { break } listOpt.Page = resp.NextPage } return 0 , fmt.Errorf( "cannot find installation id on %s" , org) } func HandleRequest(ctx context.Context) ( map [ string ] string , error ) { org, has := os.LookupEnv( "ORGANIZATION" ) if !has { return "" , fmt.Errorf( "No such environment: ORGANIZATION" ) } appIdStr, has := os.LookupEnv( "GITHUB_APPS_APP_ID" ) if !has { return "" , fmt.Errorf( "No such environment: GITHUB_APPS_APP_ID" ) } appId, err = strconv.ParseInt(appIdStr, 10 , 64 ) if err != nil { return "" , fmt.Errorf( "GITHUB_APPS_APP_ID is not integer: %s" , appIdStr) } privateKey, has := os.LookupEnv( "GITHUB_APPS_APP_PRIVATE_KEY" ) if !has { return "" , fmt.Errorf( "No such environment: GITHUB_APPS_APP_PRIVATE_KEY" ) } instId, err := InstallationId(org) if err != nil { return "" , err } // 2. self-hosted runners を登録する際に必要となるトークンの発行 itr, err := ghinstallation.New(http.DefaultTransport, appId, instId, privateKey) if err != nil { return "" , err } client := github.NewCient(&http.Client{Transport: itr}) resp, _, err := client.Actions.CreateOrganizationRegistrationToken(ctx, org) if err != nil { return "" , err } return resp.GetToken(), nil } func main() { lambda.Start(HandleRequest) } 先述した2つの関数を CDK として載せます。 import * as cdk from "aws-cdk-lib" ; import * as { Construct } from "constructs" ; import * as path from "path" ; // self-hosted runners をダウンロードする Lambda 関数 export class RunnerDownloader extends cdk.aws_lambda.DockerImageFunction { constructor( scope: Construct , id: string ) { super( scope , id , { code: cdk.aws_lambda.DockerImageCode.fromImageAsset ( // Lambda 関数のコードの設置先はこのソースファイルと同じ階層にある // `handler/runner-downloader` に path.join ( __dirname , "handler" , "runner-downloader" ), ), timeout: cdk.Duration.minutes ( 5 ), memorySize: 1024 , } ); const storage = new cdk.aws_s3.Bucket ( this , "Storage" , { lifecycleRules: [ // 7日間だけはキャッシュしておくが、 // 7日超えたら最新のものを取得 { enabled: true , expiration: cdk.Duration.days ( 7 ), } ] , // キャッシュとしてしか利用しないので、スタックの削除時には // このバケット内のオブジェクトを全て削除する autoDeleteObjercts: true , removalPolicy: cdk.RemovalPolicy.DESTROY , } ); storage.grantRead ( this ); storage.grantPut ( this ); this .addEnvironment ( "BUCKET" , storage.bucketName ); } } export type RegistrationTokenGeneratorProps = { githubAppsPrivateKey: string ; githubAppsAppId: string ; } ; // self-hosted runners を登録するためのトークンを生成する Lambda 関数 export class RegistrationTokenGenerator extends cdk.aws_lambda.DockerImageFunction { constructor( scope: Construct , id: string , props: RegistrationTokenGeneratorProps ) { super( scope , id , { code: cdk.aws_lambda.DockerImageCode.fromImageAsset ( // Lambda 関数のコードの設置先はこのソースファイルと同じ階層にある // `handler/registration-token-generator` に path.join ( __dirname , "handler" , "registration-token-generator" ) ), timeout: cdk.Duration.minutes ( 5 ), environment: { GITHUB_APPS_APP_ID: props.githubAppsAppId , GITHUB_APPS_PRIVATE_KEY: props.githubAppsPrivateKey , } , memorySize: 1024 , } ); } } 用意できたら self-hosted runners を起動するスクリプトを組みます。 #!/usr/bin/env bash function setup() { local -r \ runner_downloader="$1" \ registration_token_generator="$2" \ organization="$3" \ runner_directory="$4" \ mkdir "$runner_directory" && cd "$runner_directory" || return 1 # self-hosted runners を取得する local -r runner_json=/tmp/runner.json aws lambda invoke \ --function-name "$runner_downloader" \ --payload "{}" \ --cli-binary-format raw-in-base64-out \ "$runner_json" local -r runner_archive=/tmp/runner.tar.gz aws s3api get-object \ --bucket "$(jq -r .bucket "$runner_json")" \ --key "$(jq -r .key "$runner_json")" \ "$runner_archive" tar zxf "$runner_archive" # self-hosted runners の登録用トークン発行 local -r registration_token_json=/tmp/registration-token.json aws lambda invoke \ --function-name "$registration_token_generator" \ --payload "{}" \ --cli-binary-format raw-in-base64-out \ "$registration_token_json" local registration_token registration_token="$(jq -r .token "$registration_token_json")" # runner の初期設定を行う ./config.sh \ --url "https://github.com/$organization" \ --disable-update \ --ephemeral \ --unattended } function main() { local -r \ runner_downloader="$1" \ registration_token_generator="$2" \ organization="$3" # self-hosted runners を走らせるためのユーザーを作成 useradd -m -g users -G sudo -s /bin/bash runner # self-hosted runners を起動する準備を行う local runner_directory=~runner/runner su \ - runner \ -c "$(declare -f setup); setup \"$runner_downloader\" \"$registration_token_generator\" \"$organization\" \$runner_directory\"" cd "$runner_directory" && ./bin/installdependencies.sh # self-hosted runners を起動 # self-hosted runners がタスクを走り終えたら poweroff して VM を終了させる su - runner -c "cd '$runner_directory' && ./run.sh" \ && systemctl poweroff } main "$@" あとはこのスクリプトを利用して self-hosted runners ノードとなる EC2 インスタンスのための Launch Template を作成します。 import * as cdk from "aws-cdk-lib" ; import { Construct } from "constructs" ; import * as path from "path" ; // 先程用意した補助関数をインポートする。 import { RunnerDownloader } from "./runner-downloader" ; import { RegistrationTokenGenerator } from "./registration-token-generator" ; export type LaunchTemplateProps = { runnerDownloader: RunnerDownloader ; registrationTokenGenerator: RegistrationTokenGenerator ; organization: string ; } ; export class LaunchTemplate extends cdk.Resource { public readonly launchTemplate: cdk.aws_ec2.LaunchTemplate ; constructor( scope: Construct , id: string , props: LaunchTemplateProps ) { super( scope , id ); const role = new cdk.aws_iam.Role ( this , "Role" , { assumedBy: new cdk.aws_iam.ServicePrincipal ( "ec2.amazonaws.com" ), managedPolicies: [ cdk.aws_iam.ManagedPolicy.fromAwsManagedPolicyName ( "AmazonSSMManagedInstanceCore" ), ] , } ); const asset = new cdk.aws_s3_assets.Asset ( this , "UserData" , { // 先程のスクリプトを start.bash という名前で保存しておく。 path: path.join ( __dirname , "start.bash" ), } ); asset.grantRead ( role ); const userData = cdk.aws_ec2.UserData.forLinux ( { shebang: "#!/usr/bin/env bash" , } ); const filePath = this .userData.addS3DownloadCommand ( { bucket: asset.bucket , bucketKey: asset.s3ObjectKey , } ); userData.addExecuteFileCommand ( { filePath , arguments : [ props.runnerDownloader.functionArn , props.registrationTokenGenerator.funcitonArn , organization , ] .join ( " " ), } ); this .launchTemplate new cdk.aws_ec2.LaunchTemplate ( this , "LaunchTemplate" , { role , instanceType: cdk.aws_ec2.InstanceType. of( cdk.aws_ec2.InstanceClass.T3 , cdk.aws_ec2.InstanceSize.SMALL , ), // ここには GitHub Actions で利用するイメージを指定する。 // ただし、先程のスクリプト内で awscli v2 と jq を利用しているので、 // これらを含めた上で、 GitHub Actions でサポートされている // タイプの OS を指定する必要がある。 // // 参考: https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners#supported-architectures-and-operating-systems-for-self-hosted-runners machineImage: GithubActionsMachineImage , userDate , ebsOptimized: true , // シャットダウン時にはインスタンスを terminate させる。 instanceInitiatedShutdownBehavior: cdk.aws_ec2.InstanceInitiatedShutdownBehavor.TERMINATE , blockDevices: [ { deviceName: "/dev/sda1" , volume: cdk.aws_ec2.BlockDeviceVolume.ebs ( 100 , { volumeType: cdk.aws_ec2.EvsDeviceVolumeType.GP3 , deleteOnTermination: true , encrypted: true , } ) } ] } ); } } 次にこの Launch template からインスタンスを起動する Lambda 関数を書きます。 まずは関数のコードを用意します(この関数も先程までの関数と同じで Lambda コンテナイメージとして提供します)。 package main import ( "context" "fmt" "os" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" ) // あとでこの関数を SQS に接続し、 SQS からイベントをうけとったら起動するようにするため、 // ここでは events.SQSEvent を受け取るようにしています。 func HandleRequest(ctx context.Context, req events.SQSEvent) error { ltId, has := os.LookupEnv( "LAUNCHTEMPLATE_ID" ); if !has { return fmt.Errorf( "No such environment: LAUNCHTEMPLATE_ID" ); } ltVer, has := os.LookupEnv( "LAUNCHTEMPLATE_VERSION" ); if !has { return fmt.Errorf( "No such environment: LAUNCHTEMPLATE_VERSION" ) } snId, has := os.LookupEnv( "SUBNET_ID" ); if !has { return fmt.Errorf( "No such environment: SUBNET_ID" ); } cfg, err := config.LoadDefaultConfig(ctx) if err != nil { return err } client := ec2.NewFromConfig(cfg) for range req.Records { param := &ec2.RunInstancesInput{ LaunchTemplate: &types.LaunchTemplateSpecification{ LaunchTemplateId: aws.String(ltId), Version: aws.String(ltVer), }, SubnetId: aws.String(snId), MaxCount: aws.Int32( 1 ), MinCount: aws.Int32( 1 ), } resp, err := client.RunInstances(ctx, param) if err != nil { return err } } return nil } func main() { lambda.Start(HandleRequest) } この関数の CDK コードを用意します。 // 先程の LaunchTemplate クラスをインポートする。 import { LaunchTemplate } from "./launch-template" ; export type InstanceLauncherProps = { launchTemplate: LaunchTemplate ; subnet: cdk.aws_ec2.ISubnet ; } ; // self-hosted runners を登録するためのトークンを生成する Lambda 関数 export class InstanceLauncher extends cdk.aws_lambda.DockerImageFunction { constructor( scope: Construct , id: string , props: InstanceLauncherProps ) { super( scope , id , { code: cdk.aws_lambda.DockerImageCode.fromImageAsset ( // Lambda 関数のコードの設置先はこのソースファイルと同じ階層にある // `handler/registration-token-generator` に path.join ( __dirname , "handler" , "instance-launcher" ) ), timeout: cdk.Duration.minutes ( 5 ), environment: { LAUNCHTEMPLATE_ID: props.launchTemplate.launchTemplateId , LAUNCHTEMPLATE_VERSION: props.launchTemplate.latestVersionNumber , SUBNET_ID: props.subnet.subnetId , } , memorySize: 1024 , } ); this .addToRolePolicy ( new cdk.aws_iam.PolicyStatement ( { resources: [ "*" ] , // EC2 インスタンスを立ち上げるには `ec2:RunInstances` が必要。 // また、 立ち上げた EC2 インスタンスにロールを付与するために `iam:PassRole` も必要で、 // さらにタグを Launch Template を通してつけたりするためには CreateTags が必要となる。 actions: [ "ec2:RunInstances" , "iam:PassRole" , "ec2:CreateTags" ] } ) ); } } 最後にこれらをまとめたコンポーネントを作成します。 import * as cdk from "aws-cdk-lib" ; import { Construct } from "constructs" ; import { InstanceLauncher } from "./instance-launcher" ; import { RunnerDownloader } from "./runner-downloader" ; import { RegistrationTokenGenerator } from "./registration-token-generator" ; import { LaunchTemplate } from "./LaunchTemplate" ; export type SelfHostedRunnerProps = { githubAppsAppId: string ; githubAppsPrivateKey: string ; organization: string ; subnet: cdk.aws_ec2.ISubnet ; } ; export class SelfHostedRunner extends cdk.Resource { public readonly instanceLauncher: InstanceLauncher ; constructor( scope: Construct , id: string , props: SelfHostedRunnerProps ) { super( scope , id ); const runnerDownloader = new RunnerDownloader ( this , "RunnerDownloader" ); const registrationTokenGenerator = new RegistrationTokenGenerator ( this , "RegistrationTokenGenerator" , { githubAppsAppId: props.githubAppsAppId , githubAppsPrivateKey: props.githubAppsPrivateKey , } ); const launchTemplate = new LaunchTemplate ( this , "LaunchTemplate" , { runnerDownloader , registrationTokenGenerator , organization: props.organization , } ); this .instanceLauncher = new InstanceLauncher ( this , "InstanceLauncher" , { launchTemplate , subnet: props.subnet , } ); } } Webhook self-hosted runners ノードを起動する部分の作成ができたので、今度は Webhook を受信する部分を作成してきます。 最初に、 Webhook イベントを送信する SQS と、先程用意した self-hosted runners ノードを起動する Lambda 関数がその SQS のメッセージ受信したら起動するようにする設定します。 import * as cdk from "aws-cdk-lib" ; import { Construct } from "constructs" ; // instance を立ち上げる Lambda 関数をインポート import { InstanceLauncher } from "./instance-launcher" ; export type InstanceLaunchQueueProps = { // プロパティで self-hosted runners ノードを起動する Lambda 関数を渡してもらう instanceLauncher: InstanceLauncher ; } export class InstanchLaunchQueue extends cdk.aws_sqs.Queue { constructor( scope: Construct , id: string , props: InstanceLaunchQueueProps ) { super( scope , id , { visibilityTimeout: cdk.Duration.minutes ( 5 ) } ); // SQS のイベントを受信して EC2 インスタンスを立ち上げる Lambda 関数を呼ぶ props.intanceLauncher.addEventSource ( new cdk.aws_lambda_event_sourecs.SqsEventSource ( this ) ); } } 次に Webhook の受信部分となる API Gateway による Web API の作成と、その裏で動くイベントのフィルタリングを行ったあとに SQS へ Webhook イベントを送信する Lambda 関数を実装します。 イベントのフィルタリング方法ですが、 先述したとおり、 workflow_job イベントを利用します。 この workflow_job イベントのペイロードには workflow というオブジェクトが含まれており、この中に status というプロパティがあります。 この status プロパティは CI/CD ジョブのステータスを表しています。 ステータスとしては次の3つが用意されています。 queued : ジョブが開始したものの、まだ runner が割り当てられていない状態 in_progress : ジョブに runner が割り当てられて現在ジョブが走っている状態 completed : ジョブが完了したことを表す状態 このうち、 queued が今回必要になるイベントであるため、これ以外のものをまずははじくことが必要です。 さらに、 workflow_job イベントのペイロード中にある workflow オブジェクトには labels という文字配列プロパティがあり、 self-hosted runners で走らせる CI/CD ジョブが開始した場合はこの配列中に self-hosted というラベルが入っています。 なので、 self-hosted runners で走るジョブだけを抽出する際にはこの labels プロパティの要素も見てあげる必要があります。 これらを元に必要となる Webhook イベントのみを抽出し、 self-hosted runners ノードを起動するところまでを作成します 5 。 まず、イベントフィルタリングをしたあとに SQS へメッセージを送信する Lambda 関数の実装は次の通りです。 package main import ( "context" "fmt" "os" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/sqs" "github.com/google/go-github/v45/github" ) // あとで API Gateway からこの関数を呼び出す func HandleRequest(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error ) { qUrl, has := os.LookupEnv( "QUEUE_URL" ) if !has { resp := events.APIGatewayProxyResponse{ StatusCode: 500 , Body: "No such environment: QUEUE_URL" , } return resp, fmt.Errorf( "No such environment: QUEUE_URL" ) } // イベントのフィルタリング var ev github.WorkflowJobEvent if err := json.Unmarshal(req.Body, &ev); err != nil { // workflow_job イベント以外は全部無視 resp := events.APIGatewayProxyResponse{ StatusCode: 200 , Body: "ignore event" , } return resp, nil } if stat := ev.GetWorkflowJob().GetStatus(); stat != "queued" { // queued なもの以外は全部無視 resp := events.APIGatewayProxyResponse{ StatusCode: 200 , Body: "ignore event" , } return resp, nil } isShr := false for _, label := range ev.GetWorkflowJob().Labels { if label == "self-hosted" { isShr = true break } } if !isShr { // queued なもの以外は全部無視 resp := events.APIGatewayProxyResponse{ StatusCode: 200 , Body: "ignore event" , } return resp, nil } // Queue にメッセージを送信 cfg, err := config.LoadDefaultConfig(ctx) if err != nil { resp := events.APIGatewayProxyResponse{ StatusCode: 500 , Body: fmt.Sprintf( "%v" , err), } return resp, err } client := sqs.NewFromConfig(cfg) param := &sqs.SendMessageInput{ QueueUrl: aws.String(qUrl), // Queue からイベントがでればそのまま self-hosted runners が立ち上がるため、 // メッセージは何でもよい。そのため、ここでは `{}` をメッセージとして送信している。 MessageBody: aws.String( "{}" ), } if _, err = client.SendMessage(ctx, param); err != nil { resp := events.APIGatewayProxyResponse{ StatusCode: 500 , Body: fmt.Sprintf( "%v" , err), } return resp, err } resp := events.APIGatewayProxyResponse{ StatusCode: 200 , Body: "OK" , } return resp, nil } func main() { lambda.Start(HandleRequest) } この関数をプロビジョニングするための CDK コードは次の通りです。 import * as cdk from "aws-cdk-lib" ; import { Construct } from "constructs" ; import * as path from "path" ; import { InstanceLaunchQueue } from "./instance-launch-queue" ; export type EventPublisherProps = { queue: InstanceLaunchQueue ; } ; export class EventPublisher extends cdk.aws_lambda.DockerImageFunction { constructor( scope: Construct , id: string , props: EventPublisherProps ) { super( scope , id , { code: cdk.aws_lambda.DockerImageCode.fromImageAsset ( // Lambda 関数のコードの設置先はこのソースファイルと同じ階層にある // `handler/event-publisher` に path.join ( __dirname , "handler" , "event-publisher" ) ), timeout: cdk.Duration.minutes ( 5 ), environment: { QUEUE_URL: props.queue.queueUrl , } , memorySize: 1024 , } ); this .addToRolePolicy ( new cdk.aws_iam.PolicyStatement ( { resources: [ "*" ] , actions: [ "sqs:SendMessage" ] } ) ); } } 最後に、 Webhook の受け口となる API Gateway を設置し、用意した Lambda 関数を呼び出すように設定すれば作成完了です。 import * as cdk from "aws-cdk-lib" ; import { Construct } from "constructs" ; // 作成したものをインポート import { EventPublisher } from "./event-publisher" ; import { InstanceLaunchQueue } from "./instance-launch-queue" ; import { InstanceLauncher } from "./instance-launcher" ; export type WebhookProps = { instanceLauncher: InstanceLauncher ; } ; export class Webhook extends cdk.Resource { public readonly gateway: cdk.aws_apigateway.LambdaRestApi ; constructor( scope: Construct , id: string , props: InstanceLauncher ) { super( scope , id ); const queue = new InstanceLaunchQueue ( this , "Queue" , { instanceLauncher: props.instanceLauncher , } ); const eventPublisher = new EventPublisher ( this , "EventPublisher" , { queue , } ); this .gateway = new cdk.aws_apigateway.LambdaRestApi ( this , "Webhook" , { handler: eventPublisher } ); } } あとは GitHub 側で Webhook を作成し、ここでデプロイされる API Gateway の URL を Webhook として登録すれば完了です。 まとめ ここまで self-hosted runners を AWS 上に CDK を用いて構築する方法について解説しました。 多少複雑な機構となってはいますが、やりたいこととしては Webhook から CI/CD ジョブの開始イベントがきたら self-hosted runners を立ち上げるだけです。 ここでは解説していませんが、多少拡張していけば arm64 アーキテクチャな self-hosted runners 環境を作成したり、 Windows 環境を用意して実行したりできます。 この環境をベースとして皆さんでより良い開発環境を作成していただければと思います。 https://docs.github.com/ja/actions/using-github-hosted-runners/about-github-hosted-runners ↩ self-hosted runners の立ち上げは Webhook イベントの受信とは別の操作になりますし、最悪その後の処理で EC2 インスタンスが立ち上がらなくても GitHub Actions のジョブは72時間で失敗としてとりあつかわれます。さらにジョブは再度流せるので、 self-hosted runners 環境を再度立ち上げてジョブを走らせることができればいいため、切り離すことによる問題はそこまで大きくはないと思います。 ↩ https://docs.aws.amazon.com/ja_jp/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-configure-lambda-function-trigger.html ↩ Auto Scaling Group を使えば同じようにインスタンスの起動をより楽に行えますが、起動したインスタンスの管理が煩雑になります。少し試してみればわかりますが、インスタンス数を増やすのはとても簡単な一方、インスタンス数を減らすのはとても難しいです。たとえば、大抵の CI/CD ジョブはすぐに終了することが多いため、無駄なコストをかけないように土日には self-hosted runners 環境を提供する EC2 インスタンスの数を0にするとします。しかし、 ML トレーニングするインスタンス等時間のかかる CI/CD ジョブが金曜夜に投入され、土曜にジョブが割り込んだとき、上記ルールに則りインスタンスが終了させられ、ジョブが中断させられてしまいます。これをちゃんとどのインスタンスが走っているのかを把握するのは難しいため、適切なジョブ管理をする仕組みを導入しなければなりません。なので、 Auto Scaling Group を使わずにジョブの要求に応じて Launch Template から起動するようにしておき、ジョブが走った後はそのまま EC2 インスタンスを終了するようにすれば複雑なジョブの管理は必要なくなります。 ↩ 実は GitHub には Organization で共有して利用できる self-hosted runners の他に、リポジトリー毎にも self-hosted runners を登録できます。 workflow_job イベントではこれらの区別がつかないため、リポジトリー毎に登録している self-hosted runners を利用したいのか、 Organization で共有して利用できる self-hosted runners を起動したいのかを区別することは難しいです。簡単な解決策としては各 self-hosted runners に ラベル を登録できるので、これを利用してそれぞれを区別するようにすれば良いです。ただし、今回示す例ではそのようなリポジトリー毎に登録されている self-hosted runners はないものとして進めます。 ↩
はじめに DevOpsプラットフォームの取り組みを紹介する5回目の記事です。 Qmonus Value Stream 開発チームの杉野です。 連載第5回では、Qmonus Value StreamでCI/CD機能を実現するための要素技術として用いている、OSSのTektonについて紹介します。 これまでの記事をまだ見ていないという方は、Qmonus Value StreamというプラットフォームがどのようにTektonを利用しているかを 過去の記事 で述べていますので、覗いてみてください。 また、本記事ではKubernetes(以下、k8s)に関する知識がある前提で記述していますので、ご了承ください。 Tekton とは Tektonは一言でいうと、CI/CDシステムを作成するためのKubernetes Nativeなオープンソースフレームワークです。さらに噛み砕いて表現すると、k8s上で動作してCI/CDの機能を実現するソフトウェアです。 Tektonは、基盤となるTekton Pipelinesと呼ばれるコンポーネントと、Tektonをエコシステムにするための複数のサポートコンポーネントで構成されます。現在は次のようなコンポーネントが存在しています。 Tekton Pipelines : パイプラインを構築するためのビルディングブロックを提供するTektonの基盤コンポーネント Tekton Triggers : パイプラインをイベントトリガーで実行する機能を提供 Tekton CLI : パイプラインを管理するコマンドラインインタフェースを提供 Tekton Dashboard : パイプラインの確認や実行が可能なWeb UIを提供 Tekton Catalog : コミュニティが提供するビルディングブロックのリポジトリ Tekton Hub : Tekton CatalogのWeb UIを提供 Tekton Operator : Tektonのコンポーネントを管理する機能を提供 Tekton Results : パイプラインの実行履歴のストレージ機能を提供 Tekton Chains : パイプラインでサプライチェーンセキュリティの機能を提供 上記コンポーネントの中でもPipelines、Triggers、CLI、Dashboardは主要コンポーネントと呼ばれます。本記事では、Tekton Pipelinesに焦点をあてて紹介します。 Tekton Pipelines コンセプト TektonにはStep、Task、Pipelineと呼ばれる概念があります。 Step : Tektonでパイプラインを実行する際の一番小さな処理の単位です。例えば、ソースコードを取得する・コンパイルする・テストするといった処理に相当します。 Task : 順番に実行されるStepの集まりです。シーケンシャルに実行される一連の処理に相当します。 Pipeline : Taskの集まりです。Taskの順序関係の設定および直列・並列実行の制御、Taskの実行条件の設定などが可能です。 これらの概念の関係性を図示すると、以下の通り表現されます。 上記の3つの概念を用いてCI/CDパイプラインを記述し実行することで、k8sのワークロードリソースが生成されて、タスク処理が実行されます。前述の図にk8sリソースとの関係性を記述してみます。 上図にあるように、StepはContainerに、TaskはPodに対して1対1に対応しています。k8sの目線で考えると、Taskの実行はPodを起動すること、Stepを順番に処理することはPod内のコンテナを順番に処理することに相当します。データ共有に関しては、Step間ならばコンテナ間で共有、Task間ならPod 間でデータ共有することのように言い換えられます。 TektonにはTask/Pipelineの実行の概念であるTaskRun/PipelineRunが存在します。TaskRunによってTaskの内容をもとにPodが起動され、Stepであるコンテナの処理が順次実行されます。PipelineRunはPipelineの内容を元に各TaskをTaskRunを利用して実行します。 Tekton Pipelinesは、上記で説明した Task 、 Pipeline 、 TaskRun 、 PipelineRun をk8sのカスタムリソースとして提供しています。ユーザはそれらのリソースを組み合わせてパイプラインを構築します。 パイプラインの作成と実行例 ここでは、具体的なカスタムリソースの定義と実行例を紹介します。 以下の実行環境で動作を確認しています。 Component Version Kubernetes v1.24.1 Tekton Pipelines v0.38.0 Tekton CLI v0.25.0 Task の定義と実行 サンプルとして、次の仕様を満たすTaskのカスタムリソースを定義します。 パラメータ名 message として文字列を受け取る。受け取らない場合は、"Hello, Tekton Task"をデフォルト値とする。 step-1、step-2のstepが順番に実行され、受け取ったパラメータをechoする。 apiVersion : tekton.dev/v1beta1 kind : Task metadata : name : echo namespace : sample-tekton spec : params : - name : message type : string description : echo this message default : Hello, Tekton Task steps : - name : step-1 image : ubuntu command : - echo args : - $(params.message) - name : step-2 image : ubuntu command : - echo args : - $(params.message) サンプル中の以下の部分では、実行の際にTaskが受け取るパラメータを定義しています。パラメータは配列形式で複数定義でき、パラメータ名、型、デフォルト値を指定できます。パラメータの型として、stringかarrayが指定可能です。 params : - name : message type : string description : echo this message default : Hello, Tekton Task Task内でStepなどを定義する際に $(params.<パラメータ名>) の形式で記述することで、Taskが実行される際に受け取ったパラメータの値で置換できます。 次に、Taskが実行するStepについてです。Stepは次のように配列として定義できます。コンセプトで説明した通り、Stepの1つ1つがコンテナに相当します。 steps : - name : step-1 image : ubuntu command : - echo args : - $(params.message) steps配列に定義される各stepの定義は、Podで設定するコンテナの指定に似ています。今回の場合だと、 step-1 という名前のstep(コンテナ)は ubuntu コンテナイメージを使い、 echo コマンドを実行し、 $(params.message) を置換した値が引数として渡されるという意味になります。また、後ほどの例にでてきますが、commandの代わりにscriptフィールドを指定することで、コンテナで実行するスクリプトを記述するような方法もあります。 TaskRunリソースを作成することで、上記で定義したTaskを実行します。以下にTaskRunのサンプルを示します。 apiVersion : tekton.dev/v1beta1 kind : TaskRun metadata : generateName : echo- spec : params : - name : message value : Hello taskRef : name : echo spec.params には、Taskのパラメータ名と渡したい値のペアを配列で指定します。 spec.taskRef には、このTaskRunで実行するTaskを指定します。TaskRunリソースではTaskリソースを参照する他にインラインで定義することも可能ですが、ここでは割愛します。 metadata.generateName が書かれたマニフェストを作成すると、生成されたリソースの名前にランダムなサフィックスが付与されます。これにより、生成されたTaskRunリソースの名前が重複しなくなるため、同じTaskRunマニフェストを用いて複数個のTaskRunリソースを作成し、何回でもTaskを実行できるようになります。 これらの設定をk8sクラスタに適用しCLIで確認すると、以下のような結果を得ることができます。ここで tkn はTekton CLIをインストールすることで利用可能になるバイナリです。 > tkn taskrun logs -n sample-tekton -f [step-1] Hello [step-2] Hello 以上のように、Taskリソースを定義することで、複数のコンテナを組み合わせて任意の処理を実行するCI/CDタスクを組み上げることができます。 Pipeline の定義と実行 サンプルとして、次の仕様を満たすPipelineのカスタムリソースを定義します。 前述のecho Taskに異なるパラメータを与えて、2つのTaskを実行する 2つのTaskは並列に実行する apiVersion : tekton.dev/v1beta1 kind : Pipeline metadata : name : hello namespace : sample-tekton spec : params : - name : task1-message type : string default : Hello, Task1 - name : task2-message type : string default : Hello, Task2 tasks : - name : task1 taskRef : name : echo params : - name : message value : $(params.task1-message) - name : task2 taskRef : name : echo params : - name : message value : $(params.task2-message) params ではTaskと同様に、実行の際にPipelineが受け取るパラメータを定義します。 Pipelineが実行するTaskは、次のように配列として定義します。 tasks : - name : task1 taskRef : name : echo params : - name : message value : $(params.task1-message) PipelineでのTaskの指定の仕方は、前述のTaskRunリソースでの指定の仕方と同じです。Taskを直接実行する際にTaskRunを生成するのと同様に、PipelineがTaskを実行する際にもTaskRunが生成されるためです。ここで指定したTask定義は、PipelineがTaskRunを生成する際に使用されます。列挙したTaskは、後述のrunAfterが宣言されていない限り、すべて並列に実行されます。 PipelineRunリソースを作成することで、上記で定義したPipelineを実行します。以下にPipelineRunのサンプルを示します。 apiVersion : tekton.dev/v1beta1 kind : PipelineRun metadata : generateName : hello- namespace : sample-tekton spec : pipelineRef : name : hello params : - name : task1-message value : Hello, Task1 - name : task2-message value : Hello, Task2 spec.pipelineRef には、このPipelineRunで実行するPipelineを指定します。 spec.params には、Pipelineのパラメータ名と渡したい値のペアを配列で指定します。 これらの設定をk8sクラスタに適用しCLIで確認すると、以下のような結果を得ることができます。 > tkn pipelinerun logs -n sample-tekton -f [task2 : step-1] Hello, Task2 [task2 : step-2] Hello, Task2 [task1 : step-1] Hello, Task1 [task1 : step-2] Hello, Task1 以上のように、Pipelineリソースを定義することで、複数のTaskを組み合わせてより複合的なCI/CDタスクを組み上げることができます。 Task の実行順序制御 コンセプトの項目でも触れましたが、PipelineはTaskの実行順序を制御できます。 あるTaskを別のTaskの後で実行したいときには、Pipelineリソースで runAfter を指定します。前述のPipelineにおいてtask2をtask1の後で実行したい場合は、パイプラインで次のように指定します。 tasks : - name : task1 taskRef : name : echo params : - name : message value : $(params.task1-message) - name : task2 taskRef : name : echo params : - name : message value : $(params.task2-message) runAfter : # task1 との依存関係を定義 - task1 上記の指定にあるように、 runAfter は配列の形式で指定できます。そのため、複数のTaskとの依存関係を記述でき、 ファンインとファンアウトの両方を構成できます。 runAfter をうまく活用することで、並列実行と直列実行を組み合わせて複雑なPipelineを組むことが可能です。 データの共有 複数の処理で構成されたパイプラインを実行した際、処理間でデータの共有を必要とする場合があります。Tektonでは、そのようなケースで利用できる機能として results と workspaces が提供されています。 results : resultsは、Pipelineを介してTask間でTask実行結果を共有できる機能です。例えば、ビルドしたコンテナイメージの Tagやハッシュ値を後続のTaskで利用するなどの使い方があります。 workspaces : workspacesは、Task実行時に1つ以上のボリュームをマウント可能とする機能です。Step間のみの共有ならばemptyDir、Task間共有が必要ならばPersistentVolumeと使い分けが可能です。例えば、ソースコードをcloneしてきたあと、後続の処理で使うといった使い方があります。 上記機能のうち、resultsの例を紹介します。 サンプルのパイプラインを次の仕様で作成します。 パラメータとして与えられた文字列を連結するTaskと文字列をechoするTaskの2つが実行される 文字列を連結するTaskの実行結果をresultsとして保存し、echo Taskでresultsをecho する 文字列を連結するTaskを次のように定義します。 apiVersion : tekton.dev/v1beta1 kind : Task metadata : name : concat namespace : sample-tekton spec : results : - name : concat-msg description : A string that is a concatenation of two strings params : - name : msg1 type : string - name : msg2 type : string steps : - name : concat image : ubuntu script : | #!/usr/bin/env bash echo -n $(params.msg1) $(params.msg2) > $(results.concat-msg.path) resultsを出力するTaskは、Task内で以下のように spec.results を定義する必要があります。 results : - name : concat-msg description : A string that is a concatenation of two strings その上で、Step内の実装に、resultsを書き出す処理を追加します。resultsとして出力したい文字列を $(results.<results名>.path) に対して書き出すことで、書き出した値をTaskのresultとして扱えるようになります。 2つのTaskを順番に実行するPipelineは、以下のように記述されます。 apiVersion : tekton.dev/v1beta1 kind : Pipeline metadata : name : concat-msg namespace : sample-tekton spec : params : - name : msg1 type : string - name : msg2 type : string tasks : - name : concat taskRef : name : concat params : - name : msg1 value : $(params.msg1) - name : msg2 value : $(params.msg2) - name : echo taskRef : name : echo params : - name : message value : $(tasks.concat.results.concat-msg) ポイントは、後続のecho Taskのパラメータに指定している $(tasks.concat.results.concat-msg) の記述です。Pipeline上で $(tasks.<task名>.results.<result名>) のフォーマットで記述することで、task名で指定したTaskが出力したresultsの値で置換できます。 以下のPipelineRunリソースを作成して、Pipelineを実行します。 apiVersion : tekton.dev/v1beta1 kind : PipelineRun metadata : generateName : concat-msg- namespace : sample-tekton spec : pipelineRef : name : concat-msg params : - name : msg1 value : Hello - name : msg2 value : World 実行した後の確認結果は次のようになりました。連結された文字列が適切にecho Taskに渡ったのが確認できます。 > tkn pipelinerun logs -n sample-tekton -f [echo : step-1] Hello World [echo : step-2] Hello World ここで1点補足です。 runAfter を指定しない場合、Taskは並列実行されると前述しました。その場合、適切にresultsの値がとれるのか?と疑問を覚えると思います。この点に関しては、task間でresultsの参照関係がある場合には実行順序制御が自動で行われるため、明示的にrunAfterを記述する必要はありません。ただ、明示的に記述した方がわかりやすいとの意見もありますので、チームメンバーと方針を決めるのがよいかと思います。 Tekton Pipelinesの機能紹介はここまでとなります。本記事では紹介していない機能もありますので、興味を持たれた方はぜひ試してみてください。 何故、Tekton を選んだか 最後に、Qmonus Value Streamの要素技術としてTektonを選択した理由について紹介します。 まず、CI/CD機能そのものを提供するOSSやSaaSを選択しなかった理由について説明します。当初、私達がDevOpsの取り組みの中でCI/CDパイプラインの作成や改善を続けていくにあたり、SaaSやOSSの検証も行いました。どのサービス・ソフトウェアもよくできていて学ぶことはとても多いのですが、私達のユースケースを実現するためには課題もありました。 例えば、以下のような課題がありました。 構築したいCI/CDのサイクルが複雑で、シンプルな機能だけでは構築が難しい。例えば、複数のコンポーネントを複数の異なるバージョンで組み合わせて動作させたいケースなど。 k8sだけでなく、各パブリッククラウド特有の挙動を考慮したロジックを組み立てる必要がある。 このような背景があり、ある程度決まった枠組みが作られているサービス・ソフトウェアよりも、ワークフローのエンジンとなるような技術の方が私達に適しているのではないか、という結論に至りました。 Tektonには、ほかのプロダクトにはない次の特徴がありました。これらは私達のユースケースに合致するものでした。 Kubernetes Nativeなソフトウェアのため、宣言的記述・スケーリングなどのk8sが持つメリットを活用できる。 最小の処理単位(step)をコンテナとして作るため処理を再利用しやすく、また既存のコンテナ資産を流用できる。 一連のシーケンシャルな処理の集合(Task)の順序関係を定義して、複雑なパイプラインを構築できる。 TaskをPodとして実現するため、各処理間(コンテナ間)でリソースをシェアしやすい。 これらの特徴を踏まえて、私達が目指すDevOpsの仕組みを実現するにはTektonを利用するのが良いだろうと総合的に判断し、選択しました。 おわりに Tektonのプロジェクトは開発が活発であり、どのコンポーネントも進化スピードが早いです。今回はTekton Pipelinesの基本的な機能の紹介のみでしたが、今後、新機能や変更点に関するキャッチアップ、運用してみての知見などを紹介していきたいと考えています。 Qmonus Value Streamチームは、メンバ一人一人が、利用者であるアプリケーション開発者のために信念を持って活動を続けています。 プラットフォームを内製開発するポジション、プラットフォームを使ってNTTグループのクラウド基盤やCI/CDを構築支援するポジションそれぞれで、一緒に働く仲間を募集しています。興味を持たれた方は応募いただけると嬉しいです。 CI/CDプラットフォーム開発、ソフトウェアエンジニア NTTグループのクラウド基盤構築およびCI/CD導入、ソリューションアーキテクト 次回は、CI/CDパイプライン作成時のパラメータバインディングの課題と、Qmonus Value Streamにおける改題解決のアプローチについて紹介します。お楽しみに!
こんにちは、イノベーションセンター Agile CoE PJリーダーの岩瀬(以下、iwashi)です。 NTT Comで技術顧問をされており、アジャイルコーチでもある吉羽さん(以下、ryuzee)に「普段からどうやって学んで、どうやって活用しているのか?」という問いを1on1で投げかけてみました。本記事では、1on1模様の一部を公開します。 情報収集やナレッジマネジメントのテクニックが載っていますので、興味ある方はぜひご覧ください。というわけで、早速いってみましょう! RSSリーダーを使って購読 iwashi: ryuzeeさんは海外記事の翻訳など、かなり幅広く情報収集をされている印象があります。普段はどうやって情報をみつけているんですか? ryuzee: RSSリーダーにたくさん登録しています。最近はRSSリーダーが下火で悲しいんですが、feedlyを使って色々なRSSを購読しています。 海外の記事と国内の記事の両方が対象です。 はてなブックマークの記事には タグごとにRSSを出力 できるので、それで登録しているものもあります。 たとえば、Scrumってついていたり、Agileってついていたりするのはダイレクトに記事をみつけられます。 もちろん、玉石混交になるため、すべての記事をじっくり読むわけでもありませんが、世の中の意見はだいぶわかります。 iwashi: なるほど。個人的にはSlackで特定のチャンネルにRSSを流して確認しています。ryuzeeさんは何を使って中身を見ているのでしょうか? ryuzee: Macだと Reeder というRSS専用アプリがあって、それを使ってます。 無限連鎖で情報を探す iwashi: そもそもRSSで購読する種はどうやって探していますか? ryuzee: 良いサイトから無限連鎖するんですよ。良い記事は、良い情報を引用していることが多いので、引用先を見に行っていくつか読んでみて、良かったら購読してます。 あとはTwitterで海外の人をフォローしているので、そこで流れているサイトで良さそうなものがあれば追加しています。ちなみに、Twitterだと by name で探しています。たとえば、 Management 3.0 の @jurgenappelo や、スクラムの @jeffsutherland とか。他には認定スクラムトレーナーの有名人をフォローしていると、たまに何かしら流れてくるので、その辺の情報を確認しています。 iwashi: そうしていくと、RSSの量がかなり多そうなんですが、いつ見ているんですか? ryuzee: いつでも見ていますよ(笑)。四六時中、時間が空いたら。特に、朝一に見ることが多いです。コーヒーを飲みながら読んでます。 もちろん1回で全部読むというわけではなくて、スクリーニングしてしっかり読む・読まないを識別しています。 たとえば、「これは知っているから読まなくていい」「これはよく知らないからちゃんと読もう」など。 iwashi: トリアージみたいな感じです。 英語記事の対応 iwashi: 英語の記事を読むときは、どうされていますか? ryuzee: ざっと読むときはまるごとDeepLに突っ込んでしまうこともあります。 また、構造やフレームワークを説明する記事は、そのパターンを再利用できることも多いので、仕事の道具箱に加えるイメージで Obsidian に入れています。Obsidianに入れるときは、タグをつけて他のメモとリンクを貼っています。 こうやって整理していくと毎日メモが増えていく感じになり、日本語・英語関係なく増えています。 iwashi: Twitterのタイムラインの途中に英語があると、目が滑っちゃう問題ってありませんか? ryuzee: 海外の情報源は、英語専用のリストを作って TweetDeck のリストで見ています。ある程度真面目に読んでます。 それから、TwitterだとOGP画像が含まれているので、それも識別材料になります。 日々の「メンテナンス」がすべて iwashi: 少し話題を変えて、ここまで読んでいた記事の内容を実際の業務に活かすコツってありますか? コンテキストが同じというものは滅多にないので、そのまま活かせるものは少ない、と考えているのですがどうでしょうか? ryuzee: 毎日メンテナンスしておくこと、これに尽きます。メンテナンスしておいた内容はすぐに取り出して活用できるようになります。 クリップツールやメモツールは無数にあるんですが、メンテナンスしないでWebページをクリップし続けると、ゴミ箱になって2度と見なくなります(笑)。ナレッジベースは常にメンテナンスするのが大事です。 たとえば、スクラムのトレーニングを業務で提供していますが、よく聞かれる質問ってあるんですよ。ナレッジベースにそのあたりは溜めてあります。 iwashi: 以前に、あるWebクリップツールを使っていたんですが、メンテナンスしておらず、途中で読まなくなってしまったのを思い出しました。たしかにメンテナンスが大事ですね…。 ryuzee: そうなんです。ツールに関係なくゴミ箱になります。どんなツールを使っていても、日々メンテナンスしないと捨てることになる。もちろん日々のメモとして書き捨てのものは構わないですよ。 iwashi: これってシンプルなんですけど、なぜできないんでしょうね? 大変だからですか? ryuzee: やっただけのリターンが得られないと続かないんです。コンサルティング業やコーチ業は圧倒的瞬発力が求められます。いろんなネタを瞬時に投下しないといけない。だから、認定スクラムマスターの研修だと、たとえば数百個個ぐらいはコピペで応えられる情報を用意してあります。 そういったものは全部 Obsidian にいれてます。過去のスクラムガイドも全部入ってます。 最近だと さらば、翻訳調の文章! 技術者向け校正ルール という10年以上も前の記事が素晴らしかったので、早速Obsidianに入れました。ただし、Obsidianに入れるときはhtmlを除去して、plain markdown にする必要があるので、ツールも使います。ある程度、手をかけてます。 iwashi: ナレッジベースがどんどん大きくなると、もう必要ないメモも増えていきますよ。これはどうされているんですか? ryuzee: 不要になったテストコードを捨てるのと一緒で、いらなくなったメモは捨てています。上位互換の記事が出て、それだけ見ればよいときもありますし。過去のものとの差分が必要になるときもありますが、多くの場合はいりません。差分が必要なのは、スクラムガイドなどです。 iwashi: なるほど。それから記事・メモが増えたときによくある困ることとして、探し出す難しさがあるかと思います。これはどのように対応されていますか? ryuzee: よくある方法にフォルダによる整理がありますが、あれは無理筋です。1つの断面に切れないんですよ。そのため、タグをつけてあとは検索、という感じになります。 アプリもよりますが、検索が強いものは、検索で十分にがんばれます。Obsidianが最高なのは、中身がマークダウンであり、テキストファイルという点です。テキストファイル最高!という話は、 達人プログラマー の「プレインテキストの威力」という話にもつながります。 iwashi: たしかにテキストファイルの互換性は、時代を経たとしても強いです。30年前のファイルでも読めますし、30年後でも読めると思います。似た話で、 画像をスクリーンショットで作っていくとよい、なぜならば画像も時間が経ってから読める という記事も最近読んでいて通じるものがあると思いました。 長くなってきたので、そろそろ終わりにします。色々と教えていただきありがとうございました! まとめ ということで今回は、技術顧問のryuzeeさんがどうやって学んでいるのか?どうやって学びを活用しているのか?というテーマで1on1した話を共有してみました。 ナレッジマネジメントは、個々人によってスタイルが違うものかと思いますが、少しでも参考にできる部分があれば幸いです!
はじめに こんにちは、イノベーションセンターの鈴ヶ嶺です。普段はクラウドサービスをオンプレ環境でも同様のUI/UXで使用を可能とするハイブリッドクラウド製品の技術検証をしています。 NTT Comでは以下の過去の記事のように、AWSのハイブリッドクラウドソリューションAWS Outposts ラックの導入や技術検証を進めています。 engineers.ntt.com engineers.ntt.com engineers.ntt.com 本記事では、AWS Outpostsで実現するオンプレ環境におけるデータレイクのユースケースについて紹介します。 データレイクとは構造化データ、非構造化データに関わらず全てのデータを一元的に保存可能なストレージを意味しています。 このユースケースにより、低遅延性が求められる、もしくは秘匿性の高い大規模なデータをオンプレ環境で一元的に取り扱うことが可能となります。 オンプレ環境におけるデータレイクの背景と課題 膨大なユーザデータやIoTセンサーデータなどのビックデータを企業が適切に管理と分析を行う事が期待されています。そこから、社会の中で企業は更にサービスの質を向上させていくことが求められていることにより、データレイクの重要性は年々増しています。 データレイクを利用する中で、特に高速にデータ処理するために低遅延性が求められるケース、秘密情報を取り扱うためにセキュリティ保護上データの所在をコントロールしたいケースなどではオンプレ環境に構築する必要があります。 次にオンプレ環境にデータレイクを構築する場合の課題を3つ挙げます。 ストレージ・データ分析基盤の構築 オンプレ環境に大規模なデータを安価に格納可能なストレージや、それらのデータを処理するための並列分散処理基盤の構築が必要となります。これらはデータや分析需要の増加に対応するため、スケーラブルな設計を考慮している事が求められます。 保守運用コスト それぞれのストレージ・並列分散処理基盤のハードウェア管理やソフトウェアのversion管理などの保守運用も多くの稼働が必要となります。故障対応なども製品ごとにエスカレーション先が異なる場合に製品同士の依存関係を把握して対応する稼働コストも製品数に比例して増大すると考えられます。 オンプレ環境のセキュリティ 構築したオンプレサービス群に対して適切なアカウント管理やリソースのアクセス制限などのセキュリティ対策が必要です。オンプレ環境では、特に不十分な例として認証方式の不統一や共通されたパスワード運用による脆弱性が問題視されています。 AWS Outpostsで実現するオンプレデータレイク 先ほど述べた3つのストレージ・データ分析基盤の構築、保守運用コスト、オンプレ環境のセキュリティの課題を解決するためにAWSのハイブリッドクラウドであるAWS Outpostsを用いたオンプレデータレイクシステムを提案します。 S3 on Outposts、Auto Scaling Groupを活用したスケールするストレージ・データ分析基盤の構築 AWS Outpostsには事前にデータサイズを決定せずに大規模データを貯蔵可能な S3 on Outposts というオブジェクトストレージサービスがあります。また、オンプレ環境で Auto Scaling Group を利用することで柔軟にスケールする並列分散基盤を作成可能です。これにより先の述べたストレージ・データ分析基盤の構築の課題を解決できます。 AWSによる保守運用の一括管理 AWS Outpostsはフルマネージド型インフラストラクチャであり、ハードウェアの故障対応なども含めて全て一括でAWSが管理することで高い稼働コストを削減できます。 AWS IAMによるセキュリティ対策 AWS Outpostsの上で動作するリソースは全て AWS IAM というきめ細かなアクセス制御を担うサービスで管理できるので最小限のアクセス権限やアカウントの二段階認証導入による対策により高いセキュリティを担保可能です。 アーキテクチャ ここからAWS Outpostsで実現するオンプレデータレイクのアーキテクチャとそれぞれのシステム詳細について紹介します。 1. オンプレ環境の多数のIoTセンサーデータ オンプレ環境の多数のIoTセンサーデータをAWS Outpostsに送信します。AWS Outpostsは Local Gateway を用いることでオンプレ環境と通信できます。これによりセキュリティ要件上オンプレ環境からのみ取得可能なデータにアクセスすることを可能とし、定められた時間内に処理することが求められるデータを低遅延に蓄積・処理が可能となります。 2. Spark・Delta Lakeによるデータ更新 データレイクには Delta Lake というデータレイクの信頼性、セキュリティ、性能を高め、ストリーミング/バッチ処理の両方を柔軟に対応するオープンソースのストレージレイヤーを利用します。このストレージレイヤーをAuto Scaling Groupを用いて並列に構築された Spark で実行します。これによりスケーラビリティを担保して大規模なデータを蓄積・処理が可能となります。 3. S3 on Outpostsによるセキュアなビックデータ管理 S3 on Outpostsは事前にデータサイズを決定する必要がない、柔軟に大規模なデータをオンプレ環境に貯蔵可能なオブジェクトストレージサービスです。Delta Lakeで処理されたデータはこのS3 on Outpostsに貯蔵されます。2022年8月時点でS3 on Outpostsに貯蔵可能なデータ量の最大は380TBです。 また、S3 on Outpostsはインターネットなどの外部からデータにアクセス不可能で、AWS Outpostsやオンプレ環境からのみアクセス可能です。さらにAWS IAMを用いることで最小限のデータアクセス制御を実装可能です。 これらのことからS3 on Outpostsによって、オンプレ環境でセキュアなビックデータの管理を実現可能です。 データレイクの性能検証 ここでは、データレイクの性能を検証します。 データセットとしてUCI Machine Learning Repositoryの KASANDR Data Set を利用します。これはヨーロッパのeコマース広告のKelkoo社の顧客の行動を記録したデータです。今回は学習データのtrain_de.csv(3.14GB)を使用します。 以下に実際のデータ例を抜粋しました。時刻データやカテゴリデータが含まれるデータとなっています。 userid offerid countrycode category merchant utcdate rating fa937b779184527f12e2d71c711e6411236d1ab59f8597d7494af26d194f0979 c5f63750c2b5b0166e55511ee878b7a3 de 100020213 f3c93baa0cf4430849611cedb3a40ec4094d1d370be8417181da5d13ac99ef3d 2016-06-14 17:28:47.0 0 f6c8958b9bc2d6033ff4c1cc0a03e9ab96df4bcc528913be886254d1ad8c7c44 19754ec121b3a99fff3967646942de67 de 100020213 21a509189fb0875c3732590121ff3fc86da770b0628c1812e82f023ee2e0f683 2016-06-14 17:28:48.0 0 02fe7ccf1de19a387afc8a11d08852ffd2b4dabaed4e2d2af9130e9c5dfb9ee8 5ac4398e4d8ad4167a57b43e9c724b18 de 125801 b042951fdb45ddef8ba6075ced0e5885bc2fa4c4470bf790432e606d85032017 2016-06-14 17:28:50.0 0 9de5c06d0a16256b13b8e7cdc50bf203ecef533eb5cbe18d514947b1c5296f33 be83df9772ec47fd210b28091138ff11 de 125801 4740b6c83b6e12e423297493f234323ffd1c991f3d4496a71daebbef01d6dcf0 2016-06-14 17:29:19.0 0 データサイズの比較 次のように、データセットをDelta Lake形式でS3 on Outpostsに保存します。 実際の処理はspark-shell 3.3.0を用いました。 2022年8月時点では、以下のようにAmazon EMR on OutpostsではS3 on Outpostsをサポートしていないため、S3 on Outpostsへのデータ保存に対応するため、 Hadoop-AWS に対して改善パッチを当てたものを使用しました。 また、Hadoopに改善パッチ取り込みを 要求中 です。 S3 is the only supported option for Amazon EMR on Outposts. S3 on Outposts is not supported for Amazon EMR on AWS Outposts. https://docs.aws.amazon.com/emr/latest/ManagementGuide/emr-plan-outposts.html 以下がS3 on OutpostsにDelta Lake形式でデータを保存するスクリプトです。 sc.hadoopConfiguration.set( "fs.s3a.bucket.delta-bucket.accesspoint.arn" , "arn:aws:s3-outposts:ap-northeast-1:123456789:outpost/op-123456789/accesspoint/delta-lake" ) val base_path = "s3a://delta-bucket/delta" val df = spark.read.format( "csv" ).option( "header" , "true" ).option( "inferSchema" , "true" ).option( "delimiter" , "\t" ).load( "train_de.csv" ) df.write.format( "delta" ).mode( "overwrite" ).save(base_path) データの実態は次のように Apache Parquet として保存されます。 > aws s3 ls s3://arn:aws:s3-outposts:ap-northeast-1:123456789:outpost/op-123456789/accesspoint/delta-lake --summarize --recursive 2022-07-14 09:36:17 21697 delta/_delta_log/00000000000000000000.json 2022-07-14 09:34:43 55352961 delta/part-00000-2de3a57b-1c68-4679-8c9d-c950c7e45de6-c000.snappy.parquet 2022-07-14 09:34:43 55539573 delta/part-00001-884e0e49-84d9-49c3-933e-9eddb364f674-c000.snappy.parquet 2022-07-14 09:34:52 55236752 delta/part-00002-e64a5825-7425-4435-aace-7fd77d4ba19a-c000.snappy.parquet 2022-07-14 09:34:52 55669516 delta/part-00003-d15c00cc-a2b9-4be0-9029-9132fa14a98f-c000.snappy.parquet 2022-07-14 09:35:01 55512327 delta/part-00004-73c45fe3-7572-4989-8b68-63b786f2c328-c000.snappy.parquet 2022-07-14 09:35:01 55560218 delta/part-00005-94f8f7aa-b2e5-4d61-93bd-94b11518c662-c000.snappy.parquet 2022-07-14 09:35:09 55337097 delta/part-00006-4bc8a8ae-67f9-4941-967e-4392d78161e6-c000.snappy.parquet 2022-07-14 09:35:09 55284570 delta/part-00007-e8d4d3f6-f550-4321-afbe-8ca4dd50277f-c000.snappy.parquet 2022-07-14 09:35:18 55354729 delta/part-00008-f442a193-1286-46a2-80f2-eeb25e4c75ec-c000.snappy.parquet 2022-07-14 09:35:18 55283349 delta/part-00009-973a1e4f-cccd-484e-bf59-72db29b42e3e-c000.snappy.parquet 2022-07-14 09:35:26 55466100 delta/part-00010-dd722212-766e-434c-b6d9-772bb4ca5293-c000.snappy.parquet 2022-07-14 09:35:26 55163670 delta/part-00011-e324ec88-218d-426f-bc2a-a982538f9bde-c000.snappy.parquet 2022-07-14 09:35:34 55244413 delta/part-00012-9789b931-a5b8-4ea6-957c-6d6d9c751821-c000.snappy.parquet 2022-07-14 09:35:35 55321192 delta/part-00013-b7a43423-2a53-4a41-a6f4-a8883083315c-c000.snappy.parquet 2022-07-14 09:35:42 55569791 delta/part-00014-c1b90126-3e54-4730-8ffa-7ab79a9ab112-c000.snappy.parquet 2022-07-14 09:35:44 55471295 delta/part-00015-69d1f3b7-b7ac-4357-af09-1c45ba360a3a-c000.snappy.parquet 2022-07-14 09:35:50 55426860 delta/part-00016-a276226c-169c-40b2-9cb7-c8b28d335160-c000.snappy.parquet 2022-07-14 09:35:53 55399812 delta/part-00017-2275eb45-d961-41f3-8e6f-46cd5b59bf11-c000.snappy.parquet 2022-07-14 09:35:58 55366224 delta/part-00018-38c48ee9-98f8-44f8-a30d-5898cb962e3f-c000.snappy.parquet 2022-07-14 09:36:01 55470407 delta/part-00019-05744950-7c1f-439a-a2d9-8515f21b87c6-c000.snappy.parquet 2022-07-14 09:36:06 55318799 delta/part-00020-99108c63-21e3-4560-9667-c9dfd2cce99e-c000.snappy.parquet 2022-07-14 09:36:09 55274819 delta/part-00021-cada37d7-8ecc-4d4c-8bac-97afde209cb3-c000.snappy.parquet 2022-07-14 09:36:14 55666013 delta/part-00022-16bff2b2-2346-4507-9fde-08f3d60d0f5b-c000.snappy.parquet 2022-07-14 09:36:13 22632601 delta/part-00023-b13c0d3b-e3a7-46f7-a345-fde187d4d08e-c000.snappy.parquet Total Objects: 25 Total Size: 1296944785 データ形式 データサイズ CSV 3.14GB Delta Lake 1.30GB このDelta Lake形式で保存されたデータと元のCSVデータのデータサイズを上記の表を用いて比較します。Delta Lake形式にすることでCSV形式よりもおよそ59%のデータサイズが削減可能です。これはDelta Lakeの内部形式であるApache Parquetの高い圧縮率が主要な要因であると考えられます。 データ処理時間の比較 データ処理時間の比較のため、S3 on OutpostsにCSV, Delta Lake形式で同様のデータを保存し処理の時間をそれぞれ30回計測します。処理内容は次のように特定のカラム(category)のグループ集計をとるものとします。 sc.hadoopConfiguration.set( "fs.s3a.bucket.delta-bucket.accesspoint.arn" , "arn:aws:s3-outposts:ap-northeast-1:123456789:outpost/op-123456789/accesspoint/delta-lake" ) // CSV形式で処理する時間を計測する val csv_path = "s3a://delta-bucket/csv/train_de.csv" val csv_df = spark.read.format( "csv" ).option( "header" , "true" ).option( "inferSchema" , "true" ).option( "delimiter" , "\t" ).load(csv_path) spark.time(csv_df.groupBy( "category" ).count().show()) // Delta Lake形式で処理する時間を計測する val base_path = "s3a://delta-bucket/delta" val delta_df = spark.read.format( "delta" ).load(basePath) spark.time(delta_df.groupBy( "category" ).count().show()) データ形式 処理時間(sec) CSV 26.3 Delta Lake 4.3 上記が平均の処理時間の表になります。Delta Lake形式の方がCSV形式よりも高速に処理可能であることが分かります。CSVのデメリットとしてデータ形式上特定の列データ(category)のみを読み込むことができないため、今回の処理では全データを読み込むオーバーヘッドが生じます。一方、Delta Lake形式は内部形式Apache Parquetが次の画像のように列指向フォーマットで保存されているため特定の列データ(category)のみを読み込むことが可能となり、不必要なデータを読み飛ばしたことでCSVよりも高速に処理できました。 https://parquet.apache.org/docs/file-format/ まとめ 以上のようにAWS Outpostsを用いることでオンプレ環境にスケーラブルなストレージ・分析基盤の構築や、稼働コストを抑えた保守運用、AWSサービスを包括的に使用することで高いセキュリティを達成可能であることを紹介しました。 また、性能検証の結果から従来のようにCSV形式のままで保存するよりもDelta Lakeを利用することでデータサイズの大幅な圧縮とデータ処理時間の高速化というメリットが得られることを示しました。
サマリ Multi-AS で構成されるネットワークにおける Traffic Engineering (TE) の実現 IOS XR の機能を用いて、 TE metric に基づき AS Border Router(ASBR) を選択する TE の動作検証に成功 この記事は Multi-AS Segment Routing 検証連載の第 5 回です。目次は こちら 概要 イノベーションセンターの岩田です。本記事では、Multi-AS での Segment Routing(SR) での Traffic Engineering(TE)の実現手法についてご紹介します。 Multi-AS において SR-TE で実現したい要求としては大きく以下の 2 つに分けられます。 (a) AS 内で意図した経路を選択したい(AS 内での TE) (b) 他 AS との接続トポロジーを考慮した経路を選択したい(AS 間での TE) 本記事ではこの 2 つの TE を SR でどう実現するかについてご紹介します。 特に (a) では出口となる ASBR の選択を AS 内で行う必要があります。そのために SR-TE の metric に基づいた egress node(ASBR)選択の実現例について検証パートでご紹介します。 (a) AS 内での TE Multi-AS で AS 内での TE を実現する上での考慮点として、ある 1 つの宛先に対し egress node(ASBR)となる node 候補が複数存在する際にどう TE を実現するか、という点があります。 1 つの宛先に対し、1 つの ASBR のみが egress node となる場合の TE は endpoint の選択余地がないので Single-AS と同様に考えることができます。これに関しては 第 4 回の記事 で紹介したのでそちらをご参照ください。 ここでは、以下の図のような 1 つの ingress node に対し 複数の ASBR が egress node になれる構成時の TE について考えます。 SR-TE を用いた経路制御をする場合には、SR Policy で定義した metric に基づいて、より優位な ASBR を選択するような経路を選択したいという要求が考えられます。通常は、各 BGP 経路の egress node となる ASBR は BGP ベストパス決定アルゴリズムにより決定されてしまいます 1 。 SR-TE で ASBR の選択をする場合には、 ingress node において何らかの方法で SR Policy の評価結果を BGP のベストパスに反映させる必要があります。 この要求を実現する機能として、 BGP ベストパス決定アルゴリズム時に SR Policy を評価するための機能が、IOS XR 7.3.2 以降で提供されています 2 。本記事の検証パートでは、この機能を用いて ASBR 選択を含めた Color-Based Steering を行うための設定例をご紹介します。 (b) AS 間での TE AS 間での TE の実現手法としては BGP Egress Peer Engineering(EPE) があります。EPE は ASBR において Inter-AS 情報から BGP Peering Segments、もしくは BGP Peering SIDs と呼ばれる segment を作成し IGP へ広告することで、AS 内の ingress node から EBGP Peer の選択を可能とします。これにより、接続した AS も含めた TE が実現されます。 本連載で我々が取り組んでいる Inter-AS Option-B において EPE を用いた TE を実現するためには、 ASBR において VPN SID と BGP Peering SID の 2 つを同時に処理する必要がありますが、2022 年 8 月時点ではどの機器にも実装されていないため本記事では扱いません。 検証 本章では、ある宛先に対し複数の egress node(ASBR)が選択可能な Multi-AS/Multi-domain SR-MPLS 上での L3VPN において、ASBR 選択を含めた Color-Based Steering を行うための設定例をご紹介します。 トポロジー 下記のようなトポロジーを作成します。 3 つのルーターによる AS を 2 つ接続したコア網で、顧客 A を収容します。 検証環境は Multi-vendor に構成します。使用する機器は下記の通りです。 SR Domain 1 PE1、ASBR1: Cisco IOS XR 7.4.1 ASBR2: Junos OS 21.3R1.9 SR Domain 2 ASBR3: Cisco IOS XR 7.4.1 PE2、ASBR4: Junos OS 21.3R1.9 検証の流れ 本検証では、下図に示す AS65000 の各リンクの TE metric を設定し変更することにより ASBR の選択が変わることを示します。 具体的には ingress node(PE) において定義された、 TE metric を指標とした dynamic path を計算する SR Policy が、より小さい metric の経路を選択することを確認します。 また設定は、以下の手順で実施していきます。 TE metric に基づく経路制御をする SR Policy を作成する PE1 から ASBR1 への経路の metric が最小となるように TE metric を設定し、意図通り ASBR1 が egress node として選択される事を確認する PE1 から ASBR2 への経路の metric が最小となるように TE metric を設定変更し、意図通り egress node が ASBR2 に変更される事を確認する アンダーレイの構築に関しては、 第 2 回 の検証例のアンダーレイと同様のため省略いたします。 1. TE metric に基づく経路制御のための SR Policy を作成する 各リンクで設定した TE metric を広告するための設定 本検証では、dynamic path を用いた TE により経路制御します。 各リンクへ設定した TE metric 情報を Traffic Engineering Database (TED) に反映し、経路制御を行えるように Cisco ルーターへ以下の設定を追加します。 router isis 1 distribute link-state address-family ipv4 unicast mpls traffic-eng level-2-only ! ! TE metric に基づく経路を選択するための SR Policy の設定 各 egress node(ASBR)を endpoint とする、 color が 100 に対応する SR Policy を作成します。以下の設定を PE1(XR)へすることにより PE と endpoint 間の各リンクの TE metric の合計値が最小となるような経路を表す segment list を生成します。 segment-routing traffic-eng policy path-based-te-metric-to-asbr1 color 100 end-point ipv4 10.255.0.2 candidate-paths preference 10 dynamic metric type te ! ! ! ! ! ! ! segment-routing traffic-eng policy path-based-te-metric-to-asbr2 color 100 end-point ipv4 10.255.0.3 candidate-paths preference 10 dynamic metric type te ! ! ! ! ! ! ! BGP color と SR Policy を関連付けを行う設定 本検証では Color-Based Steering を行うため、以下のように BGP 経路に紐付ける color を設定します。 PE1(XR) vrf 100 address-family ipv4 unicast export route-policy add-color-100 ! ! extcommunity-set opaque color-100 100 end-set ! route-policy add-color-100 set extcommunity color color-100 end-policy ! PE2(Junos) set policy-options community COLOR-100 members color:0:100 delete policy-options policy-statement EXPORT-POLICY-100 set policy-options policy-statement EXPORT-POLICY-100 term ROUTE-TARGET then community add VRF100-65001-RT set policy-options policy-statement EXPORT-POLICY-100 term ADD-COLOR-100 then community add COLOR-100 set policy-options policy-statement EXPORT-POLICY-100 term ROUTE-TARGET then community add VRF100-SHARED-RT set policy-options policy-statement EXPORT-POLICY-100 term REDIST-DIRECT from protocol direct set policy-options policy-statement EXPORT-POLICY-100 term REDIST-DIRECT then accept PE1 が PE2 から受け取った経路に color がついていることを確認します。 RP/0/RP0/CPU0:PE1#show bgp vpnv4 unicast vrf 100 192.168.1.0/24 Wed Jul 20 13:54:17.734 JST BGP routing table entry for 192.168.1.0/24, Route Distinguisher: 65000:100 Versions: Process bRIB/RIB SendTblVer Speaker 33 33 Last Modified: Jul 20 13:37:04.989 for 00:17:12 Paths: (1 available, best #1) Not advertised to any peer Path #1: Received by speaker 0 Not advertised to any peer 65001 10.255.0.3 C:100 (bsid:24009) (metric 10) from 10.255.0.3 (10.255.0.3) Received Label 16 Origin IGP, localpref 100, valid, internal, best, group-best, import-candidate, imported Received Path ID 0, Local Path ID 1, version 33 Extended community: Color:100 RT:64999:100 RT:65001:100 SR policy color 100, up, not-registered, bsid 24009 Source AFI: VPNv4 Unicast, Source VRF: default, Source Route Distinguisher: 65001:100 PE2 が PE1 から受け取った経路に color がついていることを確認します。 user@ASBR2> show route table bgp.l3vpn.0 detail bgp.l3vpn.0: 2 destinations, 4 routes (2 active, 0 holddown, 0 hidden) 65000:100:192.168.0.0/24 (2 entries, 0 announced) *BGP Preference: 170/-101 Route Distinguisher: 65000:100 Next hop type: Indirect, Next hop index: 0 Address: 0x704e3d4 Next-hop reference count: 3 Source: 10.255.0.3 Next hop type: Router, Next hop index: 644 Next hop: 10.0.1.2 via ge-0/0/1.0, selected Label operation: Push 16 Label TTL action: prop-ttl Load balance label: Label 16: None; Label element ptr: 0x782ed48 Label parent element ptr: 0x782ea50 Label element references: 1 Label element child references: 0 Label element lsp id: 0 Session Id: 0x142 Protocol next hop: 10.255.0.3 Label operation: Push 16 Label TTL action: prop-ttl Load balance label: Label 16: None; Indirect next hop: 0x7199084 1048575 INH Session ID: 0x143 State: <Active Int Ext ProtectionPath ProtectionCand> Local AS: 65001 Peer AS: 65001 Age: 23:07 Metric2: 10 Validation State: unverified ORR Generation-ID: 0 Task: BGP_65001.10.255.0.3 AS path: 65000 ? Communities: target:64999:100 target:65000:100 color:0:100 Import Accepted VPN Label: 16 Localpref: 100 Router ID: 10.255.0.3 Secondary Tables: 100.inet.0 Thread: junos-main BGP Preference: 170/-101 Route Distinguisher: 65000:100 Next hop type: Indirect, Next hop index: 0 Address: 0x704e8b4 Next-hop reference count: 2 Source: 10.255.0.2 Next hop type: Router, Next hop index: 0 Next hop: 10.0.0.2 via ge-0/0/0.0, selected Label operation: Push 24005 Label TTL action: prop-ttl Load balance label: Label 24005: None; Label element ptr: 0x782ee60 Label parent element ptr: 0x782ea50 Label element references: 1 Label element child references: 0 Label element lsp id: 0 Session Id: 0x0 Protocol next hop: 10.255.0.2 Label operation: Push 24005 Label TTL action: prop-ttl Load balance label: Label 24005: None; Indirect next hop: 0x71ac104 - INH Session ID: 0x0 State: <NotBest Int Ext Changed ProtectionPath ProtectionCand> Inactive reason: Not Best in its group - IGP metric Local AS: 65001 Peer AS: 65001 Age: 22:12 Metric2: 20 Validation State: unverified ORR Generation-ID: 0 Task: BGP_65001.10.255.0.2 AS path: 65000 ? Communities: target:64999:100 target:65000:100 color:0:100 Import Accepted VPN Label: 24005 Localpref: 100 Router ID: 10.255.0.2 Secondary Tables: 100.inet.0 Thread: junos-main SR Policy の評価結果を BGP ベストパスに反映するための設定 BGP ベストパス選択アルゴリズムの評価時に SR Policy が評価されるように、 PE1 に以下のような設定を追加します。 router bgp 65000 nexthop validation color-extcomm sr-policy bgp bestpath igp-metric sr-policy ! この設定を追加すると Cisco ルーターの BGP ベストパス選択アルゴリズム 3 の Step 8 において、 IGP metric の代わりに SR Policy path metric が評価されるようになります。Step 8 よりも優先度の高い評価項目である local preference や MED が設定されている場合は SR Policy の評価前にベストパスが決定し、経路へ反映されない事がある点にご注意ください。 また、SR Policy で定義できる metric type は以下の 5 つがあり、この機能において SR Policy の 優先度は administrative distance values(admin values)として表されます。SR Policy の metric type と 対応する admin values は以下の通りで、値が小さいほど優先されます。 latency(admin values: 10) TE(admin values: 20) IGP(admin values: 30) hopcount(admin values: 40) NONE/UNKNOWN metric type (for explicit segment list policies) (admin values: 100) 作成した SR Policy の確認 定義した SR Policy の評価結果を確認します。 まだ各リンクへ TE metric を設定していないため、両 SR Policy 共に path accumulated metric が 10(1リンク当たりのデフォルト値)と計算されている事が分かります。 RP/0/RP0/CPU0:PE1#show segment-routing traffic-eng policy Sat Jul 16 16:20:32.590 JST SR-TE policy database --------------------- Color: 100, End-point: 10.255.0.2 Name: srte_c_100_ep_10.255.0.2 Status: Admin: up Operational: up for 1d02h (since Jul 15 13:39:47.290) Candidate-paths: Preference: 10 (configuration) (active) Name: path-based-te-metric-to-asbr1 Requested BSID: dynamic Protection Type: protected-preferred Maximum SID Depth: 10 Dynamic (valid) metric Type: TE, Path Accumulated metric: 10 16001 [Prefix-SID, 10.255.0.2] Attributes: Binding SID: 24008 Forward Class: Not Configured Steering labeled-services disabled: no Steering BGP disabled: no IPv6 caps enable: yes Invalidation drop enabled: no Color: 100, End-point: 10.255.0.3 Name: srte_c_100_ep_10.255.0.3 Status: Admin: up Operational: up for 00:00:05 (since Jul 16 16:20:26.833) Candidate-paths: Preference: 10 (configuration) (active) Name: path-based-te-metric-to-asbr2 Requested BSID: dynamic Protection Type: protected-preferred Maximum SID Depth: 10 Dynamic (valid) metric Type: TE, Path Accumulated metric: 10 16002 [Prefix-SID, 10.255.0.3] Attributes: Binding SID: 24009 Forward Class: Not Configured Steering labeled-services disabled: no Steering BGP disabled: no IPv6 caps enable: yes Invalidation drop enabled: no path accumulated metric が等しい場合、この時点では ASBR1(10.255.0.2)が egress node として選択されていることが確認できました。 RP/0/RP0/CPU0:PE1#show route vrf 100 Sat Jul 16 16:51:50.976 JST Codes: C - connected, S - static, R - RIP, B - BGP, (>) - Diversion path D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2 E1 - OSPF external type 1, E2 - OSPF external type 2, E - EGP i - ISIS, L1 - IS-IS level-1, L2 - IS-IS level-2 ia - IS-IS inter area, su - IS-IS summary null, * - candidate default U - per-user static route, o - ODR, L - local, G - DAGR, l - LISP A - access/subscriber, a - Application route M - mobile route, r - RPL, t - Traffic Engineering, (!) - FRR Backup path Gateway of last resort is not set C 192.168.0.0/24 is directly connected, 00:05:33, GigabitEthernet0/0/0/2 L 192.168.0.254/32 is directly connected, 00:05:33, GigabitEthernet0/0/0/2 B 192.168.1.0/24 [200/0] via 10.255.0.2 (nexthop in vrf default), 00:04:17 以下のように VPN 経路を確認すると (admin 20) と出力され、admin values が 20 である TE metric に基づいた SR Policy が適用されていることも確認できました。 RP/0/RP0/CPU0:PE1#show bgp vpnv4 unicast vrf 100 192.168.1.0/24 Wed Jul 20 14:00:12.294 JST BGP routing table entry for 192.168.1.0/24, Route Distinguisher: 65000:100 Versions: Process bRIB/RIB SendTblVer Speaker 42 42 Last Modified: Jul 20 13:59:56.989 for 00:00:15 Paths: (1 available, best #1) Not advertised to any peer Path #1: Received by speaker 0 Not advertised to any peer 65001 10.255.0.2 C:100 (bsid:24008) (admin 20) (metric 10) from 10.255.0.2 (10.255.0.2) Received Label 24005 Origin IGP, localpref 100, valid, internal, best, group-best, import-candidate, imported Received Path ID 0, Local Path ID 1, version 42 Extended community: Color:100 RT:64999:100 RT:65001:100 SR policy color 100, up, not-registered, bsid 24008 Source AFI: VPNv4 Unicast, Source VRF: default, Source Route Distinguisher: 65001:100 これで準備は完了です。次の節から各リンクへ TE metric を設定することでより metric 値が小さい経路となる ASBR が選択できることを確認していきます。 2. PE1、ASBR1 間経路の metric が最小となるように設定し、ASBR1 が egress node として選択される事を確認する 上図のように、TE metric を設定し、ASBR1 が egress node として選択される事を確認していきます。 TE metric の設定 PE1 と ASBR1 間に metric を設定(PE1) segment-routing traffic-eng interface GigabitEthernet0/0/0/0 metric 50 ! ! ! PE1 と ASBR2 間に metric を設定(PE1) segment-routing traffic-eng interface GigabitEthernet0/0/0/1 metric 100 ! ! ! ASBR1 と ASBR2 間に metric を設定(ASBR1) segment-routing traffic-eng interface GigabitEthernet0/0/0/1 metric 100 ! ! ! SR Policy の評価結果 以下のように、PE1 と ASBR1 間の metric が 50、PE1 と ASBR2 間の metric が 100 とそれぞれ計算されていることが確認できました。 RP/0/RP0/CPU0:PE1#show segment-routing traffic-eng policy Wed Jul 20 14:09:55.662 JST SR-TE policy database --------------------- Color: 100, End-point: 10.255.0.2 Name: srte_c_100_ep_10.255.0.2 Status: Admin: up Operational: up for 00:37:58 (since Jul 20 13:31:57.085) Candidate-paths: Preference: 10 (configuration) (active) Name: path-based-te-metric-to-asbr1 Requested BSID: dynamic Protection Type: protected-preferred Maximum SID Depth: 10 Dynamic (valid) Metric Type: TE, Path Accumulated Metric: 50 16002 [Prefix-SID, 10.255.0.2] Attributes: Binding SID: 24008 Forward Class: Not Configured Steering labeled-services disabled: no Steering BGP disabled: no IPv6 caps enable: yes Invalidation drop enabled: no Color: 100, End-point: 10.255.0.3 Name: srte_c_100_ep_10.255.0.3 Status: Admin: up Operational: up for 00:37:58 (since Jul 20 13:31:57.087) Candidate-paths: Preference: 10 (configuration) (active) Name: path-based-te-metric-to-asbr2 Requested BSID: dynamic Protection Type: protected-preferred Maximum SID Depth: 10 Dynamic (valid) Metric Type: TE, Path Accumulated Metric: 100 16003 [Prefix-SID, 10.255.0.3] Attributes: Binding SID: 24009 Forward Class: Not Configured Steering labeled-services disabled: no Steering BGP disabled: no IPv6 caps enable: yes Invalidation drop enabled: no egress node として、ASBR1 が採用されていることも確認できました。 RP/0/RP0/CPU0:PE1#show bgp vpnv4 unicast vrf 100 192.168.1.0/24 detail Wed Jul 20 14:10:37.611 JST BGP routing table entry for 192.168.1.0/24, Route Distinguisher: 65000:100 Versions: Process bRIB/RIB SendTblVer Speaker 43 43 Flags: 0x00003001+0x00000000; Last Modified: Jul 20 14:09:45.989 for 00:00:51 Paths: (1 available, best #1) Not advertised to any peer Path #1: Received by speaker 0 Flags: 0xa000000005060005, import: 0x80 Not advertised to any peer 65001 10.255.0.2 C:100 (bsid:24008) (admin 20) (metric 50) from 10.255.0.2 (10.255.0.2), if-handle 0x00000000 Received Label 24005 Origin IGP, localpref 100, valid, internal, best, group-best, import-candidate, imported Received Path ID 0, Local Path ID 1, version 42 Extended community: Color:100 RT:64999:100 RT:65001:100 SR policy color 100, up, not-registered, bsid 24008 Source AFI: VPNv4 Unicast, Source VRF: default, Source Route Distinguisher: 65001:100 traceroute による経路確認 traceroute により、ASBR1 経由で転送されていることが確認できました。 RP/0/RP0/CPU0:PE1#traceroute 192.168.1.254 vrf 100 Wed Jul 20 14:11:09.185 JST Type escape sequence to abort. Tracing the route to 192.168.1.254 1 10.0.0.2 [MPLS: Label 24005 Exp 0] 56 msec 12 msec 7 msec 2 10.100.0.2 [MPLS: Label 24007 Exp 0] 29 msec 5 msec 15 msec 3 192.168.1.254 15 msec 3 msec 7 msec 3. PE1、ASBR2 間経路の metric が最小となるように設定し、ASBR2 が egress node として選択される事を確認する 上図のように、TE metric を設定し、ASBR1 が egress node として選択される事を確認していきます。 TE metric の設定 PE1 と ASBR1 間に metric を設定(PE1) segment-routing traffic-eng interface GigabitEthernet0/0/0/0 metric 100 ! ! ! PE1 と ASBR2 間に metric を設定(PE1) segment-routing traffic-eng interface GigabitEthernet0/0/0/1 metric 50 ! ! ! ASBR2 と ASBR1 間に metric を設定(ASBR2) set protocols isis interface ge-0/0/1.0 level 2 te-metric 100 SR Policy の評価結果 以下のように、PE1 と ASBR1 間の metric が 100、PE1 と ASBR2 間の metric が 50 とそれぞれ計算されていることが確認できました。 RP/0/RP0/CPU0:PE1#show segment-routing traffic-eng policy Wed Jul 20 14:11:44.058 JST SR-TE policy database --------------------- Color: 100, End-point: 10.255.0.2 Name: srte_c_100_ep_10.255.0.2 Status: Admin: up Operational: up for 00:39:47 (since Jul 20 13:31:57.085) Candidate-paths: Preference: 10 (configuration) (active) Name: path-based-te-metric-to-asbr1 Requested BSID: dynamic Protection Type: protected-preferred Maximum SID Depth: 10 Dynamic (valid) Metric Type: TE, Path Accumulated Metric: 100 16002 [Prefix-SID, 10.255.0.2] Attributes: Binding SID: 24008 Forward Class: Not Configured Steering labeled-services disabled: no Steering BGP disabled: no IPv6 caps enable: yes Invalidation drop enabled: no Color: 100, End-point: 10.255.0.3 Name: srte_c_100_ep_10.255.0.3 Status: Admin: up Operational: up for 00:39:47 (since Jul 20 13:31:57.087) Candidate-paths: Preference: 10 (configuration) (active) Name: path-based-te-metric-to-asbr2 Requested BSID: dynamic Protection Type: protected-preferred Maximum SID Depth: 10 Dynamic (valid) Metric Type: TE, Path Accumulated Metric: 50 16003 [Prefix-SID, 10.255.0.3] Attributes: Binding SID: 24009 Forward Class: Not Configured Steering labeled-services disabled: no Steering BGP disabled: no IPv6 caps enable: yes Invalidation drop enabled: no egress node として、ASBR2 が採用されていることも確認できました。 RP/0/RP0/CPU0:PE1#show bgp vpnv4 unicast vrf 100 192.168.1.0/24 detail Wed Jul 20 14:12:06.724 JST BGP routing table entry for 192.168.1.0/24, Route Distinguisher: 65000:100 Versions: Process bRIB/RIB SendTblVer Speaker 49 49 Flags: 0x00003001+0x00000000; Last Modified: Jul 20 14:11:34.989 for 00:00:31 Paths: (1 available, best #1) Not advertised to any peer Path #1: Received by speaker 0 Flags: 0xa000000005060005, import: 0x80 Not advertised to any peer 65001 10.255.0.3 C:100 (bsid:24009) (admin 20) (metric 50) from 10.255.0.3 (10.255.0.3), if-handle 0x00000000 Received Label 16 Origin IGP, localpref 100, valid, internal, best, group-best, import-candidate, imported Received Path ID 0, Local Path ID 1, version 49 Extended community: Color:100 RT:64999:100 RT:65001:100 SR policy color 100, up, not-registered, bsid 24009 Source AFI: VPNv4 Unicast, Source VRF: default, Source Route Distinguisher: 65001:100 パケットカウンタによる経路確認 我々の環境では IOS XR の PE から Junos の ASBR を経由する経路に対し traceroute を実行すると Junos ルーターが応答せず経路確認できないため、ASBR2 と ASBR4 間リンクの ASBR2 側のインタフェース(ge-0/0/2)の Output packets を計測することにより、パケットが ASBR2 を経由したと確認していきます。 以下のように該当インタフェースの統計情報をクリアします。 user@ASBR2> clear interfaces statistics ge-0/0/2 user@ASBR2> show interfaces ge-0/0/2 statistics | match "Output packets" Output packets: 0 次に対向へパケットを一定数送ります。 これで疎通できていることは確認できました。 RP/0/RP0/CPU0:PE1#ping 192.168.1.254 vrf 100 count 1000 size 100 interval 1 Wed Jul 20 14:23:45.959 JST Type escape sequence to abort. Sending 1000, 100-byte ICMP Echos to 192.168.1.254, timeout is 2 seconds: !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!! Success rate is 100 percent (1000/1000), round-trip min/avg/max = 1/4/21 ms インタフェースの Output packets を確認すると以下のようにパケットカウンタが増え、ASBR2 を経由して通信できていることが確認できました。 user@ASBR2> show interfaces ge-0/0/2 statistics | match "Output packets" Output packets: 1004 まとめ 本記事では、Multi-AS での SR-TE を実現する上での考慮点を紹介し、Multi-AS/Multi-domain SR-MPLS 上での L3VPN において、TE metric に基づいた ASBR 選択を含む Color-Based Steering による経路制御の検証結果を紹介しました。次回の記事では、Flexible Algorithm を用いた TE について紹介予定です。 (2022/08/22 追記) 公開しました: [Multi-AS Segment Routing 検証連載 #6] IGP Flexible Algorithm Segment Routing Policy Architecture https://datatracker.ietf.org/doc/html/rfc9256#section-2.5 ↩ Deploy BGP Soft Next-Hop in Cisco IOS XR, BGP Best Path Selection Forcing SR Policy Paths https://www.cisco.com/c/en/us/support/docs/ip/border-gateway-protocol-bgp/217744-deploy-bgp-soft-next-hop-in-cisco-ios-xr.html?dtid=osscdc000283#anc12 ↩ BGP Best Path Selection Algorithm https://www.cisco.com/c/en/us/support/docs/ip/border-gateway-protocol-bgp/13753-25.html ↩
はじめに DevOpsプラットフォームの取り組みを紹介する4回目の記事です。 Qmonus Value Stream の開発チームの會澤です。 連載4回目では、Qmonus Value Streamの重要な構成要素である CUE言語 についてご紹介します。 前回の記事 では、Infrastructuer as Code (以下IaC)の課題と、Cloud Native AdapterというQmonus Value Streamチームの独自技術について解説しました。 Cloud Native Adapterは、「インフラストラクチャの構成」と「ワークフロー」をCUE言語を使って宣言します。 CUE言語は、複雑なシステム構成をスケーラブルに管理できることから、近年注目を集めているデータ記述言語です。 前回の記事でご紹介したとおり、 KubeVela や Dagger など最近リリースされたIaCソリューションで採用されていることが多く、メルカリがKubernetes基盤を管理するために活用していることでも有名です 1 。 今回は、CUE言語の言語としての特徴と、Cloud Native Adapterの開発においてCUE言語をどのように活用しているかについてご紹介します。 Kubernetesにおけるアプリケーションのデプロイ GCPやAWS、Azureといったパブリッククラウド環境を利用してサービスを公開する際、Kubernetes上に「マニフェスト」を適用してアプリケーションをデプロイする機会は多いと思います。 以下のYAMLで記述されたマニフェストは、Kubernetesの 公式サイト から例として引用したものになります。 このようなマニフェストをKubernetesクラスタに適用することで、予め指定したパラメータを利用して、アプリケーションをクラウド上にデプロイできます。 apiVersion : apps/v1 kind : Deployment metadata : name : nginx-deployment labels : app : nginx spec : replicas : 3 selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - name : nginx image : nginx:1.14.2 ports : - containerPort : 80 こういったマニフェストを利用することで、インフラストラクチャのリソース構成や設定をコードで記述・適用し、同じ設定を再利用できます。 その一方で、YAMLを使ってマニフェストを管理することで、以下のような難しさを感じることもあるのではないでしょうか。 データのKeyとValueやリストの構造をインデントによって構成しているため、ネストのズレにより意図しない構造でデータを記述することがある データの定義に誤りがある場合でも、実際に適用してみるまで分からない マニフェストで定義するデータを使い回すことができず、再利用性が低い 複数のマニフェストで定義されたパラメータを一括で変更できない CUE言語は、これらの課題をどのように解決するのでしょうか。 Better YAMLとしてのCUE言語 CUE言語では、YAMLやJSONといった一般的なデータ記述言語と同じように、数値や文字列、リスト、辞書といった型を組み合わせて任意のデータを記述できます。 上記のKubernetesマニフェストをCUE言語で書く場合は、以下のようになります。 apiVersion: "apps/v1" kind: "Deployment" metadata: { name: "nginx-deployment" labels: app: "nginx" } spec: { replicas: 3 selector: matchLabels: app: "nginx" template: { metadata: labels: app: "nginx" spec: containers: [{ name: "nginx" image: "nginx:1.14.2" ports: [{ containerPort: 80 }] }] } } カッコを使ってデータ構造を表現しておりJSONに似た文法ではありますが、単一の値を定義するときは apiVersion: "apps/v1" のように { } を省略できます。 これにより、 selector: matchLabels: app: "nginx" といった形で、階層構造の深いデータを一行にまとめて表現することもできます。 YAMLと違ってカッコで辞書やリスト構造を表現しているため、インデントが無くてもデータの階層構造を正しく解釈できます。 また、利用者が記述するデータに対して型を指定することもできます。 以下のコードは上記のKubernetesマニフェストからPodのレプリカ数に関する定義を抜き出したものに、型による制約を追加したものになります。 spec: { replicas: int } spec: { replicas: 3 } 上記の例は、レプリカ数が整数であるという、型による制約を追加したものになります。 CUE言語によるデータの定義では、文字列や数値といった具体的な値とそのデータの型を同等のものとして記述でき、それによって型による制約が適用されます。 また、以下のように型による制約と並べて、具体的な値による制約を記述することもできます。 replicas: int replicas: >2 replicas: 3 CUE言語では cue というCLIが提供されており、 cue eval コマンドを用いてデータを評価することでその定義に矛盾がないか検証されます。 上記の例であれば、 cue eval コマンドを実行することで、いずれの制約も満たす具体的な値が出力されます。 replicas: 3 もし、以下のような矛盾した制約を追加した場合、 cue eval コマンドを実行することでエラーとして設定の誤りを検証できます。 replicas: int replicas: >5 replicas: 3 replicas: invalid value 3 (out of bound >5): ./intro.cue:2:11 ./intro.cue:3:11 CUE言語では、JSONやYAMLとの相互変換もサポートされています。 前述のCUE言語によるKubernetes Deploymentのマニフェストは cue import nginx-deployment.yaml というコマンドを利用し、YAMLで定義されたマニフェストをインポートして作成しています。 また、CUE言語で作成されたマニフェストも cue export nginx-deployment.cue --out yaml というコマンドでYAMLにエクスポートできます。 CUE言語による柔軟なデータ表現 この章では、CUE言語におけるいくつかの特徴的な機能に着目して、実際にKubernetesマニフェストを生成します。 以下の画像は、CUE言語を利用してプラットフォーム管理者とアプリケーション開発者が協力しながらKubernetesマニフェストを作成するシチュエーションをイメージしたフローチャートになります。 このフローチャートの流れに則り、Templating、Composite、API Schema Validationといった機能を確認しながらCUE言語のエッセンスに触れていきます。 Templating CUE言語では、テンプレートとなる内容を予め作成しておき、そのテンプレートに従って新たなデータを定義できます。 ここでは、以下のようにDeploymentのマニフェストを作成するためのテンプレートを定義してみます。 deployments: [_name=string]: { apiVersion: "apps/v1" kind: "Deployment" metadata: { name: _name + "-deployment" labels: app: _name } spec: { replicas: 3 selector: matchLabels: app: _name template: { metadata: labels: app: _name spec: containers: [{ name: _name image: string ports: [{ containerPort: int }] }] } } } このテンプレートの1行目の [_name=string] と記述している箇所で、テンプレート利用者が宣言したマニフェスト名を受け取って _name という変数にエイリアスしています。 この _name 変数は、テンプレート内で参照できます。 また、 metadata: name に _name 変数で指定された文字列と -deployment を結合した文字列を設定しています。 このテンプレートを利用してマニフェストを作成します。 以下の例では、1行目の deployments: nginx: という記述によりテンプレートの _name 変数に nginx を代入しています。 deployments: nginx: { spec: template: spec: containers: [{ image: "nginx:1.14.2" ports: [{ containerPort: 80 }] }] } このように定義されたデータは、以下のように -e オプションで出力する要素を指定して export コマンドを実行することで、YAMLとしてマニフェストを出力できます。 cue export templating.cue -e deployments.nginx --out yaml spec : ... containers : - name : nginx image : nginx:1.14.2 ports : - containerPort : 80 さらに一歩踏み込んで、テンプレートの中で変数を利用する例を見ていきます。 以下の例では、テンプレートの内容はほとんど同じですが、コンテナのイメージ名、ポート番号に変数を利用するよう変更を加えています。 deployments: [_name=string]: { _image: string _port: int apiVersion: "apps/v1" kind: "Deployment" metadata: { name: _name + "-deployment" labels: app: _name } spec: { replicas: 3 selector: matchLabels: app: _name template: { metadata: labels: app: _name spec: containers: [{ name: _name image: _image ports: [{ containerPort: _port }] }] } } } 今回は、テンプレートの定義の冒頭に _image と _port という変数を追加しています。 変数の先頭には _ を付けることで、 cue export コマンドを使用してマニフェストを出力する際に、これらの定義が出力されないようにしています。 この新しいテンプレートを使い、以下のようなアプリケーション用のマニフェストを記述することで、最小限の変数の指定からマニフェストを定義できます。 deployments: nginx: { _image: "nginx:1.14.2" _port: 80 } 変数で指定する値を変えるだけで、利用するイメージとポート番号が異なる別のアプリケーション用のマニフェストを定義することもできます。 deployments: postgres: { _image: "postgres:12.11" _port: 5432 } ここで仮に、アプリケーションをデプロイするプラットフォーム側の要望として Security Context の設定をマニフェストに追加したくなった場合を考えてみます。 以下のようにテンプレートの内容を一部変更してみます。 (前述のマニフェストと重複する箇所は省略しています。) deployments: [_name=string]: { _image: string _port: int ... spec: { ... template: { metadata: labels: app: _name spec: { securityContext: { runAsUser: 1000 runAsGroup: 3000 fsGroup: 2000 } containers: [{ name: _name image: _image ports: [{ containerPort: _port }] }] } } } } ここでは、マニフェストの spec の中に securityContext の設定を追加しました。 これにより、このテンプレートを利用しているアプリケーション側のマニフェストを変更することなく、最終的に出力されるDeploymentのマニフェストにも、以下のように securityContext の設定が反映されます。 spec : ... spec : securityContext : runAsUser : 1000 runAsGroup : 3000 fsGroup : 2000 containers : - name : nginx image : nginx:1.14.2 ports : - containerPort : 80 テンプレート機能を利用してマニフェストを記述することで、重複した構成を省略し、再利用が容易な形でデータを定義できます。 Composite CUE言語におけるTemplating機能を利用することで、自由に設定できる変数を事前に決めておき、それに従って値を埋めてマニフェストを作成できました。 ですが、実際にアプリケーションを開発していく中では、より自由に設定を追加したいことも多々あります。 そこで、CUE言語のComposite機能により値を設定する例を確認してみます。 以下の例は、テンプレート側の定義を変更せず、アプリケーション側の設定にコンテナで利用する環境変数を追加したデータ定義になります。 // create configuration with the template deployments: nginx: { _image: "nginx:1.14.2" _port: 80 } // composite extra configuration deployments: nginx: spec: template: spec: containers: [{ env: [{ name: "NGINX_HOST" value: "example.com" }] }] 今回はアプリケーション側の設定を変更することで、コンテナに環境変数の定義を追加しました。 このデータを利用することで、以下のように環境変数の設定を追加したマニフェストを出力できます。 spec : ... containers : - env : - name : NGINX_HOST value : example.com name : nginx image : nginx:1.14.2 ports : - containerPort : 80 上記の例のように、CUE言語ではテンプレート側で全ての設定を明示していない場合であっても、別のデータ定義と結合して新たな設定を追加できます。 テンプレートで宣言したデータと追加で宣言したデータは、任意の階層で結合されます。結合の順序に関係なく一貫した結果が生成されるため、テンプレートを使う定義と追加定義の順番を入れ替えても同じ結果が出力されます。 一方で、CUE言語の仕様として、一度定義された値を後から変更・削除できません。 例えば、テンプレートで定義された securicyContext を上書きして、 runAsUser を変えるようなマニフェストは宣言できません。 これにより、テンプレートで予め定義された設定内容を遵守しつつも、必要に応じて設定を追加するといった、堅牢さと柔軟さを兼ね備えたデータ定義が可能となります。 API Schema Validation これまでも設定の中で使ってきたように、CUE言語では int や string などの型を利用して、定義された値を型検査できます。 これにより、YAMLとしてマニフェストを生成するタイミングで無効な値が設定されていないか検証でき、マニフェストをKubernetesクラスタに適用する際の手戻りを減らすことができます。 その一方で、マニフェストで設定する全ての値の型情報を予め記述するのは非常に困難です。 このような課題に対し、CUE言語は、Go実装の構造体定義からCUE言語として評価可能な型定義を生成する機能を提供しています。 ここでは、マニフェストを作成するにあたり、事前にKubernetes APIの型定義を生成して、型検査に適用する方法を紹介します。 Kubernetes APIの型定義を取得するには、 Go言語 で利用される go と cue のCLIを利用して以下のコマンドを実行します。 go mod init example.com go get k8s.io/api/apps/v1 cue get go k8s.io/api/apps/v1 このコマンドを実行することで、カレントディレクトリに cue.mod ディレクトリが作成され、その中に型情報を定義したCUEファイルが保存されます。 ここで保存されるファイルは、Kubernetes APIを実装しているGo言語のソースコードから自動生成されたものになります。 KubernetesのDeploymentに関する型定義は k8s.io/api/apps/v1 というパッケージに #Deployment という名前で記述されているため、以下のようにマニフェストを修正して定義を参照できます。 import ( "k8s.io/api/apps/v1" ) deployments: [_name=string]: v1.#Deployment & { _image: string _port: int ... } 上記のマニフェストでは、5行目のテンプレートのデータ構造を定義する箇所で、 v1.#Deployment & という記述を追加しています。 この記述により、定義されたデータ構造が #Deployment として定義された型を利用して自動的に型検査されます。 例えば、コンテナの name を宣言し忘れたDeploymentマニフェストを生成しようとした際、 #Deloyment で定義された型に違反したことでCUEの評価が失敗します。 このように、実際のKubernetes APIを呼び出す前に、API schemaを使ってデータを検査できます。 ここで利用した型定義の出力機能は、Go言語のソースコードに対してだけでなく、 Protocol Buffers や OpenAPI でも利用できます。 この機能を活用することで、様々なAPIに対してCUE言語による型によって安全性が保証されたデータの定義を実現できます。 CUE言語とCloud Native Adapter ここまでの説明で、Templating、Composite、API Schema Validation といったCUE言語の特徴的な機能を確認してきました。 Qmonus Value Streamでは、CUE言語が提供されている Go API を活用して言語機能を拡張し、Cloud Native Adapterという独自のIaCを実装しています。 Qmonus Value Streamのユーザは、Cloud Native AdapterをCUE言語を用いて記述することで、自身のユースケースに合わせた「インフラストラクチャの構成」と「ワークフロー」を構築できます。 加えて、Qmonus Value Stream独自のComposite機能により、汎化されたCloud Native Adapterを自由に選択・結合することが可能となり、ユーザ間でのモジュールの再利用・プラクティスの共有を促します。 Cloud Native Adapterについては、 前回の記事 で解説していますので、興味があればそちらもご覧ください。 また、CUE言語のより詳細な情報については、以下のwebサイトも合わせてご覧ください。 Documentation | CUE (CUE言語の公式ドキュメント) Cuetorials おわりに DevOpsプラットフォームの取り組み連載の4回目の記事として、Qmonus Value Streamチームが利用しているCUE言語について紹介しました。 Qmonus Value Streamチームは、メンバ一人一人が、利用者であるアプリケーション開発者のために信念を持って活動を続けています。 プラットフォームを内製開発するポジション、プラットフォームを使ってNTTグループのクラウド基盤やCI/CDを構築支援するポジションそれぞれで 、一緒に働く仲間を募集していますので、興味を持たれた方は応募いただけると嬉しいです。 CI/CDプラットフォーム開発、ソフトウェアエンジニア NTTグループのクラウド基盤構築およびCI/CD導入、ソリューションアーキテクト 次回は、Qmonus Value StreamチームがCI/CDパイプラインを構築するために利用している Tekton について紹介します。お楽しみに! https://engineering.mercari.com/en/blog/entry/20220127-kubernetes-configuration-management-with-cue/ ↩
こんにちは。マネージド&セキュリティサービス部セキュリティサービス部門の閏間です。総合リスクマネジメントサービス「 WideAngle 」のサービス企画を担当しています。 皆さんは CISSP という資格をご存じでしょうか。CISSPとはCertified Information Systems Security Professionalの略で、セキュリティに関する国際的な認定資格です。去る6月23日に社内でCISSP資格保有者による小さな座談会を開催したのですが、本記事では、開催するに至った経緯や、開催してみてどうだったかなどについて紹介したいと思います。 勉強会開催の狙いは、部署でセキュリティ学習の機運を高めること 私が所属している部署は、企業向けにセキュリティサービスを開発して提供することをミッションとしています。メンバーの役割は色々あるので、必ずしも全員がセキュリティに関する深い知識を求められるわけではありません。しかし、部署全体でセキュリティに関する知識やスキルをより高めることは、業務遂行にプラスになるだろうと以前から感じていました。 そしてそのような状況を作り出すことに少しでも貢献できたらと思い、あるとき思い立って、以下のメッセージをチーム内のチャットに投稿してみました。 対象の資格としてCISSPを選んだ理由は2つあります。 CISSPは国際的な認定資格であり、セキュリティ分野を体系的に広くカバーしているため 私もCISSP資格保有者なので、勉強会の企画、運営だけでなく、情報を伝える面でも貢献できるため 投稿の結果は…"いいね"が複数ついたので、私の所属部署を対象に、勉強会を開催することを決意しました。 ちなみに開催を決意できた背景には、会社の自由な雰囲気というものがあります。NTT Comでは、ランチタイムの勉強会や、ときには夜間の勉強会など、有志による勉強会が盛んに行われています。このような状況は昨年私がNTT Comに中途入社して初めて知ったのですが、大企業なのにすごいと思った記憶があります。そうした光景を見て、自分もやっちゃえ、と思うことができました。 参加者の関心度を高めるために、身近な人による座談会形式にしました メンバーのCISSPに対する認識や考え方は人それぞれです。CISSPに関心がなかったり、関心があっても取得するつもりはなかったり、取得したいと思っているけど時期は決めていなかったり…。そのため、各人のCISSPに対する気持ちが一段階上げられれば、という想いで準備を進めることにしました。 工夫した点として、勉強会の形式は職場の資格保有者による座談会形式としました。資格保有者にパネリストとして体験談を語ってもらった後、パネリスト同士で会話したり、参加者から質問を受け付けたりするスタイルです。 座談会形式にしようと思った理由は次の通りです。 ネット上には有益なCISSPの勉強法や受験体験記はたくさんあり、今でもどんどん増え続けています(私も受検の際はかたっぱしから記事を読みましたが、学習計画を考える上でとても参考になったので、大変ありがたかったです)。ですが、そうした記事はCISSPに対する関心が薄い人には読まれないかもしれません。 そこで、身近な人に直接体験談を語ってもらうことで、興味を持って聞いてもらえるのではないか?と考えました。幸い私の周囲にはCISSP資格保有者が何人もいまして、3名の方に声をかけたところ、全員が協力依頼に快く応じてくれました。 なお、座談会当日の進行がスムーズにいくよう、パネリストの皆さんとは一度だけ事前ミーティングを実施しました。 その場で、「メンバーのスキル底上げにつながることに貢献したい」「カジュアルな雰囲気で楽しい会にしたい」という想いをパネリストの方にしっかり伝え、協力をお願いしました(カジュアルな雰囲気にしたいので、当日は顔出しお願いします、とも!)。 あとは以下のようなお題でエピソードを用意しておくことをお願いし、当日に向けて資料準備などを進めました。 当日は有意義な情報交換が行われました 座談会はリモート会議(Teams)で実施し、参加者数は40人弱でした。 ※NTT Comではリモートワーク実施率は80%を超えていますが、私の所属部署でも在宅で業務を行うことが多いため、今回の座談会に限らず、打ち合わせは基本的にはリモート会議で行っています。 アジェンダは以下の通りです。最初に私から簡単なCISSPの概要説明と、NTT ComのCISSP取得支援制度として受験費用や研修受講費に対する支援があることを紹介をしました。 そしてその後はメインである、パネリストによる体験談紹介です。体験談を語ってもらい、その後パネリスト同士で質問し合ったり、参加者からの質問やコメント(チャット or 音声)に答える形で進行させました。 なお、全員の体験談の紹介が終わった後に自由に質疑する時間をとりたかったのですが、体験談パートが盛り上がったため時間不足となり、残念ながら質疑パートはなしとなってしまいました。 パネリストとして当部署の事業の責任者の方にも参加してもらいましたが、その方からはNTT ComとCISSPの関わりについての昔話も聞けて興味深かったです。ISC2(CISSPの運営団体)の日本でのサービス開始は2004年ですが、そこにNTT Comが関わっており、関係したメンバーは必須に近い形でCISSPをとる必要があったそうです。 また、参加者からは、CISSP合格後の資格維持に必要となる継続教育(CPE;Continuing Professional Educationsと呼ばれています)について、どのようなことを行っているかとか、大変さなどについて、前のめりな質問ももらいました。 他にもパネリストの体験談や参加者からの質問はいろいろありました。主なものをご紹介します。 パネリストの体験談やアドバイス お客さまがCISSP資格保有者の場合にも対等な立場でディスカッションできるように、というのも取得を目指したひとつのきっかけ。 名刺にCISSPと記載されているだけで、とくに欧米のビジネスパーソンとは信頼関係を固く結べる。 公式問題集を繰り返し解くのが合格の近道。ただし問題と解答を覚えるのではなく、「なぜその解答が正しいのか」を理解するのが重要。 公式問題集を繰り返し解くには、 Anki というアプリを利用するのがお勧め(英単語の単語帳のような使い方ができるアプリで、忘却曲線に基づいて出題頻度を管理できる、暗記に適したツール)。 将来セキュリティ関連の業務を離れたとしても、形として残すことができるので資格取得するのはお勧めできる。 資格取得したことで自信がついた。その一方で専門家として見られるので、日々勉強しようという気持ちになる。 参加者からの質問やコメント CompTIA Security+との難易度の差は? 知識のアップデートは大事ですね。 Ankiや学習方法など、大変勉強になった。 ※ちなみにAnkiについては、座談会後に実際に使い始めた方もいます。 以上のように、座談会当日は活発な情報交換がなされ、とても有意義な会となりました。うれしいことに、座談会で交わされた話に触発されて学習にとりかかった、という方もいます。また、会社とCISSPの関わりの歴史を知れたとか、業務上関わりの薄かった人と準備を通して接することができたといった、企画側にとってうれしいこともありました。このように、座談会を開催したことは、参加者、企画側の双方に有益なものとなりました。 まとめ 社内で開催したCISSP座談会について、開催の経緯や、開催することで参加者、企画側の双方にメリットがあったということを紹介しました。一度座談会をやったくらいで何かが大きく変わることはないと思いますが、CISSP取得を目指す人が少しでも増えたらうれしいです。今後も継続的にこういった活動を行い、所属部署を盛り上げることに少しでも貢献できればと考えています。
目次 目次 はじめに 論文紹介 The Norm Must Go On: Dynamic Unsupervised Domain Adaptation by Normalization OcclusionFusion: Occlusion-aware Motion Estimation for Real-time Dynamic 3D Reconstruction EPro-PnP: Generalized End-to-End Probabilistic Perspective-N-Points for Monocular Object Pose Estimation Cascade Transformers for End-to-End Person Search TrackFormer: Multi-Object Tracking With Transformers Global Tracking Transformers TransWeather: Transformer-based Restoration of Images Degraded by Adverse Weather Conditions 最後に はじめに こんにちは、イノベーションセンターの鈴ヶ嶺・加藤・齋藤です。前編では、CVPR2022の概要とメンバーが参加したワークショップの模様をご紹介しました。後編では、CVPR2022本会議に採択された論文を7つご紹介します。 engineers.ntt.com 論文紹介 The Norm Must Go On: Dynamic Unsupervised Domain Adaptation by Normalization 1 実装: https://github.com/jmiemirza/DUA この論文では、ドメイン適応への応用を目的としたBatch Normalizationの改良が提案されています。 ドメイン適応(domain adaptation)には大量のラベルありorラベル無しデータが必要です。そのため、自動運転中の天候変化など、ドメインシフトが連続的に起こる環境ではデータが十分に収集できないことがあります。 この論文ではBatch Normalizationレイヤの統計量(running mean, running variance)をいじることで少量のラベル無しデータでのドメイン適応を実現しました。これにより、連続的なドメインシフトが起こる環境でもオンラインで適応させることが可能になります。 Dynamic Unsupervised Adaptation Batch Normalizationレイヤではミニバッチ内の平均と分散を移動平均を用いて記録し、データセット全体においてそのレイヤに入力されるデータの分布を正規化します。テスト時にはそれらの統計量は固定されますが、学習データの分布から離れた入力がくると出力分布が崩れてしまい、パフォーマンスが大きく劣化してしまいます。 そこでテスト時にもこれらの統計量(running mean, running variance)を更新することで、入力分布の変化に適応させます。 統計量はサンプルによって大きく変化しうるため、Batch Normalizationをそのまま適用するだけでは収束しにくいという問題がありました。この論文では、モーメンタムにexponential decayを適用することで、異なるドメインへの収束効率を向上させることが提案されています。 また、更新統計量を計算するときに左右反転、クロッピング、回転などのdata augmentationを施すことで性能をより良くできることが示されています。 所感 実装がシンプルで様々なタスクに利用できそうです。 一方で、ドメイン適応を自動化するためには統計量を監視しドメインシフトの発生を検出する必要がありそうです。 OcclusionFusion: Occlusion-aware Motion Estimation for Real-time Dynamic 3D Reconstruction 2 この論文では、RGB-Dカメラを入力とする、オクルージョンに強いリアルタイム3次元復元手法を提案しています。 LSTMにgraph neural networkを組み込み、今見えている場所の動きを蓄積することで見えていない部分の動きも予測するのが特徴です。ネットワークは物体表面をメッシュグラフで表現した時の各ノードの動きと予測自信度(予測分布の分散)を出力しています。 近年の3D復元はFusion-basedな手法が高い性能を誇っています。これは前景の物体を自動で追跡しながら3D復元できるため、対象物体の3Dモデルを必要とせず、また非剛体的な動き(タオルなど)も事前情報なしに予測できることが大きな要因です。 しかし単一視点の入力でオンライン処理をしようとすると、連続するフレーム間で信頼性のある対応点を取りづらく、誤差が蓄積されトラッキングに失敗することがあります。 本手法では、単一視点の困難の1つである「見えない部分のモーション予測」を、これまでの動きや物体形状による制約を活用することで高精度化し、オフライン手法をも超える予測精度を達成しました。 実行速度はNVIDIAのGeForce RTX 2080Ti x2, 解像度480pで36ミリ秒だとしています。 Network architecture 入力画像から2D optical flowを計算し、深度情報と併せて3D optical flowとします。これと対象物体のノードグラフを入力することで、各ノードに3D座標と(カメラから見えているなら)3Dモーションベクトルが特徴量として与えられます。この時あらかじめ物体全体の剛体運動(回転+平行移動の )を計算しておくことで、ネットワークは相対的な非剛体運動のみを推定させています。 そして各ノードにLSTMを導入してステートを持たせることで、これまでの動き情報も推論に活用できるようになっています。 学習時は予測値と予測分散に対してガウス分布のlog-likelihood lossを与えてネットワークを学習しています。 Confidence guided non-rigid reconstruction 前処理で使ったoptical flowやgraph neural networkの出力した予測モーションと確信度を使い、次のコスト関数の重み付き和を最小化するような各頂点のモーションを求めます。 深度制約:測定した深度と予測位置に一貫性を持たせる。前フレームの予測頂点と2D optical flowからその頂点の今フレームにおけるカメラ座標を計算し、その位置の深度情報をもとに3D空間に逆投影する。深度マップのサブピクセル分だけずれが生じるので、ピクセルをポリゴンとして3D空間に逆投影し、ポリゴンと予測頂点位置との距離をコストとする。 モーション制約:最終的な予測モーションとGraph neural networkの予測モーションとの一貫性を保つ。予測自信度で重み付けされたモーションベクトルとのユークリッド誤差をコストとする。 2D制約:最終的な予測モーションと2D optical flowとの一貫性を保つ。物体の各頂点をカメラ座標に投影したときの2次元モーションと2D optical flowとのユークリッド誤差をとる。 剛体制約:物体の硬さを制約にする。隣接する頂点同士に対してお互いの予測モーションベクトルのユークリッド誤差をとる。 これで最適化したのちにモデルの最終的な形状を決定します。新しい頂点をトラッキングに追加する方法はDynamicFusion 3 を利用しています。 EPro-PnP: Generalized End-to-End Probabilistic Perspective-N-Points for Monocular Object Pose Estimation 4 PnP問題のエンドツーエンド確率推論を実現する手法であるEPro-PnPが提案されています。 エンドツーエンドの確率的PnP 以下が全体のアーキテクチャです。注目するポイントは 学習時にEPro-PNPで事後位置姿勢分布 を導出している点です。最終的には、特殊ユークリッド群SE(3)多様体上の位置姿勢の分布を連続空間に対応可能なSoftmaxを用いて出力します。推論時にはソルバーを用いて位置姿勢 を推定しています。 PnP問題の最終的な目的は の2D, 3DのN点の対応点を求めることです。それを定式化すると式(1)のように最小化する関数が導けます。 は回転ベクトル、 は並進ベクトル、 はカメラの内部キャリブレーションのパラメーターを含めた射影関数、 は要素積を示しています。 また、 は3Dから2Dに再射影した誤差を表しています。 式(1)は非線型最小問題のため一意な解でない可能性があり扱いづらいので、エンドツーエンド学習のために微分可能な確率密度を出力するPnPをモデル化しています。累積誤差は次のように定義される尤度関数の負の対数として考えています。 事前位置姿勢分布 を用いてベイズの定理より事後位置姿勢分布 は次のように導出されます。 式(3)は連続空間に対応可能なSoftmaxとして解釈可能です。 学習時には次の式ように教師位置姿勢分布 と のKLダイバージェンスを最小化することで学習します。 式(2)を代入すると以下のような簡略化した損失関数が定義できます。 の積分についてはGPUで計算しやすいK点のサンプルから計算するAMIS(Adaptive Multiple Importance Sampling) 5 という効率的なモンテカルロ手法を利用します。 不確実性と識別のバランス 損失関数に対する重み の勾配は次の式(8)ように導出されます。ここで非加重再投影誤差を として扱います。 最初の項の は負の符号であり再射影誤差が大きく、つまり不確かさが大きい対応点ほど重みづけをしないことを示しています。 次の項の は予測された姿勢に対する再射影の分散を示しています。また正の符号であるため感度の高い対応点にはより強く位置姿勢判別ができるためより重みづけをする必要があることを示しています。 最終的な勾配は図3のようなものとなります。このように学習された対応点の重みは、逆不確実性と識別性に分解できます。既存の研究では前者のみを考慮するため識別性能が不足していました。 実験結果 表4ではnuScenes Benchmark 6 を用いた結果を示しています。ここでは3つのEPro-PnPを評価しています。 EPro-PnP 座標回帰損失 7 を追加したもの test-time flip augmentation (TTA) 8 をさらに追加したもの EPro-PnPはベースラインのFCOS3Dを大きく上回っています(nuScenes Detection Score (NDS) 0.425 vs 0.372)。この結果から大規模なデータセットに適切なエンドツーエンドのパイプラインを設定することで直接の位置姿勢推定を上回る結果となることを示唆しています。また、ポーズ推定精度を反映するmAOEについても基本的に他の手法よりもEPro-PnPが上回っています(mAOE 0.337 vs. 0.36)。TTAを用いた結果ではthe state of the artを上回る結果を示しています(NDS 0.439 vs. 0.422)。 定性的な結果として図7で既存手法のCDPN 9 と学習した重みを比べると提案手法はあまりエッジ部分の詳細をキャプチャできていないです。また、提案手法の座標マップはCDPN 79.46 よりも 79.96 となっているため性能が良いことが分かります。 実際に動作させた結果 以下に実際に動作させた例を記載します。 以下で公開されているpretrain modelの中から今回は epropnp_det_v1b_220411.pth を利用しました。 https://drive.google.com/drive/folders/1AWRg09fkt66I8rgrp33Lwb9l6-D6Gjrg export PATH= /usr/ local /cuda-11. 3 /bin: $PATH export LD_LIBRARY_PATH= /usr/ local /cuda-11. 3 /lib64: $LD_LIBRARY_PATH conda create -y -n epropnp_det python = 3 . 7 conda activate epropnp_det conda install -y pip pip install torch = = 1 . 10 . 1 +cu113 torchvision = = 0 . 11 . 2 +cu113 torchaudio = = 0 . 10 . 1 -f https://download.pytorch.org/whl/torch_stable.html pip install mmcv-full == 1 . 4 . 1 -f https://download.openmmlab.com/mmcv/dist/cu113/torch1. 10 . 0 /index.html conda install -y -c fvcore -c iopath -c conda-forge -c bottler fvcore iopath nvidiacub git clone https://github.com/facebookresearch/pytorch3d cd pytorch3d && git checkout v0. 6 . 1 && pip install -v -e . && cd .. git clone https://github.com/tjiiv-cprg/EPro-PnP.git cd EPro-PnP/EPro-PnP-Det pip install -v -e . # sample image inference python demo/infer_imgs.py demo/ configs/epropnp_det_v1b_220411.py epropnp_det_v1b_220411.pth --show-views 3d bev mc # my image inference cp -r demo mydemo # setup my image(file path: mydemo/demo.png) vim mydemo/nus_cam_front.csv # edit camera intrinsic matrix python mydemo/infer_imgs.py mydemo/ configs/epropnp_det_v1b_220411.py epropnp_det_v1b_220411.pth --intrinsic mydemo/nus_cam_front.csv --show-views 3d bev mc 次の結果は、MIT DriveSegのデータセット 10 の画像を推論した結果になります。 右奥の車両の検出はされていませんが、確率的に存在する可能性の高さを2Dのマップ画像から確認できます。また、奥のいる人などの位置関係はおおむね適切にマッピングされていることが分かります。 所感 微分が難しいPnP問題に対して確率的なレイヤーを加えて柔軟性を持たせることでエンドツーエンド学習が可能となる点が興味深かったです。また、この仕組みは既存のPnP推定ネットワークにも広く応用できるので応用力も期待できると感じました。 Cascade Transformers for End-to-End Person Search 11 画像データベース(gallery set)からクエリ画像に映る特定の人物を検索するperson searchタスクに対し、 この論文ではCascade Occluded Attention Transformer (COAT) を提案し、E2Eな人物検索を実現します。 実装: https://github.com/Kitware/COAT Person search 人物検索タスクに対する一般的なアプローチは、大きく(1)人物検出、(2)特徴量抽出、(3)特徴量の比較に基づくgallery画像のランキング生成から構成されます。(2)と(3)は、所謂 person re-identification(ReID)と同一です。 このperson searchタスクでは次のような困難が挙げられます。 人物検出とReIDそれぞれで必要とする特徴量の性質が異なる 人物検出ではどんな見た目の人間でも検出できるような汎用性のある特徴量を学習する必要があるのに対し、ReIDでは個人ごとに分別可能な特徴量を必要とするため、単一の特徴量抽出器で両方をこなすのは難しい。 E2Eモデルでは検出用とReID用のどちらを先に特徴抽出するかでモデルの構造が変わる。 ReID first: 多様性のある特徴量マップを計算したのち、人間にあたる特徴を検出する detection first: 人間を検出するネットワークでRoI(region of interest)を列挙し、それぞれのbounding boxからReID用特徴を計算する 多様なサンプルに対応する必要がある scale variations. 写っている人物のサイズが極端に異なる pose/viewpoint change. 写っている人物の向きや姿勢が異なる occlusion. 体が別の人や障害物に隠れている 人物検出器 本研究では物体検出器としてCascade R-CNN 12 を適用しています。小さなIoU閾値で学習させる弱い検出器から得たbounding boxを、より厳しいIoU閾値で学習させる検出器のproposalに使い、さらにそこから得たbounding boxをもっと強い検出器のproposalに...という流れでbounding boxを段階的に洗練させるのが特徴です。 これはfaster R-CNNにおけるRegion proposal network (RPN) を複数段に拡張したものといえます。 Cascade Occluded Attention Transformer 本手法の提案アーキテクチャです。オクルージョンに強い特徴量が得られるOccluded Attention Transformerを多段に繋ぎ、前ステージで推測したbounding boxを次のステージのproposalに使い、そこから推測したbounding boxをさらに次のステージのproposalに...という流れで段階的に人物検出とReID特徴を洗練させます。 本手法ではTransformerへの入力前に以下のような工夫を施しています。 マルチスケール特徴を入力とする。各proposalの特徴マップをチャンネル方向にいくつか分割したのち、convolution layerで特定のスケールに縮小する。そうして得られたマルチスケール特徴をtokenとしてtransformerの入力に使う。 Data augmentationでオクルージョンをシミュレートする。具体的にはCutMix 13 の要領で一部のtokenをバッチ間でシャッフルする(切り出す座標と貼り付ける座標は同一)。これにより、各サンプルが一部別の物体によって隠れることを再現できる。学習時のみ有効。 評価 CUHK-SYSU 14 , PRW 15 datasetで評価し、sotaまたはそれに近い性能を達成しました。また、cascadeの各ステージで人物検出の正解判定に用いるIoU閾値は段階的に増やすことで性能が良くなること、また本手法のoccluded attention transformerは、他のattention機構やcutout/mixup等のdata augmentationを使うよりも高い性能を出すことが示されています。 推論速度はNVIDIA A100 GPU, Pytorch実装, 入力サイズ900x1500で11.14FPSでした。 TrackFormer: Multi-Object Tracking With Transformers 16 この論文では、Tracking-by-attentionという新しいトラッキングパラダイムに基づく、エンドツーエンド学習可能なMOT手法を提案しています。 Tracking-by-attention MOTでは、Tracking-by-detectionと呼ばれる物体検出と検出結果のフレーム間対応付けをカスケードに行う手法が一般的です。それに対して、この提案法ではフレーム特徴と物体・追跡クエリに対するAttentionを基にフレーム間の関連付けを行い物体追跡を実現するTracking-by-attentionを提案しています。物体クエリはバウンディングボックスなどの物体検出に利用される埋め込みベクトルであり、追跡クエリは自己回帰的に移動する位置に対応しフレーム間のオブジェクトを追跡しそのIDを引き継ぐ埋め込みベクトルです。また物体・追跡クエリは連結して利用されるため、この提案法は検出と追跡を同時に出力できます。 MOTを集合予測問題として定式化 提案するモデルは図2のように、以下の連続したステップでフレームごとの物体のバウンディングボックスとIDに関するクラスを予測する集合予測問題として定式化しています。 Res-Net-50 17 などの一般的なCNN backboneを用いて 時点の入力からフレーム特徴を抽出する Transformerエンコーダ 18 のself-attentionでフレーム特徴をエンコードする Transformerデコーダで 時点の物体・追跡クエリ用いてMLPでバウンディングボックスとクラス予測にマッピングする 実験結果 表1では、MOT17 19 のデータセットを用いて評価した結果で優れた精度を示していることが分かります。 図3は、MOTS20 20 データセットを用いてR-CNNと比較した結果です。提案法のセグメンテーションの性能が優れていることが分かります。 実際に動作させた結果 以下に実際に動作させた例を記載します。 conda create --name trackformer python = 3 . 7 cmake -y conda activate trackformer conda install pytorch = = 1 . 5 . 0 torchvision = = 0 . 6 . 0 -c pytorch git clone https://github.com/timmeinhardt/trackformer.git cd trackformer pip install -r requirements.txt pip install -U ' git+https://github.com/timmeinhardt/cocoapi.git#subdirectory=PythonAPI ' python src/trackformer/models/ops/setup.py build --build-base=src/trackformer/models/ops/ install cd models wget https://vision.in.tum.de/webshare/u/meinhard/trackformer_models_v1.zip unzip trackformer_models_v1.zip cd ../ # sample video inference ffmpeg -i data/snakeboard/snakeboard.mp4 -vf fps = 30 data/snakeboard/%06d.png python src/track.py with dataset_name =DEMO data_root_dir =data/snakeboard output_dir =data/snakeboard write_images =pretty # my video inference # setup my video(file path: data/myvideo/myvideo.mp4) ffmpeg -i data/myvideo/myvideo.mp4 -vf fps = 30 data/myvideo/%06d.png python src/track.py with dataset_name =DEMO data_root_dir =data/myvideo output_dir =data/myvideo write_images =pretty 次の結果は、DanceTrack 21 の映像に本手法を適用した結果になります。 所感 Tracking-by-attentionによりエンドツーエンドでトラッキングを可能としている点が興味深く、今後のこのような手法に期待させられるような内容でした。 Global Tracking Transformers 22 Transformerベースのマルチオブジェクトトラッキング手法を提案している論文です。 Local tracker と Global tracker 主に従来のトラッキング手法ではフレームごとに物体を関連づけるLocal trackersという手法が用いられていました。この提案手法では、短い動画クリップを入力として全てのフレームを通して物体を関連づけるGlobal trackerが用いられています。 Global tracking transformers(GTR) この提案法は上記のように最初に、全フレームから独立して物体を検出しGlobal tracking transformers(GTR)のエンコーダーに入力されます。またGTRは軌跡クエリをデコーダーの入力として各クエリと物体の関連性スコアを出力し関連づけます。 評価結果 表1にGTRの異なる時間窓の性能をTAO 23 、MOT17のデータセットを用いて評価しています。基本的な傾向としてより長い時間窓では性能が向上しています。 表3の結果はMOT17のテストデータを用いた結果です。この結果からGTRはトップクラスの性能を達成していることが分かります。 実際に動作させた結果 以下に実際に動作させた例を記載します。 conda create --name gtr python = 3 . 8 -y conda activate gtr conda install pytorch torchvision torchaudio cudatoolkit = 11 . 1 -c pytorch-lts -c nvidia pip install " git+https://github.com/facebookresearch/detectron2.git " git clone https://github.com/xingyizhou/GTR.git --recursive cd GTR pip install -r requirements.txt # setup pretrain model(file path: models/GTR_TAO_DR2101.pth) # https://drive.google.com/file/d/1TqkLpFZvOMY5HTTaAWz25RxtLHdzQ-CD/view?usp=sharing # sample video inference python demo.py --config-file configs/GTR_TAO_DR2101.yaml --video-input docs/yfcc_v_acef1cb6d38c2beab6e69e266e234f.mp4 --output output/demo_yfcc.mp4 --opts MODEL.WEIGHTS models/GTR_TAO_DR2101.pth # my video inference # setup my video(file path: myvideo.mp4) python demo.py --config-file configs/GTR_TAO_DR2101.yaml --video-input myvideo.mp4 --output output/demo_myvideo.mp4 --opts MODEL.WEIGHTS models/GTR_TAO_DR2101.pth 次の結果は、DanceTrackの映像に本手法を適用した結果になります。 非常にオクルージョンが多いデータとなっており、IDの切り替わりなどが多少見受けられる結果になりました。 所感 フレームごとにトラッキングするのではなく、グローバルにトラッキングするという方針が興味深かったです。また、現状GPUメモリの制約があるため実装上では時間窓を32に設定するのが限界のようですが、今後のGPU性能がスケールした際の性能向上に期待が持てると思いました。 TransWeather: Transformer-based Restoration of Images Degraded by Adverse Weather Conditions 24 TransWeatherとは カメラに映り込む悪天候時の雨粒、霧、雪片を除去するために特定のネットワークの構築や複数のEncoder-Decoderのネットワークを構築していましたが、パラメータ数が大きくなる問題がありました。そこで、TransWeatherでは、単一のEncoder-Decoderのネットワークで雨、霧、雪の除去できるネットワークを構築し、特定の状況に特化したモデルと同程度もしくは良い結果を出しています。 ネットワークの説明 ネットワークの構成は、単一のEncoder-Decoderネットワークであり、シンプルな構成となっています。TransWeatherのEncoderは、主に3つの構成要素があります。 図1の左から見ていくと、Intra Patch Transformerは、雨、霧、雪などの細かいものをとらえるために、Patchを縦横半分のサイズでconcatして入力画像として渡しています。Transformer blockでは、計算量を抑えるためにダウンサンプリングの工夫を凝らしているPyramid Vision Transformerを採用しています。TransformerのFFNの層では、Pyramid Vision TransformerのDWConvとMLPの層を設けている構成となっています。 Decoder部分では、通常のViTではdecoderの入力(Q,K,V)は、encoderの最終層から出力された特徴量を使いますが、TransWeatherはクエリ(Q)に天候を入力し、KとVについてencoderの最終層から出力された特徴量を入力としています。 クエリ(Q)に天候を入力している部分の実装 を追ってみると、ランダムな1×48のTensorを設定しています。体感としてはここは雨、雪、霧の状態を表すTensorを想定していたので、クエリがこのTensorで妥当なのか、私は実装からは読み取ることができませんでした。 self.task_query = nn.Parameter(torch.randn(1,48,dim)) TransfromerのEncoderとDecoderから出力された特徴量を、MSBDN 25 のDecoderに渡し、Dehazeの処理をします。それにより、クリーンな画像が出力されます。 Figure2. TransWeatherのネットワークアーキテクチャ図 実験結果 学習のデータセットはAll in One 26 で使用された学習のデータセットと同じものを使っています。Snow100K 27 の9,000画像、Raindrop 28 の1,069画像、Outdoor-Rain 29 の9,000画像を利用しています。 テスト画像には、Test1、RainDrop、Snow100k-Lを使用しています。 Table1. 既存のモデルとの比較 評価指標 - PSNR 2枚の画像で同じ位置同士のピクセルの輝度の差分の2乗を計算した指標です。 - SSIM 輝度、コントラスト、構造を軸にして周囲のピクセル平均、分散、共分散をとることで、ピクセル単体のみならず、周囲のピクセルとの相関を取り込んだ指標となっています。 この評価指標においてTransWeatherは、PSNR、SSIMどちらにおいても既存のモデルを上回っているということを述べています。 パラメータ数も減少しており、Transweatherはパラメータ数が31Mであり、All in One Networkのパラメータ数44Mと比較するとより少なくなっています。 Figure4. モデルごとのDehazeした結果比較 動作結果 コードを動作させるためにはutil.pyが必要なのですが、github上に実装がなかったためSyn2Realのutil.pyを引用しました。学習済みモデルは見つけることができませんでした。 そのため、配布されている学習セットを使用して手元で学習させました。 # このままcloneしただけでは動かない git clone https://github.com/jeya-maria-jose/TransWeather.git cd TransWeather # NVIDIA RTX3090を用いたのでcudaバージョンはcuda11.0を使用 # torch 1.8.0以上だとsixがないため動かないので、今回はtorch1.7.1をインストール conda env create -f environment.yml conda activate transweather pip install torch1. 11 . 0 +cu113 torchvision0. 12 . 0 +cu113 torchaudio0. 11 . 0 --extra-index-url https://download.pytorch.org/whl/cu113 pip install mmcv-full1. 5 . 3 -f https://download.openmmlab.com/mmcv/dist/cu111/torch1. 10 . 0 /index.html # epoch数はデフォルトの250だったためそのまま採用 python train.py -train_batch_size 32 -exp_name Transweather -epoch_start 0 -num_epochs 250 python test_test1.py -exp_name TransWeather 論文では、Test1、RainDrop、Snow100k-Lを利用していると述べていました。そこに加えcityscapes dataset(leftimg8bit_trainval_rain)とドメインが全く異なるiPoneで撮影した画像を使用してテストを行いました。snow100k、test1、RainDropでテスト画像を何枚使用しているかは不明であったため、論文のテストセットとは同一ではないと思われます。 Table2. 論文での実験結果 Table3. 手元のテスト結果 表1と表2を比較すると、psnrとssimの評価指標が論文中の実験と比較し、下回っているように思えます。 次に、1024×576のiPhoneで撮影した画像に対して定性的な評価をしていきます。入力画像を上に、dehazeが行われた後画像の出力画像を下に載せます。赤枠で囲んだ部分を見ると、小さな雪片がうまく消せていることから、学習がうまくいき、Dehazeができていることがわかります。ただ、大きな雪片については、Dehazeが一部できていないように見えます。 雨天の入力をした場合においても、赤枠の部分を注目すると両方の画像において、入力画像にたいして出力画像が雨粒を一部消せていることがわかります。 所感 TransWeatherは単一のEncoder-Decoderネットワークを使用していることからAll in Oneの手法で明確な違いがあることやIntra-patchの導入をしました。それが、既存のモデルを評価指標の面に置いて良い結果につながっていること示しています。公開コードの学習推論を一通り実行することはできたものの、テストセットの詳細が不明なため論文に示されてる結果の再現実験は困難な印象を持ちました。また、これは本手法に限りませんが、dehazeなどの処理が悪天候時の映像を入力とした物体認識にどの程度効果があるのかについても調査してみたいと考えています。 最後に 本ブログでは、CVPR2022への参加者が興味を持った論文をご紹介しました。 NTT Comでは、今回ご紹介した論文調査、画像や映像、更には音声言語も含めた様々なメディアAI技術の研究開発に今後も積極的に取り組んでいきます。 アカデミックな研究に注力したくさん論文を書きたい 最新の技術をいちはやく取り入れ実用化に結び付けたい AIアルゴリズムに加え、AI/MLシステム全体の最適な設計を模索したい という方々に活躍していただけるフィールドがNTT Comには多くあります。今後も私たちの取り組みをブログ等で発信していきますので、興味を持ってくださった方は是非今後もご注目ください! M. Jehanzeb Mirza, Jakub Micorek, Horst Possegger, Horst Bischof. The Norm Must Go On: Dynamic Unsupervised Domain Adaptation by Normalization, In CVPR, 2022. ↩ Wenbin Lin, Chengwei Zheng, Jun-Hai Yong, Feng Xu. OcclusionFusion: Occlusion-aware Motion Estimation for Real-time Dynamic 3D Reconstruction. In CVPR, 2022. ↩ Richard A. Newcombe, Dieter Fox, Steven M. Seitz. DynamicFusion: Reconstruction and Tracking of Non-rigid Scenes in Real-Time. In CVPR, 2015. ↩ Chen, Hansheng et al. “EPro-PnP: Generalized End-to-End Probabilistic Perspective-N-Points for Monocular Object Pose Estimation.” Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR). 2022. ↩ Jean-Marie Cornuet, Jean-Michel Marin, Antonietta Mira, and Christian P. Robert. Adaptive multiple importance sampling. Scandinavian Journal of Statistics, 39(4):798–812, 2012. 3 ↩ Holger Caesar, Varun Bankiti, Alex H. Lang, Sourabh Vora, Venice Erin Liong, Qiang Xu, Anush Krishnan, Yu Pan, Giancarlo Baldan, and Oscar Beijbom. nuscenes: A multimodal dataset for autonomous driving. In CVPR, 2020. 1, 5, 6 ↩ Hansheng Chen, Yuyao Huang, Wei Tian, Zhong Gao, and Lu Xiong. Monorun: Monocular 3d object detection by reconstruction and uncertainty propagation. In CVPR, 2021. 2, 3, 4, 5, 6, 7, 8 ↩ Tai Wang, Xinge Zhu, Jiangmiao Pang, and Dahua Lin. FCOS3D: Fully convolutional one-stage monocular 3d object detection. In ICCV Workshops, 2021. 5, 6, 7, 8 ↩ Zhigang Li, Gu Wang, and Xiangyang Ji. Cdpn: Coordinates-based disentangled pose network for real-time rgb-based 6-dof object pose estimation. In ICCV, 2019. 1, 2, 5, 6, 7, 8 ↩ Ding, Li et al. “MIT DriveSeg (Manual) Dataset for Dynamic Driving Scene Segmentation.” (2020). ↩ Rui Yu, Dawei Du, Rodney LaLonde, Daniel Davila, Christopher Funk, Anthony Hoogs, Brian Clipp. Cascade Transformers for End-to-End Person Search. In CVPR, 2022. ↩ Zhaowei Cai, Nuno Vasconcelos. Cascade R-CNN: delving into high quality object detection. In CVPR, 2018. ↩ Sangdoo Yun, Dongyoon Han, Seong Joon Oh, Sanghyuk Chun, Junsuk Choe, Youngjoon Yoo. CutMix: Regularization Strategy to Train Strong Classifiers With Localizable Features. In ICCV, 2019. ↩ Tong Xiao, Shuang Li, Bochao Wang, Liang Lin, Xiaogang Wang. Joint detection and identification feature learning for person search. In CVPR, 2017. ↩ Liang Zheng, Hengheng Zhang, Shaoyan Sun, Manmohan Chandraker, Yi Yang, Qi Tian. Person re-identification in the wild. In CVPR, 2017. ↩ Meinhardt, Tim et al. “TrackFormer: Multi-Object Tracking With Transformers.” Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR). 2022. ↩ Kaiming He, Xiangyu Zhang, Shaoqing Ren, and Jian Sun. Deep residual learning for image recognition. In IEEE Conf. Comput. Vis. Pattern Recog., 2016. 2, 3, 6 ↩ Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N Gomez, Ł ukasz Kaiser, and Illia Polosukhin. Attention is all you need. In Adv. Neural Inform. Process. Syst., 2017. 1, 2, 3, 4, 9 ↩ A. Milan, L. Leal-Taixe, I. Reid, S. Roth, and K. ´ Schindler. MOT16: A benchmark for multi-object tracking. arXiv:1603.00831, 2016. 2, 3, 5, 6 ↩ Paul Voigtlaender, Michael Krause, Aljosa Osep, Jonathon Luiten, Berin Balachandar Gnana Sekar, Andreas Geiger, and Bastian Leibe. Mots: Multi-object tracking and segmentation. In IEEE Conf. Comput. Vis. Pattern Recog., 2019. 2, 6, 7, 8 ↩ Sun, Peize et al. “DanceTrack: Multi-Object Tracking in Uniform Appearance and Diverse Motion.” Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR). 2022. ↩ Zhou, Xingyi et al. “Global Tracking Transformers.” Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR). 2022. ↩ Achal Dave, Tarasha Khurana, Pavel Tokmakov, Cordelia Schmid, and Deva Ramanan. Tao: A large-scale benchmark for tracking any object. In ECCV, 2020. 3, 4, 5, 6, 7 ↩ Jeya Maria Jose Valanarasu, Rajeev Yasarla, Vishal M. Patel.TransWeather: Transformer-based Restoration of Images Degraded by Adverse Weather Conditions. In CVRP2022 ↩ Hang Dong, Jinshan Pan, Lei Xiang, Zhe Hu, Xinyi Zhang, Fei Wang, and Ming-Hsuan Yang. Multi-scale boosted dehazing network with dense feature fusion. In Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition, pages 2157–2167, 2020 ↩ Ruoteng Li, Robby T. Tan, Loong-Fah Cheong1.All in One Bad Weather Removal using Architectural Search. In CVRP2020 ↩ Yun-Fu Liu, Da-Wei Jaw, Shih-Chia Huang, and Jenq-Neng Hwang. Desnownet: Context-aware deep network for snow removal. CoRR, abs/1708.04512, 2017. ↩ Rui Qian, Robby T Tan, Wenhan Yang, Jiajun Su, and Jiaying Liu. Attentive generative adversarial network for raindrop removal from a single image. In Proceedings of the IEEE conference on computer vision and pattern recognition, pages 2482–2491, 2018. ↩ Ruoteng Li, Loong-Fah Cheong, and Robby T Tan. Heavy rain image restoration: Integrating physics model and conditional adversarial learning. In Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition, pages 1633–1642, 2019 ↩
目次 目次 はじめに CVPR2022概要 Workshop on Image Matching: Local Features & Beyond SuperPoint and SuperGlue: Lessons Learned Large-scale 3D reconstruction Deployment - Successes, Challenges, Open Problems Unstructured Object Matching using Co-Salient Region Segmentation Nerfels: Renderable Neural Codes for Improved Camera Pose Estimation Feature Query Networks: Neural Surface Description for Camera Pose Refinement Learning Co-segmentation by segment swapping for retrieval and discovery DA-AE: Disparity-Alleviation Auto-Encoder Towards Categorization of Heritage Images for Aggrandized 3D Reconstruction Detecting and Suppressing Marine Snow for Underwater Visual SLAM A case for using rotation invariant features in state of the art feature matchers Kaggle IMC 2022 感想戦 1st place solution 5th place solution 7th place solution 最後に はじめに こんにちは、イノベーションセンターの鈴ヶ嶺・加藤・齋藤です。普段はコンピュータビジョンの技術開発やAI/MLシステムの検証に取り組んでいます。6月19日から24日にかけて、コンピュータービジョン分野におけるトップカンファレンスのひとつである CVPR2022 がオンラインで開催され、NTT Comからは複数名オンラインで参加しました。その参加レポートを前後編に分けて紹介します。 前編では会議の概要とワークショップ、Kaggle IMC 2022 感想戦について紹介します。後編では、論文の紹介をしたいと思います。 CVPR2022概要 CVPRは毎年6月に開催されるコンピュータービジョン分野におけるトップカンファレンスのひとつです。今年は現地(New Orlens)とオンラインのハイブリッド開催でした。採択率は25.3%(2064/8161)となっており、昨年(採択率1661/7093=23.6%)同様当該分野に高い関心が寄せられていることが分かります。以下の図のように国別の採択割合で見ると、昨年に引き続き中国、次いでアメリカ、韓国が割合として多くを占めています。 以下は、分野別の採択率を示したグラフです。3D from multi-view and sensors、Pose estimation and tracking分野の採択率が多い傾向にありました。 オープニングメッセージ から引用 採択論文のタイトルをワードクラウドで可視化すると以下の図が得られます。Image、Video、Learningといったある意味自明なキーワードに加え、Transformerや3Dなどの出現頻度も高いことが見て取れます。 Workshop on Image Matching: Local Features & Beyond https://image-matching-workshop.github.io ここからワークショップについての紹介になります。 このワークショップでは、カメラ姿勢の推定や3次元再構成の基礎となる画像マッチング技術を取り扱っています。 これに合わせてKaggleでは画像マッチングのコンペティション(Kaggle IMC 2022)が開催され、ワークショップ終盤では上位陣のアプローチが紹介されました。 この章では各発表の概要を紹介していきます。 SuperPoint and SuperGlue: Lessons Learned by Tomasz Malisiewicz, Meta Reality Labs この発表ではDeep Learningベースの画像マッチング手法であるSuperPoint(CVPR workshop 2018)およびSuperGlue(CVPR 2020)の概要と、その開発の過程で得られた教訓が紹介されました。 SuperPoint: Self-Supervised Interest Point Detection and Description 1 SuperPointとは、Visual SLAMのフロントエンド(特徴点抽出)に使われる畳み込みニューラルネットです。 Fully Convolutional Networkなので、計算の途中で特徴点周りの画像パッチを切り出すようなことはせず、特徴点の検出と記述子の計算が同時に行われることが特徴です。 記述子はSiamese trainingによるmetric learningで学習する一方、特徴点検出の学習や、記述子の学習に必要な点対応の生成はself-supervised learningでおこなっています。 self-supervised learningでは、まず特徴点が自明な人工データ(単純な立体のCG画像)で学習し、これによって得たモデルにMSCOCOデータを入力しそれらの擬似ラベルを生成します。そして画像をランダムに変形することで、特徴点とその対応関係が分かっている画像ペアが手に入ります。 これを教師データとみなしてSuperPointの全体を学習できます。 画像の変形はあくまで擬似的な変形であり、3次元空間でのカメラ移動を再現できませんが、実データでの評価により3次元的な画像の変化に対しても汎化できていることがわかっています。 SuperPoint開発過程で得られた教訓 畳み込みニューラルネットワークで直接6-DoF relative poseを推定させたがうまくいかなかった。 Object detectionの技法が点検出に役立った。 in-houseなプライベートデータセットではなくMSCOCOを使うという決断は、汎化性能を向上するという意味でもオープンソースの精神という意味でも有利に働いた。 人工データセットの構築は学習レシピをシンプルにしてくれた。 SuperGlue: Learning Feature Matching with Graph Neural Networks 2 SuperGlueは、特徴点とその記述子をグラフニューラルネットワークに入れて、self-attention(同一画像内の点同士の関係をエンコード)とcross-attention(異なる画像内の点同士の関係をエンコード)モジュールを利用しマッチング用の特徴量を計算します。 各点の特徴量を計算した後はお互いの類似度を計算し、Sinkhorn Algorithmによって最終的な対応関係を算出します。このソルバは微分可能なので、学習時の勾配を前段のグラフニューラルネットワークまで伝播させることができます。 記述子にSIFT、SuperPointどちらを用いた場合でも、SuperGlueによるマッチング抽出は高い性能を発揮します。 SuperGlue開発過程で得られた教訓 実用システムから離れるという決断。記述子から特徴量を再計算するのはあまり実用的でないが、新しい手法の提案につながった。 Q&A 特徴点対応の算出を経由しないポーズ推定は今後可能になるだろうか? 昔ながらのCNNでは無理かもしれない。実際SuperPointの開発過程でやってみたがうまくいかなかった。しかし、将来的に新しいテクニック(ViTとか)で可能になるかもしれない。 Large-scale 3D reconstruction Deployment - Successes, Challenges, Open Problems by Ales Hrabalik, Capturing Reality/Epic Games この講演では、3次元再構成のアプリケーション、現在の技術のlimitationとその対策、未解決問題について紹介されました。以下に各々の詳細を記載します。 3次元再構成の応用先 近年3次元再構成を行う様々なアプリケーションが登場しており、以下のような応用が期待されています。 家具などをキャプチャして内装設計に利用 文化遺産をキャプチャして教材に利用 ドローンなどで地形をキャプチャして解析 Limitationとその対策 様々な視点からの画像を集約し各視点の位置関係と物体の3次元形状を推定するという性質上、3次元再構成が困難な物体が存在します。それらの例と、そのようなケースに対する緩和策として以下が挙げられます。 動物など動くもの。大量のカメラを使い一発で全方位から撮る。 のっぺりしていて画像から特徴的な点を抽出しにくい物体。3D laser scanningなど、カメラ以外の測定器を活用する。 鏡面反射があるため視点によって見た目が変化する物体。scanning sprayと呼ばれる、光反射を抑えるスプレーを吹き付ける。 透明な物体。scanning sprayを利用する。 未解決問題 前節とは異なり緩和が難しいケースとして以下のような困難が挙げられます。 髪の毛などの表現が非常に難しい。平面的なテクスチャでは細かい凹凸が表現できない。 複雑な形のせいでカメラに映らない箇所が生じ、出力モデルの一部が欠損する。 future challenge 3次元再構成を改良するにあたり以下のような取り組みが行われています。 物体の反射特性の推定。これによってキャプチャ時の光源環境による影響を取り除き(delighting)、合成先の光源環境に適応させること(relighting)が可能になる。 表現力の高いreflectanceモデルの適用。physically-basedな手法(5params)はLambert(3params)よりもリアルな表現を可能にする。 color correction前処理。世のカメラはphoto finishingという非線形処理を施すため、これの較正が必要。 Unstructured Object Matching using Co-Salient Region Segmentation 形状が大きく変わりうる物体、例えば: 人が着ている状態の服と畳まれている服 プールで使われている様子の浮き輪としぼんでいる浮き輪 などが写っている写真のマッチングを行いたいというのが本研究のモチベーションでした。 ドキュメントの性質によっては写真の撮り方が変わるため、商品検索やcompliance checkの際はこれを考慮した画像マッチングを行う必要があります。 (注 compliance checkについて:米国では子供用玩具に対し安全基準が定められており、amazonに出品する時はその類の書類を提出する必要があります。そしてamazon側はその書類中の画像とカタログの画像が同じ製品を撮ったものかどうかをチェックする必要があります。) Toys In Context Dataset: 子供用品のカタログ写真から収集したデータセットの提案 おもちゃ ぬいぐるみ パジャマ ... 物体検出フレームワーク SCOTTの提案 SCOTTはobjectを抽出するネットワーク(ここでsaliency mapと特徴量を得る)とマッチングを行うネットワークで構成されています。 baselineに対してF1-scoreの観点で最も良いパフォーマンスを達成したことが論文中に示されています。 Nerfels: Renderable Neural Codes for Improved Camera Pose Estimation image matching & localizationではIndirect & sparseとDense Alignmentの2つの戦略があります。それぞれのメリットとデメリットは以下です。 Indirect & sparse: 点を抽出してマッチング ORB-SLAM 3 などで使われる 処理が軽い 検出位置の精度が重要。サブピクセル単位の座標が欲しい。 Dense Alignment: 全てのピクセルを使ってポーズ推定 DTAM 4 など 処理が重い 画像の検出点周りを切り出しそれらのみをピクセルレベルでアラインメントすることで、2つのアプローチのいいとこ取りをしようというのがこの論文の目的です。 切り出された部分はカメラ移動によって見た目が変化しますが、これはNerfelという新しいレンダリング手法によって予測します。 そして検出点の位置によるreprojection lossと、Nerfelでレンダリングされた画像パッチによるphotometric lossの2つでマッチングを最適化しています。 Feature Query Networks: Neural Surface Description for Camera Pose Refinement structure-based localization あるシーンの3Dモデル(点群)が手に入っている時、クエリ画像から検出した特徴点と3次元点群との対応をとることで、2D-3D matchingによりカメラの姿勢(座標と回転の6自由度)を推定できます。しかしながら、3Dモデル構成時に使った参照画像と推論時のクエリ画像のカメラ姿勢が大きく異なる場合、同じ点に対する特徴量が異なってしまいます。本手法では、 視点に依存しない特徴量を抽出するのではなく、視点の変化に対応する特徴量が抽出できるMLPを構成し、マッチングの精度を高めました。 Feature Query Networks (FQNs) 学習するMLPへの入力はシーンの3Dモデルにおける参照点の座標(3d)・カメラの視線方向(2d)・カメラのroll回転(1d)・カメラの焦点距離(1d)・カメラから参照点までの距離(1d)の計8次元であり、ここから予測特徴量を出力します。 クエリ画像のカメラ姿勢を推定するときはFQNsに入力する情報が未知ですが、適当な推定値を初期値として以下の手順を繰り返すことで精度の良いカメラ姿勢を推定できます。 推定カメラ姿勢から特徴量を予測 クエリ画像の特徴量とマッチング RANSACでポーズ推定 推定カメラ姿勢の更新 Learning Co-segmentation by segment swapping for retrieval and discovery https://imagine.enpc.fr/~shenx/SegSwap 例えば以下のような異なるスタイルの画像間で同一パターンをco-segmentation(画像ペア中の同一物体をセグメンテーションするタスク)として検出するのが本研究の目的です。 彫刻 vs. 油画 昼画像 vs. 夜画像 スタイルの異なる画像間で同一パターンを含む公知のデータセットはないため、この研究では以下の方法で画像を生成しデータセットを構築しています。 MSCOCOデータセットのセグメンテーションタスクに登場する物体を他画像にPoisson blendで合成 Style transferで画像の見た目を多様化 Transformer, Sparse-Ncnet 5 の2つのモデルを学習して実験し、さまざまな場所の写真を集めたデータベースを基にクエリ画像の撮影場所を特定するPlace recognitionタスクを扱う以下のデータセットで評価しました。 Pitts30K 6 : Google Street Viewを用いて収集した約1万枚のピッツバーグの画像データベースから、全く異なる時期に撮られた画像をクエリとしてその撮影場所を特定する。 Tokyo 24/7 7 : Google Street Viewを用いて収集した約76000枚の昼の東京の画像データベースから、様々な時間帯で撮られた画像をクエリとしてその撮影場所を特定する。 DA-AE: Disparity-Alleviation Auto-Encoder Towards Categorization of Heritage Images for Aggrandized 3D Reconstruction クラウドソーシングで画像を収集して3次元復元しようとすると、無関係の画像が紛れ込むのでそれを除去しなければならないため、DA-AEと呼ばれるオートエンコーダーを提案しました。 これを用いて画像をクラスタリングすることで、異なる建築物の写真を分離できました。 Detecting and Suppressing Marine Snow for Underwater Visual SLAM 海底の物体を3次元再構成する時、特徴点検出器がマリンスノーを捉えてしまいその後の処理に悪影響を与えることがわかっています。 つまり海底ではRANSAC + Ratio test + Motion modelなどの後処理を加えてもSLAMがうまく動きません。 そこでマリンスノー由来の特徴点を検出結果から除去するために、Marine Snow Datasetを提案しました。 海底以外の方向を見れば基本的に単調な背景の(すなわち切り出しやすい)マリンスノー画像が得られる。 綺麗な海底画像にマリンスノーを合成する。 まともなkeypointとマリンスノー由来のkeypointを収集し、"Clean" or "Snow"を各点にアノテーションする。 これでkeypointに付随するdescriptorからそのkeypointがまともかどうかを判定する分類器が作れます。制約として、後ろがテクスチャのある背景だと分類が難しくなるようです。 A case for using rotation invariant features in state of the art feature matchers state-of-the-art (sota)なマッチング手法であるLoFTR 8 を回転に強くするために、SE(2)-LoFTRが提案されました。 これはSteerable CNNs(参考:General E(2) - Equivariant Steerable CNNs, NeurIPS 2019)の考え方をベースにしているようです。 提案されたネットワークでは、中間レイヤーや出力層での特徴量が回転不変になっています。 future workとしては以下の点が挙げられています。 transformerのpositional encodingでは回転不変性が保証されていない scale invarianceの獲得 Kaggle IMC 2022 感想戦 https://www.kaggle.com/competitions/image-matching-challenge-2022 大体同じ場所で撮られた2枚の写真をもとに、その2つのカメラの相対的な姿勢変化を求めることが本コンペティションの目標です。具体的にはfundamental matrixを求め提出することが求められます。 上位陣はsotaなlocal feature matching(LoFTR, SuperGlueなど)を活用し、複数のモデルから特徴点を集めていました。また、モデルのバリエーションだけでなく、入力画像を回転させるなどしてTest time augmentationを行なっているチームもあったようです。 1st place solution concat keypoints:すなわちキーポイント検出器のアンサンブルであり、今回のコンペでかなり重要。後段のcropがより精密にもなる。 840px、1024px、1280pxへリサイズした画像にSuperPoint+SuperGlueをそれぞれ適用し得られた特徴点を集約する 840pxへリサイズした画像にLoFTRを適用し特徴点を得る DBSCAN 9 でco-visible boxesを抽出。keypointが多く残るようなクロップを見つける どのクラスタを選択する?選択されたキーポイントの割合が閾値を超えるまでクラスタを結合 クロップは少し広めにとる クロップして再マッチング クロップからはみ出た正しいマッチングを拾うために、クロップ前の結果とクロップ後の結果をアンサンブル 5th place solution COTR 10 を改良した自作のモデルECO-TR(非公開)を用いて、SuperPoint+SuperGlueから得られた特徴点のrefinementを行った。 7th place solution LoFTRとDKM 11 のアンサンブル K-Meansでkeypointの集まっているところをクロップし再マッチング 推定Homographyを元に画像を変形し再マッチング Homography推定にはVSAC 12 を使った。Opencv MAGSAC++ 13 より速い 最後に 本ブログでは、CVPR2022の概要と参加者が興味を持ったワークショップ、Kaggle IMC 2022をご紹介しました。後編では、参加者が気になった論文を紹介するのでぜひご覧になってください。 NTT Comでは、今回ご紹介した論文調査、画像や映像、更には音声言語も含めた様々なメディアAI技術の研究開発に今後も積極的に取り組んでいきます。 アカデミックな研究に注力したくさん論文を書きたい 最新の技術をいちはやく取り入れ実用化に結び付けたい AIアルゴリズムに加え、AI/MLシステム全体の最適な設計を模索したい という方々に活躍していただけるフィールドがNTT Comには多くあります。今後も私たちの取り組みをブログ等で発信していきますので、興味を持ってくださった方は是非今後もご注目ください! Daniel DeTone, Tomasz Malisiewicz, Andrew Rabinovich. “SuperPoint: Self-Supervised Interest Point Detection and Description.” Proceedings of the IEEE/CVF conference on computer vision and pattern recognition workshops. 2018. ↩ Sarlin, Paul-Edouard and DeTone, Daniel and Malisiewicz, Tomasz and Rabinovich, Andrew. “SuperGlue: Learning Feature Matching With Graph Neural Networks.” Proceedings of the IEEE/CVF conference on computer vision and pattern recognition. 2020. ↩ Mur-Artal, Raul, Jose Maria Martinez Montiel, and Juan D. Tardos. “ORB-SLAM: a versatile and accurate monocular SLAM system.” IEEE transactions on robotics 31.5 (2015): 1147-1163. ↩ Newcombe, Richard A., Steven J. Lovegrove, and Andrew J. Davison. “DTAM: Dense tracking and mapping in real-time.” 2011 international conference on computer vision. IEEE, 2011. ↩ Rocco, Ignacio, Relja Arandjelović, and Josef Sivic. “Efficient neighbourhood consensus networks via submanifold sparse convolutions.” European conference on computer vision. Springer, Cham, 2020. ↩ Torii, Akihiko and Sivic, Josef and Okutomi, Masatoshi and Pajdla, Tomas. “Visual Place Recognition with Repetitive Structures.” Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2013. ↩ Torii, Akihiko and Arandjelovic, Relja and Sivic, Josef and Okutomi, Masatoshi and Pajdla, Tomas. “24/7 Place Recognition by View Synthesis.” Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2015. ↩ Sun, Jiaming, et al. “LoFTR: Detector-free local feature matching with transformers.” Proceedings of the IEEE/CVF conference on computer vision and pattern recognition. 2021. ↩ Ester, Martin, et al. “A density-based algorithm for discovering clusters in large spatial databases with noise.” kdd. Vol. 96. No. 34. 1996. ↩ Jiang, Wei and Trulls, Eduard and Hosang, Jan and Tagliasacchi, Andrea and Yi, Kwang Moo. “COTR: Correspondence Transformer for Matching Across Images.” Proceedings of the IEEE/CVF International Conference on Computer Vision. 2021. ↩ Edstedt, Johan et al. “Deep Kernelized Dense Geometric Matching”. arXiv preprint arXiv:2202.00667. (2022). ↩ Ivashechkin, Maksym, Daniel Barath, and Jiří Matas. “VSAC: Efficient and Accurate Estimator for H and F.” Proceedings of the IEEE/CVF International Conference on Computer Vision. 2021. ↩ Barath, Daniel, et al. “MAGSAC++, a fast, reliable and accurate robust estimator.” Proceedings of the IEEE/CVF conference on computer vision and pattern recognition. 2020. ↩
はじめに イノベーションセンターの松下です。 NTTコミュニケーションズ株式会社 (以下、NTT Com) は、Interop Tokyo 2022の ShowNet において、 低遅延ライブ配信プラットフォーム Smart vLive による4K低遅延ライブ配信を展示しました。 この記事では、ShowNetでの展示の模様をお伝えします。 Smart vLive Smart vLiveは、1秒未満の遅延で映像をライブ配信できるサービスです。 ブラウザでの通話やWeb会議に用いられる技術であるWebRTCをライブ配信に応用することで、既存の技術では難しかった1秒未満の遅延を実現しています。 これまでWebRTCを用いたライブ配信では同時視聴者数に制約がありましたが、Smart vLiveはライブ配信に特化したシステムとして設計され、視聴者数が1万人を超える大規模なイベントにも対応できます。 また、複数のアングルを同時に配信できるマルチアングル機能により、視聴者が好みの映像を選択して楽しむこともできます。 2021年7月のサービス開始以降、スポーツ・エンタメ・オークション・金融など、様々な業界でご活用いただいています。 Smart vLiveのシステム構成 Smart vLiveは2段構成のシステムとなっています。 1段目のメディアサーバは、一般的なライブ配信サービスと同様にRTMPで映像を受信します。 メディアサーバは受信した映像をリアルタイムで処理しつつ、2段目のWebRTC配信サーバに送ります。 WebRTC配信サーバが視聴者との間でWebRTC通信を行い、映像を配信します。 多段構成を取ることで、視聴者が増加してもシステムをスケールできる構成となっています。 各コンポーネントでの映像処理では細かいチューニングを重ねて低遅延を追求しています。 ShowNetにおけるMedia over IPの取り組み 今回のShowNetでは、初めての取り組みとして Media over IP Pavilion が設置されました。 Media over IP Pavilionでは、映像と音声といったコンテンツをIPネットワークを使って伝えるための最新のソリューションが紹介されました。 IP中継車や本格的な映像関連の機器が展示され、多くの人が来場していました。 また、ShowNetのラックにもMedia over IP関連製品が展示され、これまでのShowNetでは見慣れないような製品に注目が集まりました。 ShowNetにおける展示内容 NTT Comは、Media over IP Pavilion内に設置された ShowNetスタジオ の模様を4Kでライブ配信するデモを行いました。 Smart vLiveでは1080pまでの解像度をサポートしていますが (2022年6月現在)、4K解像度で安定して低遅延ライブ配信を提供し続けられるか検証することが今回の課題でした。 今回の実験では、IP中継車から受け取ったShowNetスタジオの映像をエンコードし、ShowNetのネットワークを通じてクラウド上にあるSmart vLiveの配信エンドポイントにアップロードしています。 ShowNetスタジオでは、ShowNetに関わるエンジニアによる最新の技術に関するセッションが行われました。 セッションの内容はSmart vLiveを通じて配信され、会場の内外で視聴されました。 なかには、Interopのブース内でSmart vLiveを通じてセッションを視聴して簡易的な生中継のようにお使いいただくなど、低遅延を生かした新たな利用シーンも見つかりました。 この写真は、ShowNetスタジオのセッションをライブ配信している様子です。 写真の右側のモニタがIP中継車から受け取った映像、左側がSmart vLiveを通じて視聴している映像です。 4K解像度でも1秒未満の遅延でライブ配信できることが確認でき、来場者の方にも遅延の小ささを体感いただけました。 おわりに Interop 2022のShowNetにおける、Smart vLiveによる4K低遅延ライブ配信のデモについて紹介しました。 Media over IPの取り組みは初めてのことが多く準備期間中は様々な課題に直面しましたが、関係者の皆様のご尽力によって無事に会期を終えることができました。 今回のデモでは、Smart vLiveを用いて3日間の会期中安定して4K解像度でライブ配信を実施できました。 今後は、今回得た知見を活かしながらSmart vLiveのさらなる機能追加や品質向上に取り組んでいきます。 Smart vLiveの開発チームでは、新たなライブ配信・コミュニケーションのプラットフォーム開発を担当いただけるエンジニアを 募集しています 。 低遅延かつスケーラブルなライブ配信システムの開発は技術的にも多くの課題があり、エンジニアとして未知の領域を楽しみながら開発しています。 今回の記事を読んでご興味を持っていただけましたら、お気軽にお問合せいただければ幸いです。
はじめに こんにちは!初めまして、イノベーションセンターテクノロジー部門、Smart World 向けAI開発PJの藤原です。 我々は日々 Smart World の実現に向け、主に製造業向けのデータ分析、AIの研究開発を行なっております。 弊チームでは学会/論文投稿など積極的な学術活動も行なっており、IJCAI、KDD、CVPRなどのワークショップ採択されたり、最近ではAISTATSの本会議にも採択され ニュースリリース を出しました。 そうした学術活動の一環として、先日弊チームは技術調査 & 研究成果発表 & 企業紹介展示を目的に 2022年度人工知能学会全国大会 (JSAI2022) へ参加しました( 毎年弊チームではJSAIに数名が登壇しています! )。今年は藤原、木村、市川の3名(+インダストリアルセッションにて企業紹介1名)がそれぞれ発表を行ないました。今回はそれぞれの発表について、 因果探索編 、 異常検知編 、 データ拡張編 の三本立てでご紹介したいと思います! JSAI2022について 著名なAI関連国内学会の1つで、今回は京都で対面、リモートのハイブリッド開催でした。1つ特徴として、発表内容について募集要項が以下のようになっています。 論文の要件:論文該当分野に示される人工知能およびその関連分野の学術論文,事例報告 このように新規手法だけでなく事例報告も推奨されている点があり、企業としては特に参考になるような実施例も数多く聴講できました。NTT Com 以外にも数多くの企業が参加しており、企業ブースなども大盛り上がりでした。 今回の発表内容 では今回JSAI2022で発表した Just-In-Timeモデルを利用した非定常非線形データに対する因果探索 について、内容をざっくりと説明したいと思います! 背景 ビジネスにおけるXAIの重要性 さて前述のように我々は日々製造業向けにAIソリューションの提供や技術開発を行なっているのですが、その典型的な実施例の1つが以下のようなものになります(詳しくは、 AIプラント運転支援ソリューション を参照)。企業様から工場のセンサーデータなどを受け取り、以下のようにAIでプラント操作量の推奨値を提示するものになります。ですが、 一般的にAIはその判断根拠などがブラックボックスになりがちで、ビジネスではそこが課題となります。 一般的に、AIに対してそういった解釈性、説明性を持たせる試みを XAI (explainable AI) と言います。 因果探索とは また、特に弊チームが注力している分野に 因果 があります。因果の考え方では、相関や予測に対する寄与度を見るだけでなくデータから変数間の因果関係を求めたり、求まった因果を利用して、他の変数を固定した上である変数を動かしたときに(≒介入)どれだけの効果が生まれるか(≒因果効果)などを知ることができます。例えばプラントデータなら因果関係に基づき、生産量やCO2排出量に対してのある操作量の最適化であったり、AI判断根拠のより強力な解釈と、変数選択に基づく精度改善などを行うことができます。 今回の発表はタイトルにあるように、データのみからこのような因果関係を求める、因果探索に関する新規手法の提案になります。 実時系列データ解析の困難 注力分野のもう1つに、 時系列データ解析 があります。先述のプラントの例などで得られるデータというのは基本的に(連続)時系列データが多いです。時系列データとは、時間的な順序が意味を持つデータになります。 現実の時系列データ解析ではしばしば一般的な手法で前提とされる理想的な仮定などが崩れていて、安定した解析やAIモデルの作成が困難になります。 例えば、時々刻々システムの状態が変化している非定常性や、変数間の因果関係が簡単に記述できない非線形なものである場合があります。具体的に応用における1つ有名な困難の例を挙げると、 Concept Drift といって、学習時のデータ分布と予測時のデータ分布が異なってしまうことによる精度低下の問題などが知られています。 研究の目的 以上から、本研究の目的は以下のようになります。 背景 ビジネスなどの応用上でXAIが重要になっている。さらには踏み込んで、因果探索が重要。 課題 一方で、応用上で現れる実時系列データでは、因果探索、時系列解析における理想的な条件が崩れている。 目的 実時系列データで頻出の非定常性、非線形性、分布シフトなどに対して頑健な(≒強い/安定な)因果探索手法を開発する。 提案手法 このような非常に複雑な現実の時系列データに対して、因果関係をデータのみから読み取ることを考えます。 キーとなるアイデアは、 Just-In-Timeモデル [ Stenman 96-99 ][ 山本 13 ]と呼ばれる既存のフレームワークの応用です。 Just-In-Timeモデル Just-In-Timeモデルでは、出力を知りたい新しい入力データが来る度に、その近傍データを検索します。 検索した結果得られた近傍データセットを用いてAIモデルを学習し、入力データをそこに入力して、出力を逐次で得ます。これは、全てのデータを使い一度だけ学習したAIモデルを使い回して入力を入れて出力を得る通常のモデルとは異なるアプローチです。 近傍探索を行うことで近い状態のデータが集まることを期待でき、先ほど述べたような分布シフトの問題などに対応できると期待できます。 また、非線形の関数も入力の近傍を取ることで線形関数という単純&解釈が容易なモデルで近似できます(参考: テイラーの定理 )。 要するに、 複雑な現象も局所的に切り出せば単純な現象でモデル化できるはず 、といった感じです。 JIT-LiNGAM 因果探索には非線形の手法もいくつか知られていますが、計算量や解釈性の観点で依然問題があります。また、実時系列データの解析で起こる前述の問題(非定常性、非線形性、分布シフトなど)に対して安定性があるかは保証されません。 そこで、 上述のJust-In-Timeモデルを因果探索に応用して、比較的単純な線形の因果探索手法を使った局所的に線形、大域的に非線形な因果探索手法を考えます 。線形の因果探索手法の代表として、 LiNGAM [ Shimizu 06 ][ Shimizu 11 ]を利用します。Just-In-Timeモデルの局所線形近似を使うことで、Additive Noise Model (ANM) [ Hoyer 08 ]と呼ばれる、ノイズが加法的な広い非線形のクラスに対して、LiNGAMのアルゴリズムで解くことができるようになることを定式化により示せます(論文参照)。アルゴリズムの概要は以下の通りです。 実験 では実際にこの手法で人工データに対して実験した例を見てみましょう。今回はまず初期段階として簡単な非時系列/非線形モデルで実験します(図1)。比較対象としては因果探索アルゴリズムはLiNGAMで、観測された全てのサンプルを使うもの(Cumulative ALL)、観測されたサンプルからK個ランダムに使うもの(K-Random)が用意されています。 提案手法は距離指標や近傍選択の方法によって4パターン試しました。距離指標はユークリッド距離とマハラノビス距離のもので2種。 近傍選択方法は、入力データに対して近い順にK個のデータを抽出する方法(KNN)、入力データを中心に半径 以内にあるデータからランダムにK個抽出する方法(ERN)の2種。2種×2種で4パターンになります。 図2においてSHDという因果グラフに対する距離指標を用いて確認したところ、提案手法群の優位性を確認できます。 考察 近傍の取り方によって一定の傾向が見られたので、こちらについて考察していきます。 距離指標は観測が増えるごとにKNNだと増加傾向、ERNだと減少傾向になっています(低いほど精度が高い)。 JIT本来の期待では、観測データが増えるほど似通った状態を見つけやすくなったり、より詳細な局所モデルを作れるようになるため精度向上が期待されます。ですが、KNNでは実際にはそうなっていません。 KNNで精度が悪化している原因として、データ密度の増加による近傍半径の縮小が原因と考察します。 まず図3を見るとERNでは半径 を設定した影響で、近傍で取ってきたデータの標準偏差がほぼ一定値なのが分かります。一方で、KNNは標準偏差(≒半径)が観測増加と共に縮小している様子が確認できます。 次にこれがどのように因果探索精度に悪影響を及ぼすか考察します。図3の紫色の線が、0.05で、データ生成の際に加えたラプラスノイズのスケールを目安として表示したものになっています。KNNは半径が縮小し、最終的にはこの0.05を下回るほどにまでなっていますが、これが影響していると考えます。因果を識別するためには、変数 の内在的な揺らぎが因果関係 によって拡大され、これによって発生した揺らぎ を捉えることが必要になります。しかし、これを近傍探索によってノイズのスケールよりも小さい範囲でカットしてしまうと、観測ノイズによる揺らぎ と因果関係による揺らぎ を識別することが困難になり、因果探索アルゴリズムの精度が減少すると考えられます。今回はこのような考察を行いましたが、この点については引き続き数理的な観点を含めた検討が必要です。 まとめ 今回の発表ではJust-In-Timeモデルの近傍探索と線形の因果探索手法LiNGAMを組み合わせることで、非定常性、非線形性、分布シフトといった、現実のデータがしばしば抱える困難に対して安定性のある因果探索手法、JIT-LiNGAMを提案しました。また提案手法について、基礎的な実験を行い、従来のLiNGAMに対しての優位性を示すと共に、有効な近傍探索方法への示唆を得ました。 今後の発展としては、時系列モデルへの拡張、他の非線形因果探索モデルとの比較実験、近傍探索方法が精度に与える影響についてより数理的な考察などを行っていく予定です。 おわりに 以上が発表内容になります!今回は数式を抑え目に、あくまで何が課題で、何ができるのかと、その実現アルゴリズムの最低限の部分を重視してお伝えしました。JIT-LiNGAMの詳細や定式化については、 今回の論文 や、 参考文献 をご覧になって下さい。 宣伝 Node-AI/データ分析コンサルティング 私の所属するイノベーションセンターテクノロジー部門 Smart World向けAI開発PJ では、日々AI/データ分析に関するアルゴリズムなどの研究開発をしています。これらの技術は同じくイノベーションセンターテクノロジー部門の Node-AI PJ によって内製開発されているノンコーディングAIモデル開発ツール、 Node-AI (本ブログでの解説記事は こちら ) に搭載されたり、同じく データ分析コンサルティングPJ と共同で、データ分析案件にてお客様に提供されています。 特に、Node-AIにはまだ今回の発表のアルゴリズムは乗っていませんが、基本的な時系列因果分析機能が搭載されており、今回の例のようにデータ間の因果関係を求めグラフ上に可視化できます。またこれは時系列因果モデルなので、時間遅れを考慮した変数間の因果関係を知ることができ、「センサーAの値が5分遅れでセンサーBに効いている」といった解析が可能です! 弊チームでは、企業様と共同での今回のような因果分析を含め主に時系列データ解析/予測/異常検知/最適化を対象としたPoC、各種究機関との共同研究案件、Node-AIのご契約を募集中ですので、ご興味があればこちらまでご連絡ください!(メール:ai-deep-ic[at]ntt.com) 採用 弊チームではリサーチャー、MLエンジニアを募集中です。今回のようにメンバーの学会発表も推奨され、前述のようなチーム連携により開発アルゴリズムが即座にビジネスサイドで検証、社会実装される素晴らしい環境です。ぜひご応募下さい。採用情報については こちら からよろしくお願いします。(新卒入社をお考えの場合は新卒採用情報をご覧下さい) 参考文献 [今回の論文] 藤原 大悟, 小山 和輝, 切通 恵介, 大川内 智海, 泉谷 知範, 浅原 啓輔, 清水 昌平: Just-In-Timeモデルを利用した非定常非線形時系列データに対する因果探, 人工知能学会全国大会論文集, Vol. JSAI2022, pp. 3E4GS205-3E4GS205, (2022), 論文リンク(フリー) [Stenman 96-99] Stenman, A., Gustafsson, F., Ljung, L.: Just in time models for dynamical systems, Proceedings of 35th IEEE Conference on Decision and Control, Vol. 1, pp. 1115--1120 (1996) など [山本 13] 山本茂:Just-In-Time 予測制御: 蓄積データに基 づく予測制御, 計測と制御, Vol. 52, No. 10, pp. 878–884 (2013) [Shimizu 06] Shimizu, S., Hoyer, P. O., Hyv¨arinen, A., Kerminen, A., and Jordan, M.: A linear non-Gaussian acyclic model for causal discovery., Journal of Machine Learning Research, Vol. 7, No. 10 (2006) [Shimizu 11] Shimizu, S., Inazumi, T., Sogawa, Y., Hyv¨arinen, A., Kawahara, Y., Washio, T., Hoyer, P. O., and Bollen, K.: DirectLiNGAM: A direct method for learning a linear non-Gaussian structural equation model, The Journal of Machine Learning Research,、 Vol. 12, pp. 1225–1248 (2011) [Hoyer 08] Hoyer, P., Janzing, D., Mooij, J. M., Peters, J., and Sch¨olkopf, B.: Nonlinear causal discovery with additive noise models, Advances in neural information processing systems, Vol. 21, pp. 689–696 (2008)
サマリ Single-AS で構成されるネットワークにおいて、 SR-MPLS + VPNv4 + explicit path による Traffic Engineering (TE) を実現 IOS XR + Junos の Multi-vendor 環境で動作検証に成功 この記事は Multi-AS Segment Routing 検証連載の第 4 回です。目次は こちら 概要 イノベーションセンターの竹中です。普段の業務では Multi-AS Segment Routing に関する技術検証や、ネットワーク運用効率化のためのコントローラー開発などを行なっています。 本記事では Single-AS において SR-MPLS を用いた TE についてご紹介します。 第 0 回の記事 でもご紹介した通り、我々は Multi-AS/Multi-domain SR-MPLS における VPN と TE の実現手法を検証しています。 第 1 回の記事 、 第 2 回の記事 、 第 3 回の記事 を通して、 Multi-AS/Multi-domain SR-MPLS における L3VPN、L2VPN について説明、検証しました。 本記事では SR を用いた TE 手法についてご紹介する上で、まずは TE に必要となる SR Policy について説明します。 その後に 第 1 回の記事 で作成した Single-AS アンダーレイでの Color-Based Steering の手法を、サンプルトポロジーや設定例を添えながら紹介します。 Multi-AS での TE は第 5 回の記事にて紹介する予定です。 SR Policy SR Policy とは SR Policy 1 とは、SR で構成されたネットワークにおいて、運用者の意図に基づいたソースルーティングを行うためポリシーであり、下記の3つの要素で構成されます。 SR domain の Tailend である endpoint TE 経路を指定する Segment List ポリシーと経路を紐づける識別子である color SR Policy の適用方法はいくつか存在しますが、本記事においては MP-BGP で広告された VPN 経路に対して SR Policy を適用する流れを紹介します。 VPN 経路への SR Policy の適用方法としては、 SR Policy を適用対象の color と endpoint を指定して定義しておき、 BGP で受け取った経路のうち Color Extended Community と nexthop の組み合わせが一致する経路に適用する方法があります。 BGP の color はネットワークの運用意図の伝達として用いることができ、同じ endpoint に対して color ごとに異なる SR Policy を定義可能です。このように、経路に付加された color に応じて SR Policy を適用する TE 手法を Color-Based Steering と呼びます。 本記事においては color と endpoint を指定して SR Policy を適用しますが、color のみを指定して endpoint を動的に評価し SR Policy をインスタンス化する On-Demand Next-Hop (ODN) と呼ばれる技術も存在します。ただし、この技術は本記事の検証トポロジーでは使わないため、後続の記事にて紹介・検証します。 explicit path と dynamic path SR Policy の持つ経路は、その生成手法によって explicit path と dynamic path に分類されます。 explicit path は、Segment List をあらかじめ設定し、静的に経路を指定します。 一方で dynamic path は制約に基づいた Segment List の計算方法を表現するもので、SR Policy の設定時やトポロジー、メトリックなどの状態変更時に Segment List を動的に計算します。 本記事においては、TE の実現方法として explcit path を用いた検証を紹介します。 dynamic path を用いた検証例は後続の記事をご参照ください。また、我々の dynamic path に関する取り組みについては NTT Tech Conference 2022 や ENOG74 でも発表しています。 color の仕組みと利用例 BGP で経路を export または import する際に Color Extended Community を経路に付加することで、その経路への SR Policy 適用の可否、ならびに適用する SR Policy の決定が可能です。 BGP 経路に対する Color Extended Community の付加範囲は様々です。特定のネットワークの経路に対して付加することで当該ネットワークのみ挙動を変えたり、特定の Route-Distinguisher (RD) や Route-Target (RT) を持つ経路情報に対して付加することで当該 VRF 全体の挙動を変えたりできます。 上記の図は Color-Based Steering の模式図で、Single-AS の SR-MPLS において VPN 経路ごとの TE を実装する様子を表しています。なお、TE で利用するラベルのみ図中に明記し、各顧客を示す VPN ラベルは省略しています。 SR Policy に従った TE までの手順は以下の通りです。 ingress PE (PE1) にて SR Policy を設定する egress PE (PE2) で IBGP による Customer A、B の経路を IBGP で広告する 広告の際に各 VRF の RD ごとに Color Extended Community を付加する IBGP によって広告された経路を PE1 で受け取り、各 VRF table に import する import の際に経路に対して color、endpoint の一致する SR Policy を適用する SR Policy を適用した経路を参照するパケットを各 Customer から受け取った時に、SR Policy に従った Segment List を付与し SR-MPLS で構成されるコアネットワークに送出する 次章の検証例では、上記手順に沿った検証を紹介します。 検証例の紹介 下記のトポロジーを利用し、 Color-Based Steering の検証例を紹介します。 また、L3VPN 環境準備までの設定例は こちら を参照ください。 以降は Color-Based Steering にて Customer ごとに下図の TE を行うための設定と確認方法を記載します。ingress PE には SR Policy の設定、egress PE には VPN 経路に Color Extended Community を付加する設定を入れます。 Customer A の TE Customer B の TE SR Policy の設定と確認 まず SR Policy を設定します。explicit path を利用する場合は Segment List の設定も必要です。 PE1 (IOS XR) 側 segment-routing traffic-eng segment-list vrf-100-segment-list index 10 mpls label 16002 index 20 mpls label 16004 index 30 mpls label 16003 ! segment-list vrf-200-segment-list index 10 mpls label 16004 index 20 mpls label 16002 index 30 mpls label 16003 ! policy vrf-100-policy color 100 end-point ipv4 10.255.0.3 candidate-paths preference 50 explicit segment-list vrf-100-segment-list ! ! ! ! policy vrf-200-policy color 200 end-point ipv4 10.255.0.3 candidate-paths preference 50 explicit segment-list vrf-200-segment-list ! ! ! ! ! ! データベースに SR Policy が追加されたことを確認します。 RP/0/RP0/CPU0:PE1#show segment-routing traffic-eng policy Tue Jul 12 11:38:51.837 JST SR-TE policy database --------------------- Color: 100, End-point: 10.255.0.3 Name: srte_c_100_ep_10.255.0.3 Status: Admin: up Operational: up for 00:05:04 (since Jul 12 11:33:47.109) Candidate-paths: Preference: 50 (configuration) (active) Name: vrf-100-policy Requested BSID: dynamic Protection Type: protected-preferred Maximum SID Depth: 10 Explicit: segment-list vrf-100-segment-list (valid) Weight: 1, Metric Type: TE 16002 16004 16003 Attributes: Binding SID: 24008 Forward Class: Not Configured Steering labeled-services disabled: no Steering BGP disabled: no IPv6 caps enable: yes Invalidation drop enabled: no Color: 200, End-point: 10.255.0.3 Name: srte_c_200_ep_10.255.0.3 Status: Admin: up Operational: up for 00:00:32 (since Jul 12 11:38:19.552) Candidate-paths: Preference: 50 (configuration) (active) Name: vrf-200-policy Requested BSID: dynamic Protection Type: protected-preferred Maximum SID Depth: 10 Explicit: segment-list vrf-200-segment-list (valid) Weight: 1, Metric Type: TE 16004 16002 16003 Attributes: Binding SID: 24011 Forward Class: Not Configured Steering labeled-services disabled: no Steering BGP disabled: no IPv6 caps enable: yes Invalidation drop enabled: no PE2 (Junos) 側 set protocols source-packet-routing segment-list vrf-100-segment-list P2 label 16004 set protocols source-packet-routing segment-list vrf-100-segment-list P1 label 16002 set protocols source-packet-routing segment-list vrf-100-segment-list PE1 label 16001 set protocols source-packet-routing source-routing-path vrf-100-policy to 10.255.0.1 set protocols source-packet-routing source-routing-path vrf-100-policy color 100 set protocols source-packet-routing source-routing-path vrf-100-policy primary vrf-100-segment-list set protocols source-packet-routing segment-list vrf-200-segment-list P1 label 16002 set protocols source-packet-routing segment-list vrf-200-segment-list P2 label 16004 set protocols source-packet-routing segment-list vrf-200-segment-list PE1 label 16001 set protocols source-packet-routing source-routing-path vrf-200-policy to 10.255.0.1 set protocols source-packet-routing source-routing-path vrf-200-policy color 200 set protocols source-packet-routing source-routing-path vrf-200-policy primary vrf-200-segment-list データベースに SR Policy が追加されたことを確認します。 user@PE2> show spring-traffic-engineering lsp name vrf-100-policy detail Name: vrf-100-policy Tunnel-source: Static configuration Tunnel Forward Type: SRMPLS To: 10.255.0.1-100<c> State: Up Path: vrf-100-segment-list Path Status: NA Outgoing interface: NA Auto-translate status: Disabled Auto-translate result: N/A Compute Status:Disabled , Compute Result:N/A , Compute-Profile Name:N/A BFD status: N/A BFD name: N/A Segment ID : 128 ERO Valid: true SR-ERO hop count: 3 Hop 1 (Strict): NAI: None SID type: 20-bit label, Value: 16004 Hop 2 (Strict): NAI: None SID type: 20-bit label, Value: 16002 Hop 3 (Strict): NAI: None SID type: 20-bit label, Value: 16001 Total displayed LSPs: 1 (Up: 1, Down: 0) user@PE2> show spring-traffic-engineering lsp name vrf-200-policy detail Name: vrf-200-policy Tunnel-source: Static configuration Tunnel Forward Type: SRMPLS To: 10.255.0.1-200<c> State: Up Path: vrf-200-segment-list Path Status: NA Outgoing interface: NA Auto-translate status: Disabled Auto-translate result: N/A Compute Status:Disabled , Compute Result:N/A , Compute-Profile Name:N/A BFD status: N/A BFD name: N/A Segment ID : 128 ERO Valid: true SR-ERO hop count: 3 Hop 1 (Strict): NAI: None SID type: 20-bit label, Value: 16002 Hop 2 (Strict): NAI: None SID type: 20-bit label, Value: 16004 Hop 3 (Strict): NAI: None SID type: 20-bit label, Value: 16001 Total displayed LSPs: 1 (Up: 1, Down: 0) BGP Color Extended Community 付加の設定と確認 BGP で広告する経路に color を付加します。 PE1 (IOS XR) 側 vrf 100 address-family ipv4 unicast export route-policy add-color-100 ! ! vrf 200 address-family ipv4 unicast export route-policy add-color-200 ! ! extcommunity-set opaque color-100 100 end-set ! extcommunity-set opaque color-200 200 end-set ! route-policy add-color-100 set extcommunity color color-100 end-policy ! route-policy add-color-200 set extcommunity color color-200 end-policy 対向の PE2 にて、受け取った経路に color がついていることを確認します。 user@PE2> show route table bgp.l3vpn.0 detail bgp.l3vpn.0: 2 destinations, 2 routes (2 active, 0 holddown, 0 hidden) 65000:100:192.168.0.0/24 (1 entry, 0 announced) *BGP Preference: 170/-101 Route Distinguisher: 65000:100 Next hop type: Indirect, Next hop index: 0 Address: 0x704dfc4 Next-hop reference count: 3 Source: 10.255.0.1 Next hop type: Router, Next hop index: 0 Next hop: 10.0.2.1 via ge-0/0/0.0 Label operation: Push 24004, Push 16001(top) Label TTL action: prop-ttl, prop-ttl(top) Load balance label: Label 24004: None; Label 16001: None; Label element ptr: 0x780fa88 Label parent element ptr: 0x782ea28 Label element references: 4 Label element child references: 0 Label element lsp id: 0 Session Id: 0x0 Next hop: 10.0.3.1 via ge-0/0/1.0, selected Label operation: Push 24004, Push 16001(top) Label TTL action: prop-ttl, prop-ttl(top) Load balance label: Label 24004: None; Label 16001: None; Label element ptr: 0x780fa88 Label parent element ptr: 0x782ea28 Label element references: 4 Label element child references: 0 Label element lsp id: 0 Session Id: 0x0 Protocol next hop: 10.255.0.1 Label operation: Push 24004 Label TTL action: prop-ttl Load balance label: Label 24004: None; Indirect next hop: 0x7198904 1048576 INH Session ID: 0x144 State: <Active Int Ext ProtectionPath ProtectionCand> Local AS: 65000 Peer AS: 65000 Age: 3:58 Metric: 0 Metric2: 30 Validation State: unverified ORR Generation-ID: 0 Task: BGP_65000.10.255.0.1 AS path: ? Communities: target:65000:100 color:0:100 Import Accepted VPN Label: 24004 Localpref: 100 Router ID: 10.255.0.1 Secondary Tables: 100.inet.0 Thread: junos-main 65000:200:192.168.0.0/24 (1 entry, 0 announced) *BGP Preference: 170/-101 Route Distinguisher: 65000:200 Next hop type: Indirect, Next hop index: 0 Address: 0x704e234 Next-hop reference count: 3 Source: 10.255.0.1 Next hop type: Router, Next hop index: 0 Next hop: 10.0.2.1 via ge-0/0/0.0, selected Label operation: Push 24005, Push 16001(top) Label TTL action: prop-ttl, prop-ttl(top) Load balance label: Label 24005: None; Label 16001: None; Label element ptr: 0x780fd30 Label parent element ptr: 0x782ea28 Label element references: 4 Label element child references: 0 Label element lsp id: 0 Session Id: 0x0 Next hop: 10.0.3.1 via ge-0/0/1.0 Label operation: Push 24005, Push 16001(top) Label TTL action: prop-ttl, prop-ttl(top) Load balance label: Label 24005: None; Label 16001: None; Label element ptr: 0x780fd30 Label parent element ptr: 0x782ea28 Label element references: 4 Label element child references: 0 Label element lsp id: 0 Session Id: 0x0 Protocol next hop: 10.255.0.1 Label operation: Push 24005 Label TTL action: prop-ttl Load balance label: Label 24005: None; Indirect next hop: 0x7198a84 1048577 INH Session ID: 0x144 State: <Active Int Ext ProtectionPath ProtectionCand> Local AS: 65000 Peer AS: 65000 Age: 2:42 Metric: 0 Metric2: 30 Validation State: unverified ORR Generation-ID: 0 Task: BGP_65000.10.255.0.1 AS path: ? Communities: target:65000:200 color:0:200 Import Accepted VPN Label: 24005 Localpref: 100 Router ID: 10.255.0.1 Secondary Tables: 200.inet.0 Thread: junos-main PE2 (Junos) 側 ※ Junos のポリシーは accept の後のタームが実行されないため順序にご注意ください set policy-options community COLOR-100 members color:0:100 set policy-options community COLOR-200 members color:0:200 delete policy-options policy-statement EXPORT-POLICY-100 set policy-options policy-statement EXPORT-POLICY-100 term ROUTE-TARGET then community add VRF100-65000-RT set policy-options policy-statement EXPORT-POLICY-100 term ADD-COLOR-100 then community add COLOR-100 set policy-options policy-statement EXPORT-POLICY-100 term REDIST-DIRECT from protocol direct set policy-options policy-statement EXPORT-POLICY-100 term REDIST-DIRECT then accept set routing-instances 100 vrf-export EXPORT-POLICY-100 delete policy-options policy-statement EXPORT-POLICY-200 set policy-options policy-statement EXPORT-POLICY-200 term ROUTE-TARGET then community add VRF200-65000-RT set policy-options policy-statement EXPORT-POLICY-200 term ADD-COLOR-200 then community add COLOR-200 set policy-options policy-statement EXPORT-POLICY-200 term REDIST-DIRECT from protocol direct set policy-options policy-statement EXPORT-POLICY-200 term REDIST-DIRECT then accept set routing-instances 200 vrf-export EXPORT-POLICY-200 対向の PE1 にて、受け取った経路に color がついていることを確認します。 RP/0/RP0/CPU0:PE1#show bgp vpnv4 unicast vrf 100 192.168.1.0/24 Tue Jul 12 15:24:30.125 JST BGP routing table entry for 192.168.1.0/24, Route Distinguisher: 65000:100 Versions: Process bRIB/RIB SendTblVer Speaker 28 28 Last Modified: Jul 12 15:01:55.551 for 00:22:34 Paths: (1 available, best #1) Not advertised to any peer Path #1: Received by speaker 0 Not advertised to any peer Local 10.255.0.3 C:100 (bsid:24008) (metric 20) from 10.255.0.3 (10.255.0.3) Received Label 20 Origin IGP, localpref 100, valid, internal, best, group-best, import-candidate, imported Received Path ID 0, Local Path ID 1, version 28 Extended community: Color:100 RT:65000:100 SR policy color 100, up, not-registered, bsid 24008 Source AFI: VPNv4 Unicast, Source VRF: 100, Source Route Distinguisher: 65000:100 RP/0/RP0/CPU0:PE1#show bgp vpnv4 unicast vrf 200 192.168.1.0/24 Tue Jul 12 15:24:46.645 JST BGP routing table entry for 192.168.1.0/24, Route Distinguisher: 65000:200 Versions: Process bRIB/RIB SendTblVer Speaker 29 29 Last Modified: Jul 12 15:01:55.551 for 00:22:51 Paths: (1 available, best #1) Not advertised to any peer Path #1: Received by speaker 0 Not advertised to any peer Local 10.255.0.3 C:200 (bsid:24011) (metric 20) from 10.255.0.3 (10.255.0.3) Received Label 21 Origin IGP, localpref 100, valid, internal, best, group-best, import-candidate, imported Received Path ID 0, Local Path ID 1, version 29 Extended community: Color:200 RT:65000:200 SR policy color 200, up, not-registered, bsid 24011 Source AFI: VPNv4 Unicast, Source VRF: 200, Source Route Distinguisher: 65000:200 SR Policy が適用された経路の確認 まず PE1 から PE2 の経路について確認します。 PE1 にて、対向の Customer ネットワークへの経路の nexthop が SR Policy の処理へ転送するためのローカルラベルとなっていることが確認できます。 RP/0/RP0/CPU0:PE1#sho cef vrf 100 Tue Jul 12 11:14:45.051 JST Prefix Next Hop Interface ------------------- ------------------- ------------------ 0.0.0.0/0 drop default handler 0.0.0.0/32 broadcast 192.168.0.0/24 attached GigabitEthernet0/0/0/2 192.168.0.0/32 broadcast GigabitEthernet0/0/0/2 192.168.0.254/32 receive GigabitEthernet0/0/0/2 192.168.0.255/32 broadcast GigabitEthernet0/0/0/2 192.168.1.0/24 24008 (via-label) <recursive> 224.0.0.0/4 0.0.0.0/32 224.0.0.0/24 receive 255.255.255.255/32 broadcast RP/0/RP0/CPU0:PE1#sho cef vrf 200 Tue Jul 12 11:16:24.775 JST Prefix Next Hop Interface ------------------- ------------------- ------------------ 0.0.0.0/0 drop default handler 0.0.0.0/32 broadcast 192.168.0.0/24 attached GigabitEthernet0/0/0/3 192.168.0.0/32 broadcast GigabitEthernet0/0/0/3 192.168.0.254/32 receive GigabitEthernet0/0/0/3 192.168.0.255/32 broadcast GigabitEthernet0/0/0/3 192.168.1.0/24 24011 (via-label) <recursive> 224.0.0.0/4 0.0.0.0/32 224.0.0.0/24 receive 255.255.255.255/32 broadcast ローカルラベル処理は以下で確認できます。 RP/0/RP0/CPU0:PE1#show mpls forwarding labels 24008 detail Tue Jul 12 11:18:35.029 JST Local Outgoing Prefix Outgoing Next Hop Bytes Label Label or ID Interface Switched ------ ----------- ------------------ ------------ --------------- ------------ 24008 Pop No ID srte_c_100_e point2point 0 Updated: Jul 12 07:43:42.409 Version: 18, Priority: 2 Label Stack (Top -> Bottom): { Unlabelled Imp-Null } NHID: 0x0, Encap-ID: N/A, Path idx: 0, Backup path idx: 0, Weight: 0 MAC/Encaps: 0/0, MTU: 0 Outgoing Interface: srte_c_100_ep_10.255.0.3 (ifhandle 0x00000014) Packets Switched: 0 RP/0/RP0/CPU0:PE1#show mpls forwarding labels 24011 detail Tue Jul 12 11:17:27.943 JST Local Outgoing Prefix Outgoing Next Hop Bytes Label Label or ID Interface Switched ------ ----------- ------------------ ------------ --------------- ------------ 24011 Pop No ID srte_c_200_e point2point 0 Updated: Jul 12 07:43:37.156 Version: 14, Priority: 2 Label Stack (Top -> Bottom): { Unlabelled Imp-Null } NHID: 0x0, Encap-ID: N/A, Path idx: 0, Backup path idx: 0, Weight: 0 MAC/Encaps: 0/0, MTU: 0 Outgoing Interface: srte_c_200_ep_10.255.0.3 (ifhandle 0x0000001c) Packets Switched: 0 次に PE2 から PE1 の経路について確認します。 PE2 にて VRF routing table を確認したところ、SR Policy が適用された経路となっていないことが確認できます。 user@PE2> show route table 100.inet.0 100.inet.0: 3 destinations, 3 routes (3 active, 0 holddown, 0 hidden) + = Active Route, - = Last Active, * = Both 192.168.0.0/24 *[BGP/170] 00:00:14, MED 0, localpref 100, from 10.255.0.1 AS path: ?, validation-state: unverified to 10.0.2.1 via ge-0/0/0.0, Push 24012, Push 16001(top) > to 10.0.3.1 via ge-0/0/1.0, Push 24012, Push 16001(top) 192.168.1.0/24 *[Direct/0] 00:37:16 > via ge-0/0/2.0 192.168.1.254/32 *[Local/0] 00:37:16 Local via ge-0/0/2.0 user@PE2> show route table 200.inet.0 200.inet.0: 3 destinations, 3 routes (3 active, 0 holddown, 0 hidden) + = Active Route, - = Last Active, * = Both 192.168.0.0/24 *[BGP/170] 00:00:23, MED 0, localpref 100, from 10.255.0.1 AS path: ?, validation-state: unverified to 10.0.2.1 via ge-0/0/0.0, Push 24013, Push 16001(top) > to 10.0.3.1 via ge-0/0/1.0, Push 24013, Push 16001(top) 192.168.1.0/24 *[Direct/0] 00:37:25 > via ge-0/0/3.0 192.168.1.254/32 *[Local/0] 00:37:25 Local via ge-0/0/3.0 Junos は BGP 経路の nexthop 解決をする際にデフォルトで color を考慮しません。 次の設定を追加することで color を考慮して nexthop を解決するようになります。 set policy-options policy-statement BGP-IMPORT-POLICY term VRF-100 from community VRF100-65000-RT set policy-options policy-statement BGP-IMPORT-POLICY term VRF-100 then accept set policy-options policy-statement BGP-IMPORT-POLICY term VRF-100 then resolution-map map1 set policy-options policy-statement BGP-IMPORT-POLICY term VRF-200 from community VRF200-65000-RT set policy-options policy-statement BGP-IMPORT-POLICY term VRF-200 then accept set policy-options policy-statement BGP-IMPORT-POLICY term VRF-200 then resolution-map map1 set policy-options resolution-map map1 mode ip-color set protocols bgp group ibgp import BGP-IMPORT-POLICY 改めて PE2 にて、 SR policy が適用された経路が採用されていることが確認できます。 user@PE2> show route table 100.inet.0 100.inet.0: 3 destinations, 3 routes (3 active, 0 holddown, 0 hidden) + = Active Route, - = Last Active, * = Both 192.168.0.0/24 *[BGP/170] 02:45:08, MED 0, localpref 100, from 10.255.0.1 AS path: ?, validation-state: unverified > to 10.0.3.1 via ge-0/0/1.0, Push 24012, Push 16001, Push 16002(top) 192.168.1.0/24 *[Direct/0] 03:27:59 > via ge-0/0/2.0 192.168.1.254/32 *[Local/0] 03:27:59 Local via ge-0/0/2.0 user@PE2> show route table 200.inet.0 200.inet.0: 3 destinations, 3 routes (3 active, 0 holddown, 0 hidden) + = Active Route, - = Last Active, * = Both 192.168.0.0/24 *[BGP/170] 02:45:23, MED 0, localpref 100, from 10.255.0.1 AS path: ?, validation-state: unverified > to 10.0.2.1 via ge-0/0/0.0, Push 24013, Push 16001, Push 16004(top) 192.168.1.0/24 *[Direct/0] 03:28:14 > via ge-0/0/3.0 192.168.1.254/32 *[Local/0] 03:28:14 Local via ge-0/0/3.0 疎通確認 PE1 と PE2 から VPN 経路の traceroute を行い、TE によって経路制御されていることを確認します。 PE1 → PE2 RP/0/RP0/CPU0:PE1#traceroute 192.168.1.254 vrf 100 Wed Jul 13 19:14:09.683 JST Type escape sequence to abort. Tracing the route to 192.168.1.254 1 10.0.0.2 [MPLS: Labels 16004/16003/16 Exp 0] 17 msec 3 msec 9 msec 2 10.0.4.2 [MPLS: Labels 16003/16 Exp 0] 3 msec 5 msec 12 msec 3 192.168.1.254 3 msec 10 msec 4 msec RP/0/RP0/CPU0:PE1#traceroute 192.168.1.254 vrf 200 Wed Jul 13 19:17:27.790 JST Type escape sequence to abort. Tracing the route to 192.168.1.254 1 10.0.1.2 [MPLS: Labels 16002/16003/17 Exp 0] 13 msec 13 msec 5 msec 2 10.0.4.1 [MPLS: Labels 16003/17 Exp 0] 12 msec 14 msec 5 msec 3 192.168.1.254 11 msec 15 msec 5 msec PE2 → PE1 user@PE2> traceroute 192.168.0.254 routing-instance 100 traceroute to 192.168.0.254 (192.168.0.254), 30 hops max, 52 byte packets 1 10.0.3.1 (10.0.3.1) 3.025 ms 2.434 ms 39.277 ms MPLS Label=16002 CoS=0 TTL=1 S=0 MPLS Label=16001 CoS=0 TTL=1 S=0 MPLS Label=24012 CoS=0 TTL=1 S=1 2 10.0.4.1 (10.0.4.1) 11.748 ms 2.363 ms 2.796 ms MPLS Label=16001 CoS=0 TTL=1 S=0 MPLS Label=24012 CoS=0 TTL=2 S=1 3 10.0.0.1 (10.0.0.1) 11.454 ms user@PE2> traceroute 192.168.0.254 routing-instance 200 traceroute to 192.168.0.254 (192.168.0.254), 30 hops max, 52 byte packets 1 10.0.2.1 (10.0.2.1) 3.372 ms 2.207 ms 2.184 ms MPLS Label=16004 CoS=0 TTL=1 S=0 MPLS Label=16001 CoS=0 TTL=1 S=0 MPLS Label=24013 CoS=0 TTL=1 S=1 2 10.0.4.2 (10.0.4.2) 2.474 ms 2.358 ms 2.206 ms MPLS Label=16001 CoS=0 TTL=1 S=0 MPLS Label=24013 CoS=0 TTL=2 S=1 3 10.0.1.1 (10.0.1.1) 11.859 ms このように、Single-AS SR-MPLS で構成されたネットワークでの TE が実現できました。 まとめ 本記事では、SR における TE の実現に利用される SR Policy について説明し、Multi-vendor 環境における Single-AS SR-MPLS での Color-Based Steering の検証結果を紹介しました。次回の記事では、Multi-AS における TE について紹介予定です。 (2022/08/08 追記) 公開しました: [Multi-AS Segment Routing 検証連載 #5] Traffic Engineering in Multi-AS Internet-Drafts として https://datatracker.ietf.org/doc/draft-ietf-spring-segment-routing-policy/ で議論中。 ↩
はじめに DevOpsプラットフォームの取り組みを紹介する3回目の記事です。 Qmonus Value Stream のアーキテクトの牧志 ( @JunMakishi ) です。 本記事では、Qmonus Value Streamの独自技術であるCloud Native Adapterを紹介します。はじめにInfrastructure as Codeの課題を指摘し、Cloud Native Adapterを使ってこれらの課題をどう解決するのかを解説します。 Infrastructure as Codeの課題 Infrastructuer as Code (以下IaC) は、特定のツールを指すのではなく、インフラストラクチャをコードで記述し、ソフトウェアと同じように取り扱うプラクティスを指します。インフラストラクチャのリソース構成や設定をコードで記述・適用することで、再現性、一貫性、および透明性のあるインフラストラクチャを得られます。分散されたコンポーネントを組み合わせてシステムを組み上げるクラウドアーキテクチャを運用する上で、その構成をコードで管理することは必要不可欠であると考えます。 Cloud Native Days Tokyo 2021の 発表 では、IaCを、「User Interface」「Workflow」「Test」および「Feedback Loop」の4つに分解して考察しました(図)。Qmonus Value Streamのコア技術であるCloud Native Adapterは、これらのうち User Interface と Workflow の課題を解決するための方式です。 User Interfaceの課題 IaCにおけるUser Interfaceは、インフラストラクチャのリソース構成と設定の期待値をコードで定義するための方式です。一般的に、JSON、YAML、または専用のDSLなどが利用されています。 試験環境と商用環境の間で一貫したインフラストラクチャ構成を得るためには、IaC実装のパラメータ化を進める必要があります。例えば、APIアプリケーションをデプロイする場合、ユーザに公開するAPIのFQDNをパラメータ化するようなIaC実装が求められます。試験環境と商用環境で想定する負荷が異なり、割り当てるCPUやメモリ量をパラメータ化することもあるでしょう。 ここで、Kustomize、Python+Jinjaなどのツール、またはDSL専用の記法を用いてパラメータ化することが考えられます。しかし、コードの規模が大きくなるほど、パラメータの管理が煩雑になります。インフラストラクチャは指定可能な設定値が多岐にわたるため、複数のユースケースに対応しようとするあまり、パラメータの数が多くなる傾向にあります。IaCを実践していて、各パラメータがインフラストラクチャのどの値を設定しているかを追いかけるのが難しいと感じたことがある方は少なくないでしょう。 また、ソフトウェア開発のプラクティスに従い、インフラストラクチャ設定をグループ化・抽象化していくことで、より規模の大きいインフラストラクチャを管理できます。しかし、YAMLなどの既存のインターフェースでは、リソース間の依存関係や構成全体の見通しが悪くなる傾向にあると考えます。例えば、繰り返し処理やまとまったコンテクストを1つのモジュールにまとめる、実装したコードを事前に検証する、といったソフトウェア開発プラクティスを適用することが難しいと考えています。 私たちは、 IaCがソフトウェアエンジニアにとって、読み書きしやすく、かつメンテナンスしやすいものである必要がある と考えます。IaCの「User Interface」として、ソフトウェアプラクティスを適用でき、スケーラブルにインフラストラクチャ構成を宣言できるデータ記述言語が求められます。インフラストラクチャのコンフィグ定義を扱うのに適したもので、見通しよくIaC実装をモジュール化やグループ化できる言語が必要です。 Workflowの課題 IaCにおけるWorkflowは、インフラストラクチャを設定するまでの一連の手続きを自動化するための方式です。例えば、Terraformは、列挙されたリソース間の依存関係を解決して、リソース設定を適用する順序を制御します。 様々なIaCツールがありますが、チームのDevOpsとソフトウェアアーキテクチャにもとづき、 IaCを実装してからツールを実行してインフラストラクチャに適用するまでの一連のワークフローを簡単に自動化する手段やデファクトスタンダードがまだないと考えています。チームごとに独自のスクリプトを用意し、多様なツールを組みあわせるためのグルーコードや、前述したパラメータを埋めるための煩雑な処理などを実装する必要があります。その高いメンテナンスコストは運用フェーズでチームに重くのしかかってきます。 再現性の高いインフラストラクチャ構成を得るためにIaCを実践したとしても、それを適用するスクリプト実装を信頼性高く保たない限り、変更に弱いインフラストラクチャとなってしまうリスクを孕んでいます。そのため、パラメータ設定ミスを誘発してしまわないよう、注意深くスクリプトを修正する必要がでてきます。例えば、Jenkinsとそこで動かすスクリプトをメンテナンスするために多大な労力を割いているチームも多いことでしょう。 私たちは、 システムアーキテクチャ設計の肝の一つは、継続的にデリバリする仕組みを作ること であり、 IaC実装においてそのワークフローを考慮するべき と考えます。 IaCの「Workflow」として、インフラストラクチャをデプロイするまでの一連のワークフローを簡単に自動化し、使いたいクラウド技術・ツールを、自チームのCI/CDパイプラインに苦痛なく組み入れられるような方式が求められます。 発展途上のエコシステム 近年、前述したIaCのワークフローの課題に取り組むソリューションが台頭してきています。 Terraform Cloud は、HashiCorp社が提供するSaaSです。「Plan, Policy Check, Apply」というTerraformを実行するまでの一連の流れをSaaSで管理することを可能とし、チームで一貫したプラクティスを適用可能としています。 Waypoint は、同様にHashiCorp社が開発しているオープンソースソフトウェアです。「Build, Deploy, Release」という開発からソフトウェアリリースまでのワークフローを連結するソリューションを提供しています。 Dagger は、Dockerの創始者であるSolomon Hykesらにより開発されているオープンソースソフトウェアです。ポータブルなCI/CDワークフローを記述・実行できるエンジンを提供し、チームサイロでメンテナンスしていたGlueコードを無くすことができます。 KubeVela は、Alibaba Cloudのチームが中心となって開発しているオープンソースソフトウェアです。プラットフォームチームが、複数のKubernetesリソースとそれらをデプロイするワークフローとをセットでパッケージングすることを可能としています。 それぞれ、既存ツールでは足りなかったワークフローの自動化を達成するためのソリューションを提供しています。詳細については、それぞれのWebサイトを参照ください。 このように、前述したIaCの課題は、エコシステムの中でも近年注目されている課題であるものの、まだまだデファクトスダンダードと呼べるソリューションが確立されていません。そこで、私たちは、これらの課題を解決するための独自の方式をQmonus Value Streamに実装し、提案しています。 私たちの提案:Cloud Native Adapter Qmonus Value Streamは、Cloud Native Adapterと呼ばれる独自のIaC実装を提案し「User Interface」と「Workflow」の課題を解決しています。そのアイデアは、 KubeCon EU 2020 で「Design Pattern as Code」という名称で紹介しています。 Cloud Native Adapterのコアとなるアプローチを以下に挙げます。 ワークフロー定義を含む統一的なインターフェース CUE言語を使って、クラウドアーキテクチャを構成する「インフラストラクチャ構成」とそれをデプロイする「ワークフロー」をまとめて1つのインターフェースで宣言する。 上記によって、インフラストラクチャを継続的にデリバリする「Workflow」を提供する。 コンフィグのモジュール化と結合 宣言的に記述された「インフラストラクチャ構成」と「ワークフロー」について、再利用可能な形に分割して、そのデータ構造を隠蔽せずに「結合」する。 1の統一的なインターフェースで上記の仕組みを提供することで、見通しの良いIaC実装のモジュール化を促進し、ソフトウェアエンジニアにとってメンテナンスしやすい「User Interface」を提供する。 以下、それぞれのアプローチを紹介します。 1. ワークフロー定義を含む統一的なインターフェース 以下のイメージ図で示すように、Cloud Native Adapterは、「インフラストラクチャの構成」と「ワークフロー」をCUE言語を使って宣言します。Qmonus Value Streamは、このCloud Native Adapterのうち、インフラストラクチャ構成を宣言する箇所を読み出し、インフラストラクチャに適用するマニフェストを生成します。また、ワークフローを宣言する箇所を読み出すことで、このマニフェストを適用するためのCI/CDパイプラインを生成します。 CUE言語は、型付データ記述言語であり、データ構造をシンプルに表現・操作できるだけでなく、モジュール化などといったソフトウェアプラクティスを適用できます。前述したDaggerやKubeVelaでもCUE言語を採用しています。CUE言語の詳細は、別の記事で紹介予定です。 以下に、Cloud Native Adapterの実装例として、APIアプリケーションを公開する「GKE API Adapter」を紹介します。 resources fieldにインフラストラクチャのリソース設定を宣言し、 pipelines fieldにCI/CDパイプライン定義を宣言します。この例では、GCPへ適用する publicIp/securityPolicy 設定とKubernetesへ適用する service/ingress 設定に加えて、CI/CDパイプライン中でKubernetesへの適用後に実行すべきAPIの正常性確認 testApi タスクを1つのAdapterとしてパッケージ化しています。GCPとKubernetesという異なるAPIだけでなく、CI/CDのロジックまで1つのインターフェースで記述できることが分かります。 package api DesignPattern: { name: "GKE API Adapter" // infrastructure configuration resources: { gcpPublicIp: ... gcpSeculicyPolicy: ... k8sService: ... k8sIngress: ... } // CI/CD operations pipelines: { // tasks in "deploy" stage deploy: { applyManifest: ... // a task to test API endpoint after deploying resources testApi: ... } } } 上記の例のようにInfrastructureとCI/CDパイプラインを1つのCloud Native Adapterとしてまとめるのではなく、それぞれ異なるAdapterとして実装することも可能です。第2回連載の 前編 では、GKEでAPIサービス公開するインフラストラクチャ構成を宣言したCloud Native Adapterを、また 後編 では、bookinfoをデプロイするワークフローを宣言したCloud Native Adapterを使ったデモを紹介しました。 マルチクラウドのインフラストラクチャ構成だけでなく、その構成をデプロイするワークフローまでを1つのCloud Native Adapterとしてまとめることで、継続的なデリバリ・自動化までを視野に入れたIaCを実装できます。CUE言語を書くだけで実践的なパイプラインとともにインフラストラクチャを拡張・成長させられるようなユーザ体験(Developer Experience)を提供したいと考えています。 さらに、CUE言語を活用して、宣言したインフラストラクチャ構成およびパイプライン定義におけるデータ構造を安全に評価・検証できます。このCUE言語の強みについては、別の記事で紹介します。 2. コンフィグのモジュール化と結合 Cloud Native Adapterは、宣言的に記述した「インフラストラクチャ構成」と「ワークフロー」をグループ化、モジュール化できます。以下のイメージ図に示すように、ユーザは、再利用できる単位にパッケージ化された複数のモジュールを選択して、1つのシステムアーキテクチャを組み上げることができます。 CUE言語は、宣言的に記述されたデータ構造を結合し一貫した結果を出力できることが特徴です。Cloud Native Adapterは、インフラストラクチャ構成とワークフロー定義について、そのデータ構造を過度に隠蔽・抽象化せずに、再利用出来る単位に分割できます。Qmonus Value Streamのユーザは、このCUE言語の特徴を活用し、分割された任意のCloud Native Adapterを結合することで、目的に沿ったインフラストラクチャ構成とワークフロー定義を得られます。 上記イメージ図で示したCloud Native Adapterを結合する実装例を以下に示します。前述した「GKE API Adapter」に加えて、Securityルールを適用するAdapterや、監視 (uptime check) 設定するAdapterを composites field中に宣言しています。Qmonus Value Streamは、これをコンパイルすることで、各Adapterがそれぞれ宣言しているリソース設定とCI/CDパイプライン定義を結合して、1つのシステムとCI/CDパイプラインを生成します。さらに、コード例の下部に示しているように、 resources fieldや pipelines fieldに独自のインフラストラクチャ構成ルールやワークフロータスクを宣言することで、カスタマイズを加えることができます。 DesignPattern: { // unify exisitng Cloud Native Adapter composites: [ { pattern: api.DesignPattern params: { ... } }, { pattern: containerSecurity.DesignPattern params: { ... } }, { pattern: uptimecheck.DesignPattern params: { ... } } ] // customize resources: { // application specific configuration k8sConfigmap: { metadata: name: "application-config" data: "example.conf" : "..." // mount the configuration k8sDeployment: spec: template: spec: volumes: [ { name: "config" configMap: name: "application-config" } ] } // application specific tasks pipelines: { ... } } Helm のように自己完結型のパッケージを作るアプローチの場合、アプリケーション単体の設定を1つのパッケージにまとめられますが、クラウドプロバイダごとの設定や、セキュリティや監視といった運用設定については、別の手段で追加することになります。カスタマイズ性を追求する場合、1つのパッケージが受け取るパラメータを増やすことになり、パッケージが複雑化します。 一方、Cloud Native Adapterのアプローチでは、機能や目的単位でIaC実装をモジュール化し、ユーザ側で必要なモジュールを選択することでアーキテクチャを拡張します。具体的には、ユーザは、1つのモジュールを過度に抽象化およびパラメータ化することなく、コンテクストごとにまとまったデータだけを宣言したCloud Native Adapterを作成します。上記の例では、 resources fieldに、拡張したい追加のリソース設定だけを宣言しています。Qmonus Value Streamは、結合対象のCloud Native Adapterを読み出し、それぞれが宣言したリソース設定について、データ構造をそのままに結合します。 このアプローチによって、Qmonus Value Streamのユーザは、汎化したモジュールを作りながらIaC全体を見通しよくシンプルに記述できます。 このように、Cloud Native Adapterは、アーキテクチャやコンテクストの境界でモジュール化し、ユーザ側で再利用するモジュールを選択・結合するプラクティスを提供します。例で挙げたようなセキュリティ設定や監視設定を1つのモジュールに押し込む必要がなくなります。私たちは、モジュール化というソフトウェアプラクティスを活用し、スケーラブルで直感的にクラウドアーキテクチャを組み上げることができるユーザ体験を提供したいと考えています。 おわりに DevOpsプラットフォームの取り組み連載の3回目の記事として、Qmonus Value Streamチームが取り組んでいるInfrastructure as Codeの課題と、それを解決するための独自IaC実装であるCloud Native Adapterを紹介しました。 Qmonus Value Streamチームは、メンバ一人一人が、利用者であるアプリケーション開発者のために信念を持って活動を続けています。 プラットフォームを内製開発するポジション、プラットフォームを使ってNTTグループのクラウド基盤やCI/CDを構築支援するポジションそれぞれで 、一緒に働く仲間を募集していますので、興味を持たれた方は応募いただけると嬉しいです。 CI/CDプラットフォーム開発、ソフトウェアエンジニア NTTグループのクラウド基盤構築およびCI/CD導入、ソリューションアーキテクト 次回は、CUE言語についてさらに踏み込んでご紹介します。お楽しみに!
はじめに DevOpsプラットフォームの取り組みを紹介する2回目の記事の後半です。 Qmonus(クモナス) Value Stream の開発チームの奥井( @HirokiOkui )です。 連載第2回では、 Qmonus Value Stream を使ってアプリケーションを実際にビルド・デプロイする事例を2つ、前編・後編に分けて紹介します。 前編では、Cloud Native Adapterを用いてパブリッククラウドとKubernetesの両方をまとめてInfrastructure as Code(以下、IaC)として表現し、クラウド基盤を構築する事例を紹介しました。 後編では、Cloud Native Adapterを用いたContinuous Integration/Continuous Delivery(以下、CI/CD)パイプラインの構築を中心に説明します。 1回目、2回目前編の記事をまだご覧になってない方は、ぜひそちらもご覧ください。 1回目 Qmonus Value Streamの紹介 2回目 Qmonus Value Streamを使ってみた(前編) - クラウド基盤の構築 ユースケース2: Istio BookInfo アプリケーションのデリバリを実践する事例として、第2回前編では「KubernetesとGCPによるパブリックAPI公開」を紹介しました。後編では2つ目の事例として、Istioプロジェクトのサンプルアプリケーションである BookInfo を扱います。 BookInfoは、書籍のカタログを表示するシンプルなWebアプリケーションですが、言語の異なる4つのマイクロサービスから構成されており、実践的なアプリケーションに近い複雑さとデリバリの難しさを備えています。このため、IaCやCI/CDのツールを評価するのに適しています。 ( Istioのドキュメント より) リリースエンジニアリングの難しさ Kubernetesに対してコンテナをデリバリするためには、 Tekton CI によるビルド、 Kustomize や Helm によるマニフェスト生成、 Argo CD によるデプロイを用いてCI/CDパイプラインを構成することが一般的です。 これらを用いてBookInfoをデリバリする方法が、インプレス社の Kubernetes CI/CDパイプラインの実装 に詳しく記されています。 こちら で公開されているBookInfoのデリバリに必要なマニフェストを見ると、デリバリ対象のKustomizeマニフェスト、CI/CDのためのTektonとArgo CDのマニフェストと、多くのマニフェストとパラメータが含まれています。 CI/CDパイプラインを構築するためには、それぞれを正しく結合して使いこなす必要があります。 このように、アプリケーションをリリースするためのCI/CDパイプラインを信頼性高く構築し保守する技術領域のことを、リリースエンジニアリングと言います。 BookInfoのように、マイクロサービスアーキテクチャを採用したアプリケーションのリリースには、高度なリリースエンジニアリングのスキルが要求されます。 これを少人数の開発チームで実践するのは至難の業です。 Qmonus Value Streamを用いたCI/CDパイプラインの構成 Qmonus Value Stream(以下、QVS)では、前編で紹介したとおり、独自のIaC実装であるCloud Native Adapter(以下、Adapter)を用いて上述の問題を解決します。 以降の節で、BookInfoをデリバリするCI/CDパイプラインをAdapterを用いて構成する実例について、順を追って説明します。 ここでは、BookInfoを構成する4つのマイクロサービスのそれぞれをイメージビルドし、生成されたイメージを用いたKubernetes Deploymentと内部通信用のServiceをマイクロサービスごとにデプロイするCI/CDパイプラインを構成します。 Cloud Native Adapterの選択 まず、QVSの設定ファイルであるQVS Configを作成し、その中で使用するAdapterを選択します。 BookInfoの reviews アプリケーションでは、以下のQVS Configを使用します。 params : - name : namespace type : string - name : imageName type : string designPatterns : - pattern : qmonus.net/adapter/official/kubernetes/deployment/simple params : appName : reviews namespace : $(params.namespace) imageName : $(params.imageName) port : "9080" env : - name : LOG_DIR value : "/tmp/logs" useDefaultService : true - pattern : qmonus.net/adapter/official/pipeline/build:buildkit - pattern : qmonus.net/adapter/official/pipeline/deploy:simpleDeploy ここでは、QVSで用意している3つのAdapterを使用します。 1つ目は、Applicationマニフェストを扱う Adapterであり、Kubernetes DeploymentリソースとServiceリソースを提供します。 2つ目と3つ目は Pipelineマニフェストを扱う Adapterであり、それぞれ Buildkit を用いたBuild Taskと、IaCツールである Pulumi を用いたDeploy Taskを提供します。 BookInfoの残り3つのサービスのQVS Configも、reviewsと同様に、上記で紹介した3つのAdapterを使用します。 Tekton Pipelineの生成 次に、上述したBookInfoの4つのマイクロサービスに対する各QVS Configを指定してコンパイルします。 すると、各マイクロサービスごとのビルド用とデプロイ用のTekton Pipelineが自動生成されます。 $ kubectl get pipeline -o custom-columns=NAME:.metadata.name NAME details-build details-deploy productpage-build productpage-deploy rating-build rating-deploy reviews-build reviews-deploy Tekton Pipelineから使用されるTekton Taskも自動生成されます。 今回の場合、1つのマイクロサービスにつき5個、計20個のTekton Taskが生成されます。 $ kubectl get task -o custom-columns=NAME:.metadata.name | grep reviews reviews-app-compile-design-pattern reviews-app-deployment-worker reviews-auth-google-registry reviews-buildkit reviews-git-checkout # productpage, details, rating についても同様 生成されたTekton PipelineおよびTaskを以下の図に示します。 Build PipelineはソースコードからBuildkitを用いてdocker imageをビルドし、Google Artifact Registryにプッシュします。 Deploy Pipelineは、QVS Configで指定されているAdapterをコンパイルしてInfrastructureマニフェストを生成し、Pulumiを用いてKubernetesクラスターに対してデプロイします。 Adapterをコンパイルして生成されるTekton Pipelineは、CI/CDパイプライン全体を担うのではなく、上記例のようにBuild、Deploy、Test、Release…といったまとまったプロセス単位ごとに分割されます。 Tekton Pipelineを再利用可能で可搬性がある単位に分割することで、Tekton Pipelineをビルディングブロックのように扱い、柔軟にCI/CDパイプラインを組み立てることができます。 AssemblyLineの作成 最後に、AssemblyLineというQVS独自のリソースを用いてCI/CDパイプラインを構成します。 AssemblyLineに実行対象のTekton Pipelineと実行順序を定義することで、その順序に従いTekton Pipelineが実行されます。 今回は、BookInfoの4つのマイクロサービスをそれぞれビルドし、ステージング環境にデリバリするAssemblyLineを作成します。 作成したAssemblyLineを以下に記します。簡単のため、reviewsのビルドとデプロイに限定し、他の3つは省略しています。 apiVersion : vs.axis-dev.io/v1 kind : AssemblyLine metadata : name : bookinfo-build-and-deploy spec : params : - name : gitRevision - name : imageTag stages : - name : reviews-build-staging spec : deployment : app : reviews name : staging pipeline : reviews-build params : - name : gitRevision value : $(inputs.gitRevision) - name : imageTag value : $(inputs.imageTag) - name : reviews-deploy-staging runAfter : - reviews-build-staging spec : deployment : app : reviews name : staging pipeline : reviews-deploy params : - name : gitRevision value : $(inputs.gitRevision) - name : imageName value : $(stages.reviews-build-staging.results.imageFullNameTag) # 以下は省略 - name : rating-build-stating... - name : rating-deploy-stating... - name : productpage-build-stating... - name : productpage-deploy-stating... - name : details-build-stating... - name : details-deploy-stating.... AssemblyLineには複数のStageが設定可能であり、1つのStageは1つのTekton Pipelineで構成されます。 Stageごとに、Tekton Pipelineで記述された処理をどのマイクロサービスのどの環境向けに作用させるのか、を選択します。 各Stageの runAfter に依存するStageを指定することで、DAG(有向非巡回グラフ)が形成され、依存関係を持つ複数のStageを順番に実行できます。 また、各Stageの params を記述することで、Tekton Pipeline実行時のパラメータをどこから取得するかを定義します。 ここでは、Build Pipelineの引数である gitRevision や imageTag の値をAssemblyLine実行時のパラメータから取得しており、Deploy Pipelineの imageName の値をBuild Pipelineの実行結果から取得しています。 CI/CDパイプラインの構築が簡単に 以上、Adapterを用いてCI/CDパイプラインを構成する手順を紹介しました。 前述したように、Tekton、Kustomize、ArgoCDなどを用いてCI/CDパイプラインを構築するためには、各ツールに対するスキルに加えてツールごとに異なる様式で多くのマニフェストを記述し結合する必要があり、高い専門性が要求されます。 BookInfoの例では20〜30ほどのマニフェストが必要になり、アプリケーションの構成がより複雑化するにつれて大きく増大します。 QVSでは、ベストプラクティスとして提供されているAdapterを選択するだけで必要なTekton Pipelineが生成されるので、マニフェストを記述する必要がありません。 BookInfoのようなマイクロサービスアプリケーションの例であっても、生成されたTekton Pipelineの実行順序をAssemblyLineに記述するだけで、CI/CDパイプラインを構築できます。 リリースエンジニアリングの専門家でなくても、信頼性の高いCI/CDパイプラインを構成できるようになります。 CI/CDパイプラインの実行 前述した構成の元、 CI/CDパイプラインがどのように実行されるか説明します。 BookInfoのAssemblyLineマニフェストをQVSに登録すると、QVSのWebUI上で以下のようにAssemblyLineが表示されます。 今回は、各マイクロサービスごとにBuild PipelineのあとにDeploy Pipelineを実行するだけのシンプルなAssemblyLineなので2層だけですが、実際のAssemblyLineはCI/CDの様々なタスクや承認行為を含み、より複雑なパイプラインを構成することになります。 AssemblyLineを実行するためには、BookInfoを構成するマイクロサービスやデプロイ先の環境情報などをQVSに事前に入力する必要があります。 ここでも、マニフェストの記述量やパラメータ数を減らすための様々な工夫が導入されていますが、こちらについては別の記事で改めて紹介します。 AssemblyLineは、WebUI上から実行するか、gitのプッシュイベントをトリガとして実行されます。 実行されると、AssemblyLineの実行順序定義に従い先頭から順番にTekton PipelineRunおよびTaskRunが実行され、Kubernetes SchedulerによってPodがスケジューリングされて、各CI/CDタスクが実行されていきます。 WebUIでは、リアルタイムな進捗表示に加えて、Taskを実行しているPodのLogやEventなどの情報を確認できます。 Taskが正常に動作していない場合には、Tektonが動作しているKubernetesクラスターにアクセスすることなく、WebUI上から問題の切り分けを行うことが可能です。 以下のように全てのStageに正常終了のマークがつけば、AssemblyLineの処理は完了です。 デプロイ先のKubernetesクラスターにアクセスして、BookInfoアプリケーションが正常動作していることを確認します。以下の通り、4つのマイクロサービスのDeploymentとServiceが生成され、全てのPodが上がっていることが確認できます。 $ kubectl get all -n sample-bookinfo NAME READY STATUS RESTARTS AGE pod/details-78457fc5c9-7szn5 1/1 Running 0 12h pod/productpage-7fccdbdb9c-8cqw8 1/1 Running 0 12h pod/rating-6c8db77566-gfkxj 1/1 Running 0 12h pod/reviews-855b779f4d-xqs7t 1/1 Running 0 12h NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/details ClusterIP 10.44.12.16 <none> 9080/TCP 12h service/productpage ClusterIP 10.44.10.189 <none> 9080/TCP 12h service/rating ClusterIP 10.44.0.210 <none> 9080/TCP 12h service/reviews ClusterIP 10.44.0.249 <none> 9080/TCP 12h NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/details 1/1 1 1 12h deployment.apps/productpage 1/1 1 1 12h deployment.apps/rating 1/1 1 1 12h deployment.apps/reviews 1/1 1 1 12h NAME DESIRED CURRENT READY AGE replicaset.apps/details-78457fc5c9 1 1 1 12h replicaset.apps/productpage-7fccdbdb9c 1 1 1 12h replicaset.apps/rating-6c8db77566 1 1 1 12h replicaset.apps/reviews-855b779f4d 1 1 1 12h productpageのPodにport-forwardしてUIを表示してみると、画面が表示され、正常にデプロイできていることが確認できます。 おわりに DevOpsプラットフォームの取り組み連載の2回目の記事として、Cloud Native Adapterを使用することでクラウド基盤とCI/CDパイプラインが簡単に構築できることを、2つの事例を用いて紹介しました。 NTTコミュニケーションズでは多くのプロジェクトで内製開発をしており、開発チームがプロダクト開発の最も重要な部分に注力できるように、Qmonus Value Streamの開発とCloud Native Adapterの拡充を進めています。 Qmonus Value Streamチームは、メンバ一人一人が、利用者であるアプリケーション開発者のために信念を持って活動を続けています。 プラットフォームを内製開発するポジション、プラットフォームを使ってNTTグループのクラウド基盤やCI/CDを構築支援するポジションそれぞれで 、一緒に働く仲間を募集していますので、興味を持たれた方は応募いただけると嬉しいです。 https://hrmos.co/pages/nttcom0033/jobs/0000019 https://hrmos.co/pages/nttcom0033/jobs/1692796 次回は、Cloud Native Adapterについてさらに踏み込んで紹介します。お楽しみに!
はじめに DevOpsプラットフォームの取り組みを紹介する2回目の記事の前半です。 こんにちは。 Qmonus (クモナス) Value Stream の開発チームの松本です。 前回の記事 では、 Qmonus Value Stream の概要を紹介しました。 連載第2回となる今回は、Qmonus Value Streamの重要で特徴的な機能の1つであるCloud Native Adapterを使い、アプリケーションをデリバリする事例を2つ、前編と後編の2回に分けて紹介します。 前編では、Cloud Native Adapterを用いたクラウド基盤の構築を中心に説明します。 クラウド技術の多様化・複雑化 近年のアプリケーション開発において、GCP/Azure/AWS等のパブリッククラウドの利用はデファクトスタンダードとなりました。 また、多くのプロダクトはコンテナアプリケーションとして開発され、Kubernetesやその他のコンテナ実行環境を使ってサービス提供されています。 システムの基盤はパブリッククラウドを使う事で迅速に構築できるようになりましたが、パブリッククラウドが提供するサービスは多様化・複雑化しており、要件によっては監視や認証、メール送信などで別々のパブリッククラウドやSaaSを組み合わせる必要があります。 それぞれのサービスには適切なパラメータの決定も必要です。 商用提供しているプロダクトでは、開発環境やステージング環境そしてプロダクション環境と複数の環境を適切に構築するために、Infrastructure as Code(以下、「IaC」といいます)の導入も必要となるでしょう。 このように、クラウド技術によりアジリティは大幅に向上したものの、要件に応じてクラウドサービスを組み合わせてアーキテクチャを設計し構築するためには、高い専門性だけではなく多大な労力をも必要とするのが現状の課題です。 Qmonus Value Stream(以下、「QVS」といいます)では、このような課題をCloud Native Adapter(以下、「Adapter」といいます)を提供して解決します。 ユースケース1: KubernetesとGCPによるパブリックAPI公開 アプリケーションのデリバリを実践する1つ目の事例として、GCPのWAF/FirewallであるCloud Armorを適用したWebアプリケーションを構築します。 コンテナでWebアプリケーションを公開する際には、多くの場合GKEのようなマネージドKubernetesサービスを利用しますが、マネージドKubernetesを用意するだけではWebアプリケーションを公開できません。 例えば、WEBアプリケーションをWAFを適用して提供する場合、マネージドKubernetesの準備に付随して次の作業が必要となります。 グローバルIPアドレスの確保 確保したグローバルIPアドレスに名前解決するためのDNSレコードの設定 SSL/TLSサーバ証明書の発行 適切なSSL/TLS暗号スイートの設定 WAF/Firewallのポリシー設定 ロードバランサの設定 不適切な設定や設定漏れがあると、アプリケーションが動作しないだけではなく、最悪の場合セキュリティホールを作ってしまいプロダクトの継続に大きな悪影響を与えてしまうので、確実な実施が必要です。 これらのリソースを構築するには、TerraformでCloud ArmorやGCLBなどのGCPリソースを構築し、そして、Kustomizeなどを使ってService・Ingress・BackendConfigなど多くのKubernetesマニフェストを記述し適用する必要があります。 また、これら多くのサービスを相互参照しながら正しく構成する必要があります。意図した動作をしない場合には、利用する複数サービスのログやマニフェストを調査して修正した後に再度動作確認するということを繰り返すこととなります。 このように、QVSを使わない場合には、パブリッククラウド、Kubernetes、IaCツールに対する高い専門性と多くの時間が要求されます。 Qmonus Value Streamを用いたクラウド基盤構築 前回ご紹介したように、Adapterは、クラウド基盤とContinuous Integration/Continuous Delivery(以下、「CI/CD」といいます)パイプラインを構築するためのベストプラクティスをパッケージ化したものです。 アプリケーション開発者は、Adapterを選択しパラメータを指定するだけで、クラウド基盤とCI/CDパイプラインを高い専門知識がなくても構築できます。 Adapterは、デプロイ対象のApplicationマニフェストと、Pipelineマニフェストの両方を生成します。 Applicationマニフェストは、Kubernetesリソースやクラウドリソースにより構成され、デプロイするアプリケーションのシステム構成を示します。 Pipelineマニフェストは、CI/CDパイプラインのビルドからリリースに至る過程において「いつどのようなTaskを実行するか」を定義しています。QVSはCI/CDの実行エンジンとして Tekton を採用しており、PipelineマニフェストはTekton Pipeline/Taskリソースとして記述されます。 Adapterの選択 QVSの設定ファイルであるQVS Configを作成し、その中で使用するAdapterを選択します。 以下に、QVS Configの具体例を示します。Adapterは、 designPatterns の下に配列で配置されます。 params : - name : namespace type : string - name : imageName type : string designPatterns : - pattern : qmonus.net/adapter/official/kubernetes/deployment/simple params : appName : public-api-test-app namespace : $(params.namespace) imageName : $(params.imageName) port : "5000" replicas : "1" - pattern : qmonus.net/adapter/official/kubernetes/gke/publicapi params : appName : public-api-test-app namespace : $(params.namespace) port : "5000" domainName : public-api-app-demo1.example.com gcpExternalAddressName : matsumoto-gip2 gcpSecurityPolicyName : matsumoto-policy1 gcpSslPolicyName : matsumoto-ssl-policy - pattern : qmonus.net/adapter/official/pipeline/build:buildkit - pattern : qmonus.net/adapter/official/pipeline/deploy:simple 1つ目と2つ目のAdapterは、クラウド基盤の構築を担当し、1つ目は下図の赤破線、2つ目は下図の青破線で囲まれた場所を構築します。 1つ目のAdapterではKubernetesのDeploymentを生成し、2つ目のAdapterではGCPのWAF/FirewallであるCloud ArmorやCloud Load Balancingを構築しつつ、連携するようにKubernetesのService、Ingress、ManagedCertificate、BackendConfig、FrontendConfigを構成します。 これらのリソースを全てTerraformやKubernetesマニフェストで記述すると多くのリソースが必要になり複雑ですが、Adapterを使えば上記の通り簡潔に記述できます。 各Adapterは、実行時にパラメータを外部から注入できます。 1つ目のAdapterでは、外部から注入されたimageNameパラメータによりコンテナイメージとバージョンを受け取り、Kubernetes Deploymentを生成しています。 パラメータとして注入するコンテナイメージとバージョンを変更することで、QVS Config自体に変更を加えることなく簡単にバージョンアップが実現できます。 Adapterにおけるパラメータの正規化・外部インターフェース化の機構はCUE言語の表現力を用いて実現されています。 この詳細については、別の記事で改めて紹介します。 3つ目と4つ目のAdapterは、CI/CDパイプラインを構築するものです。 この部分の詳細は後編で説明します。 今回説明した4つのAdapterは、全てQVS開発チームが提供し保守しているOfficial Cloud Native Adapter(以下、「Official Adapter」といいます)です。 広く一般的に使われるクラウド構成や CI/CDパイプラインの基本となるタスクについては、Official Adapterに含まれているので、そのまま利用できます。 CI/CDパイプラインの構成 QVSでは、AssemblyLineという独自のリソースを用いてCI/CDパイプラインを構成し、Adapterより生成されたTekton Pipelineを実行することでアプリケーションをデリバリします。 AssemblyLineの詳細については、後編でご説明します。 CI/CDパイプラインの実行 AssemblyLineマニフェストをQVSに登録し実行すると、Build PipelineとDeploy Pipelineが実行されて、Adapterより生成される各種ApplicationマニフェストがGCPとGKEに対してデプロイされます。 AssemblyLineを実行すると、QVSのWebUIから進捗状況が確認できます。 Build PipelineとDeploy Pipelineが実行されて、処理が正常終了したことを確認できました。 デプロイ先のGKEにアクセスし、アプリケーションが正常に動作していることを確認します。以下の通り、DeploymentやService、Ingressが正しく作成されていました。 % kubectl get deployments NAME READY UP-TO-DATE AVAILABLE AGE public-api-test-app 1/1 1 1 1h %kubectl get services NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE public-api-test-app NodePort 10.0.72.51 <none> 5000:32663/TCP 1h %kubectl get ingress NAME CLASS HOSTS ADDRESS PORTS AGE public-api-test-app <none> * 203.0.113.10 80 1h また、GCPのコンソールからもWebアプリケーション提供に必要となるCloud ArmorやロードバランサなどのGCPリソースが適切に作成されていることが確認できます。 以下のCloud Armorの画面で、ロードバランサにポリシーが適用されています。 以下のロードバランサの画面では、SSLのポリシーや証明書などが設定されておりバックエンドであるKubernetesに転送されるように設定されています。 そして、実際に構築したWebアプリケーション( https://public-api-app-demo1.example.com )にアクセスすると、サンプルアプリケーションからの応答が確認できました。 前編では、複雑なTerraformやKubernetesマニフェストの記述する代わりに、QVSチームから提供されるOfficial Adapterをいくつか選択し記述するだけでクラウド基盤を構築する事例を紹介しました。 後編 に続きます。
サマリ Multi-AS で構成されるネットワークにおいて、 SR-MPLS + EVPN による L2VPN を実現 IOS XR + Junos の Multi-vendor 環境で動作検証に成功 この記事は Multi-AS SR 検証連載の第 3 回です。目次は こちら 概要 イノベーションセンターの田島です。 本記事では Single-/Multi-AS SR-MPLS での L2VPN について解説します。 我々の一連の検証では 第 0 回の記事 にもありますように、 Multi-AS でアンダーレイを構築することで拡張性や保守性を担保したまま拡張可能なネットワークを検討しています。 第 2 回の記事 では Multi-AS/Multi-domain SR-MPLS で分割されているアンダーレイを用いて L3VPN を実現しました。 本記事は同様のアンダーレイで L2VPN を実現します。 なお L2VPN という総称は、 2 点間の接続 (point-to-point) を仮想的に L2 レベルで行う Pseudo Wire や VPWS 1 とも呼ばれる E-Line と、複数拠点間 (point-to-multipoint) を仮想的に L2 スイッチングする VPLS 2 とも呼ばれる E-LAN の 2 種類を指すことがあります。 本記事では後者の、複数拠点間の E-LAN を対象にします。 このあとは L2VPN の仕組みについて概説したのち、 Multi-vendor 環境における検証トポロジーや設定例を紹介します。 Segment Routing (SR) や Multi-AS/Multi-domain SR-MPLS についての紹介は 第 0 回の記事の目次 から L3VPN の各記事をご参照ください。 EVPN による L2VPN L2VPN 実現の手段として EVPN (Ethernet VPN) があります。 EVPN は RFC7432 - BGP MPLS-Based Ethernet VPN で規格化された L2VPN の方式で、経路情報の交換や転送が定められています。 誤解を恐れずに表現すると L2 における経路とはつまり MAC アドレスのことです。 EVPN では、各ルーターは MP-BGP 3 を用いて交換された MAC アドレスを学習し、照合することで各 MAC アドレス宛てのフレームを転送します。 上記の図は L2VPN の模式図で、複数台のルーターからなるコアネットワークがあたかも単一の巨大な L2SW のように転送する様子を表しています。 顧客 A と B それぞれ専用の L2SW が存在し、各 PE からアクセスできるイメージです。 顧客 A と B の間では通信ができず、それぞれ独立しています。 なお先ほど交換する経路とは L2 の MAC アドレスのことであるとしましたが、実際のところ EVPN においては MAC アドレスの他に L3 情報である IP アドレスや Ethernet Segment 識別子 (ESI) なども交換することにより、マルチホーミングなどの単なる L2VPN 以上の機能を実現しています。 機能が広範囲にわたる EVPN では交換する情報を Route Type として定義しています。 この記事内では Type 2 を詳細に見ますが、そのほかにも Type はあります。 Type 1 は Ethernet Auto-Discovery とよばれ、 ESI を広告し障害時の高速な収束などのために使われます。 Type 2 は MAC アドレスや IP アドレスを扱います。これは文字通り、 CE 側の端末のアドレスを交換します。 Type 3 は BUM トラフィックの送信先に使われます。 Type 4 は ESI に基づきマルチホーム時のトラフィック制御に使われます。 EVPN においては VLAN との対応付けに際して、 3 つの方式があります。 VPN を識別して転送するために EVPN Instance (EVI) / Bridge Domain (BD) / Broadcast Domain (VLAN) をどう構成するかによって、下記のような 3 方式があります。 VLAN-Based: それぞれ1対1で対応する。単一の EVI には 単一の BD が対応し、それには単一の VLAN のみが対応する。 VLAN Bundle: EVI と BD は 1 対 1 だが、BD には複数の VLAN が所属する。 VLAN-Aware Bundle: EVI には 複数の BD が所属するが、各 BD にはそれぞれ単一の VLAN が対応する。 今回はわかりやすく各インスタンスが 1 対 1 で対応する 1. VLAN-Based を用いて検証します。 なお、方式ごとに設定方法があるため、特に Multi-vendor 環境においてはどの方式がどの設定に対応しているのかは要確認です。 これら3つに相互運用性はなく、同じ方式にそろえる必要があります。 実際に我々も検証中に 1. VLAN-Based と 3. VLAN-Aware が混在し相互接続できなくなり原因調査することがありました。 EVPN と Inter-AS Option B の対応状況 Multi-AS/Multi-domain SR-MPLS で分割されているアンダーレイを通じて VPN を実現する際に、拡張性を求めると Option B での実装が望ましいことは 第 2 回の記事 で示しました。 当該記事中でも言及しましたが、このようなアンダーレイにおいて EVPN を Option B で AS 間接続する実装は始まったばかりです。 具体的には Cisco ASR 9000 シリーズでは IOS XR 7.4.1 から 4 実装されています。 2022年7月現在では、 ASBR として使用できる機器が限られています。 Single-AS での L2VPN まずは単純な Single-AS でのアンダーレイ環境において L2VPN を実現することで、基本的な設定を紹介します。 トポロジーは下記のような、 IOS XR と Junos それぞれ 1 台ずつ、 PE が P ルーターを介して接続された状態です。 使用した機材および OS バージョンは次の通りです。 PE PE1: Cisco ASR9901 (IOS XR 7.6.1) PE2: Juniper MX204 (Junos 21.3R1.9) P P1: Cisco C8201 (IOS XR 7.5.1) P2: Juniper PTX10001 (Junos 21.4R1.15-EVO) IGP や SR の設定は割愛します。下記の設定および確認を順に行います。 CE 接続インターフェースの設定 EVI と BD の設定 BGP の設定 経路の確認 疎通試験 なお検証の都合上、 PE では顧客 A と B は同じ物理ポートで VLAN で分けて収容することとします。 CE 接続インターフェースの設定 PE1 (IOS XR) 側 後の BD にはタグなしとして見せるため VLAN タグを取り外す設定を入れておきます。 interface TenGigE0/0/0/32.1000 l2transport encapsulation dot1q 1000 rewrite ingress tag pop 1 symmetric ! interface TenGigE0/0/0/32.2000 l2transport encapsulation dot1q 2000 rewrite ingress tag pop 1 symmetric ! PE2 (Junos) 側 この時点で既にインターフェースに ESI を設定しておきます。 set interfaces xe-0/1/6 flexible-vlan-tagging set interfaces xe-0/1/6 encapsulation flexible-ethernet-services set interfaces xe-0/1/6 esi 00:00:00:00:00:00:00:00:00:02 set interfaces xe-0/1/6 esi all-active set interfaces xe-0/1/6 unit 1000 encapsulation vlan-bridge set interfaces xe-0/1/6 unit 1000 vlan-id 1000 set interfaces xe-0/1/6 unit 2000 encapsulation vlan-bridge set interfaces xe-0/1/6 unit 2000 vlan-id 2000 EVI と BD の設定 PE1 (IOS XR) 側 ESI と RT を設定します。 advertise-mac の有無で EVI ごとに Type-2 経路として MAC を出すかを制御できます。 evpn evi 1000 bgp route-target import 65001:1000 route-target export 65001:1000 ! control-word-disable advertise-mac ! ! evi 2000 bgp route-target import 65001:2000 route-target export 65001:2000 ! control-word-disable advertise-mac ! ! interface TenGigE0/0/0/32 ethernet-segment identifier type 0 00.00.00.00.00.00.00.00.01 ! ! ! BD を設定します。 顧客 A 用と、顧客 B 用、それぞれの EVI を追加しておきます。 l2vpn bridge group EVPN bridge-domain 1000 interface TenGigE0/0/0/32.1000 ! evi 1000 ! ! bridge-domain 2000 interface TenGigE0/0/0/32.2000 ! evi 2000 ! ! ! ! PE2 (Junos) 側 EVI 兼 BD の routing-instance を作成します。 RT も設定します。 set policy-options policy-statement EXPORT-POLICY-1000 term ROUTE-TARGET then community add RT1000 set policy-options policy-statement IMPORT-POLICY-1000 term ROUTE-TARGET-iBGP from community RT1000-iBGP set policy-options policy-statement IMPORT-POLICY-1000 term ROUTE-TARGET-iBGP then accept set policy-options community RT1000 members target:65001:1000 set policy-options community RT1000-iBGP members target:65001:1000 set policy-options policy-statement EXPORT-POLICY-2000 term ROUTE-TARGET then community add RT2000 set policy-options policy-statement IMPORT-POLICY-2000 term ROUTE-TARGET-iBGP from community RT2000-iBGP set policy-options policy-statement IMPORT-POLICY-2000 term ROUTE-TARGET-iBGP then accept set policy-options community RT2000 members target:65001:2000 set policy-options community RT2000-iBGP members target:65001:2000 set routing-instances evpn-1000 instance-type evpn set routing-instances evpn-1000 protocols evpn set routing-instances evpn-1000 vlan-id none set routing-instances evpn-1000 interface xe-0/1/6.1000 set routing-instances evpn-1000 route-distinguisher 10.255.1.2:1000 set routing-instances evpn-1000 vrf-import IMPORT-POLICY-1000 set routing-instances evpn-1000 vrf-target target:65001:1000 set routing-instances evpn-2000 instance-type evpn set routing-instances evpn-2000 protocols evpn set routing-instances evpn-2000 vlan-id none set routing-instances evpn-2000 interface xe-0/1/6.2000 set routing-instances evpn-2000 route-distinguisher 10.255.1.2:2000 set routing-instances evpn-2000 vrf-import IMPORT-POLICY-2000 set routing-instances evpn-2000 vrf-target target:65001:2000 BGP の設定 PE1 (IOS XR) 側 RT の設定などは EVI の項で行ったため、 address-family の宣言のみです。 router bgp 65001 bgp router-id 10.255.1.1 address-family l2vpn evpn ! neighbor 10.255.1.2 remote-as 65001 update-source Loopback0 address-family l2vpn evpn ! ! ! PE2 (Junos) 側 こちらもネイバーの定義のみです。 set protocols bgp family evpn signaling set protocols bgp group blog type internal set protocols bgp group blog local-address 10.255.1.2 set protocols bgp group blog family evpn signaling set protocols bgp group blog neighbor 10.255.1.1 経路の確認 まず PE1 から PE2 方向に経路を見ていきます。 PE1 での MAC アドレス学習結果と、それぞれの EVI に対する VPN ラベルがついていることが確認できます。 RP/0/RSP0/CPU0:PE1#show evpn evi vpn-id 1000 mac Wed Jun 29 15:04:57.503 JST VPN-ID Encap MAC address IP address Nexthop Label SID ---------- ---------- -------------- ---------------------------------------- --------------------------------------- -------- --------------------------------------- 1000 MPLS 0200.0011.1111 :: TenGigE0/0/0/32.1000 24010 RP/0/RSP0/CPU0:PE1#show evpn evi vpn-id 2000 mac Wed Jun 29 15:05:04.384 JST VPN-ID Encap MAC address IP address Nexthop Label SID ---------- ---------- -------------- ---------------------------------------- --------------------------------------- -------- --------------------------------------- 2000 MPLS 0200.0033.3333 :: TenGigE0/0/0/32.2000 24000 これらの VPN ラベルが着信すると EVPN として処理されます。 RP/0/RSP0/CPU0:PE1#show mpls forwarding Wed Jun 29 15:08:24.725 JST Local Outgoing Prefix Outgoing Next Hop Bytes Label Label or ID Interface Switched ------ ----------- ------------------ ------------ --------------- ------------ (snip) 24000 Pop EVPN:2000 U BD=0 E point2point 8136 (snip) 24010 Pop EVPN:1000 U BD=1 E point2point 0 (snip) PE2 側では顧客 A の経路を学習しています。相手の VPN ラベルが 24010 であることも認識しています。 PE2 に CE から着信したフレームは VPN ラベルと、 PE1 の SID を付けて網内に転送されます。 user@PE2> show route table evpn-1000.evpn.0 evpn-mac-address 02:00:00:11:11:11 extensive evpn-1000.evpn.0: 7 destinations, 15 routes (7 active, 0 holddown, 0 hidden) 2:10.255.1.1:1000::0::02:00:00:11:11:11/304 MAC/IP (3 entries, 1 announced) *BGP Preference: 170/-101 Route Distinguisher: 10.255.1.1:1000 Next hop type: Indirect, Next hop index: 0 Address: 0x853435c Next-hop reference count: 51 Source: 10.255.1.1 Protocol next hop: 10.255.1.1 Indirect next hop: 0x2 no-forward INH Session ID: 0x0 State: <Secondary Active Int Ext> Local AS: 65001 Peer AS: 65001 Age: 6:34 Metric2: 20 Validation State: unverified ORR Generation-ID: 0 Task: BGP_65001.10.255.1.1 Announcement bits (1): 0-evpn-1000-evpn AS path: I Communities: target:65001:1000 origin:10.255.1.1:1000 unknown iana evpn 0x60e:0x0:0x3e8 Import Accepted Route Label: 24010 ESI: 00:00:00:00:00:00:00:00:00:01 Localpref: 100 Router ID: 10.255.1.1 Primary Routing Table: bgp.evpn.0 Thread: junos-main Indirect next hops: 1 Protocol next hop: 10.255.1.1 Metric: 20 Indirect next hop: 0x2 no-forward INH Session ID: 0x0 Indirect path forwarding next hops: 1 Next hop type: Router Next hop: 10.1.2.1 via xe-0/1/0.0 Session Id: 0x0 10.255.1.1/32 Originating RIB: inet.3 Metric: 20 Node path count: 1 Forwarding nexthops: 1 Next hop type: Router Next hop: 10.1.2.1 via xe-0/1/0.0 Session Id: 0x0 疎通試験 顧客 A の CE に相当する VM を両端で 2 台作成し、その間で ping による通信を確認しました。 ping による通信確認のための 1 台目の IP 設定と ping の実施例です。 L3VPN とは異なり、 L2VPN では途中経路を CE 側から調査する方法がないため、 ping が通ることを確認するのみです。 user@vm01:~$ ip a (snip) 4: ens224: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000 link/ether 02:00:00:11:11:11 brd ff:ff:ff:ff:ff:ff inet6 fe80::ff:fe11:1111/64 scope link valid_lft forever preferred_lft forever (snip) 6: ens224.1000@ens224: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether 02:00:00:11:11:11 brd ff:ff:ff:ff:ff:ff inet 192.168.0.1/24 scope global ens224.1000 valid_lft forever preferred_lft forever inet6 fe80::ff:fe11:1111/64 scope link valid_lft forever preferred_lft forever user@vm01:~$ ping 192.168.0.2 PING 192.168.0.2 (192.168.0.2) 56(84) bytes of data. 64 bytes from 192.168.0.2: icmp_seq=1 ttl=64 time=0.758 ms 64 bytes from 192.168.0.2: icmp_seq=2 ttl=64 time=0.485 ms 64 bytes from 192.168.0.2: icmp_seq=3 ttl=64 time=0.440 ms ^C --- 192.168.0.2 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2025ms rtt min/avg/max/mdev = 0.440/0.561/0.758/0.140 ms 2 台目における IP 設定と、パケットキャプチャによる確認です。 user@vm02:~$ ip a (snip) 4: ens224: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000 link/ether 02:00:00:22:22:22 brd ff:ff:ff:ff:ff:ff inet6 fe80::ff:fe22:2222/64 scope link valid_lft forever preferred_lft forever (snip) 6: ens224.1000@ens224: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether 02:00:00:22:22:22 brd ff:ff:ff:ff:ff:ff inet 192.168.0.2/24 scope global ens224.1000 valid_lft forever preferred_lft forever inet6 fe80::ff:fe22:2222/64 scope link valid_lft forever preferred_lft forever user@vm02:~$ sudo tcpdump -i ens224.1000 -n -nn -e tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on ens224.1000, link-type EN10MB (Ethernet), capture size 262144 bytes 20:18:36.037958 02:00:00:11:11:11 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 60: Request who-has 192.168.0.2 tell 192.168.0.1, length 46 20:18:36.038032 02:00:00:22:22:22 > 02:00:00:11:11:11, ethertype ARP (0x0806), length 42: Reply 192.168.0.2 is-at 02:00:00:22:22:22, length 28 20:18:36.038364 02:00:00:11:11:11 > 02:00:00:22:22:22, ethertype IPv4 (0x0800), length 98: 192.168.0.1 > 192.168.0.2: ICMP echo request, id 1, seq 1, length 64 20:18:36.038394 02:00:00:22:22:22 > 02:00:00:11:11:11, ethertype IPv4 (0x0800), length 98: 192.168.0.2 > 192.168.0.1: ICMP echo reply, id 1, seq 1, length 64 20:18:37.039357 02:00:00:11:11:11 > 02:00:00:22:22:22, ethertype IPv4 (0x0800), length 98: 192.168.0.1 > 192.168.0.2: ICMP echo request, id 1, seq 2, length 64 20:18:37.039405 02:00:00:22:22:22 > 02:00:00:11:11:11, ethertype IPv4 (0x0800), length 98: 192.168.0.2 > 192.168.0.1: ICMP echo reply, id 1, seq 2, length 64 20:18:38.063289 02:00:00:11:11:11 > 02:00:00:22:22:22, ethertype IPv4 (0x0800), length 98: 192.168.0.1 > 192.168.0.2: ICMP echo request, id 1, seq 3, length 64 20:18:38.063320 02:00:00:22:22:22 > 02:00:00:11:11:11, ethertype IPv4 (0x0800), length 98: 192.168.0.2 > 192.168.0.1: ICMP echo reply, id 1, seq 3, length 64 20:18:41.054244 02:00:00:22:22:22 > 02:00:00:11:11:11, ethertype ARP (0x0806), length 42: Request who-has 192.168.0.1 tell 192.168.0.2, length 28 20:18:41.054632 02:00:00:11:11:11 > 02:00:00:22:22:22, ethertype ARP (0x0806), length 60: Reply 192.168.0.1 is-at 02:00:00:11:11:11, length 46 ^C 10 packets captured 10 packets received by filter 0 packets dropped by kernel このようにして、 Multi-vendor での Single-AS における L2VPN が実現できました。次にこれを Multi-AS へと拡張します。 Multi-AS での L2VPN Multi-AS へと拡張すると、トポロジーは次の図のようになります。 ASBR は前述の通り対応している機器が限られるため Multi-vendor の環境ではありません。 PE はそれぞれの AS で IOS XR と Junos を 1 台ずつ、 ASBR と P ルーターを介して接続された状態です。 図中ではループバックアドレスの採番や Node SID の採番が AS 間で重複しないようになっていますが、これは説明のわかりやすさのためで、実際には重複することも可能です。ただしループバックアドレスをルーター ID として使用している場合は、特に EVI の RD に気をつける必要があります。 使用した機材および OS バージョンは次の通りです。 PE PE1: Cisco ASR9901 (IOS XR 7.6.1) PE2: Juniper MX204 (Junos 21.3R1.9) P P1: Cisco C8201 (IOS XR 7.5.1) P2: Juniper PTX10001 (Junos 21.4R1.15-EVO) P3: Cisco NCS55A2 (IOS XR 7.5.1) P4: Juniper MX10003 (Junos 21.4R1.12) ASBR ASBR1 および ASBR2:Cisco ASR9902 (IOS XR 7.6.1) IGP や SR の設定、および前述の CE 接続インターフェースの設定は Single-AS と同等なので割愛します。 下記の各項目で、主に Single-AS との差異を順に示します。 EVI と BD の設定 BGP の設定 経路の確認 疎通試験 EVI と BD の設定 Route Target を相互に学習するための仮想的な AS: 64999 を設定し、有効にします。 BD の設定は Single-AS と同等なので割愛します。 PE1 (IOS XR) 側 evpn evi 1000 bgp route-target import 64999:1000 route-target import 65001:1000 route-target export 64999:1000 route-target export 65001:1000 ! control-word-disable advertise-mac ! ! evi 2000 bgp rd 65002:2000 route-target import 64999:2000 route-target import 65001:2000 route-target export 64999:2000 route-target export 65001:2000 ! control-word-disable advertise-mac ! ! interface TenGigE0/0/0/32 ethernet-segment identifier type 0 00.00.00.00.00.00.00.00.01 ! ! ! PE2 (Junos) 側 送受信で RT を増やします。 set policy-options policy-statement EXPORT-POLICY-1000 term ROUTE-TARGET then community add RT1000 set policy-options policy-statement IMPORT-POLICY-1000 term ROUTE-TARGET-iBGP from community RT1000-iBGP set policy-options policy-statement IMPORT-POLICY-1000 term ROUTE-TARGET-iBGP then accept set policy-options policy-statement IMPORT-POLICY-1000 term ROUTE-TARGET-eBGP from community RT1000-eBGP set policy-options policy-statement IMPORT-POLICY-1000 term ROUTE-TARGET-eBGP then accept set policy-options community RT1000 members [ target:64999:1000 target:65001:1000 target:65002:1000 ] set policy-options community RT1000-iBGP members target:65001:1000 set policy-options community RT1000-eBGP members target:64999:1000 set policy-options policy-statement EXPORT-POLICY-2000 term ROUTE-TARGET then community add RT2000 set policy-options policy-statement IMPORT-POLICY-2000 term ROUTE-TARGET-iBGP from community RT2000-iBGP set policy-options policy-statement IMPORT-POLICY-2000 term ROUTE-TARGET-iBGP then accept set policy-options policy-statement IMPORT-POLICY-2000 term ROUTE-TARGET-eBGP from community RT2000-eBGP set policy-options policy-statement IMPORT-POLICY-2000 term ROUTE-TARGET-eBGP then accept set policy-options community RT2000 members [ target:64999:2000 target:65001:2000 target:65002:2000 ] set policy-options community RT2000-iBGP members target:65001:2000 set policy-options community RT2000-eBGP members target:64999:2000 set routing-instances evpn-1000 instance-type evpn set routing-instances evpn-1000 protocols evpn set routing-instances evpn-1000 vlan-id none set routing-instances evpn-1000 interface xe-0/1/6.1000 set routing-instances evpn-1000 route-distinguisher 10.255.1.2:1000 set routing-instances evpn-1000 vrf-import IMPORT-POLICY-1000 set routing-instances evpn-1000 vrf-target target:64999:1000 set routing-instances evpn-2000 instance-type evpn set routing-instances evpn-2000 protocols evpn set routing-instances evpn-2000 vlan-id none set routing-instances evpn-2000 interface xe-0/1/6.2000 set routing-instances evpn-2000 route-distinguisher 10.255.1.2:2000 set routing-instances evpn-2000 vrf-import IMPORT-POLICY-2000 set routing-instances evpn-2000 vrf-target target:64999:2000 BGP の設定 このトポロジーでは下記の 3 つの BGP セッションが最低限必要です。 PE1 <-> ASBR1: AS 65001 内の IBGP ASBR1 <-> ASBR2: AS 65001 と AS 65002 間の EBGP PE2 <-> ASBR2: AS 65002 内の IBGP これらのうち 3. は 1. と同等のため割愛します。 まず 1. について、 ASBR で next-hop-self の設定が必要になります。 ASBR1 (IOS XR) 側 router bgp 65001 bgp router-id 10.255.1.7 neighbor 10.255.1.1 remote-as 65001 address-family l2vpn evpn next-hop-self 次に 2. について、 ASBR 間では Option B を有効にします。また、 EBGP ではデフォルトで経路が拒否されるため検証用にすべてを許可します。 ASBR2 側も同様です。 ASBR1 (IOS XR) 側 router bgp 65001 bgp router-id 10.255.1.7 address-family l2vpn evpn label mode per-nexthop-received-label retain route-target all option-b-asbr-only ! neighbor 10.100.2.2 remote-as 65002 address-family l2vpn evpn route-policy pass-all in route-policy pass-all out route-policy pass-all pass end-policy 経路の確認 Single-AS と同様に PE1 から PE2 方向に経路を見ていきます。 PE1 での MAC アドレス学習結果と、 VPN ラベル 24010 がついていることが確認できます。 RP/0/RSP0/CPU0:PE1#show evpn evi vpn-id 1000 mac Thu Jun 30 15:11:19.848 JST VPN-ID Encap MAC address IP address Nexthop Label SID ---------- ---------- -------------- ---------------------------------------- --------------------------------------- -------- --------------------------------------- 1000 MPLS 000c.2915.e72d :: 10.255.1.7 24010 1000 MPLS 000c.2915.e72d 192.168.0.4 10.255.1.7 24010 1000 MPLS 0200.0011.1111 :: TenGigE0/0/0/32.1000 24010 1000 MPLS 0200.0022.2222 :: 10.255.1.2 122 1000 MPLS 0200.0022.2222 192.168.0.2 10.255.1.2 122 ASBR1 では Inter-AS の VPN から受信したときに AS 65001 内の VPN へと Swap します。 ここでは Inter-AS 側から 24020 を受け取ったときに 24010 へと Swap して PE1 に送ることから、 Inter-AS ではこの VPN 用に 24020 が使われていることがわかります。 RP/0/RP0/CPU0:ASBR1#show mpls forwarding Thu Jun 30 15:13:15.420 JST Local Outgoing Prefix Outgoing Next Hop Bytes Label Label or ID Interface Switched ------ ----------- ------------------ ------------ --------------- ------------ (snip) 24020 24010 No ID 10.255.1.1 0 (snip) ASBR2 では AS 65002 内で VPN 識別に使用するラベルを生成し、そのラベルがついたものを 24020 へと Swap し Inter-AS に送り出します。 ここでは Inter-AS 向けの出力で 24020 を使用するルールを探すことで AS 65002 内では 24013 がこの VPN 用に使用されていることがわかります。 RP/0/RP0/CPU0:ASBR2#show mpls forwarding Thu Jun 30 15:18:38.479 JST Local Outgoing Prefix Outgoing Next Hop Bytes Label Label or ID Interface Switched ------ ----------- ------------------ ------------ --------------- ------------ (snip) 24013 24020 No ID Te0/0/0/25 10.100.2.1 0 (snip) PE2 で学習される経路とラベルを見ると、上記で確認した 24013 であり、 ASBR2 で正しく転送されるラベルが学習されています。 user@PE2> show route table evpn-1000.evpn.0 evpn-mac-address 02:00:00:11:11:11 extensive evpn-1000.evpn.0: 13 destinations, 22 routes (13 active, 0 holddown, 0 hidden) 2:10.255.1.1:1000::0::02:00:00:11:11:11/304 MAC/IP (2 entries, 1 announced) *BGP Preference: 170/-101 Route Distinguisher: 10.255.1.1:1000 Next hop type: Indirect, Next hop index: 0 Address: 0xc2275dc Next-hop reference count: 62 Source: 10.255.2.5 Protocol next hop: 10.255.2.1 Indirect next hop: 0x2 no-forward INH Session ID: 0x0 State: <Secondary Active Int Ext> Local AS: 65002 Peer AS: 65002 Age: 21:38 Metric2: 30 Validation State: unverified ORR Generation-ID: 0 Task: BGP_65002.10.255.2.5 Announcement bits (1): 0-evpn-1000-evpn AS path: 65001 I (Originator) Cluster list: 0.0.0.1 Originator ID: 10.255.2.1 Communities: target:64999:1000 target:65001:1000 origin:10.255.1.1:1000 unknown iana evpn 0x60e:0x0:0x3e8 Import Accepted Route Label: 24013 ESI: 00:00:00:00:00:00:00:00:00:01 Localpref: 100 Router ID: 10.255.2.1 Primary Routing Table: bgp.evpn.0 Thread: junos-main Indirect next hops: 1 Protocol next hop: 10.255.2.1 Metric: 30 Indirect next hop: 0x2 no-forward INH Session ID: 0x0 Indirect path forwarding next hops: 1 Next hop type: Router Next hop: 10.2.4.1 via et-0/0/1.0 weight 0x1 Session Id: 0x0 10.255.2.1/32 Originating RIB: inet.3 Metric: 30 Node path count: 1 Forwarding nexthops: 1 Next hop type: Router Next hop: 10.2.4.1 via et-0/0/1.0 weight 0x1 Session Id: 0x0 疎通試験 Single-AS の時と同様に、顧客 A の CE に相当する VM を両端で 2 台作成し、その間で ping による通信を確認しました。 前述の繰り返しですが、CE 側からは経路を確認できず ping の通信で確かめるため、コマンド出力は全く同じになります。掲載は省略します。 まとめ 本記事では Single-/Multi-AS SR-MPLS での L2VPN について解説しました。 VLAN-based で構築する際の、 EVI 等の設定や確認例を紹介しました。 機能実装に差があり完全な Multi-vendor ではありませんが、 Multi-AS での検証について紹介しました。 次回の記事では、 BGP color による Explicit-path TE を紹介予定です。 (2022/07/25 追記) 公開しました: [Multi-AS Segment Routing 検証連載 #4] Color-Based Steering in Single-AS Virtual Private Wire Service の略。仮想専用線サービス。 ↩ Virtual Private LAN Service の略。仮想専用 LAN サービス。 ↩ Multiprotocol BGP の略。BGP の拡張コミュニティを用いて追加情報を交換する仕組み。 ↩ Release Notes for Cisco ASR 9000 Series Routers, IOS XR Release 7.4.1 より、 Inter-AS EVPN Option B の新機能。 ↩
はじめに 本記事は前回の記事である「 ソフトウェア設計についてtwada技術顧問と話してみた 〜 A Philosophy of Software Design をベースに 〜 - NTT Communications Engineers' Blog 」の続編です。 前回の記事の内容がベースとなっていますので、「APoSD って何だっけ?」という場合はぜひ前回の記事をご覧になってから、以下にお進みください。 ということで、後編の対話パートにさっそく入っていきましょう! Pull Complexity Downwards iwashi: APoSD では、複雑性を下に追いやる(Pull Complexity Downwards)という話が出てきます。何らかの処理が複雑になる場合、それを隠蔽してインターフェースを極力シンプルに保つ、というのがAPoSDの主張です。 こちらに関しても、社内勉強会中で「議論があるよな」と twada さんがコメントされています。具体的に、どのような議論があるのでしょうか? twada: 「シンプルな実装よりも、シンプルなインタフェースを持つべきだ」に関しては、設計流派としては色々あるところです。 古くは、Worse is Better の話につながります。この話には、2つの設計の流派が出てきます。それぞれ「MIT/Stanfordアプローチ」と、「New Jerseyアプローチ」と呼ばれています。 両派とも「設計は実装とインターフェースの両面において単純でなければならない」と述べています。ここまではまあ当然のことですが、実装の単純さとインタフェースの単純さが両立できないときなどに両派の違いが出てきます。書籍『 The Art of UNIX Programming 』には次のように説明されています。 簡潔性 (Simplicity) MIT/Stanford: インターフェースが単純になることの方が、実装が単純になることより重要である New Jersey: 実装が単純な方が、インターフェースが単純なことより重要である。単純さが設計において最も重視されるべき事柄である APoSDでは、シンプルなインターフェイスで複雑な実装を隠蔽するのが良いと説いています。コードが多少複雑になろうと正しいことをせよ。コードの複雑さを抽象によって隠蔽し、認知負荷を下げよ。これがAPoSDでいう、Deep Moduleになります。 これは、MIT/Stanford アプローチと呼ばれている流派に近いように思えます。いま話しているうちに気がつきましたが、本書はStanford大学の教科書で、著者は同大学の教授でしたね。ですから、MIT/Stanfordアプローチに寄った意見になっているのも頷けます。 MIT/StanfordアプローチとNew Jerseyアプローチは、正当性や一貫性に関してもポリシーがやや異なります。書籍『 The Art of UNIX Programming 』にはこう記されています。 正当性 (Correctness) MIT/Stanford: 設計はすべての点において正しいものでなければならない。誤りは許されない New Jersey: 設計はすべての点において正しいものでなければならない。ただし、どちらかといえば、正しいことよりは単純なことの方が重要である 一貫性 (Consistency) MIT/Stanford: 設計は一貫性を欠いたものであってはならない。一貫性を保つためには完全性や単純さを多少犠牲にしてもよい。一貫性は正当性と同じぐらい重要である。 New Jersey: 設計は一貫性を大きく欠いたものであってはならない。単純さを保つために、一貫性は犠牲にされることがある。しかし、あまり起こらない状況に対応しようとして実装を複雑にしたり一貫性を欠いたものにするよりは、それを捨てる方がよい 他にもいろいろあるのですが、詳しくは書籍『The Art of UNIX Programming』や、私の講演資料( 過去を知り、未来に備える。技術選定の審美眼 2019 edition )を読んでみてください。 ( Worse Is Better - 過去を知り、未来に備える。技術選定の審美眼 2019 edition / Worse Is Better - Understanding the Spiral of Technologies 2019 edition P.35 より) もちろん APoSD は Worse Is Better 以降の議論を踏まえた内容になっていまして、なにより複雑性を最大の敵としていますから、「コードがいくら複雑になろうとも、正しいことをせよ」と言っている本ではないということは強調しておきます。複雑性を廃して、どう破綻なくソフトウェアを設計するかの本です。その上で、競合する制約に出会ったときに、どちらを取るか、またはどうバランスをとるかというところにポリシーが現れるのですね。 iwashi: このあたりの具体的な事例って、どのようなものがあるのでしょうか? twada: New Jersey派が結果的に生き残ったサンプルとしては、歴史的には、LinuxとMINIX、C言語とLisp、gitとbazaar-ngなどがあります。gitなどは典型例と言えそうですね。他には、MongoDB vs RethinkDBなどもそう考えられます。 OSSの世界においては、歴史的にはNew Jersey派が生き残ることも多かったと考えています。 iwashi: 何が理由で生き残ってきたのでしょうか? twada: 目の前の問題を解決しつつも、実装がシンプルなために、後日他の人が開発に参加できるハードルが下がるから、ですね。 iwashi: まさに、OSSと相性がいいですね。 twada: そういうことなんです。 実装の単純さを選択するときに、漏れのある抽象化があったとしても目の前の問題を解決できます。かつ実装がシンプルだと第三者が開発に参加するハードルが下がります。 これは、結果的には進化的な強さにつながります。つまり、変化に耐えて、生き残るための十分な数の目を得られる。 技術の歴史において、「なんでこっちの技術が生き残っているんだ?」みたいな話が出てきたときに、設計の一貫性・正しさ・美しさだけが生き残りを決めるファクターではない、という実例ですね。 New Jersey派はPrgramaticと表現できそうです。どちらかというとUnixカルチャー。Eric Raymond氏もそうですね。 その対立軸としてあるのが、MIT/Stanford アプローチであり「Do the Right Thing(正しいことをせよ)」という流派です。StanrfordのJohn Ousterhout教授もDo The Right Thing派に近いように思えますが、実はUncle Bobも同じ傾向がありますよね。 John Ousterhout教授とUncle Bobは批判し合っていて水と油のように見えますが、やや理想主義に寄っている点では似ています。現実よりは理想を選びがちであると。 iwashi: 世の中にあるソフトウェアを振り返って、「これってどっちに寄ってるんだろうな?」って考えるのは面白いかもしれないですね。 twada: そうですね。 実装をどれぐらい抽象で隠蔽すべきかというよりは、その発言をしている人はどういう影響下にいるのかが見えてくると誤読を避けやすくなりますし、過大評価も避けられるでしょう。 iwashi: 技術の歴史や、根っこにある流れを知っているとわかりやすいですね。 twada: 技術の歴史でいえば、もちろんMIT/Stanford流にインターフェースが優れていて生き残っている例もあるでしょう。 APoSDのShallowモジュールは、Functionalityが浅いものを意味します。New Jersey派は、過度に隠蔽しないことを重視します。漏れのある抽象化で良いので、あまり隠蔽しない方向になります。一方で、MIT/Stanford側はきちんと抽象化して認知負荷を下げる方向ですよね。 iwashi: どちらが唯一正しいわけではない、ってことですよね? twada: はい。なのでAPoSD本は、色々と本を読んでみて、自分の中で最適のバランスを探すための1つの本だ、と考えると良いです。 Uncle Bobの本を読んで「そこまではやらないけどこういう考え自体は大事だよね」と一歩引いて読むのと同じで、John Ousterhout教授はこんなことを書いているが、一歩引いて読むことができれば、設計の世界が広がる。極論があるから全体の設計空間は広がるわけです。 iwashi: そういう意味では、もし若者がAPoSDだけ読むと偏りすぎるかもしれないですね。 twada: 素晴らしい本ですが、ある意味1つの流派の総本山に連なる本でもあるので、それが正解かというとそうでもない。だから、「色々読んでみましょう」という話になります。 私自身、本読みとしては、意見の強い本は好きです。たとえば、Bertrand Meyerのオブジェクト指向入門や、アジャイル関係でいえばJim Coplienもそうですね。そういった戦うことを辞さないベテラン論客たちは視野を広げてくれます。 ある一方に心酔せず複数の意見を見て、自分の設計としてそのどこかにバランスを見出すのが大事です。特定の状況によって設計の流派を使い分けられます。 たとえば、OSSなら抽象を深くしすぎないで、第三者の参加がしやすいようにする方向性もあるでしょう。なぜなら、New Jersey派のほうが生き残りやすいだろうという算段があるためです。 それに対して、プロプライエタリな製品開発・クローズドな開発であれば、実装の単純さではなく設計の完全性や一貫性のほうが大事かもしれません。 どちらに設計の重きを置くか、というのは状況によって変わってくるわけですね。 ミッションクリティカルなシステムなのか、それともエンドユーザーが多くても一時的に落ちるのは許容されるのかでも、ソフトウェア設計で重視する内容は変わります。正確性が大事なのか、堅牢性が大事なのか。文脈によって大事にすべき品質特性は異なります。ソフトウェア設計に唯一の正解というものはないんです。 もちろん基本となるレベル、たとえば「可読性の高いコード」などはどこであっても正しいんです。そこから先で両立しにくい設計上の制約に面したときにどういう設計を選択していくかというのはソフトウェア設計の一番難しいところ。これは学んでいくしかないでしょう。 急がば回れで、失敗から繰り返して学ぶほうが多い分野です。おすすめなのは自分が設計したシステムを長い期間(たとえば2年以上)メンテナンスすることです。自分の設計判断がどういう結果になるのか、オーバーエンジニアリングやアンダーエンジニアリング、失敗や成功を繰り返すことで経験が増えるので、設計の打率が高まります。 iwashi: リーダブルコード のような書籍は汎用的ですよね。 twada: そうですね。一般的なプログラミング原則はどこでも適用可能です。たとえば、良い名前付けは、どのような状況下でも重要でしょう。 ただ、たとえば関数プログラミングとオブジェクト指向プログラミングはパラダイムが異なり、得意分野もやや異なります。「何が正しい」というよりは「向いている・向いてない」があります。ですから、自分の道具箱を増やすのが良いですね。 iwashi: 道具箱のツールを増やすために何をしたらいいでしょうか? twada: 書籍『 達人プログラマー 』に「年に1つプログラミング言語を学ぼう」という教えがあります。ちょっとずつパラダイムの違う言語をやるのがお勧めですね。 おわりに 本記事では、APoSDを起点として、MIT/StanfordアプローチとNew Jerseyアプローチについての対話を紹介しました。 当社では、このようなソフトウェア設計に興味があるソフトウェアエンジニアを募集しております! NTTコミュニケーションズ株式会社 ソフトウェアエンジニア の求人一覧 より、具体的なポストを是非ご確認ください!
はじめに DevOpsプラットフォームの取り組みを紹介する1回目の記事です。 こんにちは、イノベーションセンターでプラットフォームチームのアーキテクトをしている牧志 ( @JunMakishi ) です。NTTグループ向けのDevOpsプラットフォームである「 Qmonus (クモナス) Value Stream 」の開発をリードしています。 本日から、数週間にわたり、 Qmonus Value Stream チームの取り組みや利用技術を連載形式で紹介させていただきます。 第1回目の記事では、Qmonus Value Streamの概要を紹介します。 Qmonus (クモナス) Value Streamとは Qmonus Value Streamは、アプリケーション開発者が、ユーザにとって価値のある機能やサービスを継続的に提供するためのDevOpsプラットフォームです。Cloud Native Adapterと呼ばれる独自のInfrastructure as Code実装と、アプリケーションを商用環境にリリースするための継続的デリバリ機能を有しています。 Kubernetesを知っている方は、アプリケーション構成を管理するHelmと、CI/CDパイプライン機能をバンドルしたものをイメージいただくと良いと思います。 アプリケーション開発者は、Qmonus Value Streamを使うことで、クラウドやCI/CDに精通していなくても簡単にクラウドアーキテクチャのベストプラクティスを再利用してアプリケーションをデリバリできます。 Qmonus Value Streamの目的 ソフトウェアをアジリティ高く開発しながら安定運用するためには、クラウドコンピューティングの活用が必要不可欠です。しかし、 Cloud Native Landscape が拡大していっているように、クラウド技術は年々多様化・複雑化しており、クラウド技術を組み合わせて1つのアーキテクチャを組み上げるには高い専門性と多大な労力を必要とします。 NTTコミュニケーションズでは、多数のソフトウェア開発の現場でクラウド技術を活用していますが、個々のソフトウェアプロジェクトでクラウド技術の専門性を持つのは難易度が高いだけでなく、アーキテクチャガバナンスが効かないという課題がありました。私自身、以前、ネットワークコントローラ開発プロジェクトに所属していましたが、当時はソフトウェアデリバリやクラウド環境の管理のためだけに1つのサブチームを構成しており、主目的のコントローラアプリケーション開発以外にかかる労力は馬鹿になりませんでした。 こういったチームに代わってクラウド技術の複雑性を解決し、ユーザに価値を届けるアプリケーションの開発に注力できるよう、2020年から本格的にQmonus Value Streamの開発を開始しました。チームを超えて再利用できるInfrastructure as Codeを使って、早く簡単にクラウドを活用したアーキテクチャを構築し、アプリケーションをデリバリできるようにすることを目指しています。 Cloud Native Adapterによるベストプラクティスの提供 Cloud Native Adapterは、アプリケーションをクラウド基盤上で動作させる過程を仲介する独自のInfrastructure as Code実装です。商用環境で実績のあるシステム構成やCI/CDパイプラインを再利用可能な形でまとめることができ、アプリケーションと Cloud Native Adapter を組み合わせることで、ベストプラクティスに沿ったシステム構成やCI/CDを実現できます。 例えば、Qmonus Value Streamでは、Google Kubernetes EngineでWeb APIをセキュアに公開するためのシステム構成を、1つのCloud Native Adapterとして用意しています。このAdapterを使うだけで、Qmonus Value Streamの利用者は、コンテナを立ち上げてAPIを公開するためのマニフェストを生成できます。さらに、そのAPIにGCPのWAF/FirewallであるCloud Armorを適用して、安全にAPIを公開できます。 Cloud Native Adapterは、CUE言語で宣言的にシステム構成およびCI/CDパイプラインを記述します。CUE言語を活用することで、複数のAdapterを結合して複雑なアーキテクチャを実現できます。さらに、既存のAdapterに、独自のシステム仕様に合わせたロジックを追加することも可能です。 NTTグループで、ベストプラクティスを蓄積し育てていくことで、様々なアーキテクチャパターンを簡単に構築できる仕組みを確立し、かつアーキテクチャガバナンスを効かせることを目指しています。 アプリケーションのリリースパイプラインを統合管理 Qmonus Value Streamでは、Tektonと呼ばれるオープンソースのCI/CDフレームワークを活用し、ソフトウェアを商用環境にデリバリするためのCI/CD機能を提供しています。ユーザに価値を届けるアプリケーションを中心に据え、そのアプリケーションを開発してから商用環境にリリースするまでの一連の流れを統合管理できます。 以下の図の例のように、マージしたコードからソフトウェアをビルドし結合環境にデプロイする「ビルドパイプライン」と、総合試験環境や商用環境にソフトウェアをリリースする「リリースパイプライン」を構成し、それらを連結できます。結合環境で継続的に試験してソフトウェアを常にデプロイ可能な状態に保ち、任意のタイミングで試験を通過したソフトウェアを"Promote"して、商用環境にリリースできます。 Qmonus Value Streamによって、アプリケーションをユーザに届けるまでの一連の流れ、すなわちバリューストリームを自動化・管理することで、安全でアジリティの高いDevOpsプロセスを組織に適用することを目指しています。 Qmonus Value Streamの導入効果 Qmonus Value Streamの本格開発を開始した2020年以降、徐々にNTTコミュニケーションズ内で導入を進めてきました。2022年3月末時点で18のアクティブなソフトウェアプロジェクトで利用され、1月あたり100以上のソフトウェアリリースに活用されています。また、2022年4月からは、NTTグループ各社への提供を開始し、さらに利用が拡大しています。 Cloud Native Adapterとそれを使ってアプリケーションを継続的に商用環境にデリバリする仕組みを使うことで、アプリケーション開発者は、簡単にクラウドのベストプラクティスを活用でき、もっとも注力したいアプリケーションの開発とそのリリースに集中できます。その結果が、リリース回数という数字に表れていると考えています。 おわりに DevOpsプラットフォームの取り組みを紹介する1回目の記事として、NTTグループ向けのDevOpsプラットフォームQmonus Value Streamの概要を紹介させていただきました。fukabori.fmでも本プラットフォームについて紹介する機会をいただいたので、よかったらこちらも参考にしてください。( 50. 社内DevOps基盤、Tekton、Cuelang w/ JunMakishi ) Qmonus Value Streamチームは、メンバひとりひとりが、利用者であるアプリケーション開発者のために信念を持って活動を続けています。プラットフォームを内製開発するポジション、プラットフォームを使ってNTTグループのクラウド基盤やCI/CDを構築支援するポジションそれぞれで、一緒に働く仲間を募集していますので、興味を持たれた方は応募いただけると嬉しいです。 https://hrmos.co/pages/nttcom0033/jobs/0000019 https://hrmos.co/pages/nttcom0033/jobs/1692796 次回以降、Qmonus Value Streamチームの具体的な取り組みや要素技術を紹介していきます。お楽しみに!
サマリ Multi-AS で構成されるネットワークにおいて、 SR-MPLS + VPNv4 による L3VPN を実現 IOS XR + Junos の Multi-vendor 環境で動作検証に成功 この記事は Multi-AS SR 検証連載の第 2 回です。目次は こちら 概要 イノベーションセンターの岩田です。普段の業務では Multi-AS Segment Routing に関する技術検証や、ベンチャー企業への技術支援でスマートフォンアプリケーション開発業務などを行なっています。 本記事では我々の検証のメインターゲットである Multi-AS/Multi-domain SR-MPLS を用いた L3VPN についてご紹介します。 第 0 回の記事 でもご紹介した通り、我々は Multi-AS/Multi-domain SR により拡張性のあるネットワークの実現について検討、検証しています。本記事の流れとしては、まずアンダーレイを複数の AS やドメインに分割する利点について述べ、分割した AS をどのように接続するかについて検討します。その後、Multi-AS/Multi-domain SR-MPLS を用いた L3VPN の Multi-vendor 環境における検証例を、サンプルトポロジーや設定例を添えながら紹介します。 L3VPN や Segment Routing については 第 1 回の記事 で説明しているので、あわせてご確認ください。 アンダーレイを Multi-AS/Multi-domain に分ける利点 本節では、Multi-AS/Multi-domain で アンダーレイ(Underlay)を構築する利点について説明します。 アンダーレイを Multi-AS/Multi-domain に分けて構築する最大の利点は、アンダーレイの拡張性や保守性が担保される点です。 アンダーレイ の拡張性の観点では、単一の SR domain を構築する際に IP 重複できない制約があるため、プライベート IP を使用している場合だと規模の拡大とともに設計の制約が増加する、という課題への対処が必要になります。AS を分割することによって AS 間での IP 重複を考えずにすみます。 また保守性の観点からも、大規模ネットワークを IGP で管理している場合、機器トラブルなどが発生しルーティングに影響があった際の罹障範囲が大きくなるという課題があります。こちらも AS や SR domain を分割することで罹障範囲を局所化できます。 このように AS や SR domain を複数に分割することで、設計の自由度を保ち IGP や IBGP の影響範囲を局所化できるため、拡張性と保守性のあるネットワークが構築できます。 Inter-AS の接続方式について Multi-AS で MPLS VPN を行うためには Inter-AS の接続方式について考える必要があります。 本節では、RFC にて標準化されている 3 つの接続方式とそれらを組み合わせた方式について説明し、今回どの方式を採用するかについて説明します。 接続方式の選定方針は前節で説明した通り、設計の自由度を保ち IGP や IBGP の影響範囲を局所化し、拡張性と保守性のあるネットワークが構築できるかどうかとします。 Multi-AS で MPLS VPN を実現する方法としては RFC4364 で標準化されている Option A、B、C の3つの方式と、A と B を組み合わせた方式 D が知られています。 それぞれ以下のような特徴があります。 Option A ASBR で顧客毎に サブインタフェースと VRF を作成し EBGP セッション経由で経路を交換する ASBR 間の各サブインタフェース間では単純な IP パケットを転送し AS を接続する ASBR 間に顧客の数だけサブインターフェースを作成する必要がある EBGP セッションは各 VRF で行うため、各サブインタフェースに設定が必要になる Option B ASBR で単一のインタフェースを用いて EBGP セッションを張り VPN 経路を交換する ASBR で VPN ラベルを Swap し ASBR 間を別のラベルを用いて転送し AS を接続する Option C VPN 経路を RR 経由で交換し、互いのアンダーレイ情報を共有する ASBR に VRF が不要 Multi-AS で 単一の SR domain を構築できる アンダーレイを共有するため制約が増え、前述の Multi-AS の利点のうちの拡張性は減少する Option D(A+B) ASBR で単一のインタフェースを用いて EBGP セッションを張り VPN 経路を交換する (B と同様) ASBR 間は顧客毎に作成したサブインタフェース間で単純な IP パケットの転送をすることで AS を接続する (A と同様) ASBR 間に顧客の数だけサブインターフェースを作成する必要がある(A と同様) VPN 経路の交換は単一インタフェースを用いて行うため各インタフェースへの設定が不要(A ではインタフェース毎) まとめますと以下のようになります。 A : ASBR に顧客ごとのサブインターフェースや EBGP セッションなどの設定が必要 B : AS が制限少なく独立して存在でき、 ASBR 間も単一のセッションになる C :アンダーレイ が AS 間で広告されるため、今回の目的に不向き A+B(D): A の状況のうちサブインターフェースの設定が同様に残る その中でも、今回は Option B を採用しました。IGP、IBGP の責任範囲を疎に分離して運用できる方式であり、拡張性・保守性が他の Option に比べて高いことが選択理由です。 ただし機器と SR、VPN、Option-B を組み合わせて ASBR として用いるにはサポート状況に差があります。 L3VPN については、我々が検証を始めた時点での ASR9901(IOS XR 7.3.1) や MX204(Junos 21.2)で既にサポートされています。 しかし L2VPN(EVPN)は IOS XR 7.4.1 1 からの対応で、Junos は 2022 年 6 月時点で未対応です。 検証例の紹介 以降は L3VPN の検証例を実際の設定例を添えつつ紹介します。 サンプルトポロジー&想定するユースケース 検証のため、下記のようなトポロジーを作成します。 3 つのルーターによる AS を 2 つ接続したコア網で、顧客 A と B を収容します。 検証環境は Multi-vendor に構成します。使用する機器は下記の通りです。 SR Domain 1 PE1、ASBR1: Cisco IOS XR 7.4.1 ASBR2: Junos OS 21.3R1.9 SR Domain 2 ASBR3: Cisco IOS XR 7.4.1 PE2、ASBR4: Junos OS 21.3R1.9 この後は下記の流れに従って設定を紹介します。 IS-IS 設定と状態確認 VRF と MP-BGP 設定と状態確認 PE 間の ping 疎通試験 IS-IS - 設定 IS-IS の SR 拡張を利用し、SR-MPLS の SID 情報を広告します。 また、運用効率化のため MPLS OAM の設定もしておきます。 IOS XR(例: PE1) router isis 1 is-type level-2-only net 49.0000.0000.0aff.0001.00 segment-routing global-block 16000 23999 address-family ipv4 unicast metric-style wide segment-routing mpls ! interface Loopback0 address-family ipv4 unicast prefix-sid index 1 ! ! interface GigabitEthernet0/0/0/0 point-to-point address-family ipv4 unicast ! ! interface GigabitEthernet0/0/0/1 point-to-point address-family ipv4 unicast ! ! ! segment-routing ! Junos(例: PE2) set protocols isis interface ge-0/0/0.0 level 2 metric 10 set protocols isis interface ge-0/0/0.0 point-to-point set protocols isis interface ge-0/0/1.0 level 2 metric 10 set protocols isis interface ge-0/0/1.0 point-to-point set protocols isis interface lo0.0 passive set protocols isis source-packet-routing srgb start-label 16000 set protocols isis source-packet-routing srgb index-range 8000 set protocols isis source-packet-routing node-segment ipv4-index 1 set protocols isis level 1 disable set protocols isis level 2 wide-metrics-only set interfaces ge-0/0/0 unit 0 family inet address 10.0.0.1/30 set interfaces ge-0/0/0 unit 0 family iso set interfaces ge-0/0/0 unit 0 family mpls set interfaces ge-0/0/1 unit 0 family inet address 10.0.1.1/30 set interfaces ge-0/0/1 unit 0 family iso set interfaces ge-0/0/1 unit 0 family mpls set interfaces lo0 unit 0 family inet address 10.255.0.1/32 set interfaces lo0 unit 0 family iso address 49.0000.0000.0aff.0001.00 SR Global Block(SRGB)の変更後は再起動が必要です。 IS-IS - 動作確認 IS-IS ネイバーの状態と SID Table を確認します。 IOS XR RP/0/RP0/CPU0:pe1#show isis neighbors Fri Jun 17 13:37:40.431 JST IS-IS 1 neighbors: System Id Interface SNPA State Holdtime Type IETF-NSF asbr1 Gi0/0/0/0 *PtoP* Up 24 L2 Capable asbr2 Gi0/0/0/1 *PtoP* Up 20 L2 Capable Total neighbor count: 2 RP/0/RP0/CPU0:pe1#show mpls forwarding Fri Jun 17 13:37:45.932 JST Local Outgoing Prefix Outgoing Next Hop Bytes Label Label or ID Interface Switched ------ ----------- ------------------ ------------ --------------- ------------ 16002 Pop SR Pfx (idx 2) Gi0/0/0/0 10.0.0.2 0 16003 Pop SR Pfx (idx 3) Gi0/0/0/1 10.0.1.2 324511 24000 Pop SR Adj (idx 1) Gi0/0/0/0 10.0.0.2 0 24001 Pop SR Adj (idx 3) Gi0/0/0/0 10.0.0.2 0 24004 Pop SR Adj (idx 1) Gi0/0/0/1 10.0.1.2 0 24005 Pop SR Adj (idx 3) Gi0/0/0/1 10.0.1.2 0 Junos user@pe2> show isis adjacency Interface System L State Hold (secs) SNPA ge-0/0/0.0 asbr3 2 Up 28 ge-0/0/1.0 asbr4 2 Up 26 user@pe2> show route protocol isis table mpls.0 mpls.0: 14 destinations, 14 routes (14 active, 0 holddown, 0 hidden) + = Active Route, - = Last Active, * = Both 18 *[L-ISIS/14] 1d 03:49:30, metric 0 > to 10.0.1.2 via ge-0/0/1.0, Pop 18(S=0) *[L-ISIS/14] 1d 03:49:30, metric 0 > to 10.0.1.2 via ge-0/0/1.0, Pop 19 *[L-ISIS/14] 1d 03:48:12, metric 0 > to 10.0.0.2 via ge-0/0/0.0, Pop 19(S=0) *[L-ISIS/14] 1d 03:48:12, metric 0 > to 10.0.0.2 via ge-0/0/0.0, Pop 16002 *[L-ISIS/14] 00:08:15, metric 20 > to 10.0.0.2 via ge-0/0/0.0, Pop 16002(S=0) *[L-ISIS/14] 00:08:15, metric 20 > to 10.0.0.2 via ge-0/0/0.0, Pop 16003 *[L-ISIS/14] 1d 03:49:30, metric 10 > to 10.0.1.2 via ge-0/0/1.0, Pop 16003(S=0) *[L-ISIS/14] 1d 03:49:30, metric 10 > to 10.0.1.2 via ge-0/0/1.0, Pop Static Route(IOS XR) - 機器設定 ※こちらハマりポイントなのでご注意ください。 IOS XR で Inter-AS を行う際には、ASBR のピアの片方で /32 の Static Route を設定する必要があります。 これを行わないとコントロールプレーンは立ち上がりますがトラフィックが転送されません。 詳しくは こちら をご参照ください。 router static address-family ipv4 unicast 10.100.0.2/32 GigabitEthernet0/0/0/2 ! ! Static Route(IOS XR) - 動作確認 RP/0/RP0/CPU0:asbr1#show route static Fri Jun 17 13:29:28.349 JST S 10.100.0.2/32 is directly connected, 1d02h, GigabitEthernet0/0/0/2 VRF、MP-BGP VPNv4 - 機器設定 顧客 A、B の VRF 設定と、経路を広告する MP-BGP L3VPN を設定します。 VPN の各パラメータは下記の通りです。 SR Domain 1 AS 番号: 65000 VRF 100: RD/RT 65000:100 AS 間での VPN 経路広告用 RT: 64999:100 VRF 200: RD/RT 65000:200 AS 間での VPN 経路広告用 RT: 64999:200 SR Domain 2 AS 番号: 65001 VRF 100: RD/RT 65001:100 AS 間での VPN 経路広告用 RT: 64999:100 VRF 200: RD/RT 65001:200 AS 間での VPN 経路広告用 RT: 64999:200 PE(IOS XR : PE1) vrf 100 description for simple L3VPN rd 65000:100 address-family ipv4 unicast import route-target 64999:100 65000:100 ! export route-target 64999:100 65000:100 ! ! ! vrf 200 description for simple L3VPN rd 65000:200 address-family ipv4 unicast import route-target 64999:200 65000:200 ! export route-target 64999:200 65000:200 ! ! ! interface GigabitEthernet0/0/0/2 vrf 100 ipv4 address 192.168.0.254 255.255.255.0 ! interface GigabitEthernet0/0/0/3 vrf 200 ipv4 address 192.168.0.254 255.255.255.0 ! router bgp 65000 bgp router-id 10.255.0.1 address-family vpnv4 unicast ! neighbor-group ibgp remote-as 65000 update-source Loopback0 address-family vpnv4 unicast ! ! neighbor 10.255.0.2 use neighbor-group ibgp ! neighbor 10.255.0.3 use neighbor-group ibgp ! vrf 100 rd 65000:100 address-family ipv4 unicast label mode per-vrf redistribute connected ! ! vrf 200 rd 65000:200 address-family ipv4 unicast label mode per-vrf redistribute connected ! ! ! PE(Junos : PE2) set policy-options policy-statement EXPORT-POLICY-100 term ROUTE-TARGET then community add VRF100-65001-RT set policy-options policy-statement EXPORT-POLICY-100 term ROUTE-TARGET then community add SHARED-100-RT set policy-options policy-statement EXPORT-POLICY-100 term REDIST-DIRECT from protocol direct set policy-options policy-statement EXPORT-POLICY-100 term REDIST-DIRECT then accept set policy-options policy-statement EXPORT-POLICY-200 term ROUTE-TARGET then community add VRF200-65001-RT set policy-options policy-statement EXPORT-POLICY-200 term ROUTE-TARGET then community add SHARED-200-RT set policy-options policy-statement EXPORT-POLICY-200 term REDIST-DIRECT from protocol direct set policy-options policy-statement EXPORT-POLICY-200 term REDIST-DIRECT then accept set policy-options policy-statement IMPORT-POLICY-100 term ROUTE-TARGET-65001 from community VRF100-65001-RT set policy-options policy-statement IMPORT-POLICY-100 term ROUTE-TARGET-65001 then accept set policy-options policy-statement IMPORT-POLICY-100 term ROUTE-TARGET-SHARED from community SHARED-100-RT set policy-options policy-statement IMPORT-POLICY-100 term ROUTE-TARGET-SHARED then accept set policy-options policy-statement IMPORT-POLICY-200 term ROUTE-TARGET-65001 from community VRF200-65001-RT set policy-options policy-statement IMPORT-POLICY-200 term ROUTE-TARGET-65001 then accept set policy-options policy-statement IMPORT-POLICY-200 term ROUTE-TARGET-SHARED from community SHARED-200-RT set policy-options policy-statement IMPORT-POLICY-200 term ROUTE-TARGET-SHARED then accept set policy-options community SHARED-100-RT members target:64999:100 set policy-options community SHARED-200-RT members target:64999:200 set policy-options community VRF100-65001-RT members target:65001:100 set policy-options community VRF200-65001-RT members target:65001:200 set routing-instances 100 instance-type vrf set routing-instances 100 protocols bgp family inet unicast set routing-instances 100 interface ge-0/0/2.0 set routing-instances 100 route-distinguisher 65001:100 set routing-instances 100 vrf-import IMPORT-POLICY-100 set routing-instances 100 vrf-export EXPORT-POLICY-100 set routing-instances 100 vrf-table-label set routing-instances 200 instance-type vrf set routing-instances 200 protocols bgp family inet unicast set routing-instances 200 interface ge-0/0/3.0 set routing-instances 200 route-distinguisher 65001:200 set routing-instances 200 vrf-import IMPORT-POLICY-200 set routing-instances 200 vrf-export EXPORT-POLICY-200 set routing-instances 200 vrf-table-label set routing-options router-id 10.255.0.1 set routing-options autonomous-system 65001 set protocols router-advertisement interface fxp0.0 set protocols bgp family inet unicast set protocols bgp family inet-vpn unicast set protocols bgp group ibgp type internal set protocols bgp group ibgp local-address 10.255.0.1 set protocols bgp group ibgp family inet-vpn unicast set protocols bgp group ibgp neighbor 10.255.0.2 set protocols bgp group ibgp neighbor 10.255.0.3 ASBR(IOS XR : ASBR1) vrf 100 description for simple L3VPN rd 65000:100 address-family ipv4 unicast import route-target 64999:100 65000:100 ! export route-target 64999:100 65000:100 ! ! ! vrf 200 description for simple L3VPN rd 65000:200 address-family ipv4 unicast import route-target 64999:200 65000:200 ! export route-target 64999:200 65000:200 ! ! ! router bgp 65000 bgp router-id 10.255.0.2 address-family vpnv4 unicast ! neighbor-group ibgp remote-as 65000 update-source Loopback0 address-family vpnv4 unicast next-hop-self ! ! neighbor-group 65001 remote-as 65001 address-family vpnv4 unicast route-policy pass-all in route-policy pass-all out ! ! neighbor 10.100.0.2 use neighbor-group 65001 ! neighbor 10.255.0.1 use neighbor-group ibgp ! neighbor 10.255.0.3 use neighbor-group ibgp ! vrf 100 rd 65000:100 address-family ipv4 unicast label mode per-vrf ! ! vrf 200 rd 65000:200 address-family ipv4 unicast label mode per-vrf ! ! ! ASBR(Junos : ASBR4) set policy-options policy-statement EXPORT-POLICY-100 term ROUTE-TARGET then community add VRF100-65001-RT set policy-options policy-statement EXPORT-POLICY-100 term ROUTE-TARGET then community add SHARED-100-RT set policy-options policy-statement EXPORT-POLICY-200 term ROUTE-TARGET then community add VRF200-65001-RT set policy-options policy-statement EXPORT-POLICY-200 term ROUTE-TARGET then community add SHARED-200-RT set policy-options policy-statement IMPORT-POLICY-100 term ROUTE-TARGET-65001 from community VRF100-65001-RT set policy-options policy-statement IMPORT-POLICY-100 term ROUTE-TARGET-65001 then accept set policy-options policy-statement IMPORT-POLICY-100 term ROUTE-TARGET-SHARED from community SHARED-100-RT set policy-options policy-statement IMPORT-POLICY-100 term ROUTE-TARGET-SHARED then accept set policy-options policy-statement IMPORT-POLICY-200 term ROUTE-TARGET-65001 from community VRF200-65001-RT set policy-options policy-statement IMPORT-POLICY-200 term ROUTE-TARGET-65001 then accept set policy-options policy-statement IMPORT-POLICY-200 term ROUTE-TARGET-SHARED from community SHARED-200-RT set policy-options policy-statement IMPORT-POLICY-200 term ROUTE-TARGET-SHARED then accept set policy-options community SHARED-100-RT members target:64999:100 set policy-options community SHARED-200-RT members target:64999:200 set policy-options community VRF100-65001-RT members target:65001:100 set policy-options community VRF200-65001-RT members target:65001:200 set routing-instances 100 instance-type vrf set routing-instances 100 protocols bgp family inet unicast set routing-instances 100 route-distinguisher 65001:100 set routing-instances 100 vrf-import IMPORT-POLICY-100 set routing-instances 100 vrf-export EXPORT-POLICY-100 set routing-instances 100 vrf-table-label set routing-instances 200 instance-type vrf set routing-instances 200 protocols bgp family inet unicast set routing-instances 200 route-distinguisher 65001:200 set routing-instances 200 vrf-import IMPORT-POLICY-200 set routing-instances 200 vrf-export EXPORT-POLICY-200 set routing-instances 200 vrf-table-label set routing-options router-id 10.255.0.3 set routing-options autonomous-system 65001 set protocols router-advertisement interface fxp0.0 set protocols bgp family inet-vpn unicast set protocols bgp group ibgp type internal set protocols bgp group ibgp local-address 10.255.0.3 set protocols bgp group ibgp neighbor 10.255.0.1 set protocols bgp group 65000 type external set protocols bgp group 65000 peer-as 65000 set protocols bgp group 65000 neighbor 10.100.1.1 MP-BGP VPNv4 - 動作確認 下記3ステップで L3VPN の経路学習と転送動作を確認します。 今回は各ベンダーの ASBR の組で動作確認がしたいので、選択させたくない ASBR 間のリンクを落とすことで選択させます。 動作確認したい ASBR の組以外の ASBR 間のリンクを落とす BGP から受信した経路を確認 ping による疎通確認 IOS XR の ASBR(ASBR1->ASBR3)を利用する場合 顧客 A(VRF100)が利用する場合を想定し動作確認をしていきます。 Junos の ASBR 間(ASBR2<->ASBR4)のリンクを落とします。 user@asbr2> configure Entering configuration mode [edit] user@asbr2# set interfaces ge-0/0/2 disable [edit] user@asbr2# show | compare [edit interfaces ge-0/0/2] + disable; [edit] user@asbr2# commit commit complete user@asbr2# exit Exiting configuration mode user@asbr2> ping 10.100.1.2 count 5 PING 10.100.1.2 (10.100.1.2): 56 data bytes ^C --- 10.100.1.2 ping statistics --- 5 packets transmitted, 0 packets received, 100% packet loss 次に、PE1(IOS XR) が受信した経路を確認します。 RP/0/RP0/CPU0:pe1#show bgp vpnv4 unicast vrf 100 192.168.1.0/24 detail Mon Jun 20 14:14:13.121 JST BGP routing table entry for 192.168.1.0/24, Route Distinguisher: 65000:100 Versions: Process bRIB/RIB SendTblVer Speaker 62 62 Flags: 0x00001001+0x00000000; Last Modified: Jun 20 14:12:27.073 for 00:01:46 Paths: (1 available, best #1) Not advertised to any peer Path #1: Received by speaker 0 Flags: 0x2000000005060005, import: 0x80 Not advertised to any peer 65001 10.255.0.2 (metric 20) from 10.255.0.2 (10.255.0.2), if-handle 0x00000000 Received Label 24007 Origin IGP, localpref 100, valid, internal, best, group-best, import-candidate, imported Received Path ID 0, Local Path ID 1, version 62 Extended community: RT:64999:100 RT:65001:100 Source AFI: VPNv4 Unicast, Source VRF: default, Source Route Distinguisher: 65001:100 RP/0/RP0/CPU0:pe1#show route vrf 100 Mon Jun 20 12:35:07.389 JST Codes: C - connected, S - static, R - RIP, B - BGP, (>) - Diversion path D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2 E1 - OSPF external type 1, E2 - OSPF external type 2, E - EGP i - ISIS, L1 - IS-IS level-1, L2 - IS-IS level-2 ia - IS-IS inter area, su - IS-IS summary null, * - candidate default U - per-user static route, o - ODR, L - local, G - DAGR, l - LISP A - access/subscriber, a - Application route M - mobile route, r - RPL, t - Traffic Engineering, (!) - FRR Backup path Gateway of last resort is not set C 192.168.0.0/24 is directly connected, 22:01:15, GigabitEthernet0/0/0/2 L 192.168.0.254/32 is directly connected, 22:01:15, GigabitEthernet0/0/0/2 B 192.168.1.0/24 [200/0] via 10.255.0.2 (nexthop in vrf default), 00:08:01 経路が受信できている事と、ASBR1(IOS XR)が選択されていることが確認できました。 ping で疎通確認を行なってみます。 RP/0/RP0/CPU0:pe1#ping 192.168.1.254 vrf 100 Mon Jun 20 12:33:13.840 JST Type escape sequence to abort. Sending 5, 100-byte ICMP Echos to 192.168.1.254, timeout is 2 seconds: !!!!! Success rate is 100 percent (5/5), round-trip min/avg/max = 2/6/12 ms パケットが転送でき、VPN が構築できていることが確認できました。 Junos の ASBR(ASBR2->ASBR4)を利用する場合 今度は顧客 B(VRF200)が利用する場合を想定し動作確認をしていきます。 まず、IOS XR の ASBR 間(ASBR1<->ASBR3)のリンクを落とします。 RP/0/RP0/CPU0:asbr1#configure Mon Jun 20 12:40:44.843 JST RP/0/RP0/CPU0:asbr1(config)#interface gigabitEthernet 0/0/0/2 RP/0/RP0/CPU0:asbr1(config-if)#shutdown RP/0/RP0/CPU0:asbr1(config-if)#show commit changes diff Mon Jun 20 12:41:02.632 JST Building configuration... !! IOS XR Configuration 7.4.1 + interface GigabitEthernet0/0/0/2 + shutdown ! end RP/0/RP0/CPU0:asbr1(config-if)#commit Mon Jun 20 12:41:06.779 JST RP/0/RP0/CPU0:asbr1(config-if)#exit RP/0/RP0/CPU0:asbr1(config)#exit RP/0/RP0/CPU0:asbr1#ping 10.100.0.2 Mon Jun 20 12:41:56.670 JST Type escape sequence to abort. Sending 5, 100-byte ICMP Echos to 10.100.0.2, timeout is 2 seconds: UUUUU Success rate is 0 percent (0/5) 次に、Junos が受信した経路を確認します。 user@pe2> show route advertising-protocol bgp 10.255.0.3 detail 100.inet.0: 3 destinations, 3 routes (3 active, 0 holddown, 0 hidden) * 192.168.1.0/24 (1 entry, 1 announced) BGP group ibgp type Internal Route Distinguisher: 65001:100 VPN Label: 16 Nexthop: Self Flags: Nexthop Change Localpref: 100 AS path: [65001] I Communities: target:64999:100 target:65001:100 200.inet.0: 3 destinations, 3 routes (3 active, 0 holddown, 0 hidden) * 192.168.1.0/24 (1 entry, 1 announced) BGP group ibgp type Internal Route Distinguisher: 65001:200 VPN Label: 17 Nexthop: Self Flags: Nexthop Change Localpref: 100 AS path: [65001] I Communities: target:64999:200 target:65001:200 hanabi@pe2> show route receive-protocol bgp 10.255.0.3 table bgp.l3vpn.0 detail bgp.l3vpn.0: 2 destinations, 4 routes (2 active, 0 holddown, 0 hidden) * 65000:100:192.168.0.0/24 (2 entries, 0 announced) Import Accepted Route Distinguisher: 65000:100 VPN Label: 16 Nexthop: 10.255.0.3 Localpref: 100 AS path: 65000 ? Communities: target:64999:100 target:65000:100 * 65000:200:192.168.0.0/24 (2 entries, 0 announced) Import Accepted Route Distinguisher: 65000:200 VPN Label: 17 Nexthop: 10.255.0.3 Localpref: 100 AS path: 65000 ? Communities: target:64999:200 target:65000:200 user@pe2> show route 192.168.0.0/24 100.inet.0: 3 destinations, 3 routes (3 active, 0 holddown, 0 hidden) + = Active Route, - = Last Active, * = Both 192.168.0.0/24 *[BGP/170] 00:49:52, localpref 100, from 10.255.0.3 AS path: 65000 ?, validation-state: unverified > to 10.0.1.2 via ge-0/0/1.0, Push 16 200.inet.0: 3 destinations, 3 routes (3 active, 0 holddown, 0 hidden) + = Active Route, - = Last Active, * = Both 192.168.0.0/24 *[BGP/170] 00:49:52, localpref 100, from 10.255.0.3 AS path: 65000 ?, validation-state: unverified > to 10.0.1.2 via ge-0/0/1.0, Push 17 bgp.l3vpn.0: 2 destinations, 2 routes (2 active, 0 holddown, 0 hidden) + = Active Route, - = Last Active, * = Both 65000:100:192.168.0.0/24 *[BGP/170] 00:49:52, localpref 100, from 10.255.0.3 AS path: 65000 ?, validation-state: unverified > to 10.0.1.2 via ge-0/0/1.0, Push 16 65000:200:192.168.0.0/24 *[BGP/170] 00:49:52, localpref 100, from 10.255.0.3 AS path: 65000 ?, validation-state: unverified > to 10.0.1.2 via ge-0/0/1.0, Push 17 経路が受信できている事と、ASBR4(Junos)の ループバックアドレス(10.0.1.2)が nexthop として選択されていることが確認できました。 ping で疎通確認を行なってみます。 user@pe2> ping 192.168.0.254 routing-instance 200 count 5 PING 192.168.0.254 (192.168.0.254): 56 data bytes 64 bytes from 192.168.0.254: icmp_seq=0 ttl=253 time=4.264 ms 64 bytes from 192.168.0.254: icmp_seq=1 ttl=253 time=10.481 ms 64 bytes from 192.168.0.254: icmp_seq=2 ttl=253 time=11.079 ms 64 bytes from 192.168.0.254: icmp_seq=3 ttl=253 time=6.493 ms 64 bytes from 192.168.0.254: icmp_seq=4 ttl=253 time=3.516 ms --- 192.168.0.254 ping statistics --- 5 packets transmitted, 5 packets received, 0% packet loss round-trip min/avg/max/stddev = 3.516/7.167/11.079/3.114 ms パケットが転送でき、VPN が構築できていることが確認できました。 これで、ASBR が IOS XR の場合と、Junos の場合共に、VPN が構築できていることが確認できました。 まとめ 本記事では、 Multi-AS/Multi-domain での アンダーレイ 構築の利点、SR-MPLS を用いた L3VPN 実装時の Inter-AS 方式を紹介するとともに、 Multi-vendor 環境における Multi-AS での L3VPN の検証結果を紹介しました。 次回の記事では、SR-MPLS EVPN in Single/Multi-AS について紹介予定です。 (2022/07/11 追記) 公開しました: [Multi-AS Segment Routing 検証連載 #3] SR-MPLS L2VPN (EVPN) in Single-/Multi-AS 参考資料 この検証の一部は弊チームの田島、三島がそれぞれ MPLS JAPAN 2021 、社外勉強会で発表しました。以下に資料を公開していますので併せてご覧ください。 マルチASかつマルチベンダにおけるSR-MPLS TEの実現 (田島) マルチAS/マルチドメインなSegment Routingの実現 (三島) Release Notes for Cisco ASR 9000 Series Routers, IOS XR Release 7.4.1 https://www.cisco.com/c/en/us/td/docs/routers/asr9000/software/asr9k-r7-4/general/release/notes/b-release-notes-asr9000-r741.html ↩
はじめに はじめまして。 プラットフォームサービス本部 データプラットフォームサービス部門の森分です。 もともと私は、NTT Comのクラウドサービスをベースにした法人向けソリューションの個社別運用やインフラ関連のプロジェクトマネージャ業務を担当しておりました。 最近はSmart Data Platform(以下、SDPF)アーキテクトなる、お客様課題の解決やNTT Comのビジネスの中でSDPFの活用を推進する部隊に参画しています。 データ利活用を支えるSDPFのアーキテクトがデータ利活用に詳しくなければ立つ瀬がありません。 そうならないように日々研鑽を積んでいるわけですが、その中で作ったTwitter分析システムっぽいもののご紹介が本稿の趣旨となります。 本来のデータ利活用プロジェクトでは、課題および仮説をまず明確にして、それに応じたデータ解析を進めていくのですが、本稿では堅苦しいものは全部すっとばしたゆるい雰囲気でお送りします。 きっかけ 私の所属するチームは、某自治体のお客様と地域活性化のためデータ利活用するプロジェクトに参画しています。 そのノウハウをもっと広く遍く、特に中小規模の自治体向けにデータ利活用サービスとして提供できないか検討する営みがありました。 その中で、お客様が住民の思っていることをアクティブに汲み取る(更には分析する)手段がないか…と考えたときにTwitterを分析してみてはとのインスタントアイディアがきっかけです。 …というのは後付けで、リアルタイムで情報が追加されているデータを可視化できたらかっこいいんじゃんとの思いで手を動かし始めました。 実際のデータ活用基盤でもデータ更新などの運用が問題になってくることが多いので、リアルタイム更新の事例を作りにいきました。 やりたいこと 自治体の担当者様が使うことを妄想すると以下の要件がまずありそうです。 対象の自治体エリア付近で情報を絞り込めること 住民がどんなことをつぶやいているかの傾向が手間を掛けずにわかること まずは1.をかなえるため、位置情報が付属しているツイートを分析の対象としました。 Twitterでは、本文の内容から類推する以外だと、位置情報くらいしか地方自治体に紐づける属性がなさそうだったためです。 全体ツイートから見ると位置情報をもつツイートはかなり限られますが、そこそこあるだろうとのなかば見切り発車しました。 2.については、ワードクラウドを使って、頻出しているワードを可視化し、ツイートの全体傾向をざっくり掴みに行く方針としました。 つくってみた 3分クッキング方式となり恐縮ですが、某自治体のお客様の取り組みの中で、Tableauで参照できるデータベース(PostgreSQL)を構築した検証環境があり、そちらを間借りさせていただきます。 可視化については、BIに特化したデータ視覚化ツールであるTableauのおかげで盤石なので、どれだけちゃんとしたデータを準備できるかが勝負です。 簡単ですが、システム概要図です。 やっていること Twitter Developerアカウントは無料アカウントを使用 プログラム実行環境は踏み台サーバ(CentOS)にPython環境を作って実装 ツイート取得はTwitterAPIでのTwitter操作を簡単に実装できるPythonライブラリであるtweepyのStreamListenerをベースに改造 取得するツイートは緯度経度でフィルター。日本を覆うくらいの 適用に 広めで設定。 ツイートから必要な情報を掻い摘んで、DataFrameに放り込んでいく DataFrameをPostgreSQLのtweetテーブルに放り込む ツイート取得スクリプトは5分毎にcronで起動させ、エラー耐性を無理やり上げる ワードクラウドのためのワードデータはtweetテーブルのデータを形態素解析して、Wordテーブルに放り込む 形態素分析はmecabを使って、名詞のみを抽出 形態素分析は、1時間に1回、毎時10分に前の1時間分をまとめて処理 ワードクラウドはTableau内の表現で完結するので、追加で実装する必要なし(すばらしい) ちゃんとツイート取得できるか ちゃんとツイート取得できているか可視化してみます。 1時間ごとに1,000件~15,000件くらいの幅でツイート取得ができています。 深夜帯はがっつりツイートが減りますがそれでも1,000件程度あります。 位置情報をもったツイートはほとんどなかったなんてオチにならなくてよかったです。 Tableauの更新ボタン押下は必要ですが、ツイート取得のリアルタイム性も問題なさそうです。 形態素分析できてるか 次に、形態素分析して生成される名詞数を可視化してみます。 1時間当たりに5,000語~85,000語くらいが生成されている感じです。 波形がツイート数とほぼ一致していて、だいたい1ツイートあたり平均7語くらい生成されてます。 ツイートをマッピングしてみる 位置情報をもとに、Tableauに地図マッピングしてもらいましょう。 日本を覆いつくす一面のツイート。 ついでに韓国も覆いつくしてますね。中国や北朝鮮にマッピングがないのはお国柄でしょうか。 たまに、指定エリア外のデータが出現していますが、これはStreaming apiのバグということで異常値扱いとします。 ワードクラウドしてみる 次にワードクラウドを作ってみましょう。さっきのツイートマッピングに重ね合わせて配置してみます。 密度が高く見づらいですが、形にはなってます。 頻出のワードもちゃんと大きく表示されています。 位置フィルターを導入してみる 基本機能的なものは実装できたので、地域ごとにフィルターできる機能を追加します。 「指定位置」と「指定距離」のパラメータを作成して、特定位置から半径〇kmを対象にしたツイートのみをフィルターする機能を実装します。 「指定位置」は手動入力でもいいですが操作性が悪くなるので、ツイートのマークをクリックしたら自動指定されるようにしてみました。 青マークが「指定位置」のツイート、オレンジマークが「指定距離」内のツイートになります。 それに合わせてワードクラウドの対象ツイートも同期させてみました。 また、ワードクラウドの表示密度の調整として、表示させるワードの最低出現数を閾値として設定できるようにしました。 千代田区あたりを「指定位置」にして、半径50kmを対象に絞ってみました。 3月31日夜は地震が発生していたので、その傾向も見えてます。 ついでに、ワードクラウドのワードをクリックすればツイートマッピング側もそのワードを含むものでフィルターできるようにしてみます。 これで具体的なツイートを追いかけるのが楽になりました。 各地方の傾向を見てみる これまでは例として関東周辺のものを参照してきましたが、各地方の傾向を見ていきます。 札幌近郊 まずは札幌近郊から。 地名が頻出するのはどこにも同じ傾向があるようです。(位置情報ありのツイートのためでしょう) この日は日ハムがシーズン初勝利したのでその様子も反映されてます。 仙台近郊 次に仙台近郊。 関東と同様に地震の影響をうけたのがほのかに分かります。 東海エリア 愛知や静岡をメインに東海エリアを見てみます。 桜前線的にこのエリアが満開なのかなと想像できます。 この日は関東と関西で地震が起こったので、間に挟まれた東海でも地震ワードが目立ってます。 関西エリア 大阪、兵庫、京都を中心に。 関西でも地震が発生していたため、やはり地震傾向が強いです。 関東と比較すると時事関連ワードが見えるのは地域柄でしょうか。 中四国エリア 広島を中心に中四国を対象に。 かなりエリアを広げたからか地名系が多いです。 カープが開幕戦絶好調でしたので広島近郊でカープの話題が多く、その傾向も少し見えます。 九州エリア そろそろ見飽きてませんでしょうか。あと少しです。お付き合い願います。 熊本を中心に、九州7県をすっぽり対象にしました。 九州エリアも地名系が多いですが、BTSが見えているのは韓国と近いからでしょうか。 沖縄近郊 一気に傾向が変わります。沖縄近郊です。 軍事機の飛行情報をトラッキングしているMilitary Aircraft Spotterというアカウントが、嘉手納基地の情報を頻繁につぶやいているのが影響を出しているようです。 沖縄エリアのツイート数自体が少ないこともあり、頻繁につぶやいているアカウントの影響が大きくなってしまいます。 日本は笑顔で溢れている ここまで読んでくれた皆様、どうもありがとうございます。 各地方のワードクラウドを通しで眺めてみると、多くの地方で「笑顔」のワードが出現しています。 これはこの記事を読んで切る皆様、ひいては日本国民が毎日を笑顔で過ごしているいるからに他なりません。 そんな素敵な国、日本に産まれてよかったなぁと感じます。 お付き合い頂きまして、ありがとうございました。 そんなことはない わかってます。そこまで日常に笑顔があふれていないことは。調べてみましょう。 「笑顔」のワードでツイートをフィルターしてマッピングしてみます。 純粋にツイート本文に「笑顔」が入っているものはこれだけしかないため、何かしら別要因で笑顔が増殖しているはずです。 笑顔はたしかにそこにあった 辿っていくと、笑顔を吐き出しているツイート文は以下のようなものが多い傾向にありました。(下記は創作した例文です) このツイート文に対するMecabの形態素解析の結果から名詞の出力を見てみましょう。 いました「笑顔」。 笑った絵文字が笑顔を量産していたようです。 あわせて、英語の半角全角やURL情報、具体的な数値など、傾向把握の文脈上必要ない部分を前処理する必要がありそうです。 余談となりますが、"鬼滅の刃"を判断してくれるMecab、 もといmecab-ipadic-neologdは時流をとらえておりとても便利です。 ipadicはIPAが提唱した「IPA品詞体系(THiMCO97)」に基づいて作成されたデータの辞書なのですが、すでに更新が止まっています。 そこでipadicをベースにして、様々なデータベースをクロールして自動更新されているのが今回使用したmecab-ipadic-neologd。ありがたいです。(最近はこちらも更新がとまっているようで、2020年連載開始の"海が走るエンドロール"は判別できていません。) さよなら笑顔 ということで、文整形の前処理をいれ、この日を境に日本中から架空の笑顔が消えたのでした。 めでたし めでたし。 さいごに 大変長くなってしまいましたが、ご覧いただきましてありがとうございました。 本稿はもともと社内ブログとして公開した内容を、本ブログ用に加筆修正したものとなります。 ソースコード記載が皆無だったり、ストーリーがゆるかったりしておりますが、ご容赦頂けますと幸いです。 SDPFアーキテクトではお客様の課題解決やNTT Comビジネスの中でSDPF活用を推進しています。 ただ、SDPFアーキテクトはSDPF専任の導入コンサルのポジションは取っていません。 SDPF以外の商材でも課題解決に必要なものは導入し、時には自分たちで検証しながら、課題解決のサポートやNTT Comのサービスへのフィードバックするなど幅広い活動をしています。 自由度の高いミッションですし、データ利活用の道をひた走ってきた人材や膨大なSDPFの商材を知り尽くした人材ばかりではありません。 それでもこの大きい会社を次のステージのために変わっていくぞ、変えていくぞと、切磋琢磨しながら前に進めるそんな良き雰囲気がNTT Comにはあると思っています。 【余談】振り返りとSDPF データ連携がつらい 今回はPoCレベルでしたので、各システムのデータ連携部分は力業で手組みしました。 ところが、データ型の整合性を取ったり、渡し方の設計でもトライアンドエラーがあり、この規模でも結構苦労しました。 (最終的に、細かい部分はPandasに丸め込んでもらっちゃいました) 運用を考えた場合、ある程度の頻度のColumn追加やデータ仕様変更を想定するとELTやデータ仮想化などを入れて管理していかないと、運用稼働が跳ね上がり、開発に支障をきたしてしまうのではと感じました。 そんな要望を叶えるサービスがSDPFにはありますので紹介させてください。 コードを修正、動作確認、DBの中身を確認するサイクルを何度も繰り返しながら進めていた作業が、GUI上でサクっとできてしまいます。 1日かかった作業が30分で終わる感覚になれたので、ROIが高いのではないでしょうか。 データ統合インフォマティカ ソリューション 複数システムにまたがるデータを統合管理 https://www.ntt.com/business/services/data-utilization/dxplatform/sdpf/informatica.html https://sdpf.ntt.com/services/informatica/ データ仮想化 TIBCO Data Virtualization 複数のデータを複製することなくリアルタイムで統合。一元的なデータアクセスを可能に https://sdpf.ntt.com/services/tdv/ データ可視化は楽だった 逆にデータ可視化については、Tableauを使用したので地図へのマッピングや、ワードクラウド作成などの作成稼働は本当に短かったです。 これをたとえばエクセルでやれなどとなったら太刀打ちできないでしょう。リアルタイムなんて夢の夢になってしまいそうです。 データ可視化サービスとして、TIBCO SpotfireをSDPFでは提供しています。 BI/BAツール TIBCO Spotfire 直感的な操作でデータを可視化し、探索的な分析を可能にするBI/BAツール https://www.ntt.com/business/services/data-utilization/dxplatform/sdpf/tibco.html https://sdpf.ntt.com/services/spotfire/