Use Python Lambdas para executar remotamente comandos shell em instâncias do EC2

Jan 11 2023
Recentemente, trabalhei em um arquivo Terraform para Kubernetes e deparei com os limites do Terraform ao tentar atualizar um arquivo com os endereços IP dos nós no cluster. Essa etapa foi difícil de resolver com o Terraform porque esses endereços são criados somente depois que você cria os recursos e não há como inseri-los no script de inicialização no modelo do Terraform com antecedência.

Recentemente, trabalhei em um arquivo Terraform para Kubernetes e deparei com os limites do Terraform ao tentar atualizar um arquivo com os endereços IP dos nós no cluster. Essa etapa foi difícil de resolver com o Terraform porque esses endereços são criados somente depois que você cria os recursos e não há como inseri-los no script de inicialização no modelo do Terraform com antecedência.

Ao pesquisar soluções, vi o próprio Mitchell Hashimoto dizer explicitamente que o Terraform não é para gerenciar o tempo de execução:

Link

Então é isso. Você deve usar Consul ou algo como Ansible ao lidar com alterações em tempo de execução.

….mas então tive uma ideia terrível: como posso fazer um Lambda fazer isso? Depois de pesquisar e ruminar, encontrei uma maneira de alterar os nós já em execução usando a biblioteca Python da AWS, boto3, para enviar comandos shell. E porque eu amo Lambdas, gostaria de compartilhar minha pesquisa sobre isso com você hoje.

Para ser claro: você ainda deve usar ferramentas como Ansible ou Consul para realizar tarefas de tempo de execução, como bootstrapping. O que este projeto faz mostra a imensa flexibilidade do Python Lambdas e o poder que eles fornecem ao configurar a infraestrutura. Se você realmente quiser, pode usar Lambdas para quase tudo.

Visão geral

  • Parte 1: Passo a passo do script Python
  • Parte 2: Implantando o projeto com o Terraform
  • Parte 3: Compreendendo as permissões necessárias para o SSM
  • Parte 4: Testando o script no Lambda
  • Cloud 9 IDE com credenciais de administrador da AWS — criaremos políticas de IAM, trabalharemos com o Systems Manager e usaremos o Python para interagir com a AWS via boto3. O Cloud 9 também tem o Terraform pré-instalado, facilitando este passo a passo.
  • Familiaridade com Python e Boto3 — Estou operando com a suposição de que não preciso explicar os loops, listas ou bibliotecas neste passo a passo.
  • Entendimento do nível iniciante do Terraform — fornecerei os modelos e você deve saber como executá-los.

O objetivo deste script é pesquisar nossas instâncias do EC2 e executar um shell script naquelas que possuem uma tag (que determinaremos no script) e estão no estado “running”.

Vamos examinar isso primeiro no IDE. Sinta-se à vontade para acessar python3o terminal Cloud 9 e acompanhar no shell interativo.

Bem no topo do script, definiremos o script a ser executado e a tag:

#Import boto3 library
import boto3

#Define the contents of your shell script
script = """
echo "Hello World!" > /home/ec2-user/helloworld.txt
pwd >> /home/ec2-user/helloworld.txt
"""
#Define the tag possessed by the EC2 instances that we want to execute the script on
tag='Test'

#Define ec2 and ssm clients
ec2_client = boto3.client("ec2", region_name='us-east-1')
ssm_client = boto3.client('ssm')

#Gather of instances with tag defined earlier
filtered_instances = ec2_client.describe_instances(Filters=[{'Name': 'tag:Name', 'Values': [tag]}])

#Reservations in the filtered_instances
reservations = filtered_instances['Reservations']

      
                
Note: If you don’t have any instances with the matching tag, ‘Instances’ may be empty.

As instâncias estão dentro da chave do dicionário chamada “Reservas”. Uma “reserva” está associada a um determinado tipo de instância e zona de disponibilidade e pode abranger uma ou mais instâncias. Ele é criado automaticamente quando as Instâncias são lançadas. Acedemos a essa lista de “Reservas” através da reservationsvariável que definimos.

Em terceiro lugar, vamos iterar reservationse extrair as instâncias 'em execução':

#Create a an empty list for instances to execute the shell script within
exec_list=[]

#Iterate through all the instances within the collected resaervations
#Append 'running' instances to exec list, ignoring 'stopped' and 'terminated' ones'
for reservation in reservations:
    for instance in reservation['Instances']:
        print(instance['InstanceId'], " is ", instance['State']['Name'])
        if instance['State']['Name'] == 'running':
            exec_list.append(instance['InstanceId'])
    #divider between reservations
    print("**************")

      
                

Para cada instância, se o estado da instância for 'running', o ID da instância será anexado à exec_listlista.

Em quarto lugar, vamos usar o boto3 ssm_clientdefinido anteriormente para executar o script de shell nas instâncias do EC2 no exec_list:

#Run shell script
response = ssm_client.send_command(
    DocumentName ='AWS-RunShellScript',
    Parameters = {'commands': [script]},
    InstanceIds = exec_list
)

#See the command run on the target instance Ids
print(response['Command']['Parameters']['commands'])

      
                

Se eu fizer SSH na instância que já configurei com as permissões corretas, podemos ver o resultado:

Este é o resultado esperado do que colocamos na scriptvariável no topo deste script Python:

script = """
echo "Hello World!" > /home/ec2-user/helloworld.txt
pwd >> /home/ec2-user/helloworld.txt
"""

Parte 2: Implantando o projeto com o Terraform

Entrei em detalhes sobre como configurar um Lambda no Terraform em meu artigo Create a Python Lambda to Save On Your Cloud Bill with Terraform . Eu recomendo que você dê uma olhada se quiser se aprofundar na compreensão de como configurar o Lambda no Terraform. Presumo que você já tenha configurado um modelo ou dois antes para este post.

Vamos clonar meu repositório e implantá-lo usando nosso Cloud 9 CLI:

#Clone and Enter the Repo
git clone https://github.com/nickcmiller/lambda-shell-execution.git
cd lambda-shell-execution

#Initialize Terraform
terraform init

#Apply
terraform apply --auto-approve

Sinta-se à vontade para bisbilhotar se quiser ver

Caso você queira acessar a instância do EC2 via SSH, criei o script de conexão que você pode utilizar na forma de um Output:

O test_key_pair.pemestá no diretório raiz do arquivo Terraform (você pode vê-lo na parte inferior da captura de tela do diretório). Como sempre, não se esqueça chmod 400antes de usá-lo.

Agora vamos discutir como configurar permissões que permitem que o Lambda acesse instâncias.

Parte 3: Compreendendo as permissões necessárias para o SSM

Para permitir que nosso Lambda implante um script de shell no EC2, precisamos habilitar o SSM para registrar os agentes do EC2. Para fazer isso, criei uma função ec2-role-for-SSMque você verá se clicar em sua Testinstância.

Esta função utiliza uma das políticas IAM gerenciadas da Amazon ( AmazonEC2RoleforSSM ):

Esta política fornece acesso total a mensagens SSM e acesso de lista, leitura e gravação ao gerenciador de sistemas:

A Política de Papel, encontrada em Relacionamentos de Confiança , permite que ela seja assumida pelos serviços do EC2:

Se entrarmos na instância dos dados do usuário…

Veremos que estamos garantindo que a instância do EC2 tenha o agente instalado, iniciado e habilitado para futuras reinicializações:

Agora, vá até o console do Lambda e selecione nosso Lambda execute-shell-script :

Como você verá em Configuração, ele também recebeu permissões SSM:

Clique no nome da função e você verá que esta é uma política personalizada:

Suas permissões permitem acesso de lista e gravação ao EC2, bem como acesso de lista, leitura e gravação ao SSM :

De volta à função em relações de confiança , podemos ver que esta função é designada para o Lambda:

Se você não tiver cada uma dessas peças configuradas corretamente, provavelmente receberá um erro dizendo algo parecido com An error occurred (InvalidInstanceId) when calling the SendCommand operation: Instances [[i-0xxxxxxxxxxxxxx]] not in a valid state for account 2xxxxxxxxxx, o que não é um erro construtivo.

Se você ficar preso, também descobri que é útil acessar suas instâncias do EC2 e, depois de verificar se o SSM está instalado e em execução, verifico os logs com este comando sudo cat /var/log/amazon/ssm/amazon-ssm-agent.log. Se seu agente SSM estiver hibernando, certifique-se de que suas funções e políticas estejam configuradas corretamente.

Parte 4: Testando o script no Lambda

Agora vamos testar isso!

Para o meu evento de teste , deixei o JSON genérico, pois não estamos utilizando os dados do evento para este script:

Ao executá-lo, vejo que minha instância de Teste está rodando e o comando foi enviado via SSM:

Se fizermos SSH na instância de teste, veremos que foi bem-sucedido:

Essa é uma boa demonstração. Mas é apenas um arquivo de texto.

E se mudássemos a scriptvariável para algo assim?

script = """
#!/bin/bash
sudo yum update -y
sudo amazon-linux-extras install nginx1 -y
sudo systemctl enable nginx
sudo systemctl start nginx
"""

Agora isso é legal!

Embrulhar

Quando combinamos boto3 e Lambdas, podemos fazer com que nossa infraestrutura AWS faça quase tudo o que quisermos com pouca sobrecarga. E como vimos, quando usamos código para implementar nossa arquitetura, torna-se trivial fazer alterações, mesmo em tempo de execução.

Espero que este post tenha inspirado você a pensar mais sobre como usar Lambdas de forma criativa em seu ambiente.

Como sempre, por favor, deixe qualquer dúvida ou opinião nos comentários.