Fargate による HAProxy で RDS postgres のロードバランシング

リード主体のワークロードでRDSを使っている場合、リードレプリカはパフォーマンスに効果的な選択肢だと思います。
ただ、コネクションのエンドポイントはリードレプリカのインスタンス毎に分かれているので、実際に使う場合は、何らかのプロキシを経由して使うことが多いです。
Auroraを使っている場合はまずReader Endpointを使う方法が考えられます、そのほかにもIPアドレスでNLBに登録する方法があると思いますが、今回はpostgresqlでHAProxyとNetworkLoadBalancer(NLB)を使ったパターンを考えてみました。

HAProxyを使うメリットは、バランシングのアルゴリズムの選択や、ルールを柔軟に設定できる点です。
今回は、リードレプリカを時間によって落とした場合に、バックアップとしてマスタを参照するようにしたかったので、NLBに直接リードレプリカを登録した場合、ルールのフォールバックができないようなので、HAProxyを使ってそのように設定します。
また、HAProxyに限らずですが、リードレプリカにシングルエンドポイントで接続できるようになることで、シングルAZのインスタンスでも冗長性を確保でき、負荷にもスケールアウトで対応できるのは、大きなメリットだと思います。

構成はこのようになります。今回、HAProxyは2018年7月に東京リージョンに入ったFargateで作ってみようと思います。

![fagate-haproxy.png](https://qiita-image-store.s3.amazonaws.com/0/130580/10d9ed30-02e4-5069-d61b-e282bbeb0ad2.png)

リードレプリカは予め2つ作っておきます。Route53のVPCプライベートホストゾーンで、RDSインスタンスに名前を振っておきます。

* db-master.myvpc.local : マスター
* rep1.myvpc.local : リードレプリカ1
* rep2.myvpc.local : リードレプリカ2

# ECR リポジトリと、HAProxyのDocker イメージの作成

まず、HAProxyのDockerイメージを保存するリポジトリを作成します。コンテナ作成時はこのリポジトリが参照されます。

```bash
aws ecr create-repositry --repository-name rds-proxy
```

リポジトリができたら、Dockerfileを作成します。イメージはHAProxy公式のalpine linuxのものを使います。
Dockerfileで行っているのは、HAProxyのコンフィグファイルのインストールだけです。

```dockerfile:Dockerfile
FROM haproxy:alpine
COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg
```

```config:haproxy.cfg
global
    pidfile /var/run/haproxy.pid
    maxconn 3000
    user root
    group root
    daemon
    stats socket /tmp/haproxy-cli.sock

listen rdsinstances

    mode tcp
    retries 3
    timeout connect 10s
    timeout server  10s
    timeout client  10s
    bind 0.0.0.0:5432
    option pgsql-check user root
    balance roundrobin

    server rep1   db-rep1.myvpc.local port 5432 check inter 2000
    server rep2   db-rep2.myvpc.local port 5432 check inter 2000
    server master db-master.myvpc.local port 5432 backup
```

マスターを `backup` にすることで、リードレプリカを落としている間は、マスターを参照するようにします。ここでは最低限の設定のみですが、柔軟に設定することができると思います。
設定が書けたらDockerイメージをビルドします。できたイメージは先ほど作ったリポジトリにプッシュします。プッシュの手順はAWSコンソールのECRのメニューから確認することもできます。

```bash
docker build -t rds-proxy .
aws ecr get-login --no-include-email --region ap-northeast-1 | /bin/bash
docker tag rds-proxy:latest xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/rds-proxy:latest
docker push xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/rds-proxy:latest
```

# NLB、ターゲットグループの作成

次に、起動したHAProxyを登録するNetoworkLoadBalancerとターゲットグループを作成しておきます。schemeはVPC internalにします。
ここで指定するVPC、サブネットにはRDSにアクセスできる適切なものを指定します。

```bash
aws elbv2 create-target-group --name rds-haproxies --protocol TCP --port 5432 --vpc-id vpc-xxxxxxxx --target-type ip | \
  jq -r '.TargetGroups[] | [{"Type":"forward","TargetGroupArn":.TargetGroupArn}]' > defaultaction
aws elbv2 create-load-balancer --name rds-proxy --type network --scheme internal --subnets subnet-xxxxxxxx subnet-yyyyyyyy | \
  jq -r '.LoadBalancers[0].LoadBalancerArn' | \
  xargs -I% aws elbv2 create-listners --default-action file://./defaultaction

```

# ECS クラスタの作成

ECSクラスタを作成します。

```bash
ecs-cli up --c rds-proxy --region ap-northeast-1
```

# ECS タスク定義、サービスの起動

ここからは docker-compose.yml で行えます。タスクとサービスは、AWSコンソールでも設定が面倒なので、コード化できるのは大きなメリットだと思います。ECS 固有の設定は `ecs-params.yml` に書くことができます。こうすることでローカルでのテストにも支障がでません。
`task_execution_role` は予め作成しておく必要があります。ネットワークモードを `awsvpc` にすることで、既存のVPCに所属させることができます。サブネットはNLB作成時に指定したものと合わせる必要があります。

```yaml:docker-compose.yml
version: '3'
services:
  haproxy:
    image: "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/rds-proxy"
    ports:
      - "5432:5432"
    logging:
      driver: awslogs
      options:
        awslogs-group: /ecs/rds-proxy
        awslogs-region: ap-northeast-1
        awslogs-stream-prefix: ecs
    ulimits:
      nofile:
        soft: 65536
        hard: 65536
```

```yaml:ecs-params.yml
version: 1
task_definition:
  ecs_network_mode: awsvpc
  task_execution_role: ecsTaskExecutionRole
  task_size:
    cpu_limit: 256
    mem_limit: 512
run_params:
  network_configuration:
    awsvpc_configuration:
      subnets:
        - "subnet-xxxxxxxx"
        - "subnet-xxxxxxxx"
      security_groups:
        - "sg-xxxxxxxx"
      assign_public_ip: DISABLED
```

設定ができたら、最後に `ecs-cli` でサービスを作成してタスクを起動します。
起動タイプに `FARGATE` を指定します。残念ながら、ロードバランサは `ecs-params.yml` に記述することができないようなので、ここで指定します。ALB/NLBの場合は `--target-group-arn` でターゲットグループを指定します。

```bash
ecs-cli compose \
  --verbose \
  -c rds-proxy \
  --task-role-arn arn:aws:iam::xxxxxxxxxxxx:role/ecsTaskExecutionRole \
    service up \
      --launch-type FARGATE \
      --create-log-groups \
      --container-name haproxy \
      --container-port 5432 \
      --target-group-arn arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxxxx:targetgroup/rds-haproxies/xxxxxxxxxxxxxxxx
```

以上でHAProxyのタスクが1つ起動できました。
最初、`--task-role-arn` でコンテナにロールを割り当てるのを忘れていたので、タスクのステータスがPENDDINGから進まず起動できませんでした。CloudWatchLogsを使用する場合、書き込み権限のあるロールをコンテナにつける必要がありました。
docker-composeに近いI/Fで操作できるのはありがたいですが、コマンドラインオプションで指定する必要があったり、まだちょっと使いづらいと感じました。

# 確認

最後に、リードレプリカへのリクエストの分散が効いているか確認しておきます。

```bash
$ sh <<'SCRIPT' | sort | uniq -c
for run in `seq 100`; do
psql -At -U postgres -h rds-proxy-xxxxxxxxxxxxxxxx.elb.ap-northeast-1.amazonaws.com -c 'select inet_server_addr();'
done
SCRIPT

     49 172.31.27.155
     51 172.31.9.138
```

きちんとラウンドロビンでできているようです。