Sviluppo remoto su AWS: da Cloud9 a VS Code
20 Novembre 2024 - 2 min. read
Alessio Gandini
Cloud-native Development Line Manager
Dammi sei ore per abbattere un albero e spenderò le prime quattro ore per affilare l’ascia. Abraham Lincoln.
Lo confesso: sono pigro, talmente pigro che spendo la maggior parte del mio tempo ad automatizzare qualsiasi cosa, ne sanno qualcosa i miei colleghi. Spero solo di non finire mai in questo modo:
Quando si parla di deployment massivi, l'automazione è l'unica strategia di sopravvivenza nel mondo IT.
Un cliente ci ha chiesto aiuto per utilizzare la potenza computazionale inutilizzata di migliaia di server sparsi per tutta Italia. Ogni ufficio ha a disposizione un server con un hypervisor, quindi poter eseguire workload in modalità on-demand senza appesantire le operazioni di manutenzione e configurazione sarebbe la ciliegina sulla torta.
Dato che la parte computazionale sarà "on the edge" e la comunicazione di rete potrebbe non essere facile, abbiamo pensato di utilizzare AWS Greengrass per semplificare questi aspetti.
Nell'articolo di Mattia potete trovare già un esempio di come utilizzare Greengrass per fare il deploy di funzioni long-running e on-demand.
Siamo sicuri che la soluzione possa fare al caso nostro, ci manca però trovare un modo per evitare di configurare manualmente ogni singola virtual machine.
Per nostra fortuna, Greengrass Fleet provisioning è la soluzione che stiamo cercando: permette di fare deploy massivi di edge device, rendendo sicuro il processo di onboarding.
Per fare il provisioning automatico di una flotta di dispositivi dobbiamo per prima cosa trovare un meccanismo che permetta di autenticare la richiesta di onboard, altrimenti chiunque potrebbe aggiungere un dispositivo e, potenzialmente, causare danni.
Una volta fatta la prima autenticazione, al device viene assegnato un certificato "definitivo" che gli permetterrà di essere riconosciuto.
Esistono due meccanismi di autenticazione: claim e trusted users. Come vedremo, il loro utilizzo dipende dal caso d'uso.
Il provisioning mediante "trusted user" è solitamente utilizzato quando è richiesta l'interazione dell'utente, ad esempio per dispositivi wi-fi smart controllati da applicazioni mobili.
La richiesta è fatta con un utente per conto del dispositivo. Il certificato definitivo, una volta ottenuto, dovrà essere caricato sul device.
Il provisioning utilizzando claim fa in modo che sia direttamente il dispositivo ad effettuare la richiesta, utilizzando per l'autenticazione certificati "base" già installati. Questo scenario non richiede interazione da parte dell'utente, per cui i deploy massivi sono il caso d'uso perfetto.
Eureka! Abbiamo la nostra soluzione: useremo AWS IoT Greengrass Core con il plugin per il fleet provisioning.
Per prima cosa approfondiamo i passi necessari. Per usare il provisioning con claim occorre:
Siccome siamo pigri, useremo CloudFormation e script per automatizzare il più possibile. Come bonus, creeremo una virtual machine di riferimento, installando tutti i componenti che andremo ad usare.
Per prima cosa, creiamo uno script per creare e salvare su SecretsManager i certificati per il provisioning. In questo modo, riusciremo ad automatizzare il loro deploy sulla macchina virtuale
#!/bin/bash
ENV=${1}
SECRETSMANAGER_PREFIX="MyAwesomeProjectSecret"
THINGGROUP_NAME="MyThingGroup"
# Create the certificate
CERTIFICATE_ARN=$(aws iot create-keys-and-certificate \
--certificate-pem-outfile "claim-certs/claim.pem.crt" \
--public-key-outfile "claim-certs/claim.public.pem.key" \
--private-key-outfile "claim-certs/claim.private.pem.key" \
--set-as-active | jq -r .certificateArn)
SECRET_STRING=$(echo "$(cat claim-certs/claim.pem.crt)|$(cat claim-certs/claim.private.pem.key)" | tr '\n' ';' )
aws secretsmanager create-secret --name $SECRETSMANAGER_PREFIX/$ENV/claims-certificate --secret-string $SECRET_STRING
# Attach the policy to the certificate
aws iot attach-policy --policy-name GreengrassProvisioningClaimPolicy --target $CERTIFICATE_ARN
aws iot create-thing-group --thing-group-name $THINGGROUP_NAME
Questo script va usato prima di fare il deploy del template CloudFormation che segue, che si occuperà di creare il grouppo, le policy da associare con i certificati ed i ruoli necessari.
---
AWSTemplateFormatVersion: '2010-09-09'
Description: GreenGrass with Fleet Manager provisioning template
Metadata:
'AWS::CloudFormation::Interface':
ParameterGroups:
- Label: {default: 'Required parameters'}
Parameters:
- Env
- ThingGroupName
- Label: {default: 'Optional parameters'}
Parameters:
- ProjectName
Parameters:
Env:
Type: String
Description: "Insert the environment"
ProjectName:
Type: String
Description: "Insert the name of the project"
ThingGroupName:
Type: String
Description: "Insert the Thing Group name"
Resources:
GreengrassTokenExchangeRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: 'Allow'
Principal:
Service:
- 'credentials.iot.amazonaws.com'
Action:
- 'sts:AssumeRole'
- Effect: 'Allow'
Policies:
- PolicyName: 'LogPersmission'
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: 'Allow'
Action:
- "logs:CreateLogGroup"
- "logs:CreateLogStream"
- "logs:PutLogEvents"
- "logs:DescribeLogStreams"
- "s3:GetBucketLocation"
- "iam:PassRole"
Resource: '*'
RoleName: !Sub '${ProjectName}-${Env}-V2TokenExchangeRole'
GreengrassTokenExchangeRoleAlias:
Type: AWS::IoT::RoleAlias
Properties:
RoleAlias: !Sub '${ProjectName}-${Env}-CoreTokenExchangeRoleAlias'
RoleArn: !GetAtt GreengrassTokenExchangeRole.Arn
Tags:
- Key: Name
Value: !Sub "${ProjectName}-${Env}-CoreTokenExchangeRoleAlias"
GreengrassV2IoTThingPolicy:
Type: AWS::IoT::Policy
Properties:
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: 'Allow'
Action:
- "iot:Publish"
- "iot:Subscribe"
- "iot:Receive"
- "iot:Connect"
- "greengrass:*"
Resource: "*"
- Effect: 'Allow'
Action:
- "iot:AssumeRoleWithCertificate"
Resource: !GetAtt GreengrassTokenExchangeRoleAlias.RoleAliasArn
PolicyName: V2IoTThingPolicy
GreengrassFleetProvisioningRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: 'Allow'
Principal:
Service:
- 'iot.amazonaws.com'
Action:
- 'sts:AssumeRole'
Policies:
- PolicyName: 'ECRAccess'
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: 'Allow'
Action:
- "logs:CreateLogGroup"
- "logs:CreateLogStream"
- "logs:PutLogEvents"
- "logs:DescribeLogStreams"
- "s3:GetBucketLocation"
Resource: '*'
RoleName: !Sub '${ProjectName}-${Env}-FleetProvisioningRole'
GreengrassFleetProvisioningTemplate:
Type: AWS::IoT::ProvisioningTemplate
Properties:
Description: "template to provision iot fleet resources"
Enabled: true
ProvisioningRoleArn: !GetAtt GreengrassFleetProvisioningRole.Arn
TemplateBody: |
{
"Parameters": {
"ThingName": {
"Type": "String"
},
"ThingGroupName": {
"Type": "String"
},
"AWS::IoT::Certificate::Id": {
"Type": "String"
}
},
"Resources": {
"MyThing": {
"OverrideSettings": {
"AttributePayload": "REPLACE",
"ThingGroups": "REPLACE",
"ThingTypeName": "REPLACE"
},
"Properties": {
"AttributePayload": {},
"ThingGroups": [
{
"Ref": "ThingGroupName"
}
],
"ThingName": {
"Ref": "ThingName"
}
},
"Type": "AWS::IoT::Thing"
},
"MyPolicy": {
"Properties": {
"PolicyName": "V2IoTThingPolicy"
},
"Type": "AWS::IoT::Policy"
},
"MyCertificate": {
"Properties": {
"CertificateId": {
"Ref": "AWS::IoT::Certificate::Id"
},
"Status": "Active"
},
"Type": "AWS::IoT::Certificate"
}
}
}
TemplateName: !Sub "${ProjectName}-${Env}-FleetTemplate"
GreengrassProvisioningClaimPolicy:
Type: AWS::IoT::Policy
Properties:
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: 'Allow'
Action:
- "iot:Connect"
Resource: "*"
- Effect: 'Allow'
Action:
- "iot:Subscribe"
Resource:
- !Sub "arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topicfilter/$aws/certificates/create/*"
- !Sub "arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topicfilter/$aws/provisioning-templates/${GreengrassFleetProvisioningTemplate}/provision/*"
- Effect: 'Allow'
Action:
- "iot:Publish"
- "iot:Receive"
Resource:
- !Sub "arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topic/$aws/certificates/create/*"
- !Sub "arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topic/$aws/provisioning-templates/${GreengrassFleetProvisioningTemplate}/provision/*"
PolicyName: GreengrassProvisioningClaimPolicy
La risorsa
GreengrassFleetProvisioningTemplate
farà in modo che il nostro IoT device si registrerà usando le proprietà GroupName e ThingName. Le proprietà sono personalizzabili a seconda delle necessità. In seguito vedremo che "ThingName" sarà popolato con il valore dell'hostname della macchina.
Per ricevere una notifica ed eseguire azioni in modo automatico quando un dispositivo si registra, è possibile aggiungere una regola come quella che segue, aggiungendo anche un topic SNS (non includeremo il codice completo per brevità):
ThingCreationIotRule:
Type: AWS::IoT::TopicRule
Properties:
RuleName: !Sub '${ProjectName}_${Env}_greengrass_rule'
TopicRulePayload:
RuleDisabled: 'false'
Sql: SELECT * FROM '$aws/events/thing/#'
Actions:
- Lambda:
FunctionArn: !GetAtt RegisterThingLambda.Arn
ErrorAction:
Sns:
TargetArn: !Ref SNSFailedNotificationTopic
RoleArn: !GetAtt NotificationRole.Arn
È finalmente arrivata l'ora di creare una macchina virtuale. Useremo Ubuntu 22.04 per la base, va benissimo usare VirtualBox o Vmware.
Lo script che segue si occupa di installare il software e ottenere i certificati da usare per il claim provisioning. Per usarlo occorre ottenere credenziali temporanee IAM come variabili di ambiente o, ancora meglio, integrare tutto in un template packer.
#!/bin/bash
ENV=${1}
SECRETSMANAGER_PREFIX="MyAwesomeProjectSecret"
mkdir -p claim-certs
SECRET_STRING=$(aws secretsmanager get-secret-value --secret-id $SECRETSMANAGER_PREFIX/$ENV/claims-certificate --query SecretString --output text)
echo $SECRET_STRING | cut -d '|' -f 1 | tr ';' '\n' > claim-certs/claim.pem.crt
echo $SECRET_STRING | cut -d '|' -f 2 | tr ';' '\n' > claim-certs/claim.private.pem.key
export DEBIAN_FRONTEND=noninteractive
apt update && apt upgrade -y
apt -yq install python3-pip zip open-vm-tools default-jdk libgl1-mesa-glx
mkdir -p /greengrass/v2/installer
mkdir /greengrass/v2/claim-certs
mv claim-certs/claim.private.pem.key /greengrass/v2/claim-certs/
mv /claim-certs/claim.pem.crt /greengrass/v2/claim-certs/
cd /greengrass/v2
curl -o /greengrass/v2/AmazonRootCA1.pem https://www.amazontrust.com/repository/AmazonRootCA1.pem
curl -s https://d2s8p88vqu9w66.cloudfront.net/releases/greengrass-nucleus-latest.zip > greengrass-nucleus-latest.zip
unzip greengrass-nucleus-latest.zip -d installer && rm greengrass-nucleus-latest.zip
curl -s https://d2s8p88vqu9w66.cloudfront.net/releases/aws-greengrass-FleetProvisioningByClaim/fleetprovisioningbyclaim-latest.jar > installer/aws.greengrass.FleetProvisioningByClaim.jar
Tutti i componenti necessari sono ora installati. Ora manca l'ultimo tocco del sysadmin pigro: vogliamo qualcosa che, una volta avviata, si configuri in autonomia senza dover intervenire manualmente.
Faremo in modo che il campo "ThingName" sia popolato automaticamente con l'hostname grazie ad una unit systemd ed uno script.
Basta creare un file config.yml in
greengrass/v2/installer/
specificando che useremo il plugin IoT fleet provisioning.
---
services:
aws.greengrass.Nucleus:
version: "2.9.1"
aws.greengrass.FleetProvisioningByClaim:
configuration:
rootPath: "/greengrass/v2"
awsRegion: "eu-west-1"
iotDataEndpoint: "endpoint.iot.region.amazonaws.com"
iotCredentialEndpoint: "endpoint.credentials.iot.region.amazonaws.com"
iotRoleAlias: "myproject-environment-CoreTokenExchangeRoleAlias"
provisioningTemplate: "myproject-environment-FleetTemplate"
claimCertificatePath: "/greengrass/v2/claim-certs/claim.pem.crt"
claimCertificatePrivateKeyPath: "/greengrass/v2/claim-certs/claim.private.pem.key"
rootCaPath: "/greengrass/v2/AmazonRootCA1.pem"
templateParameters:
ThingName: "REPLACEME"
ThingGroupName: "MyThingGroup"
Questo file va adattato con i valori corretti. Il valore di
iotDataEndpoint
e
iotCredentialEndpoint
con questi comandi:
aws iot describe-endpoint --endpoint-type iot:Data-ATS
aws iot describe-endpoint --endpoint-type iot:CredentialProvider
Il valore di ThingName è impostato a "REPLACEME" Lo script
(/greengrass/configure-device.sh)
si occuperà di sostituire il valore e un servizio systemd sarà eseguito al primo avvio.
#!/bin/bash
set -e
sed -i s/REPLACEME/$(hostname)/g /greengrass/v2/installer/config.yaml
echo "********************** RUN SERVICE **************************"
java -Droot="/greengrass/v2" -Dlog.store=FILE \
-jar /greengrass/v2/installer/lib/Greengrass.jar \
--trusted-plugin /greengrass/v2/installer/aws.greengrass.FleetProvisioningByClaim.jar \
--init-config /greengrass/v2/installer/config.yaml \
--component-default-user ggc_user:ggc_group \
--setup-system-service true
La prima riga (sed -i s/REPLACEME/$(hostname)/g /greengrass/v2/installer/config.yaml) sostituisce il valore con l'hostname.
Ora basta creare una unit nella directory /lib/systemd/system con un nome parlante (abbiamo usato greengrass-config.service)
[Unit]
Before=systemd-user-sessions.service
Wants=network-online.target
After=network-online.target
ConditionPathExists=!/greengrass/greengrass_installed
[Service]
Type=oneshot
ExecStart=/greengrass/configure-device.sh
ExecStartPost=/usr/bin/touch /greengrass/greengrass_installed
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
Si tratta di una unit systemd quasi standard, con due piccole particolarità:
La riga
ConditionPathExists=!/greengrass/greengrass_installed
fa si che systemd esegua il comando solo se il file non esiste
ExecStartPost=/usr/bin/touch /greengrass/greengrass_installed
fa si che il file sia creato. In questop modo il serivizo sarà eseguito solo una volta.
Non ci resta che attivare ed abilitare il tutto con:
systemctl daemon-reload
systemctl enable greengrass-config.service
Finita la configurazione, basta esportare l'immagine della macchina virtuale in un formato compatibile con l'hypervisor (OVA o template VMware) ed iniziare a fare il deploy massivo!
Un ultimo appunto: questa è una configurazione di base, è necessario fare in modo che sia aggiornata ed ottimizzata (ad esempio si può ridurre la dimensione del disco disinstallando e disabilitando servizi non essenziali)
Al termine di questo percorso possiamo finalmente eseguire long-running functions usando le risorse inutilizzabili at the edge.
È possibile integrare altri servizi AWS usando questa soluzione come base: i job SageMaker sono un buon esempio (e magari uno spunto che approfondiremo in futuro).
Abbiamo visto come l'automazione ci può aiutare liberandoci dal dover eseguire compiti noiosi, anche se, a prima vista, può sembrare complesso.
Vi è mai capitato di farvi prendere dalla frenesia dell'automazione per troppa pigrizia?
Fatecelo sapere nei commenti!
Ci vediamo tra 14 giorni su #Proud2beCloud
Proud2beCloud è il blog di beSharp, APN Premier Consulting Partner italiano esperto nella progettazione, implementazione e gestione di infrastrutture Cloud complesse e servizi AWS avanzati. Prima di essere scrittori, siamo Solutions Architect che, dal 2007, lavorano quotidianamente con i servizi AWS. Siamo innovatori alla costante ricerca della soluzione più all'avanguardia per noi e per i nostri clienti. Su Proud2beCloud condividiamo regolarmente i nostri migliori spunti con chi come noi, per lavoro o per passione, lavora con il Cloud di AWS. Partecipa alla discussione!