[DOCKER] [Python] Créer un environnement Batch à l'aide d'AWS-CDK

1.Tout d'abord

Cette fois, nous implémenterons l'environnement AWS Batch à l'aide de CDK. Il existe de nombreux exemples d'implémentation dans TypeScript, mais il n'y en avait pas beaucoup en Python, j'ai donc écrit un article.

1.1 Environnement d'exécution

L'environnement d'exécution est le suivant. En particulier, je ne parlerai pas de l'installation et des paramètres initiaux de aws-cli et aws-cdk. Cependant, à titre de mise en garde, aws-cdk a une fréquence de mise à jour de version très élevée, et même le contenu actuellement écrit peut ne pas fonctionner.

1.2 Frais

Ce qui est inquiétant, c'est le prix. Lorsque je l'ai déplacé dans les conditions suivantes, je n'ai été facturé que les frais EC2, qui étaient d'environ «0,01 [$ / jour]». (Dans Batch, l'instance est créée après l'ajout de la file d'attente à la tâche à chaque fois, et elle est supprimée lorsque la tâche est terminée.)

1.3 Procédure

Suivez les étapes ci-dessous pour préparer l'environnement d'exécution par lots.

  1. Création d'un script Python
  2. Création d'un Dockerfile
  3. Inscrivez-vous avec ECR
  4. Écrivez l'application dans CDK

1.4 Préparation

La structure des dossiers est la suivante. Le numéro à droite du nom de fichier correspond au numéro dans la procédure ci-dessus.

batch_example
└── src
    ├── docker
    │   ├── __init__.py (1)
    │   ├── Dockerfile (2)
    │   ├── requirements.txt (2)
    │   └── Makefile (3)
    └── batch_environment
        ├── app.py (4)
        ├── cdk.json
        └── README.md

2. Mise en œuvre

Maintenant, procédons à l'implémentation selon la procédure ci-dessus.

2.1 Créer un script Python

Un exemple de script à exécuter dans Docker est présenté ci-dessous. click sert à passer des arguments de ligne de commande depuis CMD watchtower est utilisé pour écrire des journaux dans CloudWatch Logs.

__init__.py


#Pour l'analyse du temps
from datetime import datetime
from logging import getLogger, INFO
#Bibliothèque d'installation
from boto3.session import Session
import click
import watchtower


#Récupère la valeur de la variable d'environnement lorsqu'elle est spécifiée avec envvar
@click.command()
@click.option("--time")
@click.option("--s3_bucket", envvar='S3_BUCKET')
def main(time: str, s3_bucket: str):
    if time:
        #Temps d'analyse en supposant l'exécution de l'événement CloudWatch
        d = datetime.strptime(time, "%Y-%m-%dT%H:%M:%SZ")
        #Obtenir la date d'exécution
        execute_date = d.strftime("%Y-%m-%d")

    #paramètres de l'enregistreur
    #Le nom de l'enregistreur devient le nom du flux de journaux
    logger_name = f"{datetime.now().strftime('%Y/%m/%d')}"
    logger = getLogger(logger_name)
    logger.setLevel(INFO)
    #Spécifiez le nom du groupe de journaux CloudWatch Logs ici
    #Passer la session et envoyer le journal via le rôle IAM
    handler = watchtower.CloudWatchLogHandler(log_group="/aws/some_project", boto3_session=Session())
    logger.addHandler(handler)

    #Traitement programmé
    #Ici, écrivez uniquement la date et l'heure d'exécution dans CloudWatch Logs
    logger.info(f"{execute_date=}")
    
if __name__ == "__main__":
    """
    python __init__.py 
        --time 2020-09-11T12:30:00Z
        --s3_bucket your-bucket-here
    """
    main()

2.2 Création d'un Dockerfile

Ensuite, créez un Dockerfile qui exécute le script Python ci-dessus. Je l'ai construit en plusieurs étapes en me référant à ici.

Dockerfile


#Ceci est un conteneur pour la construction
FROM python:3.8-buster as builder

WORKDIR /opt/app

COPY requirements.txt /opt/app
RUN pip3 install -r requirements.txt

#De là, préparez le conteneur d'exécution
FROM python:3.8-slim-buster as runner

COPY --from=builder /usr/local/lib/python3.8/site-packages /usr/local/lib/python3.8/site-packages
COPY src /opt/app/src

WORKDIR /opt/app/src
CMD ["python3", "__init__.py"]

En même temps, placez la bibliothèque à utiliser dans requirements.txt.

requirements.txt


click
watchtower

2.3 Inscription à l'ECR

Après avoir créé le Dockerfile, enregistrez-le dans ECR. Tout d'abord, créez un référentiel en appuyant sur le bouton "Créer un référentiel" sur l'ECR à partir de la console.

スクリーンショット 2020-09-27 22.47.35.png

Définissez le nom du référentiel de manière appropriée.

スクリーンショット 2020-09-27 22.49.17.png

Sélectionnez le référentiel créé et appuyez sur le bouton "Show Push Command".

スクリーンショット 2020-09-27 22.50.23.png

Ensuite, les commandes nécessaires pour pousser seront affichées, donc ** copiez et exécutez sans réfléchir. ** ** Si vous échouez ici, je pense que les paramètres de l'AWS CLI ne fonctionnent pas correctement, veuillez donc consulter les paramètres de l'AWS CLI.

スクリーンショット 2020-09-27 22.51.53.png

Il est difficile de taper la commande à chaque fois, alors créez un Makefile qui est une copie de la commande ci-dessus. (La commande --username AWS dans 1 semble être une constante.)

Makefile


.PHONY: help
help:
	@echo " == push docker image to ECR == "
	@echo "type 'make build tag push' to push docker image to ECR"
	@echo ""

.PHONY: login
login:
	(1 commande)aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin {ACCOUNT_NUMBER}.dkr.ecr.ap-northeast-1.amazonaws.com

.PHONY: build
build:
	(2 commandes)docker build -t {REPOSITORY_NAME} .

.PHONY: tag
tag:
	(3 commandes)docker tag {REPOSITORY_NAME}:latest {ACCOUNT_NUMBER}.dkr.ecr.ap-northeast-1.amazonaws.com/{REPOSITORY_NAME}:latest

.PHONY: push
push:
	(4 commandes)docker push {ACCOUNT_NUMBER}.dkr.ecr.ap-northeast-1.amazonaws.com/{REPOSITORY_NAME}:latest

En utilisant ce Makefile, vous pouvez facilement raccourcir la commande comme suit. De plus, je ne pense pas qu'il y ait d'informations dangereuses dans le Makefile ci-dessus, même s'il est divulgué à l'extérieur, vous pouvez donc partager le code source.

#Connectez-vous à ECR
$ make login

#Poussez la dernière image vers ECR
$ make build tag push

2.4 Implémentation CDK

Le contenu de l'implémentation du CDK est basé sur cet article écrit en TypeScript. De plus, il est préférable d'exécuter «$ cdk dedans» dans le répertoire où app.py est implémenté à l'avance.

2.4.1 Installation des packages nécessaires à l'implémentation

Chaque nom de package est long ... De plus, le temps d'installation est assez long.

$ pip install aws-cdk-core aws-cdk-aws-stepfunctions aws-cdk-aws-stepfunctions-tasks aws-cdk-aws-events-targets aws-cdk.aws-ec2 aws-cdk.aws-batch aws-cdk.aws-ecr

2.4.2 Implémentation de app.py

Tout d'abord, créez une classe pour l'environnement à construire cette fois. Stack_name et stack_env sont définis comme arguments de la classe BatchEnvironment. Cela correspond au nom de cet environnement et à l'environnement d'exécution (vérification / développement / production). (Si vous voulez vraiment séparer l'environnement d'exécution, je pense que vous devez également modifier le référentiel ECR.)

app.py


from aws_cdk import (
    core,
    aws_ec2,
    aws_batch,
    aws_ecr,
    aws_ecs,
    aws_iam,
    aws_stepfunctions as aws_sfn,
    aws_stepfunctions_tasks as aws_sfn_tasks,
    aws_events,
    aws_events_targets,
)


class BatchEnvironment(core.Stack):
    """
Environnement Batch et Step Functions pour l'exécuter+Créer un environnement d'événement CloudWatch

    """
    #Nom du référentiel ECR créé ci-dessus
    #Extraire des images de ce référentiel lors de l'exécution dans Batch
    ECR_REPOSITORY_ARN = "arn:aws:ecr:ap-northeast-1:{ACCOUNT_NUMBER}:repository/{YOUR_REPOSITORY_NAME}"

    def __init__(self, app: core.App, stack_name: str, stack_env: str):
        super().__init__(scope=app, id=f"{stack_name}-{stack_env}")
        #L'implémentation suivante est l'image ci-dessous ici.

2.4.3 Implémentation de app.py (création d'environnement VPC)

app.py


        # def __init__(...):dans

        #CIDR est dans la plage que vous aimez
        cidr = "192.168.0.0/24"

        # === #
        # vpc #
        # === #
        #Le VPC est (devrait) être disponible gratuitement si vous utilisez uniquement le sous-réseau public
        vpc = aws_ec2.Vpc(
            self,
            id=f"{stack_name}-{stack_env}-vpc",
            cidr=cidr,
            subnet_configuration=[
                #Définir le masque de réseau pour le sous-réseau public
                aws_ec2.SubnetConfiguration(
                    cidr_mask=28,
                    name=f"{stack_name}-{stack_env}-public",
                    subnet_type=aws_ec2.SubnetType.PUBLIC,
                )
            ],
        )

        security_group = aws_ec2.SecurityGroup(
            self,
            id=f'security-group-for-{stack_name}-{stack_env}',
            vpc=vpc,
            security_group_name=f'security-group-for-{stack_name}-{stack_env}',
            allow_all_outbound=True
        )

        batch_role = aws_iam.Role(
            scope=self,
            id=f"batch_role_for_{stack_name}-{stack_env}",
            role_name=f"batch_role_for_{stack_name}-{stack_env}",
            assumed_by=aws_iam.ServicePrincipal("batch.amazonaws.com")
        )

        batch_role.add_managed_policy(
            aws_iam.ManagedPolicy.from_managed_policy_arn(
                scope=self,
                id=f"AWSBatchServiceRole-{stack_env}",
                managed_policy_arn="arn:aws:iam::aws:policy/service-role/AWSBatchServiceRole"
            )
        )

        batch_role.add_to_policy(
            aws_iam.PolicyStatement(
                effect=aws_iam.Effect.ALLOW,
                resources=[
                    "arn:aws:logs:*:*:*"
                ],
                actions=[
                    "logs:CreateLogGroup",
                    "logs:CreateLogStream",
                    "logs:PutLogEvents",
                    "logs:DescribeLogStreams"
                ]
            )
        )

        #Rôle donné à EC2
        instance_role = aws_iam.Role(
            scope=self,
            id=f"instance_role_for_{stack_name}-{stack_env}",
            role_name=f"instance_role_for_{stack_name}-{stack_env}",
            assumed_by=aws_iam.ServicePrincipal("ec2.amazonaws.com")
        )

        instance_role.add_managed_policy(
            aws_iam.ManagedPolicy.from_managed_policy_arn(
                scope=self,
                id=f"AmazonEC2ContainerServiceforEC2Role-{stack_env}",
                managed_policy_arn="arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role"
            )
        )

        #Ajouter une stratégie pour accéder à S3
        instance_role.add_to_policy(
            aws_iam.PolicyStatement(
                effect=aws_iam.Effect.ALLOW,
                resources=["*"],
                actions=["s3:*"]
            )
        )

        #Ajouter une politique pour accéder aux journaux CloudWatch
        instance_role.add_to_policy(
            aws_iam.PolicyStatement(
                effect=aws_iam.Effect.ALLOW,
                resources=[
                    "arn:aws:logs:*:*:*"
                ],
                actions=[
                    "logs:CreateLogGroup",
                    "logs:CreateLogStream",
                    "logs:PutLogEvents",
                    "logs:DescribeLogStreams"
                ]
            )
        )

        #Accorder un rôle à EC2
        instance_profile = aws_iam.CfnInstanceProfile(
            scope=self,
            id=f"instance_profile_for_{stack_name}-{stack_env}",
            instance_profile_name=f"instance_profile_for_{stack_name}-{stack_env}",
            roles=[instance_role.role_name]
        )

2.4.4 Implémentation de app.py (environnement d'exécution par lots, définition de travaux, création de files d'attente de travaux)

app.py


        #Poursuite du VPC...

        # ===== #
        # batch #
        # ===== #
        batch_compute_resources = aws_batch.ComputeResources(
            vpc=vpc,
            maxv_cpus=4,
            minv_cpus=0,
            security_groups=[security_group],
            instance_role=instance_profile.attr_arn,
            type=aws_batch.ComputeResourceType.SPOT
        )

        batch_compute_environment = aws_batch.ComputeEnvironment(
            scope=self,
            id=f"ProjectEnvironment-{stack_env}",
            compute_environment_name=f"ProjectEnvironmentBatch-{stack_env}",
            compute_resources=batch_compute_resources,
            service_role=batch_role
        )

        job_role = aws_iam.Role(
            scope=self,
            id=f"job_role_{stack_name}-{stack_env}",
            role_name=f"job_role_{stack_name}-{stack_env}",
            assumed_by=aws_iam.ServicePrincipal("ecs-tasks.amazonaws.com")
        )

        job_role.add_managed_policy(
            aws_iam.ManagedPolicy.from_managed_policy_arn(
                scope=self,
                id=f"AmazonECSTaskExecutionRolePolicy_{stack_name}-{stack_env}",
                managed_policy_arn="arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"

            )
        )

        job_role.add_managed_policy(
            aws_iam.ManagedPolicy.from_managed_policy_arn(
                scope=self,
                id=f"AmazonS3FullAccess_{stack_name}-{stack_env}",
                managed_policy_arn="arn:aws:iam::aws:policy/AmazonS3FullAccess"

            )
        )

        job_role.add_managed_policy(
            aws_iam.ManagedPolicy.from_managed_policy_arn(
                scope=self,
                id=f"CloudWatchLogsFullAccess_{stack_name}-{stack_env}",
                managed_policy_arn="arn:aws:iam::aws:policy/CloudWatchLogsFullAccess"
            )
        )

        batch_job_queue = aws_batch.JobQueue(
            scope=self,
            id=f"job_queue_for_{stack_name}-{stack_env}",
            job_queue_name=f"job_queue_for_{stack_name}-{stack_env}",
            compute_environments=[
                aws_batch.JobQueueComputeEnvironment(
                    compute_environment=batch_compute_environment,
                    order=1
                )
            ],
            priority=1
        )

        #Obtenir le référentiel ECR
        ecr_repository = aws_ecr.Repository.from_repository_arn(
            scope=self,
            id=f"image_for_{stack_name}-{stack_env}",
            repository_arn=self.ECR_REPOSITORY_ARN
        )

        #Obtenir une image d'ECR
        container_image = aws_ecs.ContainerImage.from_ecr_repository(
            repository=ecr_repository
        )

        #Définition du poste
        #Ici, utilisez-le dans un script Python`S3_BUCKET`En tant que variable d'environnement
        batch_job_definition = aws_batch.JobDefinition(
            scope=self,
            id=f"job_definition_for_{stack_env}",
            job_definition_name=f"job_definition_for_{stack_env}",
            container=aws_batch.JobDefinitionContainer(
                image=container_image,
                environment={
                    "S3_BUCKET": f"{YOUR_S3_BUCKET}"
                },
                job_role=job_role,
                vcpus=1,
                memory_limit_mib=1024
            )
        )


2.4.5 Implémentation de app.py (Create StepFunctions + CloudWatch Events)

A partir de là, il n'est pas toujours nécessaire de construire l'environnement Batch, Cela se fait en utilisant Step Functions et CloudWatch Event pour une exécution périodique.

Vous pouvez également appeler Batch directement depuis CloudWatch Event, Les fonctions d'étape sont intercalées entre elles en tenant compte de la facilité de coopération avec d'autres services et de la transmission de paramètres.

Lors de l'enregistrement en tant qu'étape Step Functions Remplacez la commande Docker CMD (= définie dans la définition de tâche de Batch) et Il prend l'argument «time» de l'événement CloudWatch et le transmet au script Python.

app.py


        #Suite du lot...

        # ============= #
        # StepFunctions #
        # ============= #

        command_overrides = [
            "python", "__init__.py",
            "--time", "Ref::time"
        ]

        batch_task = aws_sfn_tasks.BatchSubmitJob(
            scope=self,
            id=f"batch_job_{stack_env}",
            job_definition=batch_job_definition,
            job_name=f"batch_job_{stack_env}_today",
            job_queue=batch_job_queue,
            container_overrides=aws_sfn_tasks.BatchContainerOverrides(
                command=command_overrides
            ),
            payload=aws_sfn.TaskInput.from_object(
                {
                    "time.$": "$.time"
                }
            )
        )

        #Cette fois, il n'y a qu'une seule étape, donc c'est simple, mais si vous voulez connecter plusieurs étapes
        # batch_task.next(aws_sfn_tasks.JOB).next(aws_sfn_tasks.JOB)
        #Vous pouvez le transmettre avec la méthode chain comme.
        definition = batch_task

        sfn_daily_process = aws_sfn.StateMachine(
            scope=self,
            id=f"YourProjectSFn-{stack_env}",
            definition=definition
        )

        # ================ #
        # CloudWatch Event #
        # ================ #

        # Run every day at 21:30 JST
        # See https://docs.aws.amazon.com/lambda/latest/dg/tutorial-scheduled-events-schedule-expressions.html
        events_daily_process = aws_events.Rule(
            scope=self,
            id=f"DailySFnProcess-{stack_env}",
            schedule=aws_events.Schedule.cron(
                minute=31,
                hour=12,
                month='*',
                day="*",
                year='*'),
        )
        events_daily_process.add_target(aws_events_targets.SfnStateMachine(sfn_daily_process))

        #Jusqu'ici def__init__(...):

2.4.6 Implémentation de app.py (implémentation de la fonction principale)

Enfin, écrivez le processus pour exécuter le CDK et vous avez terminé.

app.py


#Ici def__init__(...):


def main():
    app = core.App()
    BatchEnvironment(app, "your-project", "feature")
    BatchEnvironment(app, "your-project", "dev")
    BatchEnvironment(app, "your-project", "prod")
    app.synth()


if __name__ == "__main__":
    main()

2.5 Déployer

Une fois le script ci-dessus terminé, vérifiez si le CDK est correctement défini avec la commande suivante, puis déployez-le. Même si vous créez un environnement Batch à partir de zéro, il sera terminé en 10 minutes environ.

#Confirmation de la définition
$ cdk synth

Successfully synthesized to {path_your_project}/cdk.out
Supply a stack id (your-project-dev, your-project-feature, your-project-prod) to display its template.

#Confirmation de l'environnement déployable
$ cdk ls

your-project-dev
your-project-feature
your-project-prod

$ cdk deploy your-project-feature

...deploying...

2.5.1 Vérifier si l'environnement est correctement créé

Lorsque le déploiement est terminé, sélectionnez les fonctions d'étape que vous avez créées à partir de la console et appuyez sur le bouton «Démarrer l'exécution».

スクリーンショット 2020-09-27 23.38.58.png

Mettez seulement l'argument du «temps»,

{
    "time": "2020-09-27T12:31:00Z"
}

スクリーンショット 2020-09-27 23.45.12.png

Si cela fonctionne correctement, vous avez terminé. Vérifiez également CloudWatch Logs pour voir si cela fonctionne comme prévu.

3. Conclusion

J'aime beaucoup CDK car vous pouvez rapidement créer et supprimer l'environnement avec des commandes!

De plus, plutôt que de créer à partir de la console, vous pouvez voir ce qui est requis par les paramètres du programme, donc Même si vous ne connaissez pas le service, j'ai pensé que ce serait bien de savoir quels paramètres sont nécessaires!

(Un jour, je développerai la source ci-dessus dans le référentiel GitHub ...!)

Recommended Posts

[Python] Créer un environnement Batch à l'aide d'AWS-CDK
Créer un environnement Python
Créer une interface graphique python à l'aide de tkinter
Créer un environnement Python sur Mac (2017/4)
Créez un environnement virtuel avec Python!
Créer un environnement python dans centos
Créez un environnement python sur votre Mac
Créons un environnement virtuel pour Python
Créez un lot planifié simple à l'aide de l'image Python de Docker et de parse-crontab
Créez un environnement de test Vim + Python en 1 minute
Créer un fichier GIF en utilisant Pillow en Python
Créer un environnement virtuel avec conda avec Python
Créer un environnement de construction python3 avec Sublime Text3
Créer une carte Web en utilisant Python et GDAL
[Venv] Créer un environnement virtuel python sur Ubuntu
Créez un fichier MIDI en Python en utilisant pretty_midi
Créer un environnement d'exécution Python sur IBM i
[Docker] Créez un environnement jupyterLab (python) en 3 minutes!
Créer un bot de collecte de données en Python à l'aide de Selenium
Créer un plugin Wox (Python)
Créer une fonction en Python
Créer un dictionnaire en Python
Comment configurer un environnement Python à l'aide de pyenv
[CRUD] [Django] Créer un site CRUD en utilisant le framework Python Django ~ 1 ~
[Python] Générer ValueObject avec un constructeur complet à l'aide de classes de données
Créez un environnement de développement Python 3 (Anaconda) confortable avec Windows
Créez un environnement de développement python avec vagrant + ansible + fabric
Créer des tickets JIRA en utilisant Python
Construire un environnement virtuel Python en utilisant venv (Django + MySQL ①)
Construire un environnement virtuel Python
Créez un environnement Python sur votre Mac en utilisant pyenv
Créer un tableau numpy python
[CRUD] [Django] Créer un site CRUD en utilisant le framework Python Django ~ 2 ~
Créer un environnement de développement Python à l'aide de pyenv sur MacOS
Créer un environnement Python hors ligne
Créer un répertoire avec python
Créez un environnement shell et python décent sur Windows
Construire un environnement virtuel Python
Mémo de construction d'environnement d'apprentissage automatique par Python
Créer un environnement de développement Python avec OS X Lion
[CRUD] [Django] Créer un site CRUD en utilisant le framework Python Django ~ 3 ~
[CRUD] [Django] Créer un site CRUD en utilisant le framework Python Django ~ 4 ~
[CRUD] [Django] Créer un site CRUD en utilisant le framework Python Django ~ 5 ~
Créer un environnement de développement Python (pyenv / virtualenv) sur Mac (Homebrew)
Construire un environnement Python sur un Mac, jusqu'au point d'utiliser Jupyter Lab
Créez un environnement de développement Python simple avec VSCode et Docker Desktop
Créer un environnement virtuel pour python sur mac [Très facile]
Construire un environnement Python sur Mac
J'ai fait un Line-bot avec Python!
Créer un conteneur DI avec Python
Construire un environnement Python sur Ubuntu
Dessiner une courbe Silverstone en utilisant Python
Créer un dictionnaire imbriqué à l'aide de defaultdict
Créer un fichier binaire en Python
Créer un environnement virtuel avec Python 3
Créer un environnement Linux sur Windows 10
Créer un framework de décorateur à usage général pour Python