はじめに
流行りのSonarQubeをコンテナ化して使ってみた。
コンテナ化しないで使ってみた系な記事は
【Developers.IO】SonarQubeでソースコードの静的解析とレビューを自動化してみる(前編)
あたりを参考に。
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を設定してあげること。
さらに、本来であればsysctlコマンドでvm.max_map_count = 262144
で設定してあげるべきところだが、残念ながらECSのSystemControlsプロパティではvm.*のカーネルパラメータ変更に対応していないようなので(と言うか、↑にも書いたがカーネルをガリガリ変更するようなものをコンテナ化しない方が良い)ので、ElasticSearchがmmapする数を抑止するオプションをつけてFargateを起動する必要がある(これを設定しないと、vm.max_map_countが足りずにコンテナが起動しない)。
あと気を付けなくてはいけないのは、以下のパラメータを充分に余裕を持った値にしておくこと。
これ、日本語がめちゃくちゃ分かりにくいのだけど、要約すると「起動後しばらくはLBにまともな応答返せないから、起動からこの時間が経つまでは200 OK
じゃなくても無視してね」という設定。ギリギリのメモリ/CPUで起動するとちゃんとヘルスチェック応答できるようになるまでに5分以上かかるようなので、それに見合った値を設定しておく。
マネコンの画面からポチポチで作るのも面倒なので、CloudFormationテンプレートで記述すると以下のような感じになる。
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コンテナと比べると動作がやや遅い。サービス向け機能ではないので、あえて高額なサーバレスにする意味はあまりない……と思う)。ログ周りの設定もかなりテキトーなので、本当にサーバレスコンテナで扱うならちゃんと考えた方が良いと思う。