{"id":5411,"date":"2023-02-01T18:26:38","date_gmt":"2023-02-01T17:26:38","guid":{"rendered":"https:\/\/blog.besharp.it\/?p=5411"},"modified":"2023-02-01T18:26:55","modified_gmt":"2023-02-01T17:26:55","slug":"come-fare-il-setup-di-migliaia-di-dispositivi-su-aws-greengrass-e-vivere-felici","status":"publish","type":"post","link":"https:\/\/blog.besharp.it\/it\/come-fare-il-setup-di-migliaia-di-dispositivi-su-aws-greengrass-e-vivere-felici\/","title":{"rendered":"Come fare il setup di migliaia di dispositivi su AWS Greengrass e vivere felici"},"content":{"rendered":"\n
Dammi sei ore per abbattere un albero e spender\u00f2 le prime quattro ore per affilare l\u2019ascia. <\/em>Abraham Lincoln.<\/strong><\/p>\n\n\n\n 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:<\/p>\n\n\n\n <\/p>\n\n\n <\/p>\n\n\n\n Quando si parla di deployment massivi, l’automazione \u00e8 l’unica strategia di sopravvivenza nel mondo IT.<\/p>\n\n\n\n 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\u00e0 on-demand senza appesantire le operazioni di manutenzione e configurazione sarebbe la ciliegina sulla torta. <\/p>\n\n\n\n Dato che la parte computazionale sar\u00e0 “on the edge” e la comunicazione di rete potrebbe non essere facile, abbiamo pensato di utilizzare AWS Greengrass<\/strong> per semplificare questi aspetti. <\/p>\n\n\n\n Nell’articolo di Mattia<\/a> potete trovare gi\u00e0 un esempio di come utilizzare Greengrass per fare il deploy di funzioni long-running e on-demand<\/a>. <\/p>\n\n\n\n Siamo sicuri che la soluzione possa fare al caso nostro, ci manca per\u00f2 trovare un modo per evitare di configurare manualmente ogni singola virtual machine. <\/p>\n\n\n\n Per nostra fortuna, Greengrass Fleet provisioning \u00e8 la soluzione che stiamo cercando: permette di fare deploy massivi di edge device, rendendo sicuro il processo di onboarding. <\/p>\n\n\n\n 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. <\/p>\n\n\n\n Una volta fatta la prima autenticazione, al device viene assegnato un certificato “definitivo” che gli permetterr\u00e0 di essere riconosciuto.<\/p>\n\n\n\n Esistono due meccanismi di autenticazione: claim<\/strong> e trusted users<\/strong>. Come vedremo, il loro utilizzo dipende dal caso d’uso. <\/p>\n\n\n\n Il provisioning mediante “trusted user” \u00e8 solitamente utilizzato quando \u00e8 richiesta l’interazione dell’utente, ad esempio per dispositivi wi-fi smart controllati da applicazioni mobili. <\/p>\n\n\n\n La richiesta \u00e8 fatta con un utente per conto del dispositivo. Il certificato definitivo, una volta ottenuto, dovr\u00e0 essere caricato sul device.<\/p>\n\n\n\n Il provisioning utilizzando claim fa in modo che sia direttamente il dispositivo ad effettuare la richiesta, utilizzando per l’autenticazione certificati “base” gi\u00e0 installati. Questo scenario non richiede interazione da parte dell’utente, per cui i deploy massivi sono il caso d’uso perfetto. <\/p>\n\n\n\n Eureka! Abbiamo la nostra soluzione: useremo AWS IoT Greengrass Core con il plugin per il fleet provisioning<\/strong>.<\/p>\n\n\n\n Per prima cosa approfondiamo i passi necessari. Per usare il provisioning con claim occorre:<\/p>\n\n\n\n Siccome siamo pigri, useremo CloudFormation e script per automatizzare il pi\u00f9 possibile. Come bonus, creeremo una virtual machine di riferimento, installando tutti i componenti che andremo ad usare.<\/p>\n\n\n\n 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<\/p>\n\n\n\n Questo script va usato prima di fare il deploy del template CloudFormation che segue, che si occuper\u00e0 di creare il grouppo, le policy da associare con i certificati ed i ruoli necessari.<\/p>\n\n\n\n La risorsa <\/p>\n\n\n\n far\u00e0 in modo che il nostro IoT device si registrer\u00e0 usando le propriet\u00e0 GroupName<\/strong> e ThingName<\/strong>. Le propriet\u00e0 sono personalizzabili a seconda delle necessit\u00e0. In seguito vedremo che “ThingName” sar\u00e0 popolato con il valore dell’hostname della macchina. <\/p>\n\n\n\n Per ricevere una notifica ed eseguire azioni in modo automatico quando un dispositivo si registra, \u00e8 possibile aggiungere una regola come quella che segue, aggiungendo anche un topic SNS (non includeremo il codice completo per brevit\u00e0):<\/p>\n\n\n\n \u00c8 finalmente arrivata l’ora di creare una macchina virtuale. Useremo Ubuntu 22.04 per la base, va benissimo usare VirtualBox o Vmware.<\/p>\n\n\n\n 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.<\/p>\n\n\n\n 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. <\/p>\n\n\n\n Faremo in modo che il campo “ThingName” sia popolato automaticamente con l’hostname grazie ad una unit systemd ed uno script. specificando che useremo il plugin IoT fleet provisioning.<\/p>\n\n\n\n Questo file va adattato con i valori corretti. Il valore di <\/p>\n\n\n\n e <\/p>\n\n\n\n con questi comandi:<\/p>\n\n\n\n Il valore di ThingName \u00e8 impostato a “REPLACEME<\/strong>” Lo script <\/p>\n\n\n\n si occuper\u00e0 di sostituire il valore e un servizio systemd sar\u00e0 eseguito al primo avvio.<\/p>\n\n\n\n La prima riga (sed -i s\/REPLACEME\/$(hostname)\/g \/greengrass\/v2\/installer\/config.yaml) sostituisce il valore con l’hostname.<\/p>\n\n\n\n Ora basta creare una unit nella directory \/lib\/systemd\/system con un nome parlante (abbiamo usato greengrass-config.service)<\/p>\n\n\n\n Si tratta di una unit systemd quasi standard, con due piccole particolarit\u00e0:<\/p>\n\n\n\n La riga <\/p>\n\n\n\n fa si che systemd esegua il comando solo se il file non esiste<\/p>\n\n\n\n fa si che il file sia creato. In questop modo il serivizo sar\u00e0 eseguito solo una volta. <\/p>\n\n\n\n Non ci resta che attivare ed abilitare il tutto con:<\/p>\n\n\n\n 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! <\/p>\n\n\n\n Un ultimo appunto: questa \u00e8 una configurazione di base, \u00e8 necessario fare in modo che sia aggiornata ed ottimizzata (ad esempio si pu\u00f2 ridurre la dimensione del disco disinstallando e disabilitando servizi non essenziali)<\/p>\n\n\n\n Al termine di questo percorso possiamo finalmente eseguire long-running functions usando le risorse inutilizzabili at the edge.<\/p>\n\n\n\n \u00c8 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). <\/p>\n\n\n\n Abbiamo visto come l’automazione ci pu\u00f2 aiutare liberandoci dal dover eseguire compiti noiosi, anche se, a prima vista, pu\u00f2 sembrare complesso. <\/p>\n\n\n\n Vi \u00e8 mai capitato di farvi prendere dalla frenesia dell’automazione per troppa pigrizia? <\/p>\n\n\n\n Fatecelo sapere nei commenti!<\/p>\n\n\n\n\n
#!\/bin\/bash\nENV=${1}\n\n\nSECRETSMANAGER_PREFIX=\"MyAwesomeProjectSecret\"\nTHINGGROUP_NAME=\"MyThingGroup\"\n\n# Create the certificate\nCERTIFICATE_ARN=$(aws iot create-keys-and-certificate \\\n \t--certificate-pem-outfile \"claim-certs\/claim.pem.crt\" \\\n \t--public-key-outfile \"claim-certs\/claim.public.pem.key\" \\\n \t--private-key-outfile \"claim-certs\/claim.private.pem.key\" \\\n \t--set-as-active | jq -r .certificateArn)\n\nSECRET_STRING=$(echo \"$(cat claim-certs\/claim.pem.crt)|$(cat claim-certs\/claim.private.pem.key)\" | tr '\\n' ';' )\naws secretsmanager create-secret --name $SECRETSMANAGER_PREFIX\/$ENV\/claims-certificate --secret-string $SECRET_STRING\n\n# Attach the policy to the certificate\naws iot attach-policy --policy-name GreengrassProvisioningClaimPolicy --target $CERTIFICATE_ARN\naws iot create-thing-group --thing-group-name $THINGGROUP_NAME<\/code><\/pre>\n\n\n\n
---\nAWSTemplateFormatVersion: '2010-09-09'\nDescription: GreenGrass with Fleet Manager provisioning template\n\nMetadata:\n\n 'AWS::CloudFormation::Interface':\n\tParameterGroups:\n \t- Label: {default: 'Required parameters'}\n \tParameters:\n \t- Env\n \t- ThingGroupName\n \t- Label: {default: 'Optional parameters'}\n \tParameters:\n \t- ProjectName\n\nParameters:\n\n Env:\n\tType: String\n\tDescription: \"Insert the environment\"\n\n ProjectName:\n\tType: String\n\tDescription: \"Insert the name of the project\"\n\n ThingGroupName:\n\tType: String\n\tDescription: \"Insert the Thing Group name\"\n\nResources:\n\n GreengrassTokenExchangeRole:\n\tType: AWS::IAM::Role\n\tProperties:\n \tAssumeRolePolicyDocument:\n \tVersion: '2012-10-17'\n \tStatement:\n \t- Effect: 'Allow'\n \tPrincipal:\n \tService:\n \t- 'credentials.iot.amazonaws.com'\n \tAction:\n \t- 'sts:AssumeRole'\n \t- Effect: 'Allow'\n \tPolicies:\n \t- PolicyName: 'LogPersmission'\n \tPolicyDocument:\n \tVersion: '2012-10-17'\n \tStatement:\n \t- Effect: 'Allow'\n \tAction:\n \t- \"logs:CreateLogGroup\"\n \t- \"logs:CreateLogStream\"\n \t- \"logs:PutLogEvents\"\n \t- \"logs:DescribeLogStreams\"\n \t- \"s3:GetBucketLocation\"\n \t- \"iam:PassRole\"\n \tResource: '*'\n \t \tRoleName: !Sub '${ProjectName}-${Env}-V2TokenExchangeRole'\n\n GreengrassTokenExchangeRoleAlias:\n\tType: AWS::IoT::RoleAlias\n\tProperties:\n \tRoleAlias: !Sub '${ProjectName}-${Env}-CoreTokenExchangeRoleAlias'\n \tRoleArn: !GetAtt GreengrassTokenExchangeRole.Arn\n \tTags:\n \t- Key: Name\n \tValue: !Sub \"${ProjectName}-${Env}-CoreTokenExchangeRoleAlias\"\n\n GreengrassV2IoTThingPolicy:\n\tType: AWS::IoT::Policy\n\tProperties:\n \tPolicyDocument:\n \tVersion: '2012-10-17'\n \tStatement:\n \t- Effect: 'Allow'\n \tAction:\n \t- \"iot:Publish\"\n \t- \"iot:Subscribe\"\n \t- \"iot:Receive\"\n \t- \"iot:Connect\"\n \t- \"greengrass:*\"\n \tResource: \"*\"\n \t- Effect: 'Allow'\n \tAction:\n \t- \"iot:AssumeRoleWithCertificate\"\n \tResource: !GetAtt GreengrassTokenExchangeRoleAlias.RoleAliasArn\n \tPolicyName: V2IoTThingPolicy\n\n GreengrassFleetProvisioningRole:\n\tType: AWS::IAM::Role\n\tProperties:\n \tAssumeRolePolicyDocument:\n \tVersion: '2012-10-17'\n \tStatement:\n \t- Effect: 'Allow'\n \tPrincipal:\n \tService:\n \t- 'iot.amazonaws.com'\n \tAction:\n \t- 'sts:AssumeRole'\n \tPolicies:\n \t- PolicyName: 'ECRAccess'\n \tPolicyDocument:\n \tVersion: '2012-10-17'\n \tStatement:\n \t- Effect: 'Allow'\n \tAction:\n \t- \"logs:CreateLogGroup\"\n \t- \"logs:CreateLogStream\"\n \t- \"logs:PutLogEvents\"\n \t- \"logs:DescribeLogStreams\"\n \t- \"s3:GetBucketLocation\"\n \tResource: '*'\n \tRoleName: !Sub '${ProjectName}-${Env}-FleetProvisioningRole'\n\n GreengrassFleetProvisioningTemplate:\n\tType: AWS::IoT::ProvisioningTemplate\n\tProperties:\n \tDescription: \"template to provision iot fleet resources\"\n \tEnabled: true\n \tProvisioningRoleArn: !GetAtt GreengrassFleetProvisioningRole.Arn\n \tTemplateBody: |\n \t{\n \t\"Parameters\": {\n \t\"ThingName\": {\n \t\"Type\": \"String\"\n \t},\n \t\"ThingGroupName\": {\n \t\"Type\": \"String\"\n \t},\n \t\"AWS::IoT::Certificate::Id\": {\n \t\"Type\": \"String\"\n \t}\n \t},\n \t\"Resources\": {\n \t\"MyThing\": {\n \t\"OverrideSettings\": {\n \t\"AttributePayload\": \"REPLACE\",\n \t\"ThingGroups\": \"REPLACE\",\n \t\"ThingTypeName\": \"REPLACE\"\n \t},\n \t\"Properties\": {\n \t\"AttributePayload\": {},\n \t\"ThingGroups\": [\n \t{\n \t\"Ref\": \"ThingGroupName\"\n \t}\n \t],\n \t\"ThingName\": {\n \t\"Ref\": \"ThingName\"\n \t}\n \t},\n \t\"Type\": \"AWS::IoT::Thing\"\n \t},\n \t\"MyPolicy\": {\n \t\"Properties\": {\n \t\"PolicyName\": \"V2IoTThingPolicy\"\n \t},\n \t\"Type\": \"AWS::IoT::Policy\"\n \t},\n \t\"MyCertificate\": {\n \t\"Properties\": {\n \t\"CertificateId\": {\n \t\"Ref\": \"AWS::IoT::Certificate::Id\"\n \t},\n \t\"Status\": \"Active\"\n \t},\n \t\"Type\": \"AWS::IoT::Certificate\"\n \t}\n \t}\n \t}\n \tTemplateName: !Sub \"${ProjectName}-${Env}-FleetTemplate\"\n\n GreengrassProvisioningClaimPolicy:\n\tType: AWS::IoT::Policy\n\tProperties:\n \tPolicyDocument:\n \tVersion: '2012-10-17'\n \tStatement:\n \t- Effect: 'Allow'\n \tAction:\n \t- \"iot:Connect\"\n \tResource: \"*\"\n \t- Effect: 'Allow'\n \tAction:\n \t- \"iot:Subscribe\"\n \tResource:\n \t- !Sub \"arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topicfilter\/$aws\/certificates\/create\/*\"\n \t- !Sub \"arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topicfilter\/$aws\/provisioning-templates\/${GreengrassFleetProvisioningTemplate}\/provision\/*\"\n \t- Effect: 'Allow'\n \tAction:\n \t- \"iot:Publish\"\n \t- \"iot:Receive\"\n \tResource:\n \t- !Sub \"arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topic\/$aws\/certificates\/create\/*\"\n \t- !Sub \"arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topic\/$aws\/provisioning-templates\/${GreengrassFleetProvisioningTemplate}\/provision\/*\"\n \tPolicyName: GreengrassProvisioningClaimPolicy<\/code><\/pre>\n\n\n\n
GreengrassFleetProvisioningTemplate <\/code><\/pre>\n\n\n\n
ThingCreationIotRule:\n\tType: AWS::IoT::TopicRule\n\tProperties:\n \tRuleName: !Sub '${ProjectName}_${Env}_greengrass_rule'\n \tTopicRulePayload:\n \tRuleDisabled: 'false'\n \tSql: SELECT * FROM '$aws\/events\/thing\/#'\n \tActions:\n \t- Lambda:\n \tFunctionArn: !GetAtt RegisterThingLambda.Arn\n \tErrorAction:\n \tSns:\n \tTargetArn: !Ref SNSFailedNotificationTopic\n \tRoleArn: !GetAtt NotificationRole.Arn<\/code><\/pre>\n\n\n\n
#!\/bin\/bash\n\nENV=${1}\n\nSECRETSMANAGER_PREFIX=\"MyAwesomeProjectSecret\"\n\nmkdir -p claim-certs\nSECRET_STRING=$(aws secretsmanager get-secret-value --secret-id $SECRETSMANAGER_PREFIX\/$ENV\/claims-certificate --query SecretString --output text)\n\necho $SECRET_STRING | cut -d '|' -f 1 | tr ';' '\\n' > claim-certs\/claim.pem.crt\necho $SECRET_STRING | cut -d '|' -f 2 | tr ';' '\\n' > claim-certs\/claim.private.pem.key\n\n\nexport DEBIAN_FRONTEND=noninteractive\napt update && apt upgrade -y\napt -yq install python3-pip zip open-vm-tools default-jdk libgl1-mesa-glx\n\n\nmkdir -p \/greengrass\/v2\/installer\nmkdir \/greengrass\/v2\/claim-certs\n\n\nmv claim-certs\/claim.private.pem.key \/greengrass\/v2\/claim-certs\/\nmv \/claim-certs\/claim.pem.crt \/greengrass\/v2\/claim-certs\/\n\n\n\ncd \/greengrass\/v2\ncurl -o \/greengrass\/v2\/AmazonRootCA1.pem https:\/\/www.amazontrust.com\/repository\/AmazonRootCA1.pem\ncurl -s https:\/\/d2s8p88vqu9w66.cloudfront.net\/releases\/greengrass-nucleus-latest.zip > greengrass-nucleus-latest.zip\nunzip greengrass-nucleus-latest.zip -d installer && rm greengrass-nucleus-latest.zip\ncurl -s https:\/\/d2s8p88vqu9w66.cloudfront.net\/releases\/aws-greengrass-FleetProvisioningByClaim\/fleetprovisioningbyclaim-latest.jar > installer\/aws.greengrass.FleetProvisioningByClaim.jar<\/code><\/pre>\n\n\n\n
Basta creare un file config.yml in <\/p>\n\n\n\ngreengrass\/v2\/installer\/ <\/code><\/pre>\n\n\n\n
---\nservices:\n aws.greengrass.Nucleus:\n\tversion: \"2.9.1\"\n aws.greengrass.FleetProvisioningByClaim:\n\tconfiguration:\n \trootPath: \"\/greengrass\/v2\"\n \tawsRegion: \"eu-west-1\"\n \tiotDataEndpoint: \"endpoint.iot.region.amazonaws.com\"\n \tiotCredentialEndpoint: \"endpoint.credentials.iot.region.amazonaws.com\"\n \tiotRoleAlias: \"myproject-environment-CoreTokenExchangeRoleAlias\"\n \tprovisioningTemplate: \"myproject-environment-FleetTemplate\"\n \tclaimCertificatePath: \"\/greengrass\/v2\/claim-certs\/claim.pem.crt\"\n \tclaimCertificatePrivateKeyPath: \"\/greengrass\/v2\/claim-certs\/claim.private.pem.key\"\n \trootCaPath: \"\/greengrass\/v2\/AmazonRootCA1.pem\"\n \ttemplateParameters:\n \tThingName: \"REPLACEME\"\n \tThingGroupName: \"MyThingGroup\"<\/code><\/pre>\n\n\n\n
iotDataEndpoint<\/code><\/pre>\n\n\n\n
iotCredentialEndpoint <\/code><\/pre>\n\n\n\n
aws iot describe-endpoint --endpoint-type iot:Data-ATS\naws iot describe-endpoint --endpoint-type iot:CredentialProvider<\/code><\/pre>\n\n\n\n
(\/greengrass\/configure-device.sh)<\/code><\/pre>\n\n\n\n
#!\/bin\/bash\n\nset -e\n\nsed -i s\/REPLACEME\/$(hostname)\/g \/greengrass\/v2\/installer\/config.yaml\necho \"********************** RUN SERVICE **************************\"\njava -Droot=\"\/greengrass\/v2\" -Dlog.store=FILE \\\n-jar \/greengrass\/v2\/installer\/lib\/Greengrass.jar \\\n--trusted-plugin \/greengrass\/v2\/installer\/aws.greengrass.FleetProvisioningByClaim.jar \\\n--init-config \/greengrass\/v2\/installer\/config.yaml \\\n--component-default-user ggc_user:ggc_group \\\n--setup-system-service true<\/code><\/pre>\n\n\n\n
[Unit]\nBefore=systemd-user-sessions.service\nWants=network-online.target\nAfter=network-online.target\nConditionPathExists=!\/greengrass\/greengrass_installed\n\n[Service]\nType=oneshot\nExecStart=\/greengrass\/configure-device.sh\nExecStartPost=\/usr\/bin\/touch \/greengrass\/greengrass_installed\nRemainAfterExit=yes\n\n[Install]\nWantedBy=multi-user.target<\/code><\/pre>\n\n\n\n
ConditionPathExists=!\/greengrass\/greengrass_installed <\/code><\/pre>\n\n\n\n
ExecStartPost=\/usr\/bin\/touch \/greengrass\/greengrass_installed <\/code><\/pre>\n\n\n\n
systemctl daemon-reload\nsystemctl enable greengrass-config.service<\/code><\/pre>\n\n\n\n