西出正美です。有益なことや無益なことなどいろいろ書いています。

Next.jsでSSG時にRemarkでPlantUMLで書いたAWSのアーキテクチャ図をSVGとして出力する

Next.jsでSSG時にRemarkでPlantUMLで書いたAWSのアーキテクチャ図をSVGにして出力してみたいと思うこと、ありますよね。
僕はあるのですがネット上にあまり情報が無かったので自分用にメモです。

今回はremark-sync-plantumlを使って実装しました。

このサイトではMarkdownをunifiedで扱っているのでremark-sync-plantumluseします。
remark-sync-plantumlはjavaでplantuml.jarを使ってMermaidをSVGにしているらしく、このサイトはGitHub Actionsでビルドしているので問題なく動かせそうなので採用しました。

import rehypeShiki from '@leafac/rehype-shiki';
import rehypeKatex from 'rehype-katex';
import rehypeStringify from 'rehype-stringify';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import remarkMermaid from 'remark-mermaidjs';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import remarkPlantUML from 'remark-sync-plantuml';
import { getHighlighter } from 'shiki';
import { unified } from 'unified';

export const markdownToHtml = async (markdown: string) =>
	(
		await unified()
			.use(remarkParse)
			.use(remarkMath)
			.use(remarkGfm)
			.use(remarkMermaid, {
				launchOptions: {
					executablePath:
						process.env.GoogleChromeExecutablePath ??
						'/opt/google/chrome/google-chrome',
				},
				svgo: false,
			})
			.use(remarkPlantUML)
			.use(remarkRehype, {
				allowDangerousHtml: true,
				footnoteLabel: '脚注',
			})
			.use(rehypeShiki, {
				highlighter: await getHighlighter({
					theme: 'github-light',
				}),
			})
			.use(rehypeKatex)
			.use(rehypeStringify, { allowDangerousHtml: true })
			.process(markdown)
	)
		.toString();

以下にサンプルを置いておきます。図がSVGになっていることがわかると思います。

AWS Architecture Diagrams

AWSがhttps://github.com/awslabs/aws-icons-for-plantumlで公開しているAWS Icons for PlantUMLも使うことができます。
これなら簡単にAWSのアーキテクチャ図を書くことができて便利です。

```plantuml
@startuml
'Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
'SPDX-License-Identifier: MIT (For details, see https://github.com/awslabs/aws-icons-for-plantuml/blob/master/LICENSE)

!define AWSPuml https://raw.githubusercontent.com/awslabs/aws-icons-for-plantuml/v15.0/dist

!include AWSPuml/AWSCommon.puml
!include AWSPuml/AWSExperimental.puml
!include AWSPuml/Groups/all.puml
!include AWSPuml/Compute/LambdaLambdaFunction.puml
!include AWSPuml/General/Documents.puml
!include AWSPuml/General/Multimedia.puml
!include AWSPuml/General/Tapestorage.puml
!include AWSPuml/General/User.puml
!include AWSPuml/MediaServices/ElementalMediaConvert.puml
!include AWSPuml/MachineLearning/Transcribe.puml
!include AWSPuml/Storage/SimpleStorageService.puml

' define custom group for Amazon S3 bucket
AWSGroupColoring(S3BucketGroup, #FFFFFF, AWS_COLOR_GREEN, plain)
!define S3BucketGroup(g_alias, g_label="Amazon S3 bucket") AWSGroupEntity(g_alias, g_label, AWS_COLOR_GREEN, SimpleStorageService, S3BucketGroup)

' Groups are rectangles with a custom style using stereotype - need to hide
hide stereotype
skinparam linetype ortho
skinparam rectangle {
		BackgroundColor AWS_BG_COLOR
		BorderColor transparent
}
sprite Callout_1 <svg width="18" height="18"><circle cx="9" cy="9" r="9" fill="black" /><text x="5" y="13" fill="#FFFFFF" font-size="12">1</text></svg>

rectangle "$UserIMG()\nユーザー" as user
AWSCloudGroup(cloud){
	RegionGroup(region) {
		S3BucketGroup(s3) {
			rectangle "$MultimediaIMG()\n\t動画\t" as video
			rectangle "$TapestorageIMG()\n\t音声\t" as audio
			rectangle "$DocumentsIMG()\n\tテキスト\t" as transcript

			user -r-> video: <$Callout_1>\lアップロード
			video -r-> audio
			audio -r-> transcript
		}

		rectangle "$LambdaLambdaFunctionIMG()\nObjectCreated\nevent handler" as e1
		rectangle "$ElementalMediaConvertIMG()\nAWS Elemental\nMediaConvert" as mediaconvert
		rectangle "$TranscribeIMG()\nAmazon Transcribe\n" as transcribe

		video -d-> e1: <$Callout_2>
		e1 -[hidden]r-> mediaconvert
		mediaconvert -[hidden]r-> transcribe
		mediaconvert -u-> audio: <$Callout_3>
		transcribe -u-> transcript: <$Callout_4>

		StepFunctionsWorkflowGroup(sfw) {
			rectangle "$LambdaLambdaFunctionIMG()\nextract audio" as sfw1
			rectangle "$LambdaLambdaFunctionIMG()\ntranscribe audio" as sfw2

			e1 -r-> sfw1: 起動
			sfw1 -r-> sfw2
			sfw1 -u-> mediaconvert
			sfw2 -u-> transcribe
		}
	}
}
@enduml
```
  AWS Cloud  Region  Amazon S3 bucket  AWS Step Functions workflowObjectCreatedevent handlerAWS ElementalMediaConvertAmazon Transcribe 動画音声テキストextract audiotranscribe audioユーザー1アップロード234起動

他のUML図

PlantUMLで書ける他のUML図の例も紹介しておきます。

シーケンス図

```plantuml
@startuml
participant User

User -> A: DoWork
activate A #FFBBBB

A -> A: Internal call
activate A #DarkSalmon

A -> B: << createRequest >>
activate B

B --> A: RequestCreated
deactivate B
deactivate A
A -> User: Done
deactivate A

@enduml
```
UserUserAABBDoWorkInternal call«createRequest»RequestCreatedDone

ユースケース図

```plantuml
@startuml
User -> (Start)
User --> (Use the application) : A small label

:Main Admin: ---> (Use the application) : This is\nyet another\nlabel

@enduml
```
UserStartUse the applicationMain AdminA small labelThis isyet anotherlabel

クラス図

```plantuml
@startuml
class Student {
  Name
}
Student "0..*" - "1..*" Course
(Student, Course) .. Enrollment

class Enrollment {
  drop()
  cancel()
}
@enduml
```
StudentNameCourseEnrollmentdrop()cancel()0..*1..*

アクティビティ図

```plantuml
@startuml
start
repeat
  :Test something;
    if (Something went wrong?) then (no)
      #palegreen:OK;
      break
    endif
    ->NOK;
    :Alert "Error with long text";
repeat while (Something went wrong with long text?) is (yes) not (no)
->//merged step//;
:Alert "Success";
stop
@enduml
```
Test somethingOKnoSomething went wrong?Alert "Error with long text"noSomething went wrong with long text?yesAlert "Success"NOKmerged step

コンポーネント図

```plantuml
@startuml

package "Some Group" {
  HTTP - [First Component]
  [Another Component]
}

node "Other Groups" {
  FTP - [Second Component]
  [First Component] --> FTP
}

cloud {
  [Example 1]
}


database "MySql" {
  folder "This is my folder" {
    [Folder 3]
  }
  frame "Foo" {
    [Frame 4]
  }
}


[Another Component] --> [Example 1]
[Example 1] --> [Folder 3]
[Folder 3] --> [Frame 4]

@enduml
```
Some GroupOther GroupsMySqlThis is my folderFooHTTPFirst ComponentAnother ComponentFTPSecond ComponentExample 1Folder 3Frame 4

マインドマップ図

```plantuml
@startmindmap
+ root node
++ some first level node
+++_ second level node
+++_ another second level node
+++_ foo
+++_ bar
+++_ foobar
++_ another first level node
-- some first right level node
--_ another first right level node
@endmindmap
```
root nodesome first level nodesecond level nodeanother second level nodefoobarfoobaranother first level nodesome first right level nodeanother first right level node

状態図

```plantuml
@startuml
scale 600 width

[*] -> State1
State1 --> State2 : Succeeded
State1 --> [*] : Aborted
State2 --> State3 : Succeeded
State2 --> [*] : Aborted
state State3 {
  state "Accumulate Enough Data\nLong State Name" as long1
  long1 : Just a test
  [*] --> long1
  long1 --> long1 : New Data
  long1 --> ProcessData : Enough Data
}
State3 --> State3 : Failed
State3 --> [*] : Succeeded / Save Result
State3 --> [*] : Aborted

@enduml
```
State1State2State3Accumulate Enough DataLong State NameJust a testProcessDataNew DataEnough DataSucceededAbortedSucceededAbortedFailedSucceeded / Save ResultAborted

オブジェクト図

```plantuml
@startuml PERT
left to right direction
' Horizontal lines: -->, <--, <-->
' Vertical lines: ->, <-, <->
title PERT: Project Name

map Kick.Off {
}
map task.1 {
    Start => End
}
map task.2 {
    Start => End
}
map task.3 {
    Start => End
}
map task.4 {
    Start => End
}
map task.5 {
    Start => End
}
Kick.Off --> task.1 : Label 1
Kick.Off --> task.2 : Label 2
Kick.Off --> task.3 : Label 3
task.1 --> task.4
task.2 --> task.4
task.3 --> task.4
task.4 --> task.5 : Label 4
@enduml
```
PERT: Project NameKicktaskKick.Offtask.1StartEndtask.2StartEndtask.3StartEndtask.4StartEndtask.5StartEndLabel 1Label 2Label 3Label 4

ネットワーク図

```plantuml
@startuml
!include <office/Servers/application_server>
!include <office/Servers/database_server>

nwdiag {
  network dmz {
      address = "210.x.x.x/24"

      // set multiple addresses (using comma)
      web01 [address = "210.x.x.1, 210.x.x.20",  description = "<$application_server>\n web01"]
      web02 [address = "210.x.x.2",  description = "<$application_server>\n web02"];
  }
  network internal {
      address = "172.x.x.x/24";

      web01 [address = "172.x.x.1"];
      web02 [address = "172.x.x.2"];
      db01 [address = "172.x.x.100",  description = "<$database_server>\n db01"];
      db02 [address = "172.x.x.101",  description = "<$database_server>\n db02"];
  }
}
@enduml
```
dmz210.x.x.x/24internal172.x.x.x/24210.x.x.1210.x.x.20172.x.x.1210.x.x.2172.x.x.2172.x.x.100172.x.x.101web01web02db01db02

ガントチャート図

```plantuml
@startgantt
<style>
ganttDiagram {
	task {
		FontName Helvetica
		FontColor red
		FontSize 18
		FontStyle bold
		BackGroundColor GreenYellow
		LineColor blue
	}
	milestone {
		FontColor blue
		FontSize 25
		FontStyle italic
		BackGroundColor yellow
		LineColor red
	}
	note {
		FontColor DarkGreen
		FontSize 10
		LineColor OrangeRed
	}
	arrow {
		FontName Helvetica
		FontColor red
		FontSize 18
		FontStyle bold
		BackGroundColor GreenYellow
		LineColor blue
		LineStyle 8.0;13.0
		LineThickness 3.0
	}
	separator {
		BackgroundColor lightGreen
		LineStyle 8.0;3.0
		LineColor red
		LineThickness 1.0
		FontSize 16
		FontStyle bold
		FontColor purple
		Margin 5
		Padding 20
	}
	timeline {
	    BackgroundColor Bisque
	}
	closed {
		BackgroundColor pink
		FontColor red
	}
}
</style>
Project starts the 2020-12-01

[Task1] lasts 10 days
sunday are closed

note bottom
  memo1 ...
  memo2 ...
  explanations1 ...
  explanations2 ...
end note

[Task2] lasts 20 days
[Task2] starts 10 days after [Task1]'s end
-- Separator title --
[M1] happens on 5 days after [Task1]'s end

<style>
	separator {
	    LineColor black
		Margin 0
		Padding 0
	}
</style>

-- end --
@endgantt
```
TuWeThFrSaSuMoTuWeThFrSaSuMoTuWeThFrSaSuMoTuWeThFrSaSuMoTuWeThFrSaSuMoTuWeThFrSaSuMoTuWe1234567891011121314151617181920212223242526272829303112345678910111213December 2020January 2021memo1 ...memo2 ...explanations1 ...explanations2 ...Task1Task2Separator titleM1end1234567891011121314151617181920212223242526272829303112345678910111213TuWeThFrSaSuMoTuWeThFrSaSuMoTuWeThFrSaSuMoTuWeThFrSaSuMoTuWeThFrSaSuMoTuWeThFrSaSuMoTuWeDecember 2020January 2021

PlantUML記法なら簡単にグラフを書けて便利なので、どんどん使っていきたいと思います。

NISHIDEMASAMI.GITHUB.IO
NISHIDE, Masami

西出正美です。有益なことや無益なことなどいろいろ書いています。

©NISHIDE, Masami Some Rights Reserved