Maison > développement back-end > Tutoriel Python > Comment déployer une API SpringBoot sur AWS ECS à l'aide de CDKTF ?

Comment déployer une API SpringBoot sur AWS ECS à l'aide de CDKTF ?

Linda Hamilton
Libérer: 2025-01-24 10:11:18
original
568 Les gens l'ont consulté

Quand un développeur Java m'a demandé comment déployer son API Spring Boot sur AWS ECS, je l'ai vu comme la chance idéale de plonger dans les dernières mises à jour du projet CDKTF (Kit de développement cloud pour Terraform).

Dans un article précédent, j'ai introduit CDKTF, un cadre qui vous permet d'écrire une infrastructure sous forme de code (IAC) en utilisant des langages de programmation à usage général tels que Python. Depuis lors, CDKTF a atteint sa première version de GA, ce qui en fait le moment idéal pour le revoir. Dans cet article, nous allons parcourir le déploiement d'une API de démarrage Spring sur AWS ECS en utilisant CDKTF.

Trouvez le code de cet article sur mon repo github.

Aperçu de l'architecture

Avant de plonger dans la mise en œuvre, passons en revue l'architecture que nous visons à déployer:

Comment déployer une API SpringBoot sur AWS ECS à laide de CDKTF ?

De ce diagramme, nous pouvons décomposer l'architecture en 03 couches:

  1. réseau :
    • VPC
    • Sous-réseaux publics et privés
    • Internet Gateway
    • Nat Gateways
  2. Infrastructure :
    • Balancer de charge d'application (ALB)
    • auditeurs
    • ECS Cluster
  3. Empilement de service :
    • Groupes cibles
    • Service ECS
    • Définitions des tâches

Étape 1: Containeriser votre application Spring Boot

L'API Java que nous déployons est disponible sur github.

Il définit une API de repos simple avec trois points de terminaison:

  1. / ping : renvoie la chaîne "pong". Ce point final est utile pour tester la réactivité de l'API. Il augmente également une contre-métrique de prometheus pour la surveillance.
  2. / HealthCheck : Renvoie "OK", servant de point de terminaison de contrôle de santé pour s'assurer que l'application fonctionne correctement. Comme / ping, il met à jour un compteur Prométhée pour l'observabilité.
  3. / Hello : accepte un paramètre de requête de nom (par défaut "Monde") et renvoie une salutation personnalisée, par exemple, "Bonjour, [Nom]!". Ce point de terminaison s'intègre également au compteur Prométhée.

Ajoutons le dockerfile :

FROM maven:3.9-amazoncorretto-21 AS builder

WORKDIR /app

COPY pom.xml .

COPY src src

RUN mvn clean package

# amazon java distribution
FROM amazoncorretto:21-alpine

COPY --from=builder /app/target/*.jar /app/java-api.jar

EXPOSE 8080

ENTRYPOINT ["java","-jar","/app/java-api.jar"]
Copier après la connexion
Copier après la connexion
Copier après la connexion

Notre application est prête à être déployée!

Étape 2: Configurer AWS CDKTF

AWS CDKTF vous permet de définir et de gérer les ressources AWS à l'aide de Python.

1. Prérequis

- [**python (3.13)**](https://www.python.org/)
- [**pipenv**](https://pipenv.pypa.io/en/latest/)
- [**npm**](https://nodejs.org/en/)
Copier après la connexion
Copier après la connexion
Copier après la connexion

2. Installez CDKTF et dépendances

Assurez-vous d'avoir les outils nécessaires en installant CDKTF et ses dépendances:

$ npm install -g cdktf-cli@latest
Copier après la connexion
Copier après la connexion

Cela installe le CDKTF CLI qui permet de faire tourner de nouveaux projets pour diverses langues.

3. Initialisez votre application CDKTF

Nous pouvons échafauner un nouveau projet Python en fonctionnant:

FROM maven:3.9-amazoncorretto-21 AS builder

WORKDIR /app

COPY pom.xml .

COPY src src

RUN mvn clean package

# amazon java distribution
FROM amazoncorretto:21-alpine

COPY --from=builder /app/target/*.jar /app/java-api.jar

EXPOSE 8080

ENTRYPOINT ["java","-jar","/app/java-api.jar"]
Copier après la connexion
Copier après la connexion
Copier après la connexion

Il existe de nombreux fichiers créés par défaut et toutes les dépendances sont installées.

Vous trouverez ci-dessous le Main.pyfile initial:

- [**python (3.13)**](https://www.python.org/)
- [**pipenv**](https://pipenv.pypa.io/en/latest/)
- [**npm**](https://nodejs.org/en/)
Copier après la connexion
Copier après la connexion
Copier après la connexion

Étape 3: Couches de construction

a pile représente un groupe de ressources d'infrastructure que CDK pour Terraform (CDKTF) compile dans une configuration Terraform distincte. Les piles permettent une gestion d'état séparée pour différents environnements au sein d'une application. Pour partager des ressources sur les couches, nous utiliserons des références à caisse croisée.

1. Couche de réseau

Ajoutez le fichier réseau_stack.py à votre projet

$ npm install -g cdktf-cli@latest
Copier après la connexion
Copier après la connexion

Ajouter le code suivant pour créer toutes les ressources réseau:

# init the project using aws provider
$ mkdir samples-fargate

$ cd samples-fargate && cdktf init --template=python --providers=aws
Copier après la connexion

Ensuite, modifiez le fichier main.py :

#!/usr/bin/env python
from constructs import Construct
from cdktf import App, TerraformStack

class MyStack(TerraformStack):
    def __init__(self, scope: Construct, id: str):
        super().__init__(scope, id)

        # define resources here

app = App()
MyStack(app, "aws-cdktf-samples-fargate")

app.synth()
Copier après la connexion

Générez les fichiers de configuration Terraform en exécutant la commande suivante:

$ mkdir infra

$ cd infra && touch network_stack.py
Copier après la connexion

Déployez la pile de réseau avec ceci:

from constructs import Construct
from cdktf import S3Backend, TerraformStack
from cdktf_cdktf_provider_aws.provider import AwsProvider
from cdktf_cdktf_provider_aws.vpc import Vpc
from cdktf_cdktf_provider_aws.subnet import Subnet
from cdktf_cdktf_provider_aws.eip import Eip
from cdktf_cdktf_provider_aws.nat_gateway import NatGateway
from cdktf_cdktf_provider_aws.route import Route
from cdktf_cdktf_provider_aws.route_table import RouteTable
from cdktf_cdktf_provider_aws.route_table_association import RouteTableAssociation
from cdktf_cdktf_provider_aws.internet_gateway import InternetGateway

class NetworkStack(TerraformStack):
    def __init__(self, scope: Construct, ns: str, params: dict):
        super().__init__(scope, ns)

        self.region = params["region"]

        # configure the AWS provider to use the us-east-1 region
        AwsProvider(self, "AWS", region=self.region)

        # use S3 as backend
        S3Backend(
            self,
            bucket=params["backend_bucket"],
            key=params["backend_key_prefix"] + "/network.tfstate",
            region=self.region,
        )

        # create the vpc
        vpc_demo = Vpc(self, "vpc-demo", cidr_block="192.168.0.0/16")

        # create two public subnets
        public_subnet1 = Subnet(
            self,
            "public-subnet-1",
            vpc_id=vpc_demo.id,
            availability_zone=f"{self.region}a",
            cidr_block="192.168.1.0/24",
        )

        public_subnet2 = Subnet(
            self,
            "public-subnet-2",
            vpc_id=vpc_demo.id,
            availability_zone=f"{self.region}b",
            cidr_block="192.168.2.0/24",
        )

        # create. the internet gateway
        igw = InternetGateway(self, "igw", vpc_id=vpc_demo.id)

        # create the public route table
        public_rt = Route(
            self,
            "public-rt",
            route_table_id=vpc_demo.main_route_table_id,
            destination_cidr_block="0.0.0.0/0",
            gateway_id=igw.id,
        )

        # create the private subnets
        private_subnet1 = Subnet(
            self,
            "private-subnet-1",
            vpc_id=vpc_demo.id,
            availability_zone=f"{self.region}a",
            cidr_block="192.168.10.0/24",
        )

        private_subnet2 = Subnet(
            self,
            "private-subnet-2",
            vpc_id=vpc_demo.id,
            availability_zone=f"{self.region}b",
            cidr_block="192.168.20.0/24",
        )

        # create the Elastic IPs
        eip1 = Eip(self, "nat-eip-1", depends_on=[igw])
        eip2 = Eip(self, "nat-eip-2", depends_on=[igw])

        # create the NAT Gateways
        private_nat_gw1 = NatGateway(
            self,
            "private-nat-1",
            subnet_id=public_subnet1.id,
            allocation_id=eip1.id,
        )

        private_nat_gw2 = NatGateway(
            self,
            "private-nat-2",
            subnet_id=public_subnet2.id,
            allocation_id=eip2.id,
        )

        # create Route Tables
        private_rt1 = RouteTable(self, "private-rt1", vpc_id=vpc_demo.id)
        private_rt2 = RouteTable(self, "private-rt2", vpc_id=vpc_demo.id)

        # add default routes to tables
        Route(
            self,
            "private-rt1-default-route",
            route_table_id=private_rt1.id,
            destination_cidr_block="0.0.0.0/0",
            nat_gateway_id=private_nat_gw1.id,
        )

        Route(
            self,
            "private-rt2-default-route",
            route_table_id=private_rt2.id,
            destination_cidr_block="0.0.0.0/0",
            nat_gateway_id=private_nat_gw2.id,
        )

        # associate routes with subnets
        RouteTableAssociation(
            self,
            "public-rt-association",
            subnet_id=private_subnet2.id,
            route_table_id=private_rt2.id,
        )

        RouteTableAssociation(
            self,
            "private-rt1-association",
            subnet_id=private_subnet1.id,
            route_table_id=private_rt1.id,
        )

        RouteTableAssociation(
            self,
            "private-rt2-association",
            subnet_id=private_subnet2.id,
            route_table_id=private_rt2.id,
        )

        # terraform outputs
        self.vpc_id = vpc_demo.id
        self.public_subnets = [public_subnet1.id, public_subnet2.id]
        self.private_subnets = [private_subnet1.id, private_subnet2.id]
Copier après la connexion

Network Deployment

Notre VPC est prêt comme indiqué dans l'image ci-dessous:

Network Map

2. Couche d'infrastructure

Ajoutez le fichier infra_stack.py à votre projet

#!/usr/bin/env python
from constructs import Construct
from cdktf import App, TerraformStack
from infra.network_stack import NetworkStack

ENV = "dev"
AWS_REGION = "us-east-1"
BACKEND_S3_BUCKET = "blog.abdelfare.me"
BACKEND_S3_KEY = f"{ENV}/cdktf-samples"

class MyStack(TerraformStack):
    def __init__(self, scope: Construct, id: str):
        super().__init__(scope, id)

        # define resources here

app = App()
MyStack(app, "aws-cdktf-samples-fargate")

network = NetworkStack(
    app,
    "network",
    {
        "region": AWS_REGION,
        "backend_bucket": BACKEND_S3_BUCKET,
        "backend_key_prefix": BACKEND_S3_KEY,
    },
)

app.synth()
Copier après la connexion

Ajoutez le code suivant pour créer toutes les ressources d'infrastructure:

$ cdktf synth
Copier après la connexion

modifier le fichier main.py :

$ cdktf deploy network
Copier après la connexion

Déployez l'infra pile avec ceci:

$ cd infra && touch infra_stack.py
Copier après la connexion

Notez le nom DNS de l'ALB, nous l'utiliserons plus tard.

ALB DNS

3. Couche de service

Ajoutez le fichier service_stack.py à votre projet

from constructs import Construct
from cdktf import S3Backend, TerraformStack
from cdktf_cdktf_provider_aws.provider import AwsProvider
from cdktf_cdktf_provider_aws.ecs_cluster import EcsCluster
from cdktf_cdktf_provider_aws.lb import Lb
from cdktf_cdktf_provider_aws.lb_listener import (
    LbListener,
    LbListenerDefaultAction,
    LbListenerDefaultActionFixedResponse,
)
from cdktf_cdktf_provider_aws.security_group import (
    SecurityGroup,
    SecurityGroupIngress,
    SecurityGroupEgress,
)

class InfraStack(TerraformStack):
    def __init__(self, scope: Construct, ns: str, network: dict, params: dict):
        super().__init__(scope, ns)

        self.region = params["region"]

        # Configure the AWS provider to use the us-east-1 region
        AwsProvider(self, "AWS", region=self.region)

        # use S3 as backend
        S3Backend(
            self,
            bucket=params["backend_bucket"],
            key=params["backend_key_prefix"] + "/load_balancer.tfstate",
            region=self.region,
        )

        # create the ALB security group
        alb_sg = SecurityGroup(
            self,
            "alb-sg",
            vpc_id=network["vpc_id"],
            ingress=[
                SecurityGroupIngress(
                    protocol="tcp", from_port=80, to_port=80, cidr_blocks=["0.0.0.0/0"]
                )
            ],
            egress=[
                SecurityGroupEgress(
                    protocol="-1", from_port=0, to_port=0, cidr_blocks=["0.0.0.0/0"]
                )
            ],
        )

        # create the ALB
        alb = Lb(
            self,
            "alb",
            internal=False,
            load_balancer_type="application",
            security_groups=[alb_sg.id],
            subnets=network["public_subnets"],
        )

        # create the LB Listener
        alb_listener = LbListener(
            self,
            "alb-listener",
            load_balancer_arn=alb.arn,
            port=80,
            protocol="HTTP",
            default_action=[
                LbListenerDefaultAction(
                    type="fixed-response",
                    fixed_response=LbListenerDefaultActionFixedResponse(
                        content_type="text/plain",
                        status_code="404",
                        message_body="Could not find the resource you are looking for",
                    ),
                )
            ],
        )

        # create the ECS cluster
        cluster = EcsCluster(self, "cluster", name=params["cluster_name"])

        self.alb_arn = alb.arn
        self.alb_listener = alb_listener.arn
        self.alb_sg = alb_sg.id
        self.cluster_id = cluster.id
Copier après la connexion

Ajouter le code suivant pour créer toutes les ressources de service ECS:

...

CLUSTER_NAME = "cdktf-samples"
...

infra = InfraStack(
    app,
    "infra",
    {
        "vpc_id": network.vpc_id,
        "public_subnets": network.public_subnets,
    },
    {
        "region": AWS_REGION,
        "backend_bucket": BACKEND_S3_BUCKET,
        "backend_key_prefix": BACKEND_S3_KEY,
        "cluster_name": CLUSTER_NAME,
    },
)
...
Copier après la connexion

Mettez à jour le main.py (pour la dernière fois?):

$ cdktf deploy network infra
Copier après la connexion

Déployez la pile de service avec ceci:

$ mkdir apps

$ cd apps && touch service_stack.py
Copier après la connexion

c'est parti!

Nous avons réussi toutes les ressources pour déployer un nouveau service sur AWS ECS Fargate.

Exécutez ce qui suit pour obtenir la liste de vos piles

from constructs import Construct
import json
from cdktf import S3Backend, TerraformStack, Token, TerraformOutput
from cdktf_cdktf_provider_aws.provider import AwsProvider
from cdktf_cdktf_provider_aws.ecs_service import (
    EcsService,
    EcsServiceLoadBalancer,
    EcsServiceNetworkConfiguration,
)
from cdktf_cdktf_provider_aws.ecr_repository import (
    EcrRepository,
    EcrRepositoryImageScanningConfiguration,
)
from cdktf_cdktf_provider_aws.ecr_lifecycle_policy import EcrLifecyclePolicy
from cdktf_cdktf_provider_aws.ecs_task_definition import (
    EcsTaskDefinition,
)
from cdktf_cdktf_provider_aws.lb_listener_rule import (
    LbListenerRule,
    LbListenerRuleAction,
    LbListenerRuleCondition,
    LbListenerRuleConditionPathPattern,
)
from cdktf_cdktf_provider_aws.lb_target_group import (
    LbTargetGroup,
    LbTargetGroupHealthCheck,
)
from cdktf_cdktf_provider_aws.security_group import (
    SecurityGroup,
    SecurityGroupIngress,
    SecurityGroupEgress,
)
from cdktf_cdktf_provider_aws.cloudwatch_log_group import CloudwatchLogGroup
from cdktf_cdktf_provider_aws.data_aws_iam_policy_document import (
    DataAwsIamPolicyDocument,
)
from cdktf_cdktf_provider_aws.iam_role import IamRole
from cdktf_cdktf_provider_aws.iam_role_policy_attachment import IamRolePolicyAttachment

class ServiceStack(TerraformStack):
    def __init__(
        self, scope: Construct, ns: str, network: dict, infra: dict, params: dict
    ):
        super().__init__(scope, ns)

        self.region = params["region"]

        # Configure the AWS provider to use the us-east-1 region
        AwsProvider(self, "AWS", region=self.region)

        # use S3 as backend
        S3Backend(
            self,
            bucket=params["backend_bucket"],
            key=params["backend_key_prefix"] + "/" + params["app_name"] + ".tfstate",
            region=self.region,
        )

        # create the service security group
        svc_sg = SecurityGroup(
            self,
            "svc-sg",
            vpc_id=network["vpc_id"],
            ingress=[
                SecurityGroupIngress(
                    protocol="tcp",
                    from_port=params["app_port"],
                    to_port=params["app_port"],
                    security_groups=[infra["alb_sg"]],
                )
            ],
            egress=[
                SecurityGroupEgress(
                    protocol="-1", from_port=0, to_port=0, cidr_blocks=["0.0.0.0/0"]
                )
            ],
        )

        # create the service target group
        svc_tg = LbTargetGroup(
            self,
            "svc-target-group",
            name="svc-tg",
            port=params["app_port"],
            protocol="HTTP",
            vpc_id=network["vpc_id"],
            target_type="ip",
            health_check=LbTargetGroupHealthCheck(path="/ping", matcher="200"),
        )

        # create the service listener rule
        LbListenerRule(
            self,
            "alb-rule",
            listener_arn=infra["alb_listener"],
            action=[LbListenerRuleAction(type="forward", target_group_arn=svc_tg.arn)],
            condition=[
                LbListenerRuleCondition(
                    path_pattern=LbListenerRuleConditionPathPattern(values=["/*"])
                )
            ],
        )

        # create the ECR repository
        repo = EcrRepository(
            self,
            params["app_name"],
            image_scanning_configuration=EcrRepositoryImageScanningConfiguration(
                scan_on_push=True
            ),
            image_tag_mutability="MUTABLE",
            name=params["app_name"],
        )

        EcrLifecyclePolicy(
            self,
            "this",
            repository=repo.name,
            policy=json.dumps(
                {
                    "rules": [
                        {
                            "rulePriority": 1,
                            "description": "Keep last 10 images",
                            "selection": {
                                "tagStatus": "tagged",
                                "tagPrefixList": ["v"],
                                "countType": "imageCountMoreThan",
                                "countNumber": 10,
                            },
                            "action": {"type": "expire"},
                        },
                        {
                            "rulePriority": 2,
                            "description": "Expire images older than 3 days",
                            "selection": {
                                "tagStatus": "untagged",
                                "countType": "sinceImagePushed",
                                "countUnit": "days",
                                "countNumber": 3,
                            },
                            "action": {"type": "expire"},
                        },
                    ]
                }
            ),
        )

        # create the service log group
        service_log_group = CloudwatchLogGroup(
            self,
            "svc_log_group",
            name=params["app_name"],
            retention_in_days=1,
        )

        ecs_assume_role = DataAwsIamPolicyDocument(
            self,
            "assume_role",
            statement=[
                {
                    "actions": ["sts:AssumeRole"],
                    "principals": [
                        {
                            "identifiers": ["ecs-tasks.amazonaws.com"],
                            "type": "Service",
                        },
                    ],
                },
            ],
        )

        # create the service execution role
        service_execution_role = IamRole(
            self,
            "service_execution_role",
            assume_role_policy=ecs_assume_role.json,
            name=params["app_name"] + "-exec-role",
        )

        IamRolePolicyAttachment(
            self,
            "ecs_role_policy",
            policy_arn="arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy",
            role=service_execution_role.name,
        )

        # create the service task role
        service_task_role = IamRole(
            self,
            "service_task_role",
            assume_role_policy=ecs_assume_role.json,
            name=params["app_name"] + "-task-role",
        )

        # create the service task definition
        task = EcsTaskDefinition(
            self,
            "svc-task",
            family="service",
            network_mode="awsvpc",
            requires_compatibilities=["FARGATE"],
            cpu="256",
            memory="512",
            task_role_arn=service_task_role.arn,
            execution_role_arn=service_execution_role.arn,
            container_definitions=json.dumps(
                [
                    {
                        "name": "svc",
                        "image": f"{repo.repository_url}:latest",
                        "networkMode": "awsvpc",
                        "healthCheck": {
                            "Command": ["CMD-SHELL", "echo hello"],
                            "Interval": 5,
                            "Timeout": 2,
                            "Retries": 3,
                        },
                        "portMappings": [
                            {
                                "containerPort": params["app_port"],
                                "hostPort": params["app_port"],
                            }
                        ],
                        "logConfiguration": {
                            "logDriver": "awslogs",
                            "options": {
                                "awslogs-group": service_log_group.name,
                                "awslogs-region": params["region"],
                                "awslogs-stream-prefix": params["app_name"],
                            },
                        },
                    }
                ]
            ),
        )

        # create the ECS service
        EcsService(
            self,
            "ecs_service",
            name=params["app_name"] + "-service",
            cluster=infra["cluster_id"],
            task_definition=task.arn,
            desired_count=params["desired_count"],
            launch_type="FARGATE",
            force_new_deployment=True,
            network_configuration=EcsServiceNetworkConfiguration(
                subnets=network["private_subnets"],
                security_groups=[svc_sg.id],
            ),
            load_balancer=[
                EcsServiceLoadBalancer(
                    target_group_arn=svc_tg.id,
                    container_name="svc",
                    container_port=params["app_port"],
                )
            ],
        )

        TerraformOutput(
            self,
            "ecr_repository_url",
            description="url of the ecr repo",
            value=repo.repository_url,
        )

Copier après la connexion

Stack List

Étape 4: GiTHub Actions Workflow

Pour automatiser les déploiements, intégrons un flux de travail GitHub Actions à notre java-api . Après avoir activé des actions GitHub, en définissant les secrets et variables pour votre référentiel, créez le fichier .github / workflows / deploy.yml et ajoutez le contenu ci-dessous:

FROM maven:3.9-amazoncorretto-21 AS builder

WORKDIR /app

COPY pom.xml .

COPY src src

RUN mvn clean package

# amazon java distribution
FROM amazoncorretto:21-alpine

COPY --from=builder /app/target/*.jar /app/java-api.jar

EXPOSE 8080

ENTRYPOINT ["java","-jar","/app/java-api.jar"]
Copier après la connexion
Copier après la connexion
Copier après la connexion

Notre flux de travail fonctionne bien:

Github Actions

Le service a été déployé avec succès comme indiqué dans l'image ci-dessous:

ECS Service

Étape 5: valider le déploiement

Testez votre déploiement à l'aide du script suivant ( Remplacez l'URL ALB par le vôtre ):

- [**python (3.13)**](https://www.python.org/)
- [**pipenv**](https://pipenv.pypa.io/en/latest/)
- [**npm**](https://nodejs.org/en/)
Copier après la connexion
Copier après la connexion
Copier après la connexion

L'ALB est maintenant prêt à servir le trafic!

Réflexions finales

En tirant parti d'AWS CDKTF, nous pouvons écrire du code IAC propre et maintenu en utilisant Python. Cette approche simplifie le déploiement d'applications conteneurisées comme une API Spring Boot sur AWS ECS Fargate.

La flexibilité de CDKTF, combinée aux capacités robustes de Terraform, en fait un excellent choix pour les déploiements de cloud modernes.

Bien que le projet CDKTF offre de nombreuses fonctionnalités intéressantes pour la gestion des infrastructures, je dois admettre que je trouve cela un peu trop verbeux.

avez-vous une expérience avec CDKTF? L'avez-vous utilisé en production?

N'hésitez pas à partager votre expérience avec nous.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

source:dev.to
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Derniers articles par auteur
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal