Terraform & Azure AppServiceでSonarQubeをホストする

## この記事について
携わっているプロジェクトで、SonarQubeをホストしたい場面がありました。
Terraformを使ってデプロイをするのに少しハマったため、備忘録として記事にしておきます。
## 目次
1. [利用するサービスについて](#利用するサービスについて)
   - [AppService](#appservice)
   - [ストレージアカウント](#ストレージアカウント)
   - [Azure Cosmos DB for PostgreSQL Cluster](#azure-cosmos-db-for-postgresql-cluster)
2. [リソースの作成](#リソースの作成)
   - [AppServiceを動かすために必要なリソース](#appserviceを動かすために必要なリソース)
   - [ストレージアカウントとファイル共有](#ストレージアカウントとファイル共有)
   - [データベースの作成](#データベースの作成)
   - [AppServiceの作成](#appserviceの作成)
3. [最後に](#最後に)

## 利用するサービスについて
### AppService
- SonarQubeをホストする基盤として使用します
- `P0v3`を使用しましたが、スムーズに動きました

### ストレージアカウント
- SonarQubeの設定値などを格納するために使用するストレージアカウントです
- 開発用途で速度や可用性を重視していないため、`StandardV2`の`LRS`を選択しました

### Azure Cosmos DB for PostgreSQL Cluster
- バックエンド用のSQLデータベースとして利用します
- コストが安いことを理由に選定し、`Burstable 1 vCore 2GiB RAM`を使用しています
- 初めは組み込みH2データベースを使用していましたが、SonarQubeのアップデート時にマイグレーションが難しかったため、ちゃんとしたSQLデータベースを導入しました

## リソースの作成
### AppServiceを動かすために必要なリソース
AppServiceを構築するために必要なリソースの定義です。

```terraform
//リソースグループ
resource "azurerm_resource_group" "sonarqube" {
  name     = "rg-sonarqube"
  location = "japaneast"
}

//AppServicePlan
resource "azurerm_service_plan" "sonarqube" {
  name                = "plan-sonarqube"
  location            = azurerm_resource_group.sonarqube.location
  resource_group_name = azurerm_resource_group.sonarqube.name
  os_type             = "Linux"
  sku_name            = "P0v3"
  worker_count        = 1
}
```
- リソースグループは説明不要ですね
- AppServicePlanではSKUを`P0v3`、Worker Countを`1`で指定しています

### ストレージアカウントとファイル共有
SonarQubeの設定情報などを格納するためのストレージ共有を作成します。パスは[こちら](https://github.com/SonarSource/docker-sonarqube/blob/master/examples.md)を参考にしました
```terraform
locals {
  sonarqube_storage_share = toset([
    "data",
    "logs",
    "extensions"
  ])
}

resource "azurerm_storage_account" "sonarqube" {
  name                     = "stsonarqubemntb92f" //名前被り防止で乱数を追加
  resource_group_name      = azurerm_resource_group.sonarqube.name
  location                 = azurerm_resource_group.sonarqube.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

resource "azurerm_storage_share" "sonarqube" {
  for_each             = local.sonarqube_storage_share
  name                 = each.key
  storage_account_name = azurerm_storage_account.sonarqube.name
  quota                = 10
}
```
- ストレージアカウントの`name`はAzure上で一意に指定しないといけないので適当な乱数を追加しておきます
- local値で必要なファイル共有を定義して`for_each`を使って3つ作っています


### データベースの作成
次に、SonarQubeのデータベースとしてAzure Cosmos DB for PostgreSQL Clusterを設定します。
``` terraform
resource "random_password" "postgre" {
  length  = 20
  special = false
}

resource "azurerm_cosmosdb_postgresql_cluster" "sonarqube" {
  name                                 = "pg-sonarqube"
  resource_group_name                  = azurerm_resource_group.sonarqube.name
  location                             = azurerm_resource_group.sonarqube.location
  administrator_login_password         = random_password.postgre.result
  coordinator_storage_quota_in_mb      = 32768
  coordinator_vcore_count              = 1
  node_count                           = 0
  coordinator_public_ip_access_enabled = true
  sql_version                          = 15
  coordinator_server_edition           = "BurstableGeneralPurpose"
  node_server_edition                  = "BurstableGeneralPurpose"
}

data "azapi_resource" "cosmosdb_cl" {
  type                   = "Microsoft.DBforPostgreSQL/serverGroupsv2@2022-11-08"
  resource_id            = azurerm_cosmosdb_postgresql_cluster.sonarqube.id
  response_export_values = ["*"]
}
```
- パスワードを考えたくないので`random_password`を使用して、DB接続用のパスワード生成しています。
- CosmosDB PostgreSQL Clusterは最小限の設定で構築しています。
- ここで使用している[azurerm_cosmosdb_postgresql_clusterのAttributes Reference](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/cosmosdb_postgresql_cluster#attributes-reference)にはDBの接続用のエンドポイントが含まれていないんです。これで少しハマりました。
    - `azapi_resource`のdataソースを使用して、エンドポイントを取得する方法を取りました
    - このdataソースを使用すると、リソースの情報がJSONで取得できるため、`jsondecode`関数を用いて、エンドポイントのFQDNを取り出せます
```terraform
 ${jsondecode(data.azapi_resource.cosmosdb_cl.output).properties.serverNames[0].fullyQualifiedDomainName
 ```
### AppServiceの作成
最後にSonarQubeを稼働させるAppServiceを作成します
 ```terraform
resource "azurerm_linux_web_app" "sonarqube" {
  name                = "ase-sonarqube-b92f" //名前被り防止で乱数を追加
  location            = azurerm_resource_group.sonarqube.location
  resource_group_name = azurerm_resource_group.sonarqube.name
  service_plan_id     = azurerm_service_plan.sonarqube.id
  https_only          = true
  dynamic "storage_account" {
    for_each = local.sonarqube_storage_share
    content {
      name         = storage_account.key
      type         = "AzureFiles"
      account_name = azurerm_storage_account.sonarqube.name
      share_name   = storage_account.key
      access_key   = azurerm_storage_account.sonarqube.primary_access_key
      mount_path   = "/opt/sonarqube/${storage_account.key}"
    }
  }
  site_config {
    application_stack {
      docker_image_name   = "sonarqube:9.9-community"
      docker_registry_url = "https://index.docker.io"
    }
  }
  app_settings = merge({ for k in local.sonarqube_storage_share : "sonarqube_${k}" => "/opt/sonarqube/${k}" }, {
    SONAR_JDBC_URL                    = "jdbc:postgresql://${jsondecode(data.azapi_resource.cosmosdb_cl.output).properties.serverNames[0].fullyQualifiedDomainName}:5432/citus?user=citus&password=${random_password.DB.result}"
    SONAR_JDBC_USERNAME               = "citus"
    SONARQUBE_JDBC_PASSWORD           = random_password.DB.result
    SONAR_ES_BOOTSTRAP_CHECKS_DISABLE = true
  })
}
```
- ストレージアカウントと同様、Azure上で一意にするため`name`に乱数を含めています
- ストレージ共有をマウントする箇所は`dynamic`ブロックを使用してストレージ共有３つをマウントしています
- `site_config`の`application_stack`で適用したいコンテナイメージを指定できます`https://hub.docker.com/_/sonarqube`で現在公開されているイメージが確認できます
- `app_settings`では環境変数を設定します
    - ストレージ共有のパスはfor文で書きました。これは以下のように展開されます
        ```terraform
        > { for k in local.sonarqube_storage_share : "sonarqube_${k}" => "/opt/sonarqube/${k}" }
        {
          "sonarqube_data"       = "/opt/sonarqube/data"
          "sonarqube_log"        = "/opt/sonarqube/log"
          "sonarqube_extensions" = "/opt/sonarqube/extensions"
        }
        ```
    - その他の値は事前に作成したPostgreSQLの設定値です
    - 上記の設定値を`merge`関数を使用してTerraformに渡しています

## 最後に
これでデプロイが完了し、AppServiceのURLにアクセスするとSonarQubeが起動し、初期設定が可能です。SonarQubeの設定方法については公式ドキュメントや他の記事を参照することをお勧めします。適宜利用用途に合わせて設定値を見直していただければと思います。お役に立てれば幸いです。