インスタンス メタデータへのアクセス

Google Cloud Platform には、お使いの App Engine インスタンスに関する詳細、たとえばこのインスタンスに含まれるプロジェクト ID、サービス アカウント、サービス アカウントで使用されるトークンなどの情報が格納されたメタデータ サーバーが用意されています。このデータには、シンプルな HTTP リクエストでアクセスでき、クライアント ライブラリは必要ありません。

このページでは、適切なメタデータ サーバーのエンドポイントに HTTP 呼び出しを実行して、デプロイ済みの Java 8 ランタイム アプリケーションからインスタンス メタデータにアクセスする方法について説明します。

この API の便利な使い方として、サービス アカウントのトークンを取得し、これを署名なしトークンとして Google Cloud APIs のいずれかの認証ヘッダーに指定して、特定の API サービスでアプリケーションの認証を行う方法があります。この署名なしトークンの使用例については、Google Cloud Translation API のドキュメントをご覧ください。

使用するメタデータ エンドポイントの識別

次の表は、特定のメタデータに対して HTTP リクエストを行うことができるエンドポイントを示しています。メタデータ サーバーには、http://metadata.google.internal でアクセスできます。

メタデータ エンドポイント 説明
/computeMetadata/v1/project/numeric-project-id プロジェクトに割り当てられているプロジェクト番号。
/computeMetadata/v1/project/project-id プロジェクトに割り当てられているプロジェクト ID。
/computeMetadata/v1/instance/zone インスタンスが実行されているゾーン。
/computeMetadata/v1/instance/service-accounts/default/aliases
/computeMetadata/v1/instance/service-accounts/default/email プロジェクトに割り当てられているデフォルトのサービス アカウントのメール。
/computeMetadata/v1/instance/service-accounts/default/ プロジェクトのすべてのデフォルトのサービス アカウントを一覧表示します。
/computeMetadata/v1/instance/service-accounts/default/scopes デフォルトのサービス アカウントでサポートされているすべてのスコープを一覧表示します。
/computeMetadata/v1/instance/service-accounts/default/token アプリケーションを他の Google Cloud APIs に認証させるための認証トークンを返します。

たとえば、プロジェクト ID を取得するには、リクエストを http://metadata.google.internal/computeMetadata/v1/project/project-id に送信します。

メタデータ リクエストの実行

次のサンプルコードを実行すると、サービス アカウントのトークンを除き、インスタンスについて利用可能なメタデータがすべて取得されて表示されます。

@SuppressWarnings("serial")
// With @WebServlet annotation the webapp/WEB-INF/web.xml is no longer required.
@WebServlet(name = "Metadata", description = "Metadata: Write info about GAE Standard",
    urlPatterns = "/metadata")
public class MetadataServlet extends HttpServlet {

  private final String[] metaPath = {
      "/computeMetadata/v1/project/numeric-project-id", //  (pending)
      "/computeMetadata/v1/project/project-id",
      "/computeMetadata/v1/instance/zone",
      "/computeMetadata/v1/instance/service-accounts/default/aliases",
      "/computeMetadata/v1/instance/service-accounts/default/email",
      "/computeMetadata/v1/instance/service-accounts/default/",
      "/computeMetadata/v1/instance/service-accounts/default/scopes",
      // Tokens work - but are a security risk to display
      //      "/computeMetadata/v1/instance/service-accounts/default/token"
  };

  final String[] metaServiceAcct = {
      "/computeMetadata/v1/instance/service-accounts/{account}/aliases",
      "/computeMetadata/v1/instance/service-accounts/{account}/email",
      "/computeMetadata/v1/instance/service-accounts/{account}/scopes",
      // Tokens work - but are a security risk to display
      //     "/computeMetadata/v1/instance/service-accounts/{account}/token"
  };

  private final String metadata = "http://metadata.google.internal";
  private TemplateEngine templateEngine;

  // Use OkHttp from Square as it's quite easy to use for simple fetches.
  private final OkHttpClient ok = new OkHttpClient.Builder()
      .readTimeout(500, TimeUnit.MILLISECONDS)  // Don't dawdle
      .writeTimeout(500, TimeUnit.MILLISECONDS)
      .build();

  // Setup to pretty print returned json
  private final Gson gson = new GsonBuilder()
      .setPrettyPrinting()
      .create();
  private final JsonParser jp = new JsonParser();

  // Fetch Metadata
  String fetchMetadata(String key) throws IOException {
    Request request = new Request.Builder()
        .url(metadata + key)
        .addHeader("Metadata-Flavor", "Google")
        .get()
        .build();

    Response response = ok.newCall(request).execute();
    return response.body().string();
  }

  String fetchJsonMetadata(String prefix) throws IOException {
    Request request = new Request.Builder()
        .url(metadata + prefix)
        .addHeader("Metadata-Flavor", "Google")
        .get()
        .build();

    Response response = ok.newCall(request).execute();

    // Convert json to prety json
    return gson.toJson(jp.parse(response.body().string()));
  }

  @Override
  public void init() {
    // Setup ThymeLeaf
    ServletContextTemplateResolver templateResolver =
        new ServletContextTemplateResolver(this.getServletContext());

    templateResolver.setPrefix("/WEB-INF/templates/");
    templateResolver.setSuffix(".html");
    templateResolver.setCacheTTLMs(Long.valueOf(1200000L)); // TTL=20m

    // Cache is set to true by default. Set to false if you want templates to
    // be automatically updated when modified.
    templateResolver.setCacheable(true);

    templateEngine = new TemplateEngine();
    templateEngine.setTemplateResolver(templateResolver);
  }

  @Override
  public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    String defaultServiceAccount = "";
    WebContext ctx = new WebContext(req, resp, getServletContext(), req.getLocale());

    resp.setContentType("text/html");

    String environment =
        (String) System.getProperties().get("com.google.appengine.runtime.environment");
    ctx.setVariable("production", environment);

    // The metadata server is only on a production system
    if (environment.equals("Production")) {

      TreeMap<String, String> m = new TreeMap<>();

      for (String key : metaPath) {
        m.put(key, fetchMetadata(key));
        if (key.contains("default/email")) {
          defaultServiceAccount = m.get(key);
        }
      }

      ctx.setVariable("Metadata", m.descendingMap());

      m = new TreeMap<>();
      for (String key : metaServiceAcct) {
        // substitute a service account for {account}
        key = key.replace("{account}", defaultServiceAccount);
        m.put(key, fetchMetadata(key));
      }
      ctx.setVariable("sam", m.descendingMap());

      // Recursivly get all info about service accounts -- Note tokens are leftout by default.
      ctx.setVariable("rsa",
          fetchJsonMetadata("/computeMetadata/v1/instance/service-accounts/?recursive=true"));
      // Recursivly get all data on Metadata server.
      ctx.setVariable("ram", fetchJsonMetadata("/?recursive=true"));
    }

    templateEngine.process("index", ctx, resp.getWriter());

  }
}

このサンプルコードでは、アプリが本番環境で実行されていることが確認されます。アプリケーションがローカルで実行されている場合、リクエストから戻るメタデータはありません。

また、Google Gson JSON シリアライザ / デシリアライザ、OkHttp HTTP および HTTP2 クライアント、Thymeleaf テンプレート生成システムを使用していることにも注意してください。これらは必須ではありませんが、独自のプロジェクトに役立つライブラリです。

ローカルでの実行

メタデータ サーバーは、デプロイされているアプリケーションを対象に利用するもので、ローカルの開発用サーバー上での実行には対応していません。上記のサンプルコードで示されるように、アプリが本番環境で実行されている場合に限りメタデータ結果を待ち受けるように、コードに環境チェックを追加できます。

String environment =
      (String) System.getProperties().get("com.google.appengine.runtime.environment");
  ctx.setVariable("production", environment);

  // The metadata server is only on a production system
  if (environment.equals("Production")) {
     ... //show metadata results
   }