Home Tags Anciens articles Mon CV

Objectif 2019 : Finally, serverless (1)

Dans cet article, j’expliquais en quoi une architecture serverless était mon objectif pour l’année 2019 et où est-ce que j’en étais. Voici un nouvel article sur mon avancement.

Dans l’article mentionné précédemment donc, j’expliquais le workflow pour lancer un conteneur Docker dans Fargate. Je vais détailler ici de tout le workflow ou presque à savoir :

  1. Créer un référentiel Docker ECR
  2. Y pousser son image
  3. Créer un cluster ECS (Fargate)
  4. Créer une définition de tâche qui se sert de mon image
  5. Créer un service associé à cette définition de tâche
  6. Et ensuite ?

Dans cet article, je vous propose d’étudier chacune des premières étapes pour vous guider à travers ce workflow afin de vous expliquer comment j’y suis parvenu.

Créer un référentiel Docker ECR

Cette partie est la plus simple mais je reviens tout de même desssus :

$ aws ecr create-repository --repository-name "cvsandrocazzaniga-ref"

La commande suivante me permet de vérifier les propriétés du référentiel créé:

$ aws ecr describe-repositories
{
    "repositories": [
        {
            "repositoryArn": "arn:aws:ecr:eu-west-1:xxxxxx:repository/cvsandrocazzaniga-ref",
            "registryId": "xxxxxx",
            "repositoryName": "cvsandrocazzaniga-ref",
            "repositoryUri": "xxxxxx.dkr.ecr.eu-west-1.amazonaws.com/cvsandrocazzaniga-ref",
            "createdAt": 1558713482.0
        }
    ]
}

Là, mon référentiel est prêt à recueillir des images.

Y pousser son image

Pour pousser la première version de mon image dans mon nouveau référentiel, je dois en premier lieu la builder en local. Mais avant ça même, je vais me connecter à mon référentiel ECS via awscli. Pour ce faire, je dois authentifier le client Docker dans mon registre, via la commande :

$ $(aws ecr get-login --no-include-email --region eu-west-1)

Maintenant, je build l’image. Une fois dans le répertoire où se situe mon Dockerfile, je lance :

$ docker build -t cvsandrocazzaniga-ref .

Maintenant, je tag l’image pour pouvoir l’envoyer à mon référentiel ECR:

$ docker tag cvsandrocazzaniga-ref:latest xxxxxx.dkr.ecr.eu-west-1.amazonaws.com/cvsandrocazzaniga-ref:latest

Dès lors que ce tag est effectué, docker sait où envoyer mon image et comment la référencer. Let’s push :

$ docker push xxxxxx.dkr.ecr.eu-west-1.amazonaws.com/cvsandrocazzaniga-ref:latest

Note : La console AWS fournit ces commandes pour chaque référentiel. Une fois connecté, allez dans ECR, cliquez sur le référentiel choisi, et faîtes (en haut à droite) “Afficher les commandes push” ou “view push commands” selon la langue choisie. A chaque fois, les commandes sont pré-personalisées en fonction du référentiel, il n’y a qu’à copier/coller. Pratique !

Mon image est maintenant présente dans mon référentiel et prête à être sollicitée.

Créer un cluster ECS (Fargate)

Avant de se lancer dans la création de tâches et de services, il nous faut créer le cluster ECS qui recoupera toutes ces ressources. Ici, mon cluster s’appellera cv-sandrocazzaniga. Le cluster peut comprendre du Fargate comme de l’EC2, pour le moment je ne lui précise donc rien à ce sujet.

$ aws ecs create-cluster --cluster-name cv-sandrocazzaniga
{
    "cluster": {
        "clusterArn": "arn:aws:ecs:eu-west-1:XXXXXXXXXXXX:cluster/cv-sandrocazzaniga",
        "clusterName": "cv-sandrocazzaniga",
        "status": "ACTIVE",
        "registeredContainerInstancesCount": 0,
        "runningTasksCount": 0,
        "pendingTasksCount": 0,
        "activeServicesCount": 0,
        "statistics": [],
        "tags": []
    }
}

La création me renvoie un JSON me décrivant mon cluster et me fournissant notamment son ARN.

Créer une définition de tâche qui se sert de mon image

Une définition de tâche est une liste de paramètres requise pour exécuter des conteneurs Docker dans le cloud AWS. On retrouve dans ces paramètres des choses comme l’image utilisée, quel(s) port(s) exposer, quel est l’entrypoint, etc. Il est à noter que awscli permet de générer un squelette d’exemple de task via la commande :

$ aws ecs register-task-definition --generate-cli-skeleton >> aws_def_task.json

Pour mon application, voici la définition de tâche :

{
    "family": "cv-sandro",
    "networkMode": "awsvpc",
    "containerDefinitions": [
        {
            "name": "cv-sandrocazzaniga-fr",
            "image": "829937339934.dkr.ecr.eu-west-1.amazonaws.com/cvsandrocazzaniga-ref:latest",
            "portMappings": [
                {
                    "containerPort": 80,
                    "hostPort": 80,
                    "protocol": "tcp"
                }
            ],
            "essential": true,
            "entryPoint": [
                "sh",
                "-c"
            ],
            "command": [
                "/usr/sbin/apache2ctl", "-D", "FOREGROUND"
            ]
        }
    ],
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "cpu": "256",
    "memory": "512"
}

Notez que les droits suivants doivent être appliqués pour que cela fonctionne. En effet (cp-cl de la doc qui explique mieux que moi), le rôle d’exécution de tâche Amazon ECS est automatiquement créé pour vous lors de la première exécution de la console Amazon ECS. Cependant, vous devez attacher manuellement la stratégie IAM gérée pour les tâches afin d’autoriser Amazon ECS à ajouter des autorisations pour les futures fonctions et améliorations au fur et à mesure qu’elles sont introduites.

Donc, étape une, je dois vérifier l’existence du rôle via :

$ aws iam get-role --role-name ecsTaskExecutionRole
{
    "Role": {
        "Path": "/",
        "RoleName": "ecsTaskExecutionRole",
        "RoleId": "AROA4CXXXXXXXXXXXXXXX",
        "Arn": "arn:aws:iam::xxxxxx:role/ecsTaskExecutionRole",
        "CreateDate": "2019-05-03T11:12:12Z",
        "AssumeRolePolicyDocument": {
            "Version": "2008-10-17",
            "Statement": [
                {
                    "Sid": "",
                    "Effect": "Allow",
                    "Principal": {
                        "Service": "ecs-tasks.amazonaws.com"
                    },
                    "Action": "sts:AssumeRole"
                }
            ]
        },
        "MaxSessionDuration": 3600
    }
}

Bon, le rôle existe. Je vais lui donner les permissions nécessaires, à savoir lui attacher la policy AmazonECSTaskExecutionRolePolicy via son ARN.

$ aws iam attach-role-policy --role-name ecsTaskExecutionRole --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

Enfin, j’importe ma task definition dans ECS. Chose qui m’a fait chercher un moment, il faut lui préciser l’ARN du rôle :

$ aws ecs register-task-definition --execution-role-arn arn:aws:iam::XXXXXXXXXXXX:role/ecsTaskExecutionRole --cli-input-json file://./ecr-task.json

Un JSON nous est renvoyé, la définition de tâche est enregistrée et je peux maintenant la retrouver dans la console ou via la cli, et surtout je peux maintenant l’utiliser via un service. Etape suivante!

Créer un service associé à cette définition de tâche

Le service dans ECS est une notion floue à mes yeux. Mais en gros, en lançant le service associé à ma tâche et en lui précisant combien d’instances de conteneur je veux derrière, il prend mon image et en lance autant d’instances que demandé dans le cluster ECS créé.

Je vais demander pour cela une instance pour le moment, sur le cluster que j’ai créé précédemment, en utilisant ma définition de tâche (qui je le rappelle, utilise elle-même l’image buildée précédemment) et je précise que je veux lancer en mode Fargate. Je lui précise également le subnet et le security group à utiliser.

$ aws ecs create-service --cluster cv-sandrocazzaniga --service-name cv-web --task-definition cv-sandro:1 --desired-count 1 --launch-type "FARGATE" --network-configuration "awsvpcConfiguration={subnets=[subnet-xxxxxxxxxxxx],securityGroups=[sg-xxxxxxxxxxxxxx]}"
{
    "service": {
        "serviceArn": "arn:aws:ecs:eu-west-1:XXXXXXXXXXXX:service/cv-sandrocazzaniga/cv-web",
        "serviceName": "cv-web",
        "clusterArn": "arn:aws:ecs:eu-west-1:XXXXXXXXXXXX:cluster/cv-sandrocazzaniga",
        "loadBalancers": [],
        "serviceRegistries": [],
        "status": "ACTIVE",
        "desiredCount": 1,
        "runningCount": 0,
        "pendingCount": 0,
        "launchType": "FARGATE",
        "platformVersion": "LATEST",
        "taskDefinition": "arn:aws:ecs:eu-west-1:XXXXXXXXXXXX:task-definition/cv-sandro:1",
        "deploymentConfiguration": {
            "maximumPercent": 200,
            "minimumHealthyPercent": 100
        },
        "deployments": [
            {
                "id": "ecs-svc/xxxxxxxxxxxxxxxxxx",
                "status": "PRIMARY",
                "taskDefinition": "arn:aws:ecs:eu-west-1:XXXXXXXXXXXX:task-definition/cv-sandro:1",
                "desiredCount": 1,
                "pendingCount": 0,
                "runningCount": 0,
                "createdAt": 1560036169.194,
                "updatedAt": 1560036169.194,
                "launchType": "FARGATE",
                "platformVersion": "1.3.0",
                "networkConfiguration": {
                    "awsvpcConfiguration": {
                        "subnets": [
                            "subnet-xxxxxxxxxxxxxxxx"
                        ],
                        "securityGroups": [
                            "sg-xxxxxxxxxxxxxxxxx"
                        ],
                        "assignPublicIp": "DISABLED"
                    }
                }
            }
        ],
        "roleArn": "arn:aws:iam::XXXXXXXXXXXX:role/aws-service-role/ecs.amazonaws.com/AWSServiceRoleForECS",
        "events": [],
        "createdAt": 1560036169.194,
        "placementConstraints": [],
        "placementStrategy": [],
        "networkConfiguration": {
            "awsvpcConfiguration": {
                "subnets": [
                    "subnet-xxxxxxxxxxxxxxxx"
                ],
                "securityGroups": [
                    "sg-xxxxxxxxxxxxxxxxx"
                ],
                "assignPublicIp": "DISABLED"
            }
        },
        "schedulingStrategy": "REPLICA",
        "enableECSManagedTags": false,
        "propagateTags": "NONE"
    }
}

Notons que nous le lançons sans IP publique, comme vous pouvez le voir "assignPublicIp": "DISABLED". En effet, c’est le load balancer frontal qui portera cette IP publique.

Afin de me dire que c’est OK, il me renvoie le JSON de ce qu’il vient de créer.

Et ensuite ?

La dernière étape afin que mon cluster soit visible depuis Internet et donc accessible, c’est de brancher un ELB (Elastic load balancer), qui portera l’IP publique ainsi que le SSL. Il ne restera plus qu’à modifier l’enregistrement DNS de type A de l’url cv.sandrocazzaniga.fr et le tour est joué (normalement).

J’avoue que la création de ce LB me donne du fil à retordre, j’ai donc souhaité publier cette première partie d’abord et revenir plus en détail sur cette dernière après.

Il me reste encore 6 mois pour compléter mon objectif :-)