SonarQubeをサーバレスコンテナで起動してみる

# はじめに
流行りのSonarQubeをコンテナ化して使ってみた。
コンテナ化しないで使ってみた系な記事は

【Developers.IO】[SonarQubeでソースコードの静的解析とレビューを自動化してみる(前編)](https://dev.classmethod.jp/articles/sonarqube-source-analytics-1/)

あたりを参考に。

# RDSの準備
コンテナ化するからにはデータを腹抱えしちゃダメでしょ、ということでDBはRDSに切り出す。
ひとまずPostgreSQLを指定して適当に起動(作成するDB名は```sonar```を指定しておく)してから、psqlクライアントが入っているEC2とかから、以下の様にコマンドを発行してログインする。

```
$ psql -U [DBアドミニストレータ名] -d sonar -h [DBインスタンスID].xxxxxxx.ap-northeast-1.rds.amazonaws.com
```

ログイン出来たら、ユーザを作成する。

```
sonar=> create role sonar with LOGIN password 'sonar';
```

これで準備完了。お手軽！
※ちゃんと使う場合はセキュリティグループとかは当然ちゃんと設定する。

# EC2の準備
とりあえずEC2上でサクっとで起動することを考える。
インスタンスタイプは、最低でもメモリサイズをsmall以上にしておく。
t2.micro ではメモリが足りなくて非コンテナでJavaがエラーを吐いたり、Dockerで起動したらストールしたりする。

インターネットで検索すると、Docker Composeしたりしているが、データをRDSに切り出しているのでコマンドラインで起動しても問題ないと考えよう。

Dockerをインストールする。

```
$ sudo yum update
$ sudo yum install -y docker
$ sudo service docker start
```

でDockerを起動してから、

```
$ sudo usermod -a -G docker ec2-user
$ cat /etc/group | grep docker
```

で docker グループにコンテナ起動ユーザを登録して、再ログイン。

ログインしたら

```
sudo docker run --ulimit nofile=65536 --ulimit nproc=4096 \
    --env SONARQUBE_JDBC_USERNAME="sonar" \
    --env SONARQUBE_JDBC_PASSWORD="sonar" \
    --env SONARQUBE_JDBC_URL="jdbc:postgresql://[RDSのエンドポイント]/sonar" \
    --name sonarqube -p 80:9000 sonarqube:7.9.3-community
```

で起動して、EC2にアクセスすれば普通に使えるようになってる！やった！

ポイントは、```--ulimit nofile=65536 --ulimit nproc=4096```の部分で、これを指定しておかないと、

```
2020.05.03 07:02:32 INFO  es[][o.e.n.Node] starting ...
ERROR: [1] bootstrap checks failed
[1]: max file descriptors [4096] for elasticsearch process is too low, increase to at least [65535]
2020.05.03 07:02:33 WARN  app[][o.s.a.p.AbstractManagedProcess] Process exited with exit value [es]: 78
```

といったエラーが出てElasticSearchが起動しない。

いや、コンテナ使うのにカーネルパラメータいじるなよ、というツッコミは抜きにしてね……。

# サーバレスを目指す
さらに、コンテナ化ができたらサーバレスにしたくなるのが人間の性というもの。

コンテナを以下のコマンドでECRに登録する。

```
$ docker tag sonarqube:7.9.3-community [AWSのアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/sonarqube:7.9.3-community
$ docker push [AWSのアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/sonarqube:7.9.3-community
```

その後、タスク登録してECSクラスタ立ててFargateの起動モードでサービス・タスクを起動する。

ポイントは、EC2でDockerコンテナで起動した時同様に、ちゃんと環境変数やulimitを設定してあげること。

- 環境変数
![キャプチャ2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/8f8c18f0-e146-a05e-79cd-c4a7d8198ae0.png)

- ulimit
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/6aad33fe-30f0-f573-3cd9-56a63164bafb.png)

さらに、本来であればsysctlコマンドで```vm.max_map_count = 262144```で設定してあげるべきところだが、残念ながらECSのSystemControlsプロパティではvm.*のカーネルパラメータ変更に対応していないようなので(と言うか、↑にも書いたがカーネルをガリガリ変更するようなものをコンテナ化しない方が良い)ので、ElasticSearchがmmapする数を抑止するオプションをつけてFargateを起動する必要がある(これを設定しないと、vm.max_map_countが足りずにコンテナが起動しない)。

![キャプチャ3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/b89bfe96-369c-1c4b-fa3f-b7b5eaf531ac.png)

あと気を付けなくてはいけないのは、以下のパラメータを充分に余裕を持った値にしておくこと。
これ、日本語がめちゃくちゃ分かりにくいのだけど、要約すると「起動後しばらくはLBにまともな応答返せないから、起動からこの時間が経つまでは```200 OK```じゃなくても無視してね」という設定。ギリギリのメモリ/CPUで起動するとちゃんとヘルスチェック応答できるようになるまでに5分以上かかるようなので、それに見合った値を設定しておく。

![キャプチャ4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/30e182d6-6bb6-c21d-19fe-cdde65d9488a.png)

マネコンの画面からポチポチで作るのも面倒なので、CloudFormationテンプレートで記述すると以下のような感じになる。

```YAML
AWSTemplateFormatVersion: "2010-09-09"
Description:
  ECS Create

Parameters:
  Prefix:
    Description: "Project name prefix"
    Type: "String"
    Default: "SonarQube"
  PrefixLower:
    Description: "Project name prefix(for URI)"
    Type: "String"
    Default: "sonarqube"
  RepositoryNameSuffix:
    Description: "RepositoryName on CodeCommit"
    Type: "String"
    Default: ""
  ECRImageNameSuffix:
    Description: "Container Image Name on ECR"
    Type: "String"
    Default: ""
  ECSClusterNameSuffix:
    Description: "ECS Cluster Name"
    Type: "String"
    Default: "-ECSCluster"
  ECSServiceNameSuffix:
    Description: "ECS Service Name"
    Type: "String"
    Default: "-ECSService"
  ECSTaskNameSuffix:
    Description: "ECS Task Name"
    Type: "String"
    Default: "-Container"
  ECSTaskContainerNameSuffix:
    Description: "Container name on ECS Task"
    Type: "String"
    Default: "-Container"
  ECSTaskLogGroupNameSuffix:
    Description: "Log Group name on ECS Task"
    Type: "String"
    Default: "-ECSTask-LogGroup"
  
Resources:
  # ------------------------------------------------------------#
  #  ECS
  # ------------------------------------------------------------#
  LOGGROUP:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /ecs/${Prefix}${ECSTaskLogGroupNameSuffix}

  ECSCLUSTER:
    Type: AWS::ECS::Cluster
    Properties: 
      ClusterName: !Sub ${Prefix}${ECSClusterNameSuffix}

  ECSTASKDEFINITION:
    Type: AWS::ECS::TaskDefinition
    Properties: 
      RequiresCompatibilities: 
        - FARGATE
      Cpu: "256"
      ExecutionRoleArn: !Sub arn:aws:iam::${AWS::AccountId}:role/ecsTaskExecutionRole
      Family: !Sub ${Prefix}${ECSTaskNameSuffix}
      Memory: "2048"
      NetworkMode: awsvpc
      TaskRoleArn: !Sub arn:aws:iam::${AWS::AccountId}:role/ecsTaskExecutionRole
      ContainerDefinitions: 
        - Name: !Sub ${Prefix}${ECSTaskContainerNameSuffix}
          Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${PrefixLower}${ECRImageNameSuffix}:7.9.3-community
          MemoryReservation: 2048
          Environment:
            - Name: SONARQUBE_JDBC_USERNAME
              Value: sonar
            - Name: SONARQUBE_JDBC_PASSWORD
              Value: sonar
            - Name: SONARQUBE_JDBC_URL
              Value: jdbc:postgresql://[RDSのエンドポイント]/sonar
          PortMappings: 
            - ContainerPort: 9000
              HostPort: 9000
              Protocol: HTTP
          Ulimits:
            - Name: nofile
              SoftLimit: 65536
              HardLimit: 65536
            - Name: nproc
              SoftLimit: 4096
              HardLimit: 4096
          Command:
            - -Dsonar.search.javaAdditionalOpts=-Dnode.store.allow_mmapfs=false
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref LOGGROUP
              awslogs-region: !Sub ${AWS::Region}
              awslogs-stream-prefix: ecs
  ECSSERVICE:
    Type: AWS::ECS::Service
    Properties: 
      Cluster: !Ref ECSCLUSTER
      ServiceName: !Sub ${Prefix}${ECSServiceNameSuffix}
      LaunchType: FARGATE
      DesiredCount: 1
      DeploymentController:
        Type: CODE_DEPLOY
      LoadBalancers: 
        - ContainerName: !Sub ${Prefix}${ECSTaskContainerNameSuffix}
          ContainerPort: 9000
          TargetGroupArn: [ALBターゲットグループのARN]
      HealthCheckGracePeriodSeconds: 600
      NetworkConfiguration: 
        AwsvpcConfiguration:
          AssignPublicIp: ENABLED
          SecurityGroups: 
            - [適切なセキュリティグループ(デフォルト設定のままなら9000番ポートのインバウンドを許容)]
          Subnets: 
            - [適切なサブネット]
      TaskDefinition: !Ref ECSTASKDEFINITION
```

環境変数はおそらく直値ではなくてParameter Storeから取得するのがベストプラクティスだが、今回はあまり気にしないことにする(Fargateは起動してはみたものの、EC2上のDockerコンテナと比べると動作がやや遅い。サービス向け機能ではないので、あえて高額なサーバレスにする意味はあまりない……と思う)。ログ周りの設定もかなりテキトーなので、本当にサーバレスコンテナで扱うならちゃんと考えた方が良いと思う。