Build determinístico de computação federada da personalização no dispositivo

As builds deterministas são necessárias para o atestado de carga de trabalho no ambiente de execução confiável (TEE) da personalização no dispositivo (ODP), disponível publicamente no Google Cloud como Confidential Space (CS).

As imagens de carga de trabalho precisam gerar um hash de imagem determinista que possa ser usado pelo CS para atestado de carga de trabalho, que usa a arquitetura de procedimentos de atestado remoto (RATS) RFC 9334 do NIST.

Este documento aborda a implementação e o suporte para builds determinísticos no repositório odp-federatedcompute. Os serviços ODP Aggregator e Model Updater serão executados no Confidential Space. O repositório oferece suporte a builds deterministas para todos os nossos serviços, que são necessários para casos de uso de produção.

Builds deterministas

As builds deterministas consistem em dois componentes principais:

  1. A compilação dos binários necessários. Isso inclui jars, bibliotecas compartilhadas e metadados.
  2. A imagem de base e as dependências de ambiente de execução. A imagem de base do ambiente de execução usada para executar os binários compilados.

No momento, o repositório do Federated Compute da ODP é compatível com os seguintes tipos de cargas de trabalho:

  • Workloads Java + Spring
    • TaskAssignment, TaskManagement, Collector
  • Java + Spring com cargas de trabalho do TensorFlow JNI
    • ModelUpdater, Aggregator
  • Workloads do Python
    • TaskBuilder

Dependências

A lista a seguir mostra as dependências que o ODP usa para manter o determinismo e a disponibilidade:

  • Bazel
  • GitHub
  • Maven
  • PyPi
  • Snapshots do Debian
  • Registro do DockerHub
  • Google Container Registry (GCR)

Cargas de trabalho deterministas

Todas as cargas de trabalho são compiladas usando o Bazel com conjuntos de ferramentas específicos da linguagem e imagens de contêiner criadas usando rules_oci. O arquivo WORKSPACE define todas as dependências com versões e hashes correspondentes.

Snapshots do Debian

Todas as imagens de carga de trabalho precisam ser criadas no dockerfile fornecido, que é baseado em um snapshot do Debian. Os snapshots do Debian fornecem um snapshot de repositório estável com:

Cargas de trabalho do Java Spring

O remotejdk_17 do Bazel é usado para fornecer um Java hermético para compilação. Outras dependências do Java são gerenciadas e definidas no arquivo WORKSPACE.

As cargas de trabalho do Java Spring são compiladas em um arquivo JAR chamado <service>_application.jar. O arquivo JAR contém:

  • Arquivos de classe Java
  • META-INF/
    • Dados do manifesto do Bazel
  • build-data.properties
    • Dados de build do Bazel
  • BOOT-INF/
    • Dependências de jar empacotadas, geradas por rules_spring.

Camadas de imagem

A imagem da carga de trabalho do Java Spring consiste em duas camadas:

Configuração de imagem

  • Ponto de entrada
    • java -jar <service>_application.jar

Cargas de trabalho do JNI Tensorflow

As cargas de trabalho do JNI Tensorflow são criadas com base nas cargas de trabalho do Java Spring. Uma cadeia de ferramentas hermética do Clang+LLVM Bazel é fornecida usando o Clang+LLVM 16 pré-criado com um sysroot fornecido pela imagem de snapshot do Debian para compilar código de máquina.

As cargas de trabalho JNI são compiladas em uma biblioteca compartilhada chamada libtensorflow.so junto com o <service>_application.jar.

Camadas de imagem

A imagem da carga de trabalho do TensorFlow JNI consiste em várias camadas:

  • Camada de imagem de base
  • Camadas de dependência de pacotes Debian. As camadas são geradas usando arquivos deb baixados do debian-snapshot e reempacotados como camadas de imagem
    • libc++1-16_amd64.tar
    • libc++abi1-16_amd64.tar
    • libc6_amd64.tar
    • libunwind-16_amd64.tar
    • libgcc-s1_amd64.tar
    • gcc-13-base_amd64.tar
  • Camada de carga de trabalho
    • binary_tar.tar
      • <service>_application.jar
      • libtensorflow-jni.so
      • libaggregation-jni.so

Configuração de imagem

  • Rótulos (somente para imagens criadas para serem executadas no TEE)
    • "tee.launch_policy.allow_env_override": "FCP_OPTS"
      • Permite que a variável de ambiente FCP_OPTS seja definida em espaço confidencial. A carga de trabalho vai consumir FCP_OPTS na inicialização para configurar os parâmetros obrigatórios.
      • A variável de ambiente FCP_OPTS é definida quando a imagem é executada (em vez de criada) para manter o determinismo de build.
    • "tee.launch_policy.log_redirect": "always"
    • "tee.launch_policy.monitoring_memory_allow": "always"
  • Ponto de entrada
    • java -Djava.library.path=. -jar <service>_application.jar

Workloads do Python

O rules_python do Bazel é usado para fornecer uma cadeia de ferramentas hermética do Python 3.10. Um arquivo requirements do pip bloqueado é usado para buscar dependências do pip de forma determinista. A imagem de snapshot do Debian garante que as distribuições deterministas sejam buscadas com base na compatibilidade da plataforma e fornece um conjunto de ferramentas C++ para compilar distribuições de origem.

As cargas de trabalho do Python serão empacotadas em um conjunto de pacotes pip baixados, uma distribuição do Python 3.10, o código-fonte do Python do ODP e um script de inicialização do Python.

  • <service>.runfiles/
    • A distribuição do Python é armazenada em python_x86_64-unknown-linux-gnu/
    • O código-fonte é armazenado em com_google_ondevicepersonalization_federatedcompute/
    • Os pacotes pip são armazenados em pypi_<dependency_name>/
  • <service>.runfiles_manifest
    • Arquivo de manifesto para o diretório <service>.runfiles/
  • <service>
    • Script Python para executar a carga de trabalho do Python usando os runfiles

Camadas de imagem

A imagem da carga de trabalho do Python consiste em quatro camadas:

  • Camada de imagem de base
  • Camada de intérprete
    • interpreter_layer.jar
      • <service>/<service>.runfiles/python_x86_64-unknown-linux-gnu/**
  • Camada de pacotes
    • packages_layer.jar
      • <service>/<service>.runfiles/**/site-packages/**
  • Camada de carga de trabalho
    • app_tar_manifest.tar
      • Contém código-fonte, script de inicialização e manifesto.
        • <service>/<service>.runfiles_manifest
        • <service>/<service>
        • <service>/<service>.runfiles/com_google_ondevicepersonalization_federatedcompute/**

Configuração de imagem

  • Ponto de entrada
    • /<service>/<service>

Criar imagens

Depois que as cargas de trabalho forem escolhidas, você poderá criar e publicar as imagens.

Pré-requisitos

  • Bazel 6.4.0
    • Requer instalações do Java e do C++
  • Docker

Procedimento

As imagens precisam ser criadas no contêiner do Docker criado pelo dockerfile fornecido. Dois scripts são fornecidos para ajudar na criação das imagens deterministas finais.

  • docker_run.sh
    • O docker_run.sh vai criar a imagem do Docker com base no Dockerfile, montar o diretório de trabalho, montar o daemon do Docker do host e executar o Docker com o comando bash fornecido. Todas as variáveis transmitidas antes do comando bash serão tratadas como flags de execução do Docker.
  • build_images.sh
    • O build_images.sh vai executar bazel build para todas as imagens e gerar os hashes de imagem para cada uma delas.

Criar todas as imagens

./scripts/docker/docker_run.sh "./scripts/build_images.sh"

Os hashes de imagem esperados para cada versão podem ser encontrados em odp-federatedcompute GitHub releases.

Publicar imagens

A publicação é configurada usando as regras oci_push do Bazel. Para cada serviço, o repositório de destino precisa ser configurado para todos:

  • agregador
  • coletor
  • model_updater
  • task_assignment
  • task_management
  • task_scheduler
  • task_builder

Publicar uma única imagem

Para publicar uma única imagem:

./scripts/docker/docker_run.sh "bazel run //shuffler/services/<servicename_no_underscore>:<servicename_with_underscore>_image_publish"

Imagens criadas

Todas as imagens criadas precisam ser armazenadas e hospedadas pelo criador, como em um registro de artefatos do GCP.