ネットワーク負荷分散 Logbook アプリのデプロイ

このチュートリアルでは、Node.js をフロントエンドとして使用し、バックエンドで MySQL を使用するサンプル Logbook アプリをデプロイします。チュートリアルの最後には、次のリソースがデプロイされています。

ネットワーク負荷分散型デプロイ リソース(クリックすると拡大)

Deployment Manager を初めて使用する方は、クイックスタートまたはステップバイステップ ガイドのチュートリアルをご覧ください。

始める前に

リソース テンプレートの作成

このサンプルでは、さまざまな Google Cloud リソースを含むデプロイを作成します。はじめに、各リソースを個別に定義するテンプレートを作成します。その後、最終設定において各テンプレートを呼び出します。デプロイには次のリソースが含まれています。

  • アプリの MySQL データベースをホストする Compute Engine インスタンス。
  • フロントエンド インスタンスのインスタンス テンプレート。Node.js アプリ用の Docker イメージを使用します。
  • マネージド インスタンス グループ。インスタンス テンプレートを使用して 2 つのフロントエンド インスタンスを作成します。
  • オートスケーラー。受信トラフィックに基づいて追加のフロントエンド インスタンスを開始または停止します。
  • フロントエンド インスタンスが使用可能かどうかを確認するヘルスチェック。
  • 転送ルールが設定されているネットワーク ロードバランサ。
  • マネージド インスタンス グループのターゲット プール。

MySQL バックエンドのテンプレートの作成

このアプリのバックエンドは、単一の Compute Engine インスタンスで、MySQL Docker コンテナを実行します。container_vm.py テンプレートは、Docker コンテナを実行できる Compute Engine インスタンスを定義します。テンプレートの形式が正しく、必要なプロパティがすべて設定されているかどうか確認するには、スキーマ ファイルも必要です。

次のテンプレートをコピーするか、GitHub リポジトリからダウンロードします。

# Copyright 2016 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Creates a Container VM with the provided Container manifest."""

from container_helper import GenerateManifest

COMPUTE_URL_BASE = 'https://www.googleapis.com/compute/v1/'

def GlobalComputeUrl(project, collection, name):
  return ''.join([COMPUTE_URL_BASE, 'projects/', project,
                  '/global/', collection, '/', name])

def ZonalComputeUrl(project, zone, collection, name):
  return ''.join([COMPUTE_URL_BASE, 'projects/', project,
                  '/zones/', zone, '/', collection, '/', name])

def GenerateConfig(context):
  """Generate configuration."""

  base_name = context.env['name']

  # Properties for the container-based instance.
  instance = {
      'zone': context.properties['zone'],
      'machineType': ZonalComputeUrl(context.env['project'],
                                     context.properties['zone'],
                                     'machineTypes',
                                     'f1-micro'),
      'metadata': {
          'items': [{
              'key': 'gce-container-declaration',
              'value': GenerateManifest(context)
              }]
          },
      'disks': [{
          'deviceName': 'boot',
          'type': 'PERSISTENT',
          'autoDelete': True,
          'boot': True,
          'initializeParams': {
              'diskName': base_name + '-disk',
              'sourceImage': GlobalComputeUrl('cos-cloud',
                                              'images',
                                              context.properties[
                                                  'containerImage'])
              },
          }],
      'networkInterfaces': [{
          'accessConfigs': [{
              'name': 'external-nat',
              'type': 'ONE_TO_ONE_NAT'
              }],
          'network': GlobalComputeUrl(context.env['project'],
                                      'networks',
                                      'default')
          }],
        'serviceAccounts': [{
            'email': 'default',
            'scopes': ['https://www.googleapis.com/auth/logging.write']
            }]
      }

  # Resources to return.
  resources = {
      'resources': [{
          'name': base_name,
          'type': 'compute.v1.instance',
          'properties': instance
          }]
      }

  return resources

テンプレートのスキーマ ファイルをダウンロードします

テンプレートには containerImage などの未定義のプロパティが含まれています。このようなプロパティは後のテンプレートで定義されます。

Compute Engine インスタンスでコンテナ イメージを使用する場合は、使用するコンテナ イメージを記述したマニフェスト ファイルも用意する必要があります。container_helper.py というヘルパー メソッドを作成して、コンテナ マニフェストを動的に定義します。

# Copyright 2016 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Helper methods for working with containers in config."""

import six
import yaml

def GenerateManifest(context):
  """Generates a Container Manifest given a Template context.

  Args:
    context: Template context, which must contain dockerImage and port
        properties, and an optional dockerEnv property.

  Returns:
    A Container Manifest as a YAML string.
  """
  env_list = []
  if 'dockerEnv' in context.properties:
    for key, value in six.iteritems(context.properties['dockerEnv']):
      env_list.append({'name': key, 'value': str(value)})

  manifest = {
      'apiVersion': 'v1',
      'kind': 'Pod',
      'metadata': {
          'name': str(context.env['name'])
          },
      'spec': {
          'containers': [{
              'name': str(context.env['name']),
              'image': context.properties['dockerImage'],
              'ports': [{
                  'hostPort': context.properties['port'],
                  'containerPort': context.properties['port']
                  }],
              }]
          }
      }

  if env_list:
    manifest['spec']['containers'][0]['env'] = env_list

  return yaml.dump(manifest, default_flow_style=False)

Node.js フロントエンドのテンプレートの作成

アプリのフロントエンドで Node.js を実行し、ユーザーがウェブページにメッセージを投稿できるようにします。フロントエンドは仮想マシン インスタンス グループ上で稼働し、オートスケーラーとロードバランサによってサポートされます。次の手順でフロントエンド テンプレートを作成します。

  1. インスタンス テンプレート リソースを作成します。

    マネージド インスタンス グループを作成するには、インスタンス テンプレートが必要です。マネージド インスタンス グループとは、単一のエンティティとして制御する同一の仮想マシン(VM)インスタンスのグループです。

    container_instance_template.py という名前のファイルを作成し、テンプレートのスキーマをダウンロードします。

    # Copyright 2016 Google Inc. All rights reserved.
    #
    # Licensed under the Apache License, Version 2.0 (the "License");
    # you may not use this file except in compliance with the License.
    # You may obtain a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.
    
    """Creates a Container VM with the provided Container manifest."""
    
    from container_helper import GenerateManifest
    
    def GenerateConfig(context):
      """Generates configuration."""
    
      image = ''.join(['https://www.googleapis.com/compute/v1/',
                       'projects/cos-cloud/global/images/',
                       context.properties['containerImage']])
      default_network = ''.join(['https://www.googleapis.com/compute/v1/projects/',
                                 context.env['project'],
                                 '/global/networks/default'])
    
      instance_template = {
          'name': context.env['name'] + '-it',
          'type': 'compute.v1.instanceTemplate',
          'properties': {
              'properties': {
                  'metadata': {
                      'items': [{
                          'key': 'gce-container-declaration',
                          'value': GenerateManifest(context)
                          }]
                      },
                  'machineType': 'f1-micro',
                  'disks': [{
                      'deviceName': 'boot',
                      'boot': True,
                      'autoDelete': True,
                      'mode': 'READ_WRITE',
                      'type': 'PERSISTENT',
                      'initializeParams': {'sourceImage': image}
                      }],
                  'networkInterfaces': [{
                      'accessConfigs': [{
                          'name': 'external-nat',
                          'type': 'ONE_TO_ONE_NAT'
                          }],
                      'network': default_network
                      }],
                    'serviceAccounts': [{
                        'email': 'default',
                        'scopes': ['https://www.googleapis.com/auth/logging.write']
                        }]
                  }
              }
          }
    
      outputs = [{'name': 'instanceTemplateSelfLink',
                  'value': '$(ref.' + instance_template['name'] + '.selfLink)'}]
    
      return {'resources': [instance_template], 'outputs': outputs}
    

    テンプレートのスキーマ ファイルをダウンロードします

  2. オートスケーラー、マネージド インスタンス グループ、ロードバランサを作成します。

    次に、container_instance_template.py テンプレートを使用する別のテンプレートを作成し、オートスケーラーやロードバランサ、マネージド インスタンス グループなど、残りのフロントエンド リソースを作成します。

このテンプレートには次のリソースが含まれています。

  1. container_instance_template.py を使用するインスタンス テンプレート

  2. インスタンス テンプレートを使用するマネージド インスタンス グループ。マネージド インスタンス グループを参照するオートスケーラー。参照を使用することで、Deployment Manager が特定の順序でリソースを作成するように設定できます。この場合、マネージド インスタンス グループはオートスケーラーの前に作成されます。

  3. 次のリソースを含むネットワーク ロードバランサ

    • インターネットにエクスポーズされた単一の外部 IP アドレスによる転送ルール。
    • 作成済みのマネージド インスタンス グループを格納するターゲット プール。
    • ターゲット プールに関連するヘルスチェック

frontend.py という名前のファイルを作成し、テンプレートのスキーマをダウンロードします。

# Copyright 2016 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Creates autoscaled, network LB IGM running specified docker image."""

def GenerateConfig(context):
  """Generate YAML resource configuration."""

  # Pull the region out of the zone
  region = context.properties['zone'][:context.properties['zone'].rfind('-')]
  name = context.env['name']

  resources = [{
      'name': name,
      'type': 'container_instance_template.py',
      'properties': {
          'port': context.properties['port'],
          'dockerEnv': context.properties['dockerEnv'],
          'dockerImage': context.properties['dockerImage'],
          'containerImage': context.properties['containerImage']
      }
  }, {
      'name': name + '-igm',
      'type': 'compute.v1.instanceGroupManager',
      'properties': {
          'zone': context.properties['zone'],
          'targetSize': context.properties['size'],
          'targetPools': ['$(ref.' + name + '-tp.selfLink)'],
          'baseInstanceName': name + '-instance',
          'instanceTemplate': '$(ref.' + name + '-it.selfLink)'
      }
  }, {
      'name': name + '-as',
      'type': 'compute.v1.autoscaler',
      'properties': {
          'zone': context.properties['zone'],
          'target': '$(ref.' + name + '-igm.selfLink)',
          'autoscalingPolicy': {
              'maxNumReplicas': context.properties['maxSize']
          }
      }
  }, {
      'name': name + '-hc',
      'type': 'compute.v1.httpHealthCheck',
      'properties': {
          'port': context.properties['port'],
          'requestPath': '/_ah/health'
      }
  }, {
      'name': name + '-tp',
      'type': 'compute.v1.targetPool',
      'properties': {
          'region': region,
          'healthChecks': ['$(ref.' + name + '-hc.selfLink)']
      }
  }, {
      'name': name + '-lb',
      'type': 'compute.v1.forwardingRule',
      'properties': {
          'region': region,
          'portRange': context.properties['port'],
          'target': '$(ref.' + name + '-tp.selfLink)'
      }
  }]
  return {'resources': resources}

テンプレートのスキーマ ファイルをダウンロードします

統合テンプレートの作成

最後に、バックエンド テンプレートとフロントエンド テンプレートを統合するテンプレートを作成します。次の内容のファイルを nodejs.py という名前で作成します。

# Copyright 2016 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Create nodejs template with the back-end and front-end templates."""

def GenerateConfig(context):
  """Generate configuration."""

  backend = context.env['deployment'] + '-backend'
  frontend = context.env['deployment'] + '-frontend'
  firewall = context.env['deployment'] + '-application-fw'
  application_port = 8080
  mysql_port = 3306
  resources = [{
      'name': backend,
      'type': 'container_vm.py',
      'properties': {
          'zone': context.properties['zone'],
          'dockerImage': 'gcr.io/qwiklabs-resources/mysql',
          'containerImage': 'family/cos-stable',
          'port': mysql_port,
          'dockerEnv': {
              'MYSQL_ROOT_PASSWORD': 'mypassword'
          }
      }
  }, {
      'name': frontend,
      'type': 'frontend.py',
      'properties': {
          'zone': context.properties['zone'],
          'dockerImage': 'gcr.io/qwiklabs-resources/nodejsservice',
          'port': application_port,
          # Define the variables that are exposed to container as env variables.
          'dockerEnv': {
              'SEVEN_SERVICE_MYSQL_PORT': mysql_port,
              'SEVEN_SERVICE_PROXY_HOST': '$(ref.' + backend
                                          + '.networkInterfaces[0].networkIP)'
          },
          # If left out will default to 1
          'size': 2,
          # If left out will default to 1
          'maxSize': 20
      }
  }, {
      'name': firewall,
      'type': 'compute.v1.firewall',
      'properties': {
          'allowed': [{
              'IPProtocol': 'TCP',
              'ports': [application_port]
          }],
          'sourceRanges': ['0.0.0.0/0']
      }
  }]
  return {'resources': resources}

テンプレートのスキーマ ファイルをダウンロードします

アプリのフロントエンドには env["deployment"]-frontend という名前が設定されます。バックエンドにも同様の名前が設定されます。アプリをデプロイすると、env["deployment"] は Deployment Manager によって自動的にデプロイ名に置換されます。

構成の作成

すべてのテンプレートが用意できたら、リソースのデプロイに使用する構成を作成します。次の内容の構成ファイルを nodejs.yaml という名前で作成します。

# Copyright 2016 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Launches an autoscaled, load-balanced frontend running nodejs for serving
# traffic. Also launches a single MySQL container instance, wires the two
# together using references, and passes them as env variables to the underlying
# frontend Docker containers.
imports:
- path: nodejs.py

resources:
- name: nodejs
  type: nodejs.py
  properties:
    zone: ZONE_TO_RUN

ZONE_TO_RUN は、リソースのゾーン(us-central1-a など)で置き換えます。

リソースのデプロイ

では、リソースをデプロイしましょう。Google Cloud CLI を使用して、次を実行します:

gcloud deployment-manager deployments create advanced-configuration --config nodejs.yaml

デプロイが完了すると、Deployment Manager は次のようなメッセージを表示し、作成されたリソースの要約を示します。

Waiting for create operation-1468522101491-5379cf2344539-5961abe8-a500190c...done.
Create operation operation-1468522101491-5379cf2344539-5961abe8-a500190c completed successfully.
NAME                                   TYPE                             STATE      ERRORS
advanced-configuration-application-fw  compute.v1.firewall              COMPLETED  []
advanced-configuration-backend         compute.v1.instance              COMPLETED  []
advanced-configuration-frontend-as     compute.v1.autoscaler            COMPLETED  []
advanced-configuration-frontend-hc     compute.v1.httpHealthCheck       COMPLETED  []
advanced-configuration-frontend-igm    compute.v1.instanceGroupManager  COMPLETED  []
advanced-configuration-frontend-it     compute.v1.instanceTemplate      COMPLETED  []
advanced-configuration-frontend-lb     compute.v1.forwardingRule        COMPLETED  []
advanced-configuration-frontend-tp     compute.v1.targetPool            COMPLETED  []

アプリのテスト

アプリをテストするには、転送ルールのクエリを行い、トラフィックを処理している外部 IP アドレスを取得します。

$ gcloud compute forwarding-rules describe advanced-configuration-frontend-lb --region us-central1
IPAddress: 104.154.81.44
IPProtocol: TCP
creationTimestamp: '2016-07-14T11:48:37.228-07:00'
description: ''
id: '9033201246750269546'
kind: compute#forwardingRule
name: advanced-configuration-frontend-lb
portRange: 8080-8080
region: https://www.googleapis.com/compute/v1/projects/myproject/regions/us-central1
selfLink: https://www.googleapis.com/compute/v1/projects/myproject/regions/us-central1/forwardingRules/advanced-configuration-frontend-lb
target: https://www.googleapis.com/compute/v1/projects/myproject/regions/us-central1/targetPools/advanced-configuration-frontend-tp

この場合の外部 IP アドレスは 104.154.81.44 です。

次に、ブラウザで、ポート 8080 を使用して外部 IP アドレスにアクセスします。たとえば、外部 IP アドレスが 104.154.81.44 の場合、URL は次のようになります。

http://104.154.81.44:8080

空白のページが表示されるはずです。これは想定どおりです。次に、メッセージをページに投稿します。次の URL に移動します。

http://104.154.81.44:8080?msg=hellothere!

メッセージが追加されたことを確認するメッセージが表示されます。メイン URL に戻ると、ページにメッセージが表示されているはずです。

hellothere!

これで、受信したメッセージをログに記録できるアプリがデプロイされました。

次のステップ

このサンプルを完成させた後は、次のステップに進むことができます。