Cloud Foundryの複数Portに対するRouting

Cloud Foundryはかつては単一ポート(通常8080)へのRoutingしかサポートしていませんでしたが、 今では任意のポートに対してHTTPでもTCPでもRouteできますし、複数ポートのサポートされています。

次のドキュメントが参考になります。

目次

Buildpackを使った普通のRouting

まずは復習として"普通のRouting"を改めて見てみます。

次のコマンドでSpring Bootのアプリケーション雛形を作成します。

curl https://start.spring.io/starter.tgz \
       -d artifactId=hello-cf \
       -d baseDir=hello-cf \
       -d dependencies=web,actuator \
       -d packageName=com.example \
       -d applicationName=HelloCfApplication | tar -xzvf -
cd hello-cf
./mvnw clean package -DskipTests=true

Routeをつけずにcf pushします。

cf push hello -p target/hello-cf-0.0.1-SNAPSHOT.jar --no-route

この後頻繁に使うため、APP_GUIDを環境変数に設定します。

APP_GUID=$(cf app hello --guid)

次のコマンドでデプロイしたアプリがサポートしているportを確認できます。

$ cf curl "/v2/apps/${APP_GUID}" | jq ".entity.ports"               
[
  8080
]

cf map-routeはこのportに対するRouteのmappingです。

この後、使う環境変数を設定しておきます。

APPS_DOMAIN=$(cf curl "/v2/shared_domains" | jq -r ".resources[0].entity.name")
CURRENT_SPACE=$(cat ~/.cf/config.json | jq -r ".SpaceFields.Name")

通常は次のコマンドでRoute(この場合はhello1.${APPS_DOMAIN})をmapします。

cf map-route hello ${APPS_DOMAIN} --hostname hello1

これは次のコマンドと概ね同じです。

cf create-route ${CURRENT_SPACE} ${APPS_DOMAIN} --hostname hello1
ROUTE_GUID=$(cf curl "/v2/routes?q=host:hello1" | jq -r ".resources[0].metadata.guid")
# 対象のROUTE_GUIDを持つRouteを対象のAPP_GUIDを持つAppの8080番ポートにマッピングする
cf curl /v2/route_mappings -X POST -d "{\"app_guid\": \"${APP_GUID}\", \"route_guid\": \"${ROUTE_GUID}\", \"app_port\": 8080}"

これで

curl -s https://hello1.${APPS_DOMAIN}/actuator

にアクセスできます。

TCP Routeを使う場合は

cf map-route ${APP_NAME} ${TCP_DOMAIN} --port 1024

または

cf create-route ${CURRENT_SPACE} ${TCP_DOMAIN} --port 1024
ROUTE_GUID=$(cf curl "/v2/routes?q=port:1024" | jq -r ".resources[0].metadata.guid")
# あとは同じ

次のコマンドでhelloのAppとRouteを削除します。

cf delete -r -f hello

Docker Imageを使った普通のRouting

次にDocker Imageをcf pushする場合にどうなるか見てみます。 題材にはZipkinを使用します。

まずはopenzipkin/zipkin-slimのDocker Imageをpushします。

cf push zipkin --docker-image openzipkin/zipkin-slim:2.20.2 --no-route

この後頻繁に使うため、APP_GUIDを環境変数に設定します。

APP_GUID=$(cf app zipkin --guid)

次のコマンドでデプロイしたアプリがサポートしているportを確認できます。

$ cf curl "/v2/apps/${APP_GUID}" | jq ".entity.ports"               
[
  9411
]

Buildpackの場合とは異なり8080ではありません。 このPortはDockerfile内のEXPOSEに設定されている値です。 https://github.com/openzipkin/zipkin/blob/2.20.2/docker/Dockerfile#L81

HTTPのRouteをmappingはBuildpack同様に次のコマンドでできます。

cf map-route zipkin ${APPS_DOMAIN} --hostname zipkin1

または

cf create-route ${CURRENT_SPACE} ${APPS_DOMAIN} --hostname zipkin1
ROUTE_GUID=$(cf curl "/v2/routes?q=host:zipkin1" | jq -r ".resources[0].metadata.guid")
# 対象のROUTE_GUIDを持つRouteを対象のAPP_GUIDを持つAppの9411番ポートにマッピングする
cf curl /v2/route_mappings -X POST -d "{\"app_guid\": \"${APP_GUID}\", \"route_guid\": \"${ROUTE_GUID}\", \"app_port\": 9411}"

あえてcf push--no-routeをつけましたが、--no-routeをつけなければRouteのmapも自動で行われます。

これで https://zipkin1.${APPS_DOMAIN} でZipkinにアクセスできます。

次のコマンドでzipkinのAppとRouteを削除します

cf delete -r -f zipkin

複数PortをExposeするDocker Imageを使ったRouting

ここまで単一portのみサポートされているAppを見てきましたが、次は複数portをサポートするAppを見てみます。 同じZipkinでもopenzipkin/zipkin Docker Imageは94109411をサポートします。

cf push zipkin --docker-image openzipkin/zipkin:2.20.2 --no-route --health-check-type process

--health-check-type processをつけた理由は後述します。

この後頻繁に使うため、APP_GUIDを環境変数に設定します。

APP_GUID=$(cf app zipkin --guid)

次のコマンドでデプロイしたアプリがサポートしているportを確認できます。

$ cf curl "/v2/apps/${APP_GUID}" | jq ".entity.ports"               
[
  9410,
  9411
]

この順番はDockerfile内のEXPOSEの順です。 https://github.com/openzipkin/zipkin/blob/2.20.2/docker/Dockerfile#L134

Diegoはこのportsの最初のportをhealth checkします。 Zipkinは9411が通常使用するHTTPのportであり、9410はLegacyなScribe Collectorを使う場合のThriftのportです。 デフォルトでScribe Collectorはdisabledになっているため、このままではport health checkがfailします。そのため、--health-check-type processをつけました。 9410でport health checkするには環境変数COLLECTOR_SCRIBE_ENABLEDtrueにする必要があります。

ここでは9410ポートを使わず、9411だけをRoute対象にします。次のコマンドで設定を行います。

cf curl "/v2/apps/${APP_GUID}" -X PUT -d "{\"ports\": [9411]}"

変更内容を確認します。

$ cf curl "/v2/apps/${APP_GUID}" | jq ".entity.ports"
[
  9411
]

これでopenzipkin/zipkin-slimを使う場合、すなわち単一portと同じ条件になり、次のコマンドでRouteをmapできます。

cf map-route zipkin ${APPS_DOMAIN} --hostname zipkin1

または

cf create-route ${CURRENT_SPACE} ${APPS_DOMAIN} --hostname zipkin1
ROUTE_GUID=$(cf curl "/v2/routes?q=host:zipkin1" | jq -r ".resources[0].metadata.guid")
cf curl /v2/route_mappings -X POST -d "{\"app_guid\": \"${APP_GUID}\", \"route_guid\": \"${ROUTE_GUID}\", \"app_port\": 9411}"

まとめるとZipkinをopenzipkin/zipkinのDocker Imageでpushして9411ポートにHTTP Routingするには

cf push zipkin --docker-image openzipkin/zipkin:2.20.2 --no-route --no-start
APP_GUID=$(cf app zipkin --guid)
cf curl "/v2/apps/${APP_GUID}" -X PUT -d "{\"ports\": [9411]}"
cf map-route zipkin ${APPS_DOMAIN} --hostname zipkin1
cf start zipkin

Docker Imageが複数のPortをExposeする場合は最初のportが使われるのが要注意点です。 openzipkin/zipkinのように最初のportが使われると不都合がある場合は上記のように設定を変更する必要があります。

複数PortへのRouting

ここまで紹介したことを組み合わせると、アプリケーションが複数Portを持つ場合に、 それぞれのPortに対してRouteをmapすることができます。

最初に作ったアプリケーションにportを追加します。

Spring BootはActuatorのportをmanagement.server.portで変更できます。環境変数の場合はMANAGEMENT_SERVER_PORTで変更できます。

https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-customizing-management-server-port

cf push hello -p target/hello-cf-0.0.1-SNAPSHOT.jar --no-route --no-start
cf set-env hello MANAGEMENT_SERVER_PORT 8081

これで8080は通常のHTTPリクエスト、8081はActuatorへのHTTPリクエストを処理するようになります。

Appが80808081 portをサポートできるように設定を変えます。

APP_GUID=$(cf app hello --guid)
cf curl "/v2/apps/${APP_GUID}" -X PUT -d "{\"ports\": [8080, 8081]}"

変更内容を確認します。

$ cf curl "/v2/apps/${APP_GUID}" | jq ".entity.ports"
[
  8080,
  8081
]

80808081 portそれぞれにRouteを作成してmapします。

# 8080番portへのRoute
cf create-route ${CURRENT_SPACE} ${APPS_DOMAIN} --hostname hello1
ROUTE_GUID1=$(cf curl "/v2/routes?q=host:hello1" | jq -r ".resources[0].metadata.guid")
cf curl /v2/route_mappings -X POST -d "{\"app_guid\": \"${APP_GUID}\", \"route_guid\": \"${ROUTE_GUID1}\", \"app_port\": 8080}"

# 8081番portへのRoute
cf create-route ${CURRENT_SPACE} ${APPS_DOMAIN} --hostname hello2
ROUTE_GUID2=$(cf curl "/v2/routes?q=host:hello2" | jq -r ".resources[0].metadata.guid")
cf curl /v2/route_mappings -X POST -d "{\"app_guid\": \"${APP_GUID}\", \"route_guid\": \"${ROUTE_GUID2}\", \"app_port\": 8081}"

Appを起動します。

cf start hello

これでActuator Endpointがhello2でのみ有効になっていることが確認できます。

$ curl -s https://hello1.${APPS_DOMAIN}/actuator | jq .
{
  "timestamp": "2020-03-22T08:00:14.398+0000",
  "status": 404,
  "error": "Not Found",
  "message": "No message available",
  "path": "/actuator"
}

$ curl -s https://hello2.${APPS_DOMAIN}/actuator | jq .
{
  "_links": {
    "self": {
      "href": "https://hello2.${APPS_DOMAIN}/actuator",
      "templated": false
    },
    "health": {
      "href": "https://hello2.${APPS_DOMAIN}/actuator/health",
      "templated": false
    },
    "health-path": {
      "href": "https://hello2.${APPS_DOMAIN}/actuator/health/{*path}",
      "templated": true
    },
    "info": {
      "href": "https://hello2.${APPS_DOMAIN}/actuator/info",
      "templated": false
    }
  }
}

複数ポートへのRoutingはDocker Imageを使う場合も同様です。EXPOSEに記述されていないportもcf curl "/v2/apps/${APP_GUID}" -X PUT -d "{\"ports\": [port1, port2, ...]}"でサポート可能です。 また、Sidecarを使う場合も同じようにRouteをmapできます。

Blue/Greenデプロイを行う場合は上記のコマンドをblueとgreenそれぞれに行う必要があるため注意です。