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.
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.
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.)
Suivez les étapes ci-dessous pour préparer l'environnement d'exécution par lots.
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
Maintenant, procédons à l'implémentation selon la procédure ci-dessus.
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()
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
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.
Définissez le nom du référentiel de manière appropriée.
Sélectionnez le référentiel créé et appuyez sur le bouton "Show Push Command".
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.
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
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.
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
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.
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]
)
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
)
)
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__(...):
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()
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...
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».
Mettez seulement l'argument du «temps»,
{
"time": "2020-09-27T12:31:00Z"
}
Si cela fonctionne correctement, vous avez terminé. Vérifiez également CloudWatch Logs pour voir si cela fonctionne comme prévu.
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