web-dev-qa-db-fra.com

Créer une vue AWS Athena par programmation

Pouvez-vous créer des vues dans Amazon Athena? décrit comment créer une vue à l'aide de l'interface utilisateur.

Je voudrais créer une vue AWS Athena par programmation, idéalement en utilisant Terraform (qui appelle CloudFormation).

J'ai suivi les étapes décrites ici: https://ujjwalbhardwaj.me/post/create-virtual-views-with-aws-glue-and-query-them-using-athena , cependant je rencontre un problème avec cela en ce que la vue devient viciée rapidement.

...._view' is stale; it must be re-created.

Le code terraform ressemble à ceci:

resource "aws_glue_catalog_table" "Adobe_session_view" {

  database_name = "${var.database_name}"
  name = "session_view"

  table_type = "VIRTUAL_VIEW"
  view_original_text = "/* Presto View: ${base64encode(data.template_file.query_file.rendered)} */"
  view_expanded_text = "/* Presto View */"

  parameters = {
    presto_view = "true"
    comment = "Presto View"
  }

  storage_descriptor {
    ser_de_info {
      name = "ParquetHiveSerDe"
      serialization_library = "org.Apache.hadoop.Hive.ql.io.parquet.serde.ParquetHiveSerDe"
    }

    columns { name = "first_column" type = "string" }
    columns { name = "second_column" type = "int" }
    ...
    columns { name = "nth_column" type = "string" }
}

Une alternative que je serais heureux d'utiliser est l'AWS CLI, cependant aws athena [option] ne fournit aucune option pour cela.

J'ai essayé:

  • create-named-query que je n'ai pas pu utiliser pour une instruction telle que CREATE OR REPLACE VIEW car cela ne semble pas être le cas d'utilisation prévu pour cette commande.
  • start-query-execution qui demande un emplacement de sortie, ce qui suggère que cela est destiné à interroger les données et à produire les résultats, par opposition à effectuer des modifications/créations avec état. Il semble également être associé à stop-query-execution .
8
tjheslin1

Comme vous l'avez suggéré, il est certainement possible de créer une vue Athena par programmation via l'AWS CLI en utilisant le start-query-execution. Comme vous l'avez souligné, cela vous oblige à fournir un emplacement S3 pour les résultats même si vous n'aurez pas besoin de vérifier le fichier (Athena mettra un fichier txt vide à l'emplacement pour une raison quelconque).

Voici un exemple:

$ aws athena start-query-execution --query-string "create view my_view as select * from my_table" --result-configuration "OutputLocation=s3://my-bucket/tmp" --query-execution-context "Database=my_database"

{
    "QueryExecutionId": "1744ed2b-e111-4a91-80ea-bcb1eb1c9c25"
}

Vous pouvez éviter que le client spécifie un compartiment en créant un groupe de travail et en y définissant l'emplacement.

Vous pouvez vérifier si la création de votre vue a réussi en utilisant le get-query-execution commande.

$ aws --region athena get-query-execution --query-execution-id bedf3eba-55b0-42de-9a7f-7c0ba71c6d9b
{
    "QueryExecution": {
        "QueryExecutionId": "1744ed2b-e111-4a91-80ea-bcb1eb1c9c25",
        "Query": "create view my_view as select * from my_table",
        "StatementType": "DDL",
        "ResultConfiguration": {
            "OutputLocation": "s3://my-bucket/tmp/1744ed2b-e111-4a91-80ea-bcb1eb1c9c25.txt"
        },
        "Status": {
            "State": "SUCCEEDED",
            "SubmissionDateTime": 1558744806.679,
            "CompletionDateTime": 1558744807.312
        },
        "Statistics": {
            "EngineExecutionTimeInMillis": 548,
            "DataScannedInBytes": 0
        },
        "WorkGroup": "primary"
    }
}

5
JD D

La création de vues par programmation dans Athena n'est pas documentée et non prise en charge, mais possible. Ce qui se passe en arrière-plan lorsque vous créez une vue à l'aide de StartQueryExecution, c'est qu'Athena laisse Presto créer la vue, puis extrait la représentation interne de Presto et la place dans le catalogue Glue.

Le problème d'obsolescence provient généralement des colonnes dans les métadonnées Presto et les métadonnées Glue étant désynchronisées. Une vue Athena contient en réalité trois descriptions de la vue: la vue SQL, les colonnes et leurs types au format Glue et les colonnes et types au format Presto. Si l'un de ceux-ci se désynchronise, vous obtiendrez le "… est périmé; il doit être recréé". Erreur.

Voici les exigences d'une table Glue pour fonctionner comme une vue Athena:

  • TableType doit être VIRTUAL_VIEW
  • Parameters doit contenir presto_view: true
  • TableInput.ViewOriginalText Doit contenir une vue Presto encodée (voir ci-dessous)
  • StorageDescriptor.SerdeInfo Doit être une carte vide
  • StorageDescriptor.Columns Doit contenir toutes les colonnes définies par la vue, avec leurs types

La partie délicate est la vue Presto encodée. Cette structure est créée par ce code: https://github.com/prestosql/presto/blob/27a1b0e304be841055b461e2c00490dae4e30a4e/presto-Hive/src/main/Java/io/prestosql/plugin/Hive/HiveUtil. L597-L6 , et c'est plus ou moins ce qu'il fait:

  • Ajoute un préfixe /* Presto View: (Avec un espace après :)
  • Ajoute une chaîne JSON codée en base 64 qui contient la vue SQL, les colonnes et leurs types, ainsi que certaines métadonnées de catalogue (voir ci-dessous)
  • Ajoute un suffixe */ (Avec un espace avant *)

Le JSON qui décrit la vue ressemble à ceci:

  • Une propriété catalog qui doit avoir la valeur awsdatacatalog.
  • Une propriété schema qui doit être le nom de la base de données dans laquelle la vue est créée (c'est-à-dire qu'elle doit correspondre à la propriété DatabaseName de la structure Glue environnante.
  • Une liste de colonnes, chacune avec un name et type
  • Une propriété originalSql avec la vue SQL réelle (n'incluant pas CREATE VIEW …, Elle doit commencer par SELECT … Ou WITH …)

Voici un exemple:

{
  "catalog": "awsdatacatalog",
  "schema": "some_database",
  "columns": [
    {"name": "col1", "type": "varchar"},
    {"name": "col2", "type": "bigint"}
  ],
  "originalSql": "SELECT col1, col2 FROM some_other_table"
}

Une mise en garde ici est que les types de colonnes sont presque, mais pas tout à fait, les mêmes que les noms dans Glue. Si Athena/Glue aurait string la valeur dans ce JSON doit être varchar. Si Athena/Glue utilise array<string>, La valeur dans ce JSON doit être array(varchar), et struct<foo:int> Devient row(foo int).

C'est assez compliqué, et mettre tout cela ensemble nécessite quelques manipulations et tests. Le moyen le plus simple de le faire fonctionner est de créer quelques vues et de décoder les instructions ci-dessus à l'envers pour voir à quoi elles ressemblent, puis essayez de le faire vous-même.

9
Theo

Mise à jour des exemples ci-dessus pour la syntaxe Terraform 0.12+ et ajout de la lecture des requêtes de vue à partir du système de fichiers:

resource "null_resource" "athena_views" {
  for_each = {
    for filename in fileset("${path.module}/athenaviews/", "**"):
           replace(filename,"/","_") => file("${path.module}/athenaviews/${filename}")
  }

  provisioner "local-exec" {
    command = <<EOF
    aws athena start-query-execution \
      --output json \
      --query-string CREATE OR REPLACE VIEW ${each.key} AS ${each.value} \
      --query-execution-context "Database=${var.athena_database}" \
      --result-configuration "OutputLocation=s3://${aws_s3_bucket.my-bucket.bucket}"
EOF
  }

  provisioner "local-exec" {
    when    = "destroy"
    command = <<EOF
    aws athena start-query-execution \
      --output json \
      --query-string DROP VIEW IF EXISTS ${each.key} \
      --query-execution-context "Database=${var.athena_database}" \
      --result-configuration "OutputLocation=s3://${aws_s3_bucket.my-bucket.bucket}"
EOF
  }
}

Notez également alors when= "destroy" bloquer pour vous assurer que les vues sont supprimées lorsque votre pile est détruite.

Placez les fichiers texte avec une requête SELECT sous votre chemin de module sous un répertoire (athenaview/dans cet exemple), et il les récupérera et créera des vues. Cela créera des vues nommées subfolder_filename, et détruisez-les si les fichiers sont supprimés.

3
Joshua Samuel

Ajout à la réponse de Theo: Dans le fichier JSON encodé en base64, le type "chaîne" n'est pas valide lors de la définition des attributs cloumn! Écrivez toujours "varchar" à ce stade.

edit: Aussi "int" doit être déclaré comme "entier"!

Je suis allé avec la solution de Theo et cela a fonctionné en utilisant des modèles AWS Cloud Formation.

Je voulais juste ajouter un petit indice, qui peut vous faire économiser des heures de débogage. Je n'écris pas ceci en tant que commentaire, car je n'ai pas encore le droit de commenter. N'hésitez pas à copier et coller ceci dans la section commentaire de la réponse de Theo.

1
TailorDurden

Pour compléter les réponses par JD D et Theo, en travaillant avec leurs solutions, nous avons trouvé comment appeler l'AWS Cli via terraform comme suit:

resource "null_resource" "athena_view" {

  provisioner "local-exec" {
    command = <<EOF
aws sts assume-role \
  --output json \
  --region my_region \
  --role-arn arn:aws:iam::${var.account_number}:role/my_role \
  --role-session-name create_my_view > /tmp/credentials.json

export AWS_SESSION_TOKEN=$(jq -r '.Credentials.SessionToken' /tmp/credentials.json)
export AWS_ACCESS_KEY_ID=$(jq -r '.Credentials.AccessKeyId' /tmp/credentials.json)
export AWS_SECRET_ACCESS_KEY=$(jq -r '.Credentials.SecretAccessKey' /tmp/credentials.json)

aws athena start-query-execution \
  --output json \
  --region my_region \
  --query-string "CREATE OR REPLACE VIEW my_view AS SELECT * FROM my_table \
  --query-execution-context "Database=${var.database_name}" \
  --result-configuration "OutputLocation=s3://${aws_s3_bucket.my-bucket.bucket}"
EOF
  }
}

Nous utilisons null_resource ... pour exécuter des provisionneurs qui ne sont pas directement associés à une ressource spécifique .

Le résultat de aws sts assume-role est sorti en JSON dans /tmp/credentials.json.

jq est utilisé pour analyser les champs nécessaires de la sortie de aws sts assume-role .

aws athena start-query-execution est alors capable de s'exécuter sous le rôle spécifié par les variables d'environnement définies.

Au lieu de --result-configuration "OutputLocation=s3://...., --work-group peut être spécifié, NOTEZ qu'il s'agit d'un indicateur distinct sur start-query-execution, ne fait pas partie du --result-configuration chaîne.

1
tjheslin1

Sur la base des réponses précédentes, voici un exemple qui exécutera des requêtes uniquement si le fichier source a changé. De plus, au lieu de coller la requête SQL dans la commande, il utilise file:// adaptateur pour le transmettre à la commande AWS CLI.

resource "null_resource" "views" {
  for_each = {
    for filename in fileset("${var.sql_files_dir}/", "**/*.sql") :
    replace(replace(filename, "/", "_"), ".sql", "") => "${var.sql_files_dir}/${filename}"
  }

  triggers = {
    md5 = filemd5(each.value)

    # External references from destroy provisioners are not allowed -
    # they may only reference attributes of the related resource.
    database_name = var.database_name
    s3_bucket_query_output = var.s3_bucket_query_output
  }

  provisioner "local-exec" {
    command = <<EOF
      aws athena start-query-execution \
        --output json \
        --query-string file://${each.value} \
        --query-execution-context "Database=${var.database_name}" \
        --result-configuration "OutputLocation=s3://${var.s3_bucket_query_output}"
EOF
  }

  provisioner "local-exec" {
    when    = destroy
    command = <<EOF
      aws athena start-query-execution \
        --output json \
        --query-string 'DROP VIEW IF EXISTS ${each.key}' \
        --query-execution-context "Database=${self.triggers.database_name}" \
        --result-configuration "OutputLocation=s3://${self.triggers.s3_bucket_query_output}"
EOF
  }
}

Pour que la destruction fonctionne correctement, nommez les fichiers exactement comme nom de fichier - example.sql se rapporte à la requête:

CREATE OR REPLACE VIEW example AS ...
1
Maciej Majewski