3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【お試し】クイック スタート:Visual Studio Code を使用して Azure で関数を作成する(JavaScript) AzureFunctions

Last updated at Posted at 2020-06-03

概要

クイック スタート:Visual Studio Code を使用して Azure で関数を作成するの内容を参考に、AzureFunctionsを触ってみます。

環境情報

2020/6/3時点

  • MacOS 10.15.5
  • Node.js v12.18.0 (最新は14.4.0でしたがLTSがおすすめらしいのでLTSのバージョンにしました。)
  • Visual Studio Code 1.45.1
    • Azure Functions Plugin 0.22.1

事前準備

  • Microsoft Azureアカウント(無料)を作成しておく
    • アカウント作成時にGitHubアカウントでログインもしくは新規でメールアドレスを登録してアカウントを作成
    • 電話番号、クレジットカード情報の登録も必要
    • クレジットカード情報は、認証に利用し、アップグレードを叫ばない限り費用が発生することはないようです。
  • Visual Studio Codeを使うのでインストールしておく
    • 拡張機能としてAzure Functionsを使うのでインストールしておく
  • Node.jsをインストールしておく

ローカルプロジェクトを作成する

ローカルプロジェクトを作成するの内容をトレースしていきます。

上記記事と、最新の環境とで差があるようですが、概ね手順の通り進められます。

新しいプロジェクトの作成

スクリーンショット 2020-06-03 19.32.57.png

プロジェクトワークスペースのディレクトリを指定

これらの手順は、ワークスペースの外部で実行するように設計されています。 ここでは、ワークスペースに含まれるプロジェクト フォルダーは選択しないでください。

という注意書きがあるので、どういうことなのかやってみました。

もともと別のソースを管理するワークスペース「SRC」のワークスペースをそのまま選択して進めてみたところ、
既存のワークスペースに「HttpExample」というディレクトリだけでなく、そのディレクトリの外側に.gitignorehost.jsonなどいろいろなファイルが生成されてしまいました。

なので、既存のワークスペースがごちゃごちゃするから、新しいフォルダを指定してくださいね。
という注意書きのようですね。
スクリーンショット 2020-06-03 19.34.03.png
スクリーンショット 2020-06-03 19.43.49.png

新規フォルダを作成してそこを指定すると、きれいに関連するファイルだけがまとまって作成されました。
スクリーンショット 2020-06-03 19.50.24.png

プロンプトで次の情報を入力する

Select a languageJavaScriptを選択する。
スクリーンショット 2020-06-03 19.34.29.png

Select a template for your project's first functionHTTP triggerを選択する。
スクリーンショット 2020-06-03 19.35.33.png

Provide a function nameで任意のプロジェクト名を入力します。
ここでは、クイックスタートのサイトで指定されているHttpExampleという名前を指定していきます。
スクリーンショット 2020-06-03 19.36.04.png

Authorization levelAnonymousを指定する。
Anonymousに設定することで、APIキーが不要な単純なHTTPリクエストによるAzure Functionを体験できるようです。
これだとPublicに公開された誰でも実行できるものになってしまうということであれば、別の承認レベルを指定する必要がありそうですね。
スクリーンショット 2020-06-03 19.36.35.png

関数をローカルで実行する

なんと、ここでAzure Functions Core Toolsというツールを使って、関数のテストを行うようです。

事前準備で、VSCodeやらNode.jsをセットアップさせておきながら、まだ必要な環境があるとは。。。

Azure Functions Core Toolsのセットアップ

Macの環境なので、Terminalを起動してbrew tap azure/functionsを実行。

$ brew tap azure/functions
Updating Homebrew...
==> Auto-updated Homebrew!
Updated 1 tap (homebrew/core).
==> New Formulae
bombadillo                                                                             coredns
==> Updated Formulae
abcmidi      aws-cdk      awscli@1     composer     dnscontrol   elektra      flow         ghq          imagemagick  liblouis     node@10      rbspy        simple-scan
aliyun-cli   awscli       buildifier   csvq         doctl        eureka       geoserver    grin-wallet  ktlint       node         node@12      semgrep
==> Deleted Formulae
baidupcs-go

==> Tapping azure/functions
Cloning into '/usr/local/Homebrew/Library/Taps/azure/homebrew-functions'...
remote: Enumerating objects: 71, done.
remote: Counting objects: 100% (71/71), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 338 (delta 48), reused 41 (delta 29), pack-reused 267
Receiving objects: 100% (338/338), 50.35 KiB | 305.00 KiB/s, done.
Resolving deltas: 100% (184/184), done.
Tapped 4 formulae (31 files, 93.6KB).

続いてbrew install azure-functions-core-tools@3を実行。

$ brew install azure-functions-core-tools@3
==> Installing azure-functions-core-tools@3 from azure/functions
==> Downloading https://functionscdn.azureedge.net/public/3.0.2534/Azure.Functions.Cli.osx-x64.3.0.2534.zip
######################################################################## 100.0%

 Telemetry 
 --------- 
 The Azure Functions Core tools collect usage data in order to help us improve your experience.
 The data is anonymous and doesn't include any user specific or personal information. The data is collected by Microsoft.
 
 You can opt-out of telemetry by setting the FUNCTIONS_CORE_TOOLS_TELEMETRY_OPTOUT environment variable to '1' or 'true' using your favorite shell.
🍺  /usr/local/Cellar/azure-functions-core-tools@3/3.0.2534: 5,114 files, 564.9MB, built in 21 seconds

VSCodeでF5キーを押下

F5キーを押下すると、こんな感じになります。

スクリーンショット 2020-06-03 20.09.51.png

Azure Functions Core Toolsをインストールしたからこんな感じになるのか、インストールしなくても実は動くのか。。。インストールする前に試せばよかったです。

とりあえずTERMINALに出力された結果としては以下のような感じでした。

> Executing task: npm install <

npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN azurefunctions@1.0.0 No description
npm WARN azurefunctions@1.0.0 No repository field.
npm WARN azurefunctions@1.0.0 No license field.

up to date in 0.653s
found 0 vulnerabilities


Terminal will be reused by tasks, press any key to close it.

> Executing task: func host start <


                  %%%%%%
                 %%%%%%
            @   %%%%%%    @
          @@   %%%%%%      @@
       @@@    %%%%%%%%%%%    @@@
     @@      %%%%%%%%%%        @@
       @@         %%%%       @@
         @@      %%%       @@
           @@    %%      @@
                %%
                %

Azure Functions Core Tools (3.0.2534 Commit hash: bc1e9efa8fa78dd1a138dd1ac1ebef97aac8d78e)
Function Runtime Version: 3.0.13353.0
[2020/06/03 11:08:07] FUNCTIONS_WORKER_RUNTIME set to node. Skipping WorkerConfig for language:python
[2020/06/03 11:08:07] FUNCTIONS_WORKER_RUNTIME set to node. Skipping WorkerConfig for language:java
[2020/06/03 11:08:07] FUNCTIONS_WORKER_RUNTIME set to node. Skipping WorkerConfig for language:powershell
[2020/06/03 11:08:07] Building host: startup suppressed: 'False', configuration suppressed: 'False', startup operation id: 'd6b60201-83fa-46d6-8a79-67bd2d429035'
[2020/06/03 11:08:08] Reading host configuration file '/Users/you_name_is_yu/Develop/AzureFunctions/host.json'
[2020/06/03 11:08:08] Host configuration file read:
[2020/06/03 11:08:08] {
[2020/06/03 11:08:08]   "version": "2.0",
[2020/06/03 11:08:08]   "extensionBundle": {
[2020/06/03 11:08:08]     "id": "Microsoft.Azure.Functions.ExtensionBundle",
[2020/06/03 11:08:08]     "version": "[1.*, 2.0.0)"
[2020/06/03 11:08:08]   }
[2020/06/03 11:08:08] }
[2020/06/03 11:08:08] Reading functions metadata
[2020/06/03 11:08:08] 1 functions found
[2020/06/03 11:08:08] Looking for extension bundle Microsoft.Azure.Functions.ExtensionBundle at /var/folders/pf/mbns7g5n6nlfj48466cy3kj40000gn/T/Functions/ExtensionBundles/Microsoft.Azure.Functions.ExtensionBundle
[2020/06/03 11:08:08] Fetching information on versions of extension bundle Microsoft.Azure.Functions.ExtensionBundle available on https://functionscdn.azureedge.net/public/ExtensionBundles/Microsoft.Azure.Functions.ExtensionBundle/index.json
[2020/06/03 11:08:09] Downloading extension bundle from https://functionscdn.azureedge.net/public/ExtensionBundles/Microsoft.Azure.Functions.ExtensionBundle/1.1.1/Microsoft.Azure.Functions.ExtensionBundle.1.1.1.zip to /var/folders/pf/mbns7g5n6nlfj48466cy3kj40000gn/T/62e2229d-4a18-4dca-92f3-7f12aef4f16e/Microsoft.Azure.Functions.ExtensionBundle.1.1.1.zip
[2020/06/03 11:08:13] Completed downloading extension bundle from https://functionscdn.azureedge.net/public/ExtensionBundles/Microsoft.Azure.Functions.ExtensionBundle/1.1.1/Microsoft.Azure.Functions.ExtensionBundle.1.1.1.zip to /var/folders/pf/mbns7g5n6nlfj48466cy3kj40000gn/T/62e2229d-4a18-4dca-92f3-7f12aef4f16e/Microsoft.Azure.Functions.ExtensionBundle.1.1.1.zip
[2020/06/03 11:08:13] Extracting extension bundle at /var/folders/pf/mbns7g5n6nlfj48466cy3kj40000gn/T/Functions/ExtensionBundles/Microsoft.Azure.Functions.ExtensionBundle/1.1.1
[2020/06/03 11:08:13] Zip extraction complete
[2020/06/03 11:08:13] Loading Extention bundle from /var/folders/pf/mbns7g5n6nlfj48466cy3kj40000gn/T/Functions/ExtensionBundles/Microsoft.Azure.Functions.ExtensionBundle/1.1.1
[2020/06/03 11:08:13] FUNCTIONS_WORKER_RUNTIME set to node. Skipping WorkerConfig for language:python
[2020/06/03 11:08:13] FUNCTIONS_WORKER_RUNTIME set to node. Skipping WorkerConfig for language:java
[2020/06/03 11:08:13] FUNCTIONS_WORKER_RUNTIME set to node. Skipping WorkerConfig for language:powershell
[2020/06/03 11:08:13] Initializing Warmup Extension.
[2020/06/03 11:08:14] Initializing Host. OperationId: 'd6b60201-83fa-46d6-8a79-67bd2d429035'.
[2020/06/03 11:08:14] Host initialization: ConsecutiveErrors=0, StartupCount=1, OperationId=d6b60201-83fa-46d6-8a79-67bd2d429035
[2020/06/03 11:08:14] LoggerFilterOptions
[2020/06/03 11:08:14] {
[2020/06/03 11:08:14]   "MinLevel": "None",
[2020/06/03 11:08:14]   "Rules": [
[2020/06/03 11:08:14]     {
[2020/06/03 11:08:14]       "ProviderName": null,
[2020/06/03 11:08:14]       "CategoryName": null,
[2020/06/03 11:08:14]       "LogLevel": null,
[2020/06/03 11:08:14]       "Filter": "<AddFilter>b__0"
[2020/06/03 11:08:14]     },
[2020/06/03 11:08:14]     {
[2020/06/03 11:08:14]       "ProviderName": "Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics.SystemLoggerProvider",
[2020/06/03 11:08:14]       "CategoryName": null,
[2020/06/03 11:08:14]       "LogLevel": "None",
[2020/06/03 11:08:14]       "Filter": null
[2020/06/03 11:08:14]     },
[2020/06/03 11:08:14]     {
[2020/06/03 11:08:14]       "ProviderName": "Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics.SystemLoggerProvider",
[2020/06/03 11:08:14]       "CategoryName": null,
[2020/06/03 11:08:14]       "LogLevel": null,
[2020/06/03 11:08:14]       "Filter": "<AddFilter>b__0"
[2020/06/03 11:08:14]     }
[2020/06/03 11:08:14]   ]
[2020/06/03 11:08:14] }
[2020/06/03 11:08:14] FunctionResultAggregatorOptions
[2020/06/03 11:08:14] {
[2020/06/03 11:08:14]   "BatchSize": 1000,
[2020/06/03 11:08:14]   "FlushTimeout": "00:00:30",
[2020/06/03 11:08:14]   "IsEnabled": true
[2020/06/03 11:08:14] }
[2020/06/03 11:08:14] SingletonOptions
[2020/06/03 11:08:14] {
[2020/06/03 11:08:14]   "LockPeriod": "00:00:15",
[2020/06/03 11:08:14]   "ListenerLockPeriod": "00:00:15",
[2020/06/03 11:08:14]   "LockAcquisitionTimeout": "10675199.02:48:05.4775807",
[2020/06/03 11:08:14]   "LockAcquisitionPollingInterval": "00:00:05",
[2020/06/03 11:08:14]   "ListenerLockRecoveryPollingInterval": "00:01:00"
[2020/06/03 11:08:14] }
[2020/06/03 11:08:14] HttpOptions
[2020/06/03 11:08:14] {
[2020/06/03 11:08:14]   "DynamicThrottlesEnabled": false,
[2020/06/03 11:08:14]   "MaxConcurrentRequests": -1,
[2020/06/03 11:08:14]   "MaxOutstandingRequests": -1,
[2020/06/03 11:08:14]   "RoutePrefix": "api"
[2020/06/03 11:08:14] }
[2020/06/03 11:08:14] Starting JobHost
[2020/06/03 11:08:14] Starting Host (HostId=younameisyupc-237003343, InstanceId=309bf51b-da3c-4db1-be93-1afdc15b677e, Version=3.0.13353.0, ProcessId=5908, AppDomainId=1, InDebugMode=False, InDiagnosticMode=False, FunctionsExtensionVersion=(null))
[2020/06/03 11:08:14] Loading functions metadata
[2020/06/03 11:08:14] 1 functions loaded
[2020/06/03 11:08:14] Loading proxies metadata
[2020/06/03 11:08:14] Initializing Azure Function proxies
[2020/06/03 11:08:16] 0 proxies loaded
[2020/06/03 11:08:16] Starting worker process:node  --inspect=9229 "/usr/local/Cellar/azure-functions-core-tools@3/3.0.2534/workers/node/dist/src/nodejsWorker.js" --host 127.0.0.1 --port 50181 --workerId c50406b7-d0c6-48fa-a29d-c66ec65f8d4f --requestId 83f76573-42dc-499a-80b9-a44874849ea9 --grpcMaxMessageLength 134217728
[2020/06/03 11:08:17] node process with Id=5919 started
[2020/06/03 11:08:17] Generating 1 job function(s)
[2020/06/03 11:08:17] Found the following functions:
[2020/06/03 11:08:17] Host.Functions.HttpExample
[2020/06/03 11:08:17] 
[2020/06/03 11:08:17] Initializing function HTTP routes
[2020/06/03 11:08:17] Mapped function route 'api/HttpExample' [get,post] to 'HttpExample'
[2020/06/03 11:08:17] 
[2020/06/03 11:08:17] Host initialized (2741ms)
[2020/06/03 11:08:17] Host started (2750ms)
[2020/06/03 11:08:17] Job host started
[2020/06/03 11:08:17] Debugger listening on ws://127.0.0.1:9229/d7c34f05-e633-4dec-88f4-140ed939a894
[2020/06/03 11:08:17] For help, see: https://nodejs.org/en/docs/inspector
Hosting environment: Production
Content root path: /Users/you_name_is_yu/Develop/AzureFunctions
Now listening on: http://0.0.0.0:7071
Application started. Press Ctrl+C to shut down.

Http Functions:

        HttpExample: [GET,POST] http://localhost:7071/api/HttpExample

[2020/06/03 11:08:17] Worker c50406b7-d0c6-48fa-a29d-c66ec65f8d4f connecting on 127.0.0.1:50181
[2020/06/03 11:08:22] Host lock lease acquired by instance ID '000000000000000000000000354A045E'.
[2020/06/03 11:08:23] Debugger attached.

気になるポイントとしてはこんな感じで、HttpFunctionが動き始めたようなメッセージが出力されている部分でしょうか。

http://localhost:7071/api/HttpExample

ただ、このURLのままアクセスしてもダメみたいで、以下のように、クエリパラメータとしてname=Functionsを指定する必要があるようです。
http://localhost:7071/api/HttpExample?name=Functions
スクリーンショット 2020-06-03 20.13.50.png

アクセスしてみると、よくあるHello Worldのような感じで、Hello Functionsという文字が出力されました。

ブラウザにアクセスした時点でTERMINALに出力される内容としては以下のような内容が出力されています。

[2020/06/03 11:13:27] Executing HTTP request: {
[2020/06/03 11:13:27]   "requestId": "816dc4fe-2914-4e86-99a5-7820ab231808",
[2020/06/03 11:13:27]   "method": "GET",
[2020/06/03 11:13:27]   "uri": "/api/HttpExample"
[2020/06/03 11:13:27] }
[2020/06/03 11:13:27] Executing 'Functions.HttpExample' (Reason='This function was programmatically called via the host APIs.', Id=32b32a60-2b58-4988-8fd5-329484b2a50d)
[2020/06/03 11:13:28] JavaScript HTTP trigger function processed a request.
[2020/06/03 11:13:28] Executed 'Functions.HttpExample' (Succeeded, Id=32b32a60-2b58-4988-8fd5-329484b2a50d)
[2020/06/03 11:13:28] Executed HTTP request: {
[2020/06/03 11:13:28]   "requestId": "816dc4fe-2914-4e86-99a5-7820ab231808",
[2020/06/03 11:13:28]   "method": "GET",
[2020/06/03 11:13:28]   "uri": "/api/HttpExample",
[2020/06/03 11:13:28]   "identities": [
[2020/06/03 11:13:28]     {
[2020/06/03 11:13:28]       "type": "WebJobsAuthLevel",
[2020/06/03 11:13:28]       "level": "Admin"
[2020/06/03 11:13:28]     }
[2020/06/03 11:13:28]   ],
[2020/06/03 11:13:28]   "status": 200,
[2020/06/03 11:13:28]   "duration": 1095
[2020/06/03 11:13:28] }
[2020/06/03 11:13:28] Executing HTTP request: {
[2020/06/03 11:13:28]   "requestId": "ec5ffac0-d06e-4795-ae24-e2bb5b2f9cd7",
[2020/06/03 11:13:28]   "method": "GET",
[2020/06/03 11:13:28]   "uri": "/favicon.ico"
[2020/06/03 11:13:28] }
[2020/06/03 11:13:28] Executed HTTP request: {
[2020/06/03 11:13:28]   "requestId": "ec5ffac0-d06e-4795-ae24-e2bb5b2f9cd7",
[2020/06/03 11:13:28]   "method": "GET",
[2020/06/03 11:13:28]   "uri": "/favicon.ico",
[2020/06/03 11:13:28]   "identities": [],
[2020/06/03 11:13:28]   "status": 404,
[2020/06/03 11:13:28]   "duration": 352
[2020/06/03 11:13:28] }

デバッガーを終了

Disconnectボタンを押下するか、Ctrl+Cを押下することでデバッグを終了します。
スクリーンショット 2020-06-03 20.17.38.png

Azureへのサインイン

Azure Functionsのアプリを発行するには、Azureへのサインインが必要です。
スクリーンショット 2020-06-03 20.19.43.png

ブラウザでのログインが求められたので、私の場合はGitHubアカウントでサインインします。
スクリーンショット 2020-06-03 20.20.29.png

サインインしてしまえば、表示されたブラウザは閉じてOKです。
スクリーンショット 2020-06-03 20.21.40.png

VSCodeでは無料試用版という表示になっているんですね。
スクリーンショット 2020-06-03 20.23.05.png

Azure にプロジェクトを発行する

スクリーンショット 2020-06-03 20.24.02.png

Create new Function App in Azureを選択する。
※Advancedは選択しないでとのこと。
スクリーンショット 2020-06-03 20.26.12.png

関数にユニークな任意の名前を指定します。
スクリーンショット 2020-06-03 20.28.25.png

関数の名前にはアンダーバーなど使えない文字があるようです。
スクリーンショット 2020-06-03 20.28.30.png

今回はNode.jsのバージョンを12.18.0としているため、Node.js 12を選択します。
スクリーンショット 2020-06-03 20.28.49.png

Japan Eastを選択します。

パフォーマンスを向上させるために、お近くのリージョンを選択してください

とのことです。
スクリーンショット 2020-06-03 20.29.24.png

上記手順まで進むと、処理が進みます。
スクリーンショット 2020-06-03 20.30.18.png

完了すると以下のようになります。
View outputボタンを押下することで、作成とデプロイの結果を確認することができます。
スクリーンショット 2020-06-03 20.35.21.png

8:30:46 PM: Creating resource group "yuyamaguchifunctionshttp" in location "japaneast"...
8:30:46 PM: Successfully created resource group "yuyamaguchifunctionshttp".
8:30:46 PM: Creating storage account "yuyamaguchifunctionshttp" in location "japaneast" with sku "Standard_LRS"...
8:31:18 PM: Successfully created storage account "yuyamaguchifunctionshttp".
8:31:18 PM: Verifying that Application Insights is available for this location...
8:31:18 PM: Creating Application Insights resource "yuyamaguchifunctionshttp"...
8:31:21 PM: Successfully created Application Insights resource "yuyamaguchifunctionshttp".
8:31:21 PM: Creating new function app "Yu-Yamaguchi-Functions-HttpExample"...
8:32:09 PM: Successfully created function app "Yu-Yamaguchi-Functions-HttpExample": https://yu-yamaguchi-functions-httpexample.azurewebsites.net
8:32:13 PM Yu-Yamaguchi-Functions-HttpExample: Creating zip package...
8:32:13 PM Yu-Yamaguchi-Functions-HttpExample: Starting deployment...
8:32:22 PM Yu-Yamaguchi-Functions-HttpExample: Updating submodules.
8:32:22 PM Yu-Yamaguchi-Functions-HttpExample: Preparing deployment for commit id 'a320bfa83a'.
8:32:23 PM Yu-Yamaguchi-Functions-HttpExample: Skipping build. Project type: Run-From-Zip
8:32:23 PM Yu-Yamaguchi-Functions-HttpExample: Skipping post build. Project type: Run-From-Zip
8:32:23 PM Yu-Yamaguchi-Functions-HttpExample: Triggering recycle (preview mode disabled).
8:32:27 PM Yu-Yamaguchi-Functions-HttpExample: Syncing 2 function triggers with payload size 158 bytes successful.
8:32:28 PM Yu-Yamaguchi-Functions-HttpExample: Deployment successful.
8:32:42 PM Yu-Yamaguchi-Functions-HttpExample: Started postDeployTask "npm install".
8:32:43 PM Yu-Yamaguchi-Functions-HttpExample: Querying triggers...
8:32:48 PM Yu-Yamaguchi-Functions-HttpExample: HTTP Trigger Urls:
  HttpExample: https://yu-yamaguchi-functions-httpexample.azurewebsites.net/api/HttpExample

Azure で関数を実行する

URLをコピーする。
スクリーンショット 2020-06-03 20.39.48.png

コピーしたURLは以下の通りです。
https://yu-yamaguchi-functions-httpexample.azurewebsites.net/api/HttpExample

これにローカル環境での動作確認でも指定したクエリ文字列?name=Functionsを指定したURLを生成し、ブラウザでアクセスします。
https://yu-yamaguchi-functions-httpexample.azurewebsites.net/api/HttpExample?name=Functions
スクリーンショット 2020-06-03 20.41.59.png

このURLをみる限り、ユニークなFunctionの名前とは、グローバルでのユニークとなるように指定する必要があるようですね。
無闇に作らず、命名には注意が必要そうです。

Azureのポータルサイトにアクセスすると、デプロイしたFunctionが確認できます。

スクリーンショット 2020-06-03 20.44.09.png

Azure Functionsはリクエスト数が多いとその分費用?がかかるような従量課金だと思いますので、ここで作ったFunctionは消しておきますw
なので、ここに載せているFunctionのURLにアクセスしても意味が無いので悪しからずm(_ _)m

以上で、クイックスタートの内容は完了です。
まだ何ができるかわかっていませんが、できればOffice365とか会社で使っているので、TeamsとかOneDriveのドキュメントとか、その辺りといい感じのことができると嬉しいなと思っています。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?