はじめに
シーケンス図やクラス図などがコードで書けるうえ、AWS
やAzure
のアイコンを使って構成図までコードで書けてしまうPlantUML
にハマり、しばらく使ってみた結果、ある程度きれいな構成図が描けるようになったため、これまでのノウハウをまとめてみようと思います。
なお、今回はPlantUML
のインストール方法や使い方、初歩的な書式等まで説明していると長くなってしまうため、要点のみまとめます。
今回描いてみた構成図
仕事で描いた図を載せるわけにもいかないので、AWSが公開しているAWSソリューションの1つである「AWSでのワークロード検出(旧AWS Perspective)」の構成図をサンプルとしてPlantUML
でできるだけ基の構成図に近づけるように描いてみました。
もしAWS Perspective
自体に興味があるようなら以前私が書いた記事も以下で紹介しておきますので参考にどうぞ。
以下基の構成図。
以下PlantUML
で描いた構成図。
上図のPlantUMLの拡大図とコード。
※QiitaのPlantUMLエンジンの仕様か、データコンポーネントと検出コンポーネント部分の透過処理が効いていませんがコードは以下拡大図のサイトで示したコードと同じです。
@startuml
!define AWSPuml https://raw.githubusercontent.com/awslabs/aws-icons-for-plantuml/v14.0/dist
!include AWSPuml/AWSCommon.puml
!include AWSPuml/General/Users.puml
!include AWSPuml/Groups/AWSCloud.puml
!include AWSPuml/Groups/Generic.puml
!include AWSPuml/Groups/GenericAlt.puml
!include AWSPuml/NetworkingContentDelivery/CloudFront.puml
!include AWSPuml/Database/DynamoDB.puml
!include AWSPuml/SecurityIdentityCompliance/Cognito.puml
!include AWSPuml/Compute/LambdaLambdaFunction.puml
!include AWSPuml/Storage/SimpleStorageService.puml
!include AWSPuml/ApplicationIntegration/APIGateway.puml
!include AWSPuml/ApplicationIntegration/AppSync.puml
!include AWSPuml/FrontEndWebMobile/Amplify.puml
!include AWSPuml/Analytics/Athena.puml
!include AWSPuml/CloudFinancialManagement/CostandUsageReport.puml
!include AWSPuml/DeveloperTools/ToolsandSDKs.puml
!include AWSPuml/ManagementGovernance/Config.puml
!include AWSPuml/Groups/VPC.puml
!include AWSPuml/Groups/PrivateSubnet.puml
!include AWSPuml/Database/Neptune.puml
!include AWSPuml/Analytics/OpenSearchService.puml
!include AWSPuml/Containers/ElasticContainerService.puml
!include AWSPuml/Containers/Fargate.puml
!include AWSPuml/Containers/ElasticContainerRegistry.puml
!include AWSPuml/DeveloperTools/CodePipeline.puml
!include AWSPuml/DeveloperTools/CodeBuild.puml
!include AWSPuml/Containers/ElasticContainerRegistryImage.puml
!include AWSPuml/AWSSimplified.puml
top to bottom direction
title AWSでのワークロード検出
skinparam shadowing false
hide stereotype
skinparam linetype ortho
skinparam rectangle {
BackgroundColor AWS_BG_COLOR
BorderColor transparent
}
!procedure $stepnum($number)
<back:royalblue><color:white><b> $number </b></color></back>
!endprocedure
rectangle "$UsersIMG()\nユーザ" as users
AWSCloudGroup(cloud){
rectangle "<font color=white>right" as right {
GenericGroup(components4,コストコンポーネント){
rectangle "$LambdaLambdaFunctionIMG()\nAWS Lambda\nCost関数" as lambda2
rectangle "$AthenaIMG()\nAmazon Athena" as athena
rectangle "$SimpleStorageServiceIMG()\nAmazon S3バケット\nCURバケット" as s33
rectangle "$CostandUsageReportIMG()\nAWSのコストと使用状況\nレポート" as cost
rectangle "$SimpleStorageServiceIMG()\nAmazon S3バケット\nAthenaResultsBucket" as s34
}
VPCGroup(vpc,VPC){
PrivateSubnetGroup(subnet,プライベートサブネット){
GenericGroup(components5,データコンポーネント) #Transparent {
rectangle "$LambdaLambdaFunctionIMG()\nAWS Lambda\nGremlin関数" as lambda3 #Transparent
rectangle "$NeptuneIMG()\nAmazon Neptune" as neptune #Transparent
rectangle "$LambdaLambdaFunctionIMG()\nAWS Lambda\nSearch関数" as lambda4 #Transparent
rectangle "$OpenSearchServiceIMG()\nAmazon OpenSearch Service\n(Amazon Elasticsearch Service\nの後継サービス)" as opensearch #Transparent
}
GenericGroup(components6,検出コンポーネント) #Transparent {
rectangle "$ElasticContainerServiceIMG()\nAmazon Elastic\nContainer Service" as ecs #Transparent
rectangle "$FargateIMG()\nAWS Fargate" as fargate #Transparent
rectangle "$ElasticContainerRegistryIMG()\nAmazon Elastic\nContainer Registry" as ecr #Transparent
}
}
}
GenericGroup(components7,イメージデプロイコンポーネント) #Transparent {
rectangle "$SimpleStorageServiceIMG()\nAmazon S3バケット\nDiscoveryBucket" as s35
rectangle "$CodePipelineIMG()\nAWS CodePipeline" as codepipeline
rectangle "$CodeBuildIMG()\nAWS CodeBuild" as codebuild
rectangle "$ElasticContainerRegistryImageIMG()\nコンテナ\nイメージ" as container
}
}
rectangle "<font color=white>left" as left {
GenericGroup(components1,ウェブUIコンポーネント){
rectangle "$DynamoDBIMG()\nAmazon DynamoDB\nSettingsテーブル" as dynamodb
rectangle "$CognitoIMG()\nAmazon Cognito" as cognito
rectangle "$SimpleStorageServiceIMG()\nAmazon S3バケット\nWebUIBucket" as s31
rectangle "$LambdaLambdaFunctionIMG()\nAWS Lambda\nSettings関数" as lambda1
rectangle "$CloudFrontIMG()\nAmazon CloudFront" as cloudfront
GenericAltGroup(clgroup,クライアントAPI){
rectangle "$APIGatewayIMG()\nAmazon API\nGateway" as apigateway1 #Transparent
rectangle "$AppSyncIMG()\nAWS AppSync" as appsync #Transparent
}
}
rectangle "<font color=white>left_down" as left_down {
rectangle "<font color=white>develop" as components3 {
rectangle "$APIGatewayIMG()\nAmazon API Gateway\nServiceGremlin API" as apigateway2
rectangle "$ToolsandSDKsIMG()\nAWS SDK" as sdk
rectangle "$ConfigIMG()\nAWS Config" as config
}
GenericGroup(components2,ストレージ管理コンポーネント){
rectangle "$AmplifyIMG()\nAWS Amplify" as amplify
rectangle "$SimpleStorageServiceIMG()\nAmazon S3バケット\nAmplifyStorageBucket" as s32
}
}
}
}
'# オブジェクト同士の接続
vpc-[hidden]r-components7
components4-[hidden]d-vpc
components5-[hidden]d-components6
'# ユーザアクセス
cloudfront<-l-users: $stepnum("1")
'# ウェブUIコンポーネント
lambda1-l->dynamodb: $stepnum("6")
cloudfront-r->s31
dynamodb-[hidden]d-cognito
s31-u->cognito: $stepnum("2")
s31-r->clgroup
clgroup--u->lambda1
clgroup-u->cognito: $stepnum("5")
s31-d->components2
apigateway1-[#EFF0F3]r-appsync: $stepnum("4")
appsync-u->lambda2
appsync-r->lambda3
'# ストレージ管理コンポーネント
amplify<-r->s32: $stepnum("3")
'# API Gateway、SDK、Config
apigateway2-[hidden]d-sdk
sdk-[hidden]d-config
apigateway2-r->lambda4
'# コストコンポーネント
lambda2-r->athena: $stepnum("9")
athena-r->s33: $stepnum("10")
cost-l->s33: $stepnum("11")
cost-[hidden]r-s34
lambda2-r->s34: $stepnum("12")
'# データコンポーネント
lambda3<-r->neptune: $stepnum("7")
lambda3-[hidden]d-lambda4
lambda4<-r->opensearch: $stepnum("8")
neptune-[hidden]d-opensearch
'# 検出コンポーネント
ecs-d->fargate: $stepnum("15")
ecs-d->ecr
fargate-[hidden]r-ecr
fargate-l->apigateway2: $stepnum("17")
fargate-l->sdk: $stepnum("16")
fargate-l->config
'# イメージデプロイコンポーネント
codepipeline-u->s35
codepipeline-d->codebuild: $stepnum("13")
codebuild-d->container
container-d->ecr: $stepnum("14")
@enduml
なぜ構成図をコードで描いたのか
仕事で構成図を描く際、毎回頭を悩ませていたこととして、以下のようなことがありました。
- 図を修正した際に修正箇所が分かりづらい
- 人によって体裁が異なる
通常の図の場合、文字列での比較と異なり、変更箇所がマーキングされたりしないので、目視で修正前後の図を見比べるようなことしかできず、いつの間にか他の人に修正されていた図の修正箇所を見つけたりするのは非常に手間です。
また、人により、例えば線の太さや文字のフォントが異なっているようなことはよくあり、見かけるとモヤモヤした気持ちになってしまいます。
そのため、コードさえ書けば誰でも同じ構成図を描くことができ、コード管理もできるPlantUML
に興味を持ち使ってみたことがきっかけです。
AWSの構成図をできるだけきれいに描くには
AWS Labs
のGitHub
で公開しているaws-icons-for-plantumlにサンプルとして用意されているコードがコンパクトなのにやってみたいことが凝縮されていて非常に参考になります。
以下サンプルコードそのままコピペ。
※QiitaのPlantUMLの仕様なのか、サンプルコードそのままではアイコンの影が表示されたので、skinparam shadowing false
だけ追加しています。
以下よりできるだけきれいなAWS構成図を描く方法をまとめていきます。
AWSアイコンは標準ライブラリのawslibではなく、AWS Labs製のaws-icons-for-plantumlを使う
どちらも使えるアイコン数にはあまり違いはありませんが、aws-icons-for-plantuml
を使うとリージョンやPublic Subnet
などのアイコン(というかグループの枠線)によるグループ化ができるので標準ライブラリのawslib
よりそれっぽい図が描けます。
また、各アイコンのオプションが多いため、慣れればきれいな図を描きやすいです。
線は角線で描く
これは描くものの内容によるところはありますが、デフォルトの直線・曲線で描くより、角張った線で描くようにするほうがきれいに描ける場合が多いです。
角張った線で描くにはコードのどこかにskinparam linetype ortho
を記載することで設定できます。
以下、デフォルトの直線・曲線で描いた例と角線で描いた例。
以下角線の場合のコード(デフォルトの直線・曲線を試したい場合はskinparam linetype ortho
を消してください)
@startuml
!define AWSPuml https://raw.githubusercontent.com/awslabs/aws-icons-for-plantuml/v14.0/dist
!include AWSPuml/AWSCommon.puml
!include AWSPuml/General/Users.puml
!include AWSPuml/AWSSimplified.puml
title 角線の場合
skinparam linetype ortho
rectangle "$UsersIMG()\nユーザ1" as users1
rectangle "$UsersIMG()\nユーザ2" as users2
rectangle "$UsersIMG()\nユーザ3" as users3
rectangle "$UsersIMG()\nユーザ4" as users4
users1-->users2
users1-->users3
users1-->users4
@enduml
オブジェクト間で線を引く場合は必ず方向指定を行う
オブジェクト間で線を引くには--
と始点、終点の矢印等の指定を行うことで引くことができますが、--
の間に方向を指定することである程度任意の方向に描くことが可能です。
方向を指定することである程度意図した方向に図を描くことができるので全ての線について方向を指定することをオススメします。
ちなみにデフォルトのtop to bottom direction
(後述)で方向を指定しない場合は下方向で描かれるようです。
方向 | 指定方法 | 指定方法(省略形) |
---|---|---|
右方向 | -right- | -r- |
左方向 | -left- | -l- |
上方向 | -up- | -u- |
下方向 | -down- | -d- |
以下右方向指定の場合のコード例だけ記載します。
@startuml
!define AWSPuml https://raw.githubusercontent.com/awslabs/aws-icons-for-plantuml/v14.0/dist
!include AWSPuml/AWSCommon.puml
!include AWSPuml/General/Users.puml
!include AWSPuml/AWSSimplified.puml
title 右方向指定の場合
rectangle "$UsersIMG()\nユーザ1" as users1
rectangle "$UsersIMG()\nユーザ2" as users2
users1-r->users2
@enduml
top to bottom direction設定を行う
PlantUML
には上から下に描画するtop to bottom direction
(デフォルト)の設定と左から右に描画するleft to right direction
の設定があります。
作成する図の流れによって使い分けると、ある程度図の破綻が避けられる気がしますが、left to right direction
を使うと方向指定した場合の方向が変わり、混乱してしまうため、基本的にはtop to bottom direction
で描いたほうが良いかと思います。
@startuml
title left to right directionを指定した場合
left to right direction
中央-r->right指定:right指定
中央-l->left指定:left指定
中央-u->up指定:up指定
中央-d->down指定:down指定
@enduml
@startuml
title top to bottom directionを指定した場合
top to bottom direction
中央-r->right指定:right指定
中央-l->left指定:left指定
中央-u->up指定:up指定
中央-d->down指定:down指定
@enduml
見えない線を繋ぐ
図を描いていると特に線は描きたくなくても、このアイコンの隣に描きたいといったことが出てくるかと思います。
そんな場合、見えない線を描いてオブジェクト間の繋がりをあえて示しておくことで想定した図に近づけることができます。
例えば「ユーザ1」と「ユーザ3」、「ユーザ2」と「ユーザ4」を単純にそれぞれ右方向の矢印で繋いだ場合、以下のように並んで表示されます。(オブジェクトの数等で並びが異なる場合はあります)
ユーザ1とユーザ2は縦に並べてユーザ3とユーザ4は右方向で繋ぎたい場合、ユーザ1とユーザ2を繋ぐ線の表記に-[hidden]down-
(省略形:-[hidden]d-
)と記載することで見えない線で繋ぐことができます。
@startuml
!define AWSPuml https://raw.githubusercontent.com/awslabs/aws-icons-for-plantuml/v14.0/dist
!include AWSPuml/AWSCommon.puml
!include AWSPuml/General/Users.puml
!include AWSPuml/AWSSimplified.puml
title ユーザ1とユーザ2を見えない下線で繋いだ場合
rectangle "$UsersIMG()\nユーザ1" as users1
rectangle "$UsersIMG()\nユーザ2" as users2
rectangle "$UsersIMG()\nユーザ3" as users3
rectangle "$UsersIMG()\nユーザ4" as users4
users1-r->users3
users1-[hidden]d-users2
users2-r->users4
@enduml
各セクション単位でグループ化するようにする
アイコンを並べる以外に、いくつかのセクションごとにグループ化することでグループ内での調整や図全体の調整がしやすくなります。
例えば今回描いた図では大きく分けて以下枠線のように分けて、グループ化した図同士で透明な線でつないだりして、なるべく破綻させないように構成図を調整しています。
また、以下のように影を表示しないスキン設定と枠線(rectangle
)の色を透明にするスキン設定を行った上で、枠線を描くことで、表示上見えない枠線を描くことができるので、便利です。
skinparam shadowing false
skinparam rectangle {
BackgroundColor AWS_BG_COLOR
BorderColor transparent
}
以下2つのS3を透明な枠線でまとめた場合の例。
rectangle
を指定する場合、名前を示す文字を1文字でも入力しないとエラーとなりますが、S3をまとめているrectangle
の文字は文字色を白に指定して見えないようにしています。
@startuml
!define AWSPuml https://raw.githubusercontent.com/awslabs/aws-icons-for-plantuml/v14.0/dist
!include AWSPuml/AWSCommon.puml
!include AWSPuml/Groups/AWSCloud.puml
!include AWSPuml/Compute/LambdaLambdaFunction.puml
!include AWSPuml/Storage/SimpleStorageService.puml
!include AWSPuml/Analytics/Athena.puml
!include AWSPuml/CloudFinancialManagement/CostandUsageReport.puml
!include AWSPuml/AWSSimplified.puml
skinparam shadowing false
skinparam rectangle {
BackgroundColor AWS_BG_COLOR
BorderColor transparent
}
AWSCloudGroup(cloud){
rectangle "$LambdaLambdaFunctionIMG()\nAWS Lambda" as lambda2
rectangle "$AthenaIMG()\nAmazon Athena" as athena
rectangle "$CostandUsageReportIMG()\nAWSのコストと使用状況\nレポート" as cost
rectangle "<font color=white>right" as right {
rectangle "$SimpleStorageServiceIMG()\nAmazon S3バケット" as s33
rectangle "$SimpleStorageServiceIMG()\nAmazon S3バケット" as s34
}
}
@enduml
なぜか線やアイコンが指定している方向と違う方向に飛んでしまうときは最後に調整する
今回のようにアイコンを多く使うような構成の場合、途中までは指定通りに描けていても、何かのタイミングで図が破綻してあらぬ方向に線やアイコンが描画される場合があります。
全体の構成として崩れているなら構成図全体を描ききってから、各セクションごとに分けたグループ内で崩れているならグループ内の構成を描ききってから調整するほうがやりやすいです。
構成図を描ききったあと、以下のような調整を行うことである程度は整えることができます。
グループごとの並びを入れ替える
全体の構成図を描ききった際、どうにもグループごとの並びが思った並びと異なる場合、調整したいグループの記載箇所を入れ替えたりすると、描画の優先度が変わり、うまく描かれるようになる場合があります。
例えば「プライベートサブネット」の「データコンポーネント」、「検出コンポーネント」の並びが想定と異なっていた場合、グループごとに分けた以下赤枠、緑枠を丸々入れ替えることでうまくいく場合があります。
線の長さを変える
下図のDynamoDB列とLambda列のように列ごとのアイコンの数が揃えられない箇所に線を引く場合、線の長さを変えることできれいに描くことができる場合があります。
線の長さは-
の数で調整でき、かなり長い指定を行うことも可能です。
clgroup--u->lambda1
clgroup-u->cognito: $stepnum("5")
基本的にはPlantUML
が良くも悪くも自動調整してくれるのでわざわざ線の長さを指定する機会は少ないですが、思ったように描画されず図が破綻してしまう場合は線の長さを調整してみると良いです。
線の方向を変える
何をやっても思っている方向と異なるところに描画される場合は、(気持ち悪いですが)線の方向を全く違う方向に指定してしまうのも1つの方法です。
今回描いた構成図でも一番左に描いているユーザアイコンからCloudFrontへ右方向の矢印を指定して描いていましたが、途中から図が破綻してしまい、-r->
で右方向を指定しているのにも関わらず、どうしても右に描かれなくなってしまったため、-l->
として左方向にしています。
'# ユーザアクセス
cloudfront<-l-users: $stepnum("1")
本記事トップのコードを見ると、ところどころ別方向の指定をしている箇所がありますが、描き途中でアイコンが少ないときには破綻せずに描かれていたため、方向を変える調整は最後に実施するようにしましょう。
どうしてもうまく行かない場合は諦める
PlantUML
はそもそも図のオブジェクトを微調整するような機能は備わっておらず、配置はPlantUML
で自動調整されてしまうため、いくら上述の方法で図を整えても思いがけない方向に描画されてしまうことが多々あります。
ここまで長々と紹介しておいて本末転倒ですが、複雑な構成図を描く場合はPlantUML
で描くことはさっぱり諦めて、Drawio
等で地道に構成図を作成するのも手かと思います。
複雑になればなるほど図が破綻することが多くなり、思い描いた図に近づけるのに時間が取られてしまうため、毎回イライラしながらPlantUML
で作成するよりは、好き勝手に描けるドローイングソフトで描いたほうが精神衛生上にも良いかと思います。
私のように構成図をコード化することが目的ということであれば、Drawio
等を使い、SVG
形式で描けば構成図をコード管理する目的はある程度達成できるため、そちらの方法も良いかと思います。
その他テクニック
きれいな構成図を描く方法とは若干異なりますが、今回紹介したコードであまり他では紹介されていないテクニックを紹介します。
aws-icons-for-plantuml
の仕様による内容も多く、他のライブラリでは使用できないテクニックもあると思いますが、参考として見てもられば幸いです。
線にのような数字を書く方法
以下のようなプロシージャで書式設定をした上で数字を描きたい線の行の後ろに:$変数名("数字")
のように指定することでのような数字を描くことができます。
(略)
!procedure $stepnum($number)
<back:royalblue><color:white><b> $number </b></color></back>
!endprocedure
(略)
'# ユーザアクセス
cloudfront<-l-users: $stepnum("1")
(略)
オブジェクトの透過処理
色のついた背景にアイコンを配置すると、アイコンと文字の部分は白で描かれ、描くオブジェクトのオブジェクト名の後ろに「#Transparent
」と指定することで以下のように透過することができます。
下の例の場合、透過処理した場合でも枠線は表示されてしまっておりますが、PlantUML
のエンジンによって違いがあるようなので、比較的新しいPlantUML
をインストールして試すと、枠線も表示されないことが確認できるかと思います。
AWSCloudGroup(cloud){
PrivateSubnetGroup(subnet,プライベートサブネット){
rectangle "$LambdaLambdaFunctionIMG()\nAWS Lambda\nGremlin関数" as lambda3 #Transparent
rectangle "$NeptuneIMG()\nAmazon Neptune" as neptune #Transparent
rectangle "$LambdaLambdaFunctionIMG()\nAWS Lambda\nSearch関数" as lambda4 #Transparent
rectangle "$OpenSearchServiceIMG()\nAmazon OpenSearch Service\n(Amazon Elasticsearch Service\nの後継サービス)" as opensearch #Transparent
}
また、グループ化した枠線を透過処理する場合、以下のような書き方をすれば枠内の色を透過することができます。(今回の場合はQiita側PlantUMLエンジンの仕様と思われる動作で透過されていませんが)
VPCGroup(vpc,VPC){
PrivateSubnetGroup(subnet,プライベートサブネット){
GenericGroup(components5,データコンポーネント) #Transparent {
おわりに
規模が大きい構成図を作る場合はストレスと戦いながら図を調整することになるかと思いますが、規模が小さい構成図やシステムの一部を部分的に切り出した構成図・配置図を作る場合は、慣れればコードのコピペでDrawio
等で普通に作図するよりきれいに早く作れるのではないかと思います。
実際に私自身も規模が大きいシステム全体構成図を作成する場合、「PlantUML
で描いて調整する時間 > Drawio
等で構成図を描く時間」という結果に陥ったため、現在ではPlantUML
で作図する場合、主に部分的な構成図の作成や通信フロー図の作成で使うところに落ち着きました。
PlantUML
がコードで任意の配置にすることができて微調整もできるようになれば、今までの常識がひっくり返る神ツールになる可能性があるので今後のアップデートを期待します。