Dans cette article, nous allons enrichir notre pipeline et déployer l'application dans une instance EC2.
CloudFormation
Makefile
pour la création et la destruction des éléments d'infraCloudFormation
Build
sur l'instance EC2SpringBoot
SystemD
Cette article est le 2e d'une série. Voir aussi:
Le but de cette série d'article est de mettre en place une pipeline de CI/CD d'une application Java dans des instances EC2 dans un autoscaling group derrière un Load Balancer, avec des tests automatisés et la publication de rapports de test.
Nous commençons par rappeler la cible, les étapes pour y arriver, et le scope de cet article. Ensuite, nous effectuerons l'implémentation pas à pas, avec des tags git pour pouvoir revenir sur les rails en cas de décrochage (ce qui est inévitable, on oublie toujours une action ou une étape)
Bon code
Le but de la série d'articles est d'avoir au final:
mvn verify
) sont éxécutés lors de la phase de Build
de la pipelinemvn verify
) est publiémvn verify
) est publiéNous allons réaliser la cible par les étapes suivantes:
Dans cet article, nous allons réaliser l'étape n°2, à savoir:
CloudFormation
Makefile
pour la création et la destruction des éléments d'infratag de départ: 1.13-buildtime-integation-test-cucumber
tag d'arrivée: 2.0-refacto-intro-makefile-infra
Dans cette étape, nous allons refacto un peu notre code d'infra. Au lieu d'utiliser un script shell qui fait tout, nous allons introduire un Makefile
pour nous permettre de créer toute l'infra, ou juste une partie, de manière un peu plus propre.
Il y a probablement une meilleure manière de faire, mais pour le moment ça fera très bien l'affaire.
SHELL := /bin/bash
ifndef APPLICATION_NAME
$(error APPLICATION_NAME is not set)
endif
include infra.env
PIPELINE_STACK_NAME=$(APPLICATION_NAME)-pipeline
all:
- $(MAKE) pipeline
pipeline:
./create-pipeline.sh $(APPLICATION_NAME) $(PIPELINE_STACK_NAME) $(GITHUB_REPO) $(GITHUB_REPO_BRANCH)
delete-all:
- $(MAKE) delete-pipeline
delete-pipeline:
./delete-stack-wait-termination.sh $(PIPELINE_STACK_NAME)
create-all.sh
dans le répertoire infra
, nous le renommons create-pipeline.sh
, et nous le modifions, de manière à ce que toutes les variables soient transmises en paramètres d'appel du script. Cela a paru approprié sur le moment#!/bin/bash
if [[ "$#" -ne 4 ]]; then
echo -e "usage:\n./create-all.sh \$APPLICATION_NAME \$PIPELINE_STACK_NAME \$GITHUB_REPO \$GITHUB_REPO_BRANCH"
exit 1
fi
export APPLICATION_NAME=$1
export PIPELINE_STACK_NAME=$2
echo -e "##############################################################################"
echo -e "creating ci/cd pipeline stack"
echo -e "##############################################################################"
aws cloudformation deploy \
--stack-name $PIPELINE_STACK_NAME \
--template-file pipeline-cfn.yml \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides \
ApplicationName=$APPLICATION_NAME \
GithubRepo=$GITHUB_REPO \
GithubRepoBranch=$GITHUB_REPO_BRANCH
Nous introduisons un script pour supprimer une stack CloudFormation
quelconque, et qui attend la fin de la suppression. Sans rentrer dans les détails, cela nous évitera des problèmes ultérieurs
#!/bin/bash
if [[ -z $1 ]]; then
echo -e "usage:\n./delete-stack-wait-termination.sh \$STACK_NAME"
exit 1
fi
STACK_NAME=$1
echo -e "##############################################################################"
echo -e "force deleting S3 buckets for stack $STACK_NAME"
echo -e "##############################################################################"
S3_BUCKETS=$(aws cloudformation describe-stack-resources --stack-name $STACK_NAME --query "StackResources[?ResourceType=='AWS::S3::Bucket'].PhysicalResourceId" --output text)
for S3_BUCKET in $S3_BUCKETS
do
aws s3 rb s3://$S3_BUCKET --force
done
echo -e "##############################################################################"
echo -e "deleting stack $STACK_NAME"
echo -e "##############################################################################"
aws cloudformation delete-stack --stack-name $STACK_NAME
aws cloudformation wait stack-delete-complete --stack-name $STACK_NAME
suppression de variables inutilisées et/ou sources d'erreur avec Makefile
:
export AWS_REGION=$(aws configure get region)
export ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)
Si vous avez toujours les stacks CloudFormation
créées dans la partie précédente, profitons-en pour vérifier que le Makefile
fonctionne bien:
cd infra
make delete-all APPLICATION_NAME=my-app
la stack de la pipeline devrait bien être supprimée:
Recréons la pipeline:
cd infra #si vous n'y êtes pas deja
make all APPLICATION_NAME=my-app
Inspectons la stack CloudFormation
:
Après avoir activé la connexion github et éventuellement relancé la pipeline si celle-ci a échoué, vous devriez avoir au final une pipeline verte:
Et les rapports de tests devraient bien être uploadés:
tag de départ: 2.0-refacto-intro-makefile-infra
tag d'arrivée: 2.1-creation-ami
Dans cette étape, nous allons créer une AMI avec toutes les dépendances et configuration nécessaires pour faire tourner notre application Java.
Nous nous baserons sur l'outil Packer
et nous baserons sur une AMI de base Ubuntu Server 20.04
.
Il existe aussi l'outil d'AWS EC2 Builder
, mais au moment de la rédaction de cette article, je ne suis pas assez familier avec EC2 Builder, je connais un peu mieux Packer
, j'ai deja un side project avec sur lequel je peux m'appuyer, je n'ai pas trop envie d'y passer beaucoup de temps, et la création d'AMI n'est pas le but principal de la série d'articles.
Cependant, EC2 Builder
fera très certainement l'objet d'un prochain side project et d'un article associé.
Les actions réalisées sont:
infra/packer-ami/ubuntu-springboot-ready.json
, qui sera utilisé par Packer
pour créer notre AMI:{
"variables": {
"ami_id": "",
"ami_name": "",
"region": ""
},
"builders": [
{
"type": "amazon-ebs",
"access_key": "",
"secret_key": "",
"region": "{{user `region`}}",
"ami_name": "{{user `ami_name`}}",
"instance_type": "t2.micro",
"source_ami": "{{user `ami_id`}}",
"ssh_username": "ubuntu"
}
],
"provisioners": [
{
"type": "shell",
"script": "setup.sh"
}
]
}
Décortiquons un peu ce fichier:
a. On définit des varibles pour notre config Packer
b. Des configs de l'instance EC2 utilisée pour constituer notre AMI
shell
et on utilise le script setup.sh
juste à côté du fichier json, dont on parlera juste aprèsinfra/packer-ami/setup.sh
, qui sera utilisé par Packer
pour provisionner l'instance EC2 à partir de laquelle nous allons créer notre AMI:#! /bin/bash
sleep 30
sudo apt update
# installing some utilities
sudo apt install -y tree ncdu mlocate tmux jq
# installing java
sudo apt install openjdk-16-jdk -y
AWS_REGION=$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/document | jq .region -r)
if [[ -z $AWS_REGION ]]; then
echo "AWS_REGION is empty, defaulting to 'us-east-1'"
AWS_REGION="us-east-1"
fi
echo "region is: $AWS_REGION"
# installing codedeploy agent
sudo apt-get install ruby wget -y
wget https://aws-codedeploy-$AWS_REGION.s3.$AWS_REGION.amazonaws.com/latest/install
chmod +x ./install
sudo ./install auto | tee /tmp/logfile
sudo systemctl start codedeploy-agent
sudo systemctl enable codedeploy-agent
Le script devrait être assez court et lisible, mais, en reprenant les commentaires, on peut le résumer de la manière suivante
a. on installe quelques utilitaires
b. on installe java
c. on installe l'agent CodeDeploy
-> CodeDeploy
est un service AWS permettant ... de déployer des applications: dans EC2, ECS, Lambda, etc. mais contrairement à Ansible
par exemple, cet outil nécessite un agent, qui va "pull" tout ce qu'il y a à déployer
infra/create-ami.sh
, qui wrappe l'appel à Packer
:#!/bin/bash
if [[ "$#" -ne 3 ]]; then
echo -e "usage:\n./create-ami.sh \$APPLICATION_NAME \$BASE_AMI_ID \$AWS_REGION"
exit 1
fi
APPLICATION_NAME=$1
BASE_AMI_ID=$2
AWS_REGION=$3
set -e
cd packer-ami
packer build \
-var "ami_name=$APPLICATION_NAME" \
-var "ami_id=$BASE_AMI_ID" \
-var "region=$AWS_REGION" \
ubuntu-springboot-ready.json
On modifie le Makefile
, de manière à rajouter les targets: ami
et delete-ami
, dont le nom devrait être suffisamment explicite
#[...]
all:
- $(MAKE) pipeline
- $(MAKE) ami
ami:
$(eval BASE_AMI_ID := $(shell aws ssm get-parameters --names /aws/service/canonical/ubuntu/server/20.04/stable/current/amd64/hvm/ebs-gp2/ami-id --query 'Parameters[0].[Value]' --output text))
$(eval AWS_REGION := $(shell aws configure get region))
./create-ami.sh $(APPLICATION_NAME) $(BASE_AMI_ID) $(AWS_REGION)
#[...]
delete-ami:
$(eval AMI_ID := $(shell aws ec2 describe-images --owners self --query "Images[?Name=='$(APPLICATION_NAME)'].ImageId" --output text))
aws ec2 deregister-image --image-id $(AMI_ID)
Vérifions que tout cela fonctionne, en déclenchant la target ami
ou all
(qui sont le facto "idempotent"):
cd infra #si vous n'y êtes pas deja
make all APPLICATION_NAME=my-app
La création de l'AMI est assez longue et prend chez moi environ 10-15 minutes, soyez patients.
Vérifions dans la console AWS:
Créons rapidement une instance à partir de cette AMI, dans la console, cliquez sur "Actions > Launch":
Cliquez sur "Review and Launch":
Cliquez sur "Launch":
Si vous avez deja une paire de clés ssh et que vous êtes familier avec la procédure, sélectionnez une paire de clés existante.
Sinon:
Create a new key pair
au lieu de Choose an existing key pair
keys.pem
Dans le menu de gauche, allez dans "Instances", vous devriez voir votre instance dans l'état "Running" au bout d'une poignée de minutes maximum:
Nous allons nous y connecter en ssh. Il y a plusieurs manières de l'effectuer, allons au plus facile:
Dans la fenêtre suivante:
Un nouvel onglet s'ouvre, avec un terminal:
root
java -version
CodeDeploy
avec systemctl status codedeploy-agent.service
Prochaine étape, automatiser cette création d'instance
CloudFormation
tag de départ: 2.1-creation-ami
tag d'arrivée: 2.2-creation-instance-ec2
Le titre est assez explicite, dans cette étape, nous allons créer une instance EC2 avec CloudFormation
Pour cela, nous allons effectuer les actions, ajouts, modifications suivantes:
Import d'une paire de clés SSH dans le compte AWS
infra/create-ssh-key-pair.sh
#!/bin/bash
if [[ "$#" -ne 2 ]]; then
echo -e "usage:\n./create-all.sh \$SSH_KEY_NAME \$SSH_KEY_PATH"
exit 1
fi
export SSH_KEY_NAME=$1
export SSH_KEY_PATH=$2
echo -e "##############################################################################"
echo -e "creating ssh key pair \"$SSH_KEY_NAME\" from $SSH_KEY_PATH"
echo -e "##############################################################################"
PUBLIC_KEY_BASE_64=$(cat $SSH_KEY_PATH | base64)
aws ec2 import-key-pair --key-name $SSH_KEY_NAME --public-key-material "$PUBLIC_KEY_BASE_64"
On ne peut pas créer de paire de clés SSH via CloudFormation
, on se contera de la CLI AWS dans ce cas.
Que dire de particulier, il faut transmettre le contenu de la clé publique encodé en base64.
infra/infra.env
export GITHUB_REPO=mbimbij/codebuild-test-report-demo # deja présent
export GITHUB_REPO_BRANCH=main # deja présent
export SSH_KEY_NAME=local
export SSH_KEY_PATH=~/.ssh/id_rsa.pub
Pour cette démo, on utilise la clé publique: ~/.ssh/id_rsa.pub
, que l'on uploadera avec l'id local
.
Ainsi, si vous avez deja une paire de clés (et en tant que programmeur, sous linux, je présume que c'est le cas), cela devrait faciliter l'utilisation de ce projet / tutorial, et la connexion à l'instance EC2.
CloudFormation
pour l'instance EC2, fichier infra/execution-environment-cfn.yml
:Parameters:
ApplicationName:
Type: String
Description: Application Name
KeyName:
Type: String
Description: Key Name
AmiId:
Type: AWS::EC2::Image::Id
Description: Ami Id
Resources:
Ec2SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: ec2 security group
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 8080
ToPort: 8080
CidrIp: 0.0.0.0/0
Ec2Instance:
Type: AWS::EC2::Instance
Properties:
ImageId: !Ref AmiId
InstanceType: t2.micro
KeyName: !Ref KeyName
SecurityGroups:
- !Ref Ec2SecurityGroup
Tags:
- Key: Name
Value: !Sub '${ApplicationName}-instance'
- Key: Application
Value: !Sub '${ApplicationName}'
Analysons ce template:
Security Group
autorisant les connexions entrantes sur les ports:
SpringBoot
, que nous allons utiliser par la suite.AmiId
, dans lequel on placera l'id de l'AMI que l'on a créé dans l'étape #1security group
à l'instanceinfra/Makefile
:SHELL := /bin/bash
ifndef APPLICATION_NAME
$(error APPLICATION_NAME is not set)
endif
include infra.env
PIPELINE_STACK_NAME=$(APPLICATION_NAME)-pipeline
EXECUTION_ENVIRONMENT_STACK_NAME=$(APPLICATION_NAME)-execution-environment
all:
- $(MAKE) pipeline
- $(MAKE) ami
- $(MAKE) execution-environment
pipeline:
./create-pipeline.sh $(APPLICATION_NAME) $(PIPELINE_STACK_NAME) $(GITHUB_REPO) $(GITHUB_REPO_BRANCH)
ami:
$(eval BASE_AMI_ID := $(shell aws ssm get-parameters --names /aws/service/canonical/ubuntu/server/20.04/stable/current/amd64/hvm/ebs-gp2/ami-id --query 'Parameters[0].[Value]' --output text))
$(eval AWS_REGION := $(shell aws configure get region))
./create-ami.sh $(APPLICATION_NAME) $(BASE_AMI_ID) $(AWS_REGION)
ssh-key-pair:
./create-ssh-key-pair.sh $(SSH_KEY_NAME) $(SSH_KEY_PATH)
execution-environment: ssh-key-pair
$(eval AMI_ID := $(shell aws ec2 describe-images --owners self --query "Images[?Name=='$(APPLICATION_NAME)'].ImageId" --output text))
./create-execution-environment.sh $(APPLICATION_NAME) $(EXECUTION_ENVIRONMENT_STACK_NAME) $(AMI_ID) $(SSH_KEY_NAME)
delete-all:
- $(MAKE) delete-pipeline
delete-pipeline:
./delete-stack-wait-termination.sh $(PIPELINE_STACK_NAME)
delete-ami:
$(eval AMI_ID := $(shell aws ec2 describe-images --owners self --query "Images[?Name=='$(APPLICATION_NAME)'].ImageId" --output text))
aws ec2 deregister-image --image-id $(AMI_ID)
delete-ssh-key-pair:
aws ec2 delete-key-pair --key-name $(SSH_KEY_NAME)
delete-execution-environment: delete-ssh-key-pair
./delete-stack-wait-termination.sh $(EXECUTION_ENVIRONMENT_STACK_NAME)
Jetons un oeil du côté des ajouts effectués:
Pas grand chose à ajouter, un ajoute des targets pour créer la paire de clés et l'instance, ainsi que pour les détruire
Vérifions la création :
cd infra #si vous n'y êtes pas deja
make all APPLICATION_NAME=my-app
L'instance est bien créé. Vérifions que l'on peut s'y connecter en utilisant notre clé ssh par défaut (sans l'expliciter)
Parfait ! Prochaine étape, revenir sur la pipeline, et réussir à pousser la sortie de l'étape de build sur l'instance EC2. Première étape pour déployer l'application Java
Build
sur l'instance EC2tag de départ: 2.2-creation-instance-ec2
tag d'arrivée: 2.3-deploy-1-copy-build-output-raw
Dans cette étape, nous allons créer un "stage" de déploiement dans la pipeline. Pour rester dans une approche baby-step, nous allons nous contenter de copier la sortie brute du stage "Build" de cette même pipeline.
Pour cela nous effectuons les actions suivantes:
infra/pipeline-cfn.yml
, pour y ajouter un stage "Deploy"
PipelineRole
pour lui permettre d'éxécuter des déploiements basés sur CodeDeploy
- Effect: Allow
Action:
- codedeploy:*
Resource: !Sub 'arn:${AWS::Partition}:codedeploy:${AWS::Region}:${AWS::AccountId}*'
buildspec
dans la ressource BuildProject
afin de copier tous les fichiers dans les "OutputArtifacts"artifacts:
files:
- '**/*'
CodeDeployApplication
qui sera le projet / application CodeDeploy
CodeDeployApplication:
Type: AWS::CodeDeploy::Application
Properties:
ApplicationName: !Sub '${ApplicationName}-deploy-application'
ComputePlatform: Server
CodeDeployDeploymentGroup
-> un groupe de déploiement de l'application précédente, qui va concrètement procéder au déploiement, à partir d'un fichier appspec.yml
dans les artefacts en entréeCodeDeployDeploymentGroup:
Type: AWS::CodeDeploy::DeploymentGroup
Properties:
ApplicationName: !Ref CodeDeployApplication
ServiceRoleArn: !GetAtt
- CodeDeployRole
- Arn
DeploymentGroupName: !Sub '${ApplicationName}-deployment-group'
DeploymentConfigName: CodeDeployDefault.OneAtATime
Ec2TagFilters:
- Key: Application
Value: !Ref ApplicationName
Type: KEY_AND_VALUE
CodeDeployRole
-> le rôle que va utiliser le groupe de déploiement. On lui attache une policy managée par AWS: AWSCodeDeployRole
CodeDeployRole:
Type: 'AWS::IAM::Role'
Description: IAM role for !Ref ApplicationName code deploy deployment group
Properties:
RoleName: !Join
- '-'
- - !Ref ApplicationName
- deploy-role
AssumeRolePolicyDocument:
Statement:
- Action: "sts:AssumeRole"
Effect: Allow
Principal:
Service:
- codedeploy.amazonaws.com
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole
Deploy
à la ressource: Pipeline
- Name: Deploy
Actions:
- Name: Deploy
InputArtifacts:
- Name: BuildOutput
ActionTypeId:
Category: Deploy
Owner: AWS
Version: 1
Provider: CodeDeploy
Configuration:
ApplicationName:
Ref: CodeDeployApplication
DeploymentGroupName:
Ref: CodeDeployDeploymentGroup
RunOrder: 1
infra/execution-environment-cfn.yml
Ec2InstanceProfile
Ec2InstanceProfile
, on y associant le rôle IAM Ec2InstanceRole
Ec2InstanceRole
, ayant les policies managées
AmazonEC2RoleforAWSCodeDeploy
, pour permettre à l'agent codedeploy
de récupérer les artefacts depuis S3AmazonSSMManagedInstanceCore
, pour permettre à l'instance de s'enregistrer auprès de System Manager
. Sans cela, le déploiement reste indéfiniment dans l'état Pending
, sans absolument aucune log ... appspec
ultra minimaliste. La présence de ce fichier est obligatoireversion: 0.0
os: linux
... Et rien de plus !
Mettons à jour notre infra:
cd infra #si vous n'y êtes pas deja
make all APPLICATION_NAME=my-app
poussons le code dans le repo git relançons une release dans la pipeline si elle n'est pas déclenchée automatiquement. Tout devrait être vert:
Allons jeter un oeil au déploiement dans CodeDeploy
. Dans "Deploy > Deployments", rafraîchir éventuellement:
J'ai plusieurs déploiement dans mon screenshot, en espérant que cela ne soit pas source de confusion.
Notons l'id du dernier déploiement
connectons-nous en ssh sur l'instance EC2 :
Last login: Thu May 27 08:15:01 2021 from 176.173.232.167
ubuntu@ip-172-31-40-53:~$ cd /opt/codedeploy-agent/deployment-root/<some-deployment-group-id>/<last-deployment-id>/deployment-archive/
ubuntu@ip-172-31-40-53:/opt/codedeploy-agent/deployment-root/1abcd735-aa71-4a4d-8f68-ccc88a65d2e4/d-C1CX5XBU8/deployment-archive$ ls
README.md appspec.yml blog-article infra pom.xml reposition-tag.sh src target
l'id du groupe de déploiement, ainsi que du dernier déploiement, seront différents chez vous.
Cependant, on peut constater que le code source et le répertoire target
sont bien présents dans l'instance EC2.
Félicitations, nous avons bien réssui à effectuer un premier déploiement.
Prochaine étape, créer une interface REST dans le code Java
, déployer l'application en tant que service et vérifier que tout fonctionne bien.
SpringBoot
tag de départ: 2.3-deploy-1-copy-build-output-raw
tag d'arrivée: 2.4-java-springboot-rest-interface
Dans cette étape, nous allons rajouter une interface REST très simple, en utilisant SpringBoot
.
Nous rajoutons ou modifions:
pom.xml
:
spring-boot-parent
spring-boot-starter-web
et spring-boot-starter-test
MySpringApplication
de la couverture de testSpringBoot
: MySpringApplication
SpringBoot
: MyRestController
MyRestControllerIT
IT
plutôt qu'en Test
, dans la mesure où un contexte Spring est créé et que ce test prend quelques seconde. Mais habituellement je suffixe avec Test
, je peux entendre les arguments dans les 2 sens, surtout quand on teste une "slice" de l'application uniquement (BDD, messaging, REST) et qu'on mock le reste. En ce qui me concerne, la question de où placer et nommer ce genre de tests de manière standard reste ouverte. On peut vérifier que tout marche comme il le faut, inspecter en local les rapports de test et de couverteure via un mvn clean verify
SystemD
tag de départ: 2.4-java-springboot-rest-interface
tag d'arrivée: 2.5-deploy-java-application-as-service
Dans cette étape, nous allons déployer l'application Java en tant que service SystemD
, puis vérifier manuellement que l'on peut accéder à l'endpoint REST, et que la pipeline déploie bien les mises à jour du code
./infra/pipeline-cfn.yml
: nous modifions le buildspec
de la ressource BuildProject
, qui devient:version: 0.2
phases:
install:
runtime-versions:
java: corretto11
build:
commands:
- mvn verify
post_build:
commands:
# move the jar (by wildcard, agnostic to its name) to top level application.jar
- mv target/*.jar application.jar
finally:
- find target/surefire-reports/ -name "*Cucumber*" -delete
- find target/failsafe-reports/ -name "*Cucumber*" -delete
reports:
BuildTimeTests:
files:
- 'target/surefire-reports/TEST*.xml'
- 'target/failsafe-reports/TEST*.xml'
- 'target/cucumber-reports/buildtime/cucumber-results.xml'
- 'target/cucumber-reports/buildtime/cucumber-integration-results.xml'
CoverageReport:
files:
- 'target/site/jacoco-aggregate/jacoco.xml'
file-format: 'JACOCOXML'
cache:
paths:
- '/root/.m2/**/*'
artifacts:
files:
- application.jar
- appspec.yml
- 'scripts/*'
- application.service
Analysons les différences:
application.jar
. Comme l'indique le commentaire, ça permet au déploiement d'être "agnostique" quand au nom du projet, de sa version, et de l'artefact généré, et permet à la conf et aux fichiers liés au déploiement d'être assez génériques et réutilisables sur d'autres projetsapplication.jar
, de manière assez évidenteappspec.yml
: le descriptif du déploiementscripts/*
: tous les scripts utilisés par appspec.yml
pour le déploiementapplication.service
, pour pouvoir créer un service SystemD
pour l'application scripts/stop_server.sh
:#! /bin/bash
systemctl stop application
scripts/before_install.sh
:#! /bin/bash
mkdir -p /opt/application/ # on installera nos artefacts dans /opt/application/
rm -rf /etc/systemd/system/application.service # on supprime le service existant
rm -rf /opt/application/application.jar # on supprime le jar existant
scripts/start_server.sh
:#! /bin/bash
systemctl daemon-reload # au cas où le nouveau fichier de service est différent de l'ancien
systemctl start application # on démarre le service
scripts/validate_service.sh
:#! /bin/bash
#! /bin/bash
PORT=8080
RETRY_INTERVAL_SECONDS=2
until curl -X GET "http://localhost:$PORT"
do
echo "Application not listening yet on port $PORT. Retrying in $RETRY_INTERVAL_SECONDS second(s)..."
sleep $RETRY_INTERVAL_SECONDS
done
application.service
:[Unit]
Description=application
After=syslog.target
[Service]
User=ubuntu
ExecStart=java -jar /opt/application/application.jar
SuccessExitStatus=143
[Install]
WantedBy=multi-user.target
Que l'on a récupéré depuis la documentation de SpringBoot https://docs.spring.io/spring-boot/docs/current/reference/html/deployment.html#deployment.installing.nix-services.system-d
appspec.yml
, dont le contenu est maintenant:version: 0.0
os: linux
files:
- source: /application.service
destination: /etc/systemd/system
- source: /application.jar
destination: /opt/application
hooks:
ApplicationStop:
- location: scripts/stop_server.sh
timeout: 10
runas: root
BeforeInstall:
- location: scripts/before_install.sh
timeout: 5
runas: root
AfterInstall:
- location: scripts/start_server.sh
timeout: 5
runas: root
ValidateService:
- location: scripts/validate_service.sh
timeout: 20
runas: root
Analysons rapidement les différences:
.service
dans l'un des répertoires utilisés par SystemD
pour référencer les déclarations de services
/opt/codedeploy-agent/deployment-root/<some-deployment-group-id>/<last-deployment-id>/deployment-archive/
.jar
vers le réperoire que l'on a choisi pour acceuillir les artefacts du déploiementApplicationStop
du cycle de vie du déploiement, on éxécute le script scripts/stop_server.sh
en tant que user root
, avec un timeout de 10 secondesBeforeInstall
du cycle de vie du déploiement, on éxécute le script scripts/before_install.sh
en tant que user root
, avec un timeout de 10 secondesAfterInstall
du cycle de vie du déploiement, on éxécute le script scripts/start_server.sh
en tant que user root
, avec un timeout de 10 secondesValidateService
du cycle de vie du déploiement, on éxécute le script scripts/start_server.sh
en tant que user root
, avec un timeout de 20 secondes. Ce script effectue un "sanity check" de l'application, et vérifie qu'elle va être en mesure de répondre à un curl sur le port 8080Plus d'infos sur le cycle de vie d'un déploiement sur EC2 / On-premises et les différents hooks dispos ici: https://docs.aws.amazon.com/codedeploy/latest/userguide/reference-appspec-file-structure-hooks.html#appspec-hooks-server
Mettons à jour notre pipeline:
cd infra #si vous n'y êtes pas deja
make pipeline APPLICATION_NAME=my-app # `make all APPLICATION_NAME=my-app` marcherait tout aussi bien
Enfin poussons nos modifications et déclenchons une release sur la pipeline au besoin, elle devrait être verte:
Le port 8080 étant accessible sur l'instance EC2, on devrait pouvoir interroger l'interface REST via curl
ou un navigateur:
joseph@joseph-ThinkPad-T480 infra ±|main ✗|→ curl ubuntu@ec2-35-181-54-196.eu-west-3.compute.amazonaws.com:8080 -w "\n"
hello world
Hourra, notre web service est bien déployé. Vérifions maintenant que notre pipeline déploie comme il faut une modification du code. Modifions la réponse du controlleur REST:
package org.example;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MyRestController {
public static final String RESPONSE = "hello world 2";
@GetMapping("/")
public String get() {
return RESPONSE;
}
}
Poussons cette modification. La pipeline devrait toujours être verte.
Une fois le déploiement terminé, effectuons à nouveau notre requête curl
:
joseph@joseph-ThinkPad-T480 infra ±|main ✗|→ curl ubuntu@ec2-35-181-54-196.eu-west-3.compute.amazonaws.com:8080 -w "\n"
hello world 2
Parfait. Nous pouvons passer à la prochaine étape.
tag de départ: 2.5-deploy-java-application-as-service
tag d'arrivée: 2.6-staging-tests-post-deploy
Dans cette étape, nous allons rajouter une action au stage "Deploy" de notre pipeline, afin de lancer un test Cucumber
, vérifiant la réponse de l'endpoint REST.
Nous commençons par corriger la target delete-all
dans le Makefile
, à laquelle il manquait des étapes
Cucumber
Nous ajoutons une dépendance Maven
:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
Nous ajoutons la feature suivante dans ./src/test/resources/features/staging/test-staging.feature
:
Feature: API test
Scenario: API returns expected response
When we call the REST endpoint "/"
Then the REST response is as following:
| httpStatus | 200 |
| body | "hello world" |
Nous ajoutons la classe d'implémentation des steps,/src/test/java/org/example/CucumberStepDefinitionsStaging.java
:
package org.example;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@Slf4j
public class CucumberStepDefinitionsStaging {
private ResponseEntity<String> responseEntity;
@When("we call the REST endpoint {string}")
public void weCallTheRESTEndpoint(String endpoint) {
String restEndpointUrl = buildEndpointUrl();
log.info("calling url: \"{}\"",restEndpointUrl);
responseEntity = new RestTemplate().getForEntity(restEndpointUrl, String.class);
}
private String buildEndpointUrl() {
String restEndpointHostname = System.getenv("REST_ENDPOINT_HOSTNAME");
String restEndpointProtocol = System.getenv("REST_ENDPOINT_PROTOCOL");
String restEndpointPort = System.getenv("REST_ENDPOINT_PORT");
return restEndpointProtocol+"://"+restEndpointHostname+":"+restEndpointPort;
}
@Then("the REST response is as following:")
public void theRESTResponseIsAsFollowing(Map<String, String> expectedValues) {
assertThat(responseEntity.getStatusCodeValue()).isEqualTo(200);
assertThat(responseEntity.getBody()).isEqualTo(MyRestController.RESPONSE);
}
}
Le runner:
package org.example;
import io.cucumber.junit.Cucumber;
import io.cucumber.junit.CucumberOptions;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@SpringBootTest(webEnvironment = RANDOM_PORT)
@RunWith(Cucumber.class)
@CucumberOptions(
plugin = {
"pretty",
"junit:target/cucumber-reports/staging/cucumber-staging-results.xml",
"usage:target/cucumber-reports/staging/cucumber-staging-usage.json"},
glue = {"org.example"},
features = "src/test/resources/features/staging")
public class CucumberRunnerStaging {
}
Nous ajoutons l'export suivant dans le template de l'instance ec2, ./infra/execution-environment-cfn.yml
:
Outputs:
Ec2InstancePublicDnsName:
Value: !GetAtt
- Ec2Instance
- PublicDnsName
Export:
Name: Ec2InstancePublicDnsName
Nous ajoutons un rôle pour le projet CodeBuild
qui lancera les tests, en tant que ressource CloudFormation
, dans le fichier ./infra/pipeline-cfn.yml
:
StagingTestsRole:
Type: 'AWS::IAM::Role'
Description: IAM role for !Ref ApplicationName staging test (using CodeBuild)
Properties:
RoleName: !Join
- '-'
- - !Ref ApplicationName
- staging-test-role
Path: /
Policies:
- PolicyName: !Join
- '-'
- - !Ref ApplicationName
- staging-test-policy
PolicyDocument:
Statement:
- Effect: Allow
Action:
- s3:PutObject
- s3:GetObject
- s3:GetObjectVersion
- s3:GetBucketAcl
- s3:GetBucketLocation
Resource:
- !Sub 'arn:${AWS::Partition}:s3:::${S3Bucket}'
- !Sub 'arn:${AWS::Partition}:s3:::${S3Bucket}/*'
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/*'
- Effect: Allow
Action:
- codebuild:CreateReportGroup
- codebuild:CreateReport
- codebuild:UpdateReport
- codebuild:BatchPutTestCases
- codebuild:BatchPutCodeCoverages
Resource: !Sub 'arn:${AWS::Partition}:codebuild:${AWS::Region}:${AWS::AccountId}:report-group/${ApplicationName}-*'
AssumeRolePolicyDocument:
Statement:
- Action: "sts:AssumeRole"
Effect: Allow
Principal:
Service:
- codebuild.amazonaws.com
Nous ajoutons un rôle pour le projet CodeBuild
qui lancera les tests, en tant que ressource CloudFormation
, dans le fichier ./infra/pipeline-cfn.yml
. Il s'agit du même rôle que le projet CodeBuild
qui s'occupe strictement du build, avec les mêmes permissions. Pour le moment, on se contente de copier-coller, mais dans un prochain temps, on cherchera à factoriser les 2 ressources:
StagingTestsRole:
Type: 'AWS::IAM::Role'
Description: IAM role for !Ref ApplicationName staging test (using CodeBuild)
Properties:
RoleName: !Join
- '-'
- - !Ref ApplicationName
- staging-test-role
Path: /
Policies:
- PolicyName: !Join
- '-'
- - !Ref ApplicationName
- staging-test-policy
PolicyDocument:
Statement:
- Effect: Allow
Action:
- s3:PutObject
- s3:GetObject
- s3:GetObjectVersion
- s3:GetBucketAcl
- s3:GetBucketLocation
Resource:
- !Sub 'arn:${AWS::Partition}:s3:::${S3Bucket}'
- !Sub 'arn:${AWS::Partition}:s3:::${S3Bucket}/*'
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/*'
- Effect: Allow
Action:
- codebuild:CreateReportGroup
- codebuild:CreateReport
- codebuild:UpdateReport
- codebuild:BatchPutTestCases
- codebuild:BatchPutCodeCoverages
Resource: !Sub 'arn:${AWS::Partition}:codebuild:${AWS::Region}:${AWS::AccountId}:report-group/${ApplicationName}-*'
AssumeRolePolicyDocument:
Statement:
- Action: "sts:AssumeRole"
Effect: Allow
Principal:
Service:
- codebuild.amazonaws.com
On ajoute la ressource CodeBuild
, chargée d'éxécuter les tests:
StagingTest:
Type: AWS::CodeBuild::Project
Properties:
Name: !Join
- '-'
- - !Ref ApplicationName
- staging-test
Description: A build project for !Ref ApplicationName
ServiceRole: !Ref BuildProjectRole
Artifacts:
Type: CODEPIPELINE
Packaging: ZIP
Environment:
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0
EnvironmentVariables:
- Name: REST_ENDPOINT_HOSTNAME
Type: PLAINTEXT
# Value: "toto"
Value: !ImportValue Ec2InstancePublicDnsName
- Name: REST_ENDPOINT_PROTOCOL
Type: PLAINTEXT
Value: 'http'
- Name: REST_ENDPOINT_PORT
Type: PLAINTEXT
Value: '8080'
Cache:
Type: S3
Location: !Sub '${S3Bucket}/maven-cache'
Source:
Type: CODEPIPELINE
BuildSpec: |
version: 0.2
phases:
install:
runtime-versions:
java: corretto11
build:
commands:
- mvn test -Dtest=CucumberRunnerStaging
reports:
Report:
files:
- 'target/cucumber-reports/staging/cucumber-staging-results.xml'
cache:
paths:
- '/root/.m2/**/*'
Ici aussi, il s'agit de la même ressource que pour le build, mais l'on a changé le buildspec. On pourra factoriser cette duplication dans un second temps.
Enfin, toujours dans le template CloudFormation
, nous ajoutons l'action de test dans la pipeline et nous ajoutons un ordre entre les actions pour s'assurer que le test se déroule après le déploiement:
- Name: Deploy
Actions:
- Name: Deploy
RunOrder: 1
# [...]
- Name: StagingTest
RunOrder: 2
InputArtifacts:
- Name: SourceOutput
ActionTypeId:
Category: Build
Owner: AWS
Version: 1
Provider: CodeBuild
Configuration:
ProjectName:
Ref: StagingTest
Avec une visualisation du diff pour plus de clareté:
Enfin, on réordonne les sub-targets de la target all
du Makefile
, execution-environment
devant s'éxécuter avant, car le template utilisé dans la target pipeline
utilise une sortie CloudFormation
exportée par le template utilisé dans execution-environment
.
Mettons à jour notre infra:
cd infra #si vous n'y êtes pas deja
make all APPLICATION_NAME=my-app
Si jamais vous avec un problème,
cd infra #si vous n'y êtes pas deja
make delete-all APPLICATION_NAME=my-app # en pensant à rajouter le correctif pour supprimer TOUTES les targets
# puis activer la connexion github
Ensuite, poussez le code et relancez une release dans la pipeline si ce n'est pas fait automatiquement:
Celle-ci devrait être verte, ici un screenshot focalisé sur le stage Deploy
:
Allons regarder le projet CodeBuild
de ces tests:
Maintenant le rapport de tests:
Et la sortie du test:
Parfait ! Nous pouvons passer à la prochaine étape, à savoir: rajouter une instance EC2 que l'on définira comme notre "prod", et redéfinir notre instance existante comme notre "staging"
tag de départ: 2.6-staging-tests-post-deploy
tag d'arrivée: 2.7-add-prod-environment
Dans cette partie nous allons:
Nous allons utiliser le même template CloudFormation
, et le script shell associé pour créer à la fois notre environnement de "production", et l'environnement actuel que l'on va requalifier en "staging"
Tout d'abord, nous renommons execution-environment
en XX-environment
, où XX
vaut staging
ou production
.
Nous commençons donc par modifier le template CloudFormation
, renommé en ./infra/environment-cfn.yml
:
Parameters:
# [...]
Environment:
Type: String
Description: Environment (staging, prod, ...)
Resources:
Ec2Instance:
Type: AWS::EC2::Instance
# [...]
Properties:
Tags:
- Key: Name
Value: !Sub '${ApplicationName}-${Environment}-instance'
Ec2InstanceRole:
Properties:
RoleName: !Join
- '-'
- - !Ref ApplicationName
- !Sub '${Environment}-ec2-instance-role'
# [...]
Outputs:
Ec2InstancePublicDnsName:
Value: !GetAtt
- Ec2Instance
- PublicDnsName
Export:
Name: !Sub '${Environment}-Ec2InstancePublicDnsName'
Visualisons les différences:
On rajoute un paramètre Environment
On renomme ensuite le script ./infra/create-execution-environment.sh
en ./infra/create-environment.sh
.
Ensuite, on le modifie de la façon suivante:
#!/bin/bash
if [[ "$#" -ne 5 ]]; then
echo -e "usage:\n./create-execution-environment.sh \$APPLICATION_NAME \$STACK_NAME \$AMI_ID \$KEY_NAME \$ENVIRONMENT"
exit 1
fi
APPLICATION_NAME=$1
STACK_NAME=$2
AMI_ID=$3
KEY_NAME=$4
ENVIRONMENT=$5
echo -e "##############################################################################"
echo -e "creating environment \"$ENVIRONMENT\""
echo -e "##############################################################################"
aws cloudformation deploy \
--stack-name $STACK_NAME \
--template-file environment-cfn.yml \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides \
KeyName=$KEY_NAME \
AmiId=$AMI_ID \
ApplicationName=$APPLICATION_NAME \
Environment=$ENVIRONMENT
Le Makefile
devient:
SHELL := /bin/bash
ifndef APPLICATION_NAME
$(error APPLICATION_NAME is not set)
endif
include infra.env
PIPELINE_STACK_NAME=$(APPLICATION_NAME)-pipeline
STAGING_ENVIRONMENT_STACK_NAME=$(APPLICATION_NAME)-staging-environment
PRODUCTION_ENVIRONMENT_STACK_NAME=$(APPLICATION_NAME)-production-environment
all:
- $(MAKE) ami
- $(MAKE) staging-environment
- $(MAKE) production-environment
- $(MAKE) pipeline
pipeline:
./create-pipeline.sh $(APPLICATION_NAME) $(PIPELINE_STACK_NAME) $(GITHUB_REPO) $(GITHUB_REPO_BRANCH)
ami:
$(eval BASE_AMI_ID := $(shell aws ssm get-parameters --names /aws/service/canonical/ubuntu/server/20.04/stable/current/amd64/hvm/ebs-gp2/ami-id --query 'Parameters[0].[Value]' --output text))
$(eval AWS_REGION := $(shell aws configure get region))
./create-ami.sh $(APPLICATION_NAME) $(BASE_AMI_ID) $(AWS_REGION)
ssh-key-pair:
./create-ssh-key-pair.sh $(SSH_KEY_NAME) $(SSH_KEY_PATH)
staging-environment: ssh-key-pair
$(eval AMI_ID := $(shell aws ec2 describe-images --owners self --query "Images[?Name=='$(APPLICATION_NAME)'].ImageId" --output text))
./create-environment.sh $(APPLICATION_NAME) $(STAGING_ENVIRONMENT_STACK_NAME) $(AMI_ID) $(SSH_KEY_NAME) staging
production-environment: ssh-key-pair
$(eval AMI_ID := $(shell aws ec2 describe-images --owners self --query "Images[?Name=='$(APPLICATION_NAME)'].ImageId" --output text))
./create-environment.sh $(APPLICATION_NAME) $(PRODUCTION_ENVIRONMENT_STACK_NAME) $(AMI_ID) $(SSH_KEY_NAME) production
delete-all:
- $(MAKE) delete-pipeline
- $(MAKE) delete-staging-environment
- $(MAKE) delete-production-environment
- $(MAKE) delete-ami
delete-pipeline:
./delete-stack-wait-termination.sh $(PIPELINE_STACK_NAME)
delete-ami:
$(eval AMI_ID := $(shell aws ec2 describe-images --owners self --query "Images[?Name=='$(APPLICATION_NAME)'].ImageId" --output text))
aws ec2 deregister-image --image-id $(AMI_ID)
delete-ssh-key-pair:
aws ec2 delete-key-pair --key-name $(SSH_KEY_NAME)
delete-staging-environment: delete-ssh-key-pair
./delete-stack-wait-termination.sh $(STAGING_ENVIRONMENT_STACK_NAME)
delete-production-environment: delete-ssh-key-pair
./delete-stack-wait-termination.sh $(PRODUCTION_ENVIRONMENT_STACK_NAME)
Et voici le diff:
Au niveau du fichier ./infra/pipeline-cfn.yml
, nous rajoutons/modifions:
PipelineRole
, de manière à:
- Effect: Allow
Action:
- codebuild:BatchGetBuilds
- codebuild:StartBuild
- codebuild:BatchGetBuildBatches
- codebuild:StartBuildBatch
Resource:
- !GetAtt
- BuildProject
- Arn
- !GetAtt
- StagingTest
- Arn
- !GetAtt
- ProductionTest
- Arn
CodeDeployDeploymentGroup
, que l'on renomme en StagingDeploymentGroup
, et dont on modifie les tags des instances à cibler, de manière à ne déployer l'application que sur l'environnement qui nous intéresse:StagingDeploymentGroup:
Type: AWS::CodeDeploy::DeploymentGroup
Properties:
ApplicationName: !Ref CodeDeployApplication
ServiceRoleArn: !GetAtt
- CodeDeployRole
- Arn
DeploymentGroupName: !Sub '${ApplicationName}-staging-deployment-group'
DeploymentConfigName: CodeDeployDefault.OneAtATime
Ec2TagSet:
Ec2TagSetList:
- Ec2TagGroup:
- Key: Application
Value: !Ref ApplicationName
Type: KEY_AND_VALUE
- Ec2TagGroup:
- Key: Environment
Value: staging
Type: KEY_AND_VALUE
Regardons le diff:
pour (3), voir la documentation pour la configuration du tagging avec CodeDeploy
: https://docs.amazonaws.cn/en_us/codedeploy/latest/userguide/instances-tagging.html#instances-tagging-example-3
ProductionDeploymentGroup
pour déployer l'application en prod:ProductionDeploymentGroup:
Type: AWS::CodeDeploy::DeploymentGroup
Properties:
ApplicationName: !Ref CodeDeployApplication
ServiceRoleArn: !GetAtt
- CodeDeployRole
- Arn
DeploymentGroupName: !Sub '${ApplicationName}-production-deployment-group'
DeploymentConfigName: CodeDeployDefault.OneAtATime
Ec2TagSet:
Ec2TagSetList:
- Ec2TagGroup:
- Key: Application
Value: !Ref ApplicationName
Type: KEY_AND_VALUE
- Ec2TagGroup:
- Key: Environment
Value: production
Type: KEY_AND_VALUE
CodeBuild
qui va éxécuter les tests en production. Il s'agit d'un copier-coller du rôle dédié aux tests en staging. On éliminera cette duplication dans un prochain temps:ProductionTestsRole:
Type: 'AWS::IAM::Role'
Description: IAM role for !Ref ApplicationName staging test (using CodeBuild)
Properties:
RoleName: !Join
- '-'
- - !Ref ApplicationName
- production-test-role
Path: /
Policies:
- PolicyName: !Join
- '-'
- - !Ref ApplicationName
- production-test-policy
PolicyDocument:
Statement:
- Effect: Allow
Action:
- s3:PutObject
- s3:GetObject
- s3:GetObjectVersion
- s3:GetBucketAcl
- s3:GetBucketLocation
Resource:
- !Sub 'arn:${AWS::Partition}:s3:::${S3Bucket}'
- !Sub 'arn:${AWS::Partition}:s3:::${S3Bucket}/*'
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/*'
- Effect: Allow
Action:
- codebuild:CreateReportGroup
- codebuild:CreateReport
- codebuild:UpdateReport
- codebuild:BatchPutTestCases
- codebuild:BatchPutCodeCoverages
Resource: !Sub 'arn:${AWS::Partition}:codebuild:${AWS::Region}:${AWS::AccountId}:report-group/${ApplicationName}-*'
AssumeRolePolicyDocument:
Statement:
- Action: "sts:AssumeRole"
Effect: Allow
Principal:
Service:
- codebuild.amazonaws.com
CodeBuild
pour effectuer les tests sur l'environnement de prod:ProductionTest:
Type: AWS::CodeBuild::Project
Properties:
Name: !Join
- '-'
- - !Ref ApplicationName
- production-test
Description: A build project for !Ref ApplicationName
ServiceRole: !Ref BuildProjectRole
Artifacts:
Type: CODEPIPELINE
Packaging: ZIP
Environment:
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0
EnvironmentVariables:
- Name: REST_ENDPOINT_HOSTNAME
Type: PLAINTEXT
Value: !ImportValue 'production-Ec2InstancePublicDnsName'
- Name: REST_ENDPOINT_PROTOCOL
Type: PLAINTEXT
Value: 'http'
- Name: REST_ENDPOINT_PORT
Type: PLAINTEXT
Value: '8080'
Cache:
Type: S3
Location: !Sub '${S3Bucket}/maven-cache'
Source:
Type: CODEPIPELINE
BuildSpec: |
version: 0.2
phases:
install:
runtime-versions:
java: corretto11
build:
commands:
- mvn test -Dtest=CucumberRunnerStaging
reports:
Report:
files:
- 'target/cucumber-reports/staging/cucumber-staging-results.xml'
cache:
paths:
- '/root/.m2/**/*'
Deploy
en Staging
:Production
avec les actions suivantes:
- Name: Production
Actions:
- Name: ApproveDeployProd
RunOrder: 1
ActionTypeId:
Category: Approval
Owner: AWS
Version: 1
Provider: Manual
Configuration:
CustomData: "Perform all necessary manual tests and verifications on \"staging\" environment before approving."
- Name: Deploy
RunOrder: 2
InputArtifacts:
- Name: BuildOutput
ActionTypeId:
Category: Deploy
Owner: AWS
Version: 1
Provider: CodeDeploy
Configuration:
ApplicationName:
Ref: CodeDeployApplication
DeploymentGroupName:
Ref: ProductionDeploymentGroup
- Name: Test
RunOrder: 3
InputArtifacts:
- Name: SourceOutput
ActionTypeId:
Category: Build
Owner: AWS
Version: 1
Provider: CodeBuild
Configuration:
ProjectName:
Ref: ProductionTest
Nous avons modifié une valeur exportée par la stack de l'environnement de staging, qui est importée dans la stack de la pipeline. Avant de pouvoir mettre à jour la stack de notre pipeline, il va nous falloir:
!ImportValue 'staging-Ec2InstancePublicDnsName'
- Name: REST_ENDPOINT_HOSTNAME
Type: PLAINTEXT
Value: "toto"
Un fois cela fait, vous pouvez relancer un déploiement:
Vous pouvez aussi vous en convaincre avec un curl
.
On peut enfin jeter un oeil au rapport de test en production:
C'est tout pour cette partie, ce qui est deja pas mal. Rendez-vous dans la prochaine partie où nous remplacerons nos environnements par des groupes d'auto-scaling derrière un load balancer.
Nous introduirons aussi un VPC et des sous-réseaux publiques et privés. Jusqu'à présent nous tous les déploiements se faisaient dans le VPC par défaut, ce qui n'est pas mauvais pour commencer, mais pour autant que je sache, c'est un anti-pattern. Ne serait-ce que parce-que nos instances sont accessibles depuis l'extérieur
CodeDeploy
: https://docs.amazonaws.cn/en_us/codedeploy/latest/userguide/instances-tagging.html#instances-tagging-example-3