Skip to main content

Criando uma ferramenta automatizada de testes de infraestrutura em nuvem com Terraform e PyTest

Escrito por

27 de março de 2020

0 minutos de leitura

Recentemente, fui encarregado de criar uma ferramenta automatizada de testes para a Fugue. A Fugue monitora recursos na nuvem para verificar conformidade e segurança, e precisávamos de uma maneira de confirmar se os resultados completos de uma verificação da Fugue estavam corretos. Meu objetivo era criar um sistema automatizado que funcionasse localmente ou em CI, provisionasse uma infraestrutura configurável, fizesse a verificação com a Fugue e validasse os resultados. Esta publicação apresenta o processo de design e implementação do que se tornou o autotest, nossa ferramenta interna de testes automatizados.

Embora este exemplo use especificamente o Terraform e a API da Fugue, o design das fixtures se aplica a qualquer comando de CLI ou API. Assim, é fácil adaptar as ideias apresentadas aqui a outros casos de uso.

Design

Antes de começar o projeto de fato, passei alguns dias pesquisando e tentando entender o problema. Depois de fazer algumas buscas no Google, decidi basear minha ferramenta no framework pytest. Já havia trabalhado com pytest para testes unitários, mas essa era a oportunidade perfeita para explorá-lo a fundo e descobrir como usá-lo como base para um teste funcional.

O procedimento de teste pode ser dividido em três etapas:

  1. Usar o Terraform para provisionar uma infraestrutura modular

  2. Criar um ambiente da Fugue que aponte para essa infraestrutura e fazer a verificação

  3. Recuperar os resultados da verificação e validá-los

Eu sabia que queria usar o Terraform na etapa 1 porque já tínhamos várias configurações do Terraform para testes de infraestrutura e porque o Terraform oferece suporte à AWS, ao Azure e a muitos outros provedores de serviços em nuvem. Na etapa 2, eu poderia usar a API da Fugue para criar o ambiente e iniciar uma verificação. Por fim, na etapa 3, poderia usar a API da Fugue novamente para obter todos os resultados da verificação e validá-los.

Depois de definir o design em alto nível, pude começar a analisar o pytest para entender como usá-lo na implementação.

Diagrama do fluxo de trabalho que mostra a criação da infraestrutura com Terraform, a configuração do cliente da API do Fugue e do ambiente, a verificação do ID do recurso e a destruição do workspace.

PyTest

O pytest é um framework de testes para Python que oferece descoberta de testes, asserções com a instrução interna <c>assert</c> do Python e fixtures para gerenciamento de dependências.

As fixtures são o pytest em sua essência. São funções decoradas que gerenciam o ciclo de vida das dependências de teste. O código de configuração e limpeza, que tradicionalmente ficaria nos métodos setUp() e tearDown(), é colocado nas fixtures. É possível definir o escopo de uma fixture para reutilizar seus resultados entre classes, módulos ou sessões.

Vamos ver um exemplo. Aqui, defino e uso uma fixture para acessar uma API. Em um teste unitário, eu usaria um objeto de API simulado; em um teste funcional, preciso usar a API real.

@pytest.fixture(scope="session")
def api_client(creds):
  api_client = api.initialize_client(creds)
  return api_client

def test_api_client_username(api_client):
  assert api_client.username == "mike"

def test_api_client_host(api_client):
  assert api_client.host == "https://api.fugue.co"

A fixture api_client é declarada com o decorador @pytest.fixture. Posso definir o scope da fixture usando o argumento scope no decorador. O cliente da API pode ser reutilizado com segurança em todos os testes, por isso defini o escopo como session. As fixtures podem ser definidas no mesmo módulo das funções ou classes de teste ou colocadas em conftest.py, o que as disponibiliza para todos os módulos de teste.

Para usar a fixture, basta incluir o nome dela como argumento em uma função de teste. Neste exemplo, usei a fixture api_client em dois testes. Como defini o escopo da fixture como session, o pytest garante que ela seja chamada apenas uma vez durante cada sessão de testes.

Talvez você tenha percebido que a fixture api_client recebe um parâmetro de entrada, creds. Nunca chamamos a função fugue_api diretamente. Então, como creds recebe um valor?

Se você deduziu que creds também é uma fixture, ganhou uma estrela dourada: ⭐️. As fixtures podem chamar outras fixtures, permitindo criar dependências complexas a partir de uma cadeia de fixtures. Vamos ver a definição de creds:

def pytest_addoption(parser):
         parser.addoption(
             "--api-secret", action="store",
             help="name of credstash secret to use when 
initializing the API"
        )

@pytest.fixture(scope="session")
def creds(request):
  """returns API credentials"""
  secret_name = request.config.getoption("--api-secret")
  return credstash.getSecret(secret_name)

Como sigo boas práticas de segurança, creds usa o credstash para armazenar e recuperar valores secretos. O nome do segredo é passado como opção de linha de comando para o pytest. Essas opções são definidas em pytest_addoption e acessadas pela fixture request. A fixture creds facilita abstrair a implementação da obtenção das credenciais e concentrar meu raciocínio nas entradas necessárias para determinado teste ou fixture. Se eu mudar a forma de acessar as credenciais mais tarde, só precisarei modificar creds.

Agora que entendemos os conceitos básicos das fixtures do pytest, vamos ver como usá-las para implementar nosso design.

Etapa 1: usando o Terraform para provisionar a infraestrutura

A primeira etapa da nossa ferramenta cria a infraestrutura que será verificada e servirá de base para a validação nas etapas 2 e 3. Sei que vou usar o Terraform para provisioná-la, mas como isso vai funcionar exatamente?

Design

├── aws
| ├── api_gateway
│ │ ├── aws_api_gateway.tf
│ │ ├── aws_api_gateway_client_certificate.tf
│ ├── autoscaling
│ │ ├── aws_autoscaling_group.tf
│ │ ├── aws_autoscaling_lifecycle_hook.tf
│ │ ├── ...

Este trecho mostra a estrutura das configurações de teste que já tínhamos. É possível provisionar essas configurações manualmente executando terraform apply a partir da raiz aws/ ou de um subdiretório de serviço, como aws/api_gateway. Quero preservar essa modularidade para podermos escolher quais serviços testar. Eu poderia chamar o Terraform na estrutura existente, mas isso seria uma má prática: todo o trabalho com o sistema de arquivos deve ser feito em diretórios temporários exclusivos, para que as execuções de teste ocorram em ambientes controlados. Portanto, preciso de uma forma de gerenciar espaços de trabalho temporários. Também preciso chamar o Terraform, registrar a saída e verificar se houve erros. Como o Terraform vai criar uma infraestrutura real (e cara), é essencial tratar os erros e destruir a infraestrutura se algo der errado.

Em resumo, os requisitos para a primeira etapa são:

  1. Criar e gerenciar espaços de trabalho temporários

  2. Copiar as configurações de teste para um espaço de trabalho

  3. Chamar o Terraform e tratar erros, destruindo a infraestrutura que já tiver sido criada

Está claro que 1 e 2 podem ser tratados em uma fixture. Mas será que uma tarefa tão complexa e sujeita a erros quanto criar infraestrutura com o Terraform também pode ser feita em uma fixture?

Se você respondeu SIM, ganhou outra estrela dourada: ⭐️. Vamos analisar melhor como definir nossas fixtures de espaço de trabalho e Terraform.

A fixture Workspace

@pytest.yield_fixture(scope="class")
def workspace(tmpdir_factory, provider, services):
    """returns an initialized workspace that closes after 
it's finished"""
    working_dir = tmpdir_factory.mktemp("autotest_tf")

    with Workspace(working_dir, provider, services) as w: 
         yield w
class Workspace:
    def __init__(self, base_dir: str, provider: str, services: List[str]) -> 
 'Workspace':
          self.provider = provider
          self.base_dir = base_dir
          self.working_dir = os.path.join(base_dir, provider)
          self.services = services

    def __enter__(self):
       return self._initialize_workspace(self.base_dir, self.provider, self.services)

    def __exit__(self, typ, value, traceback):
        pass

Há muita coisa acontecendo nas seis linhas de código de workspace. Primeiro, observe que workspace é definida como uma yield_fixture. Isso significa que a função devolve o controle ao chamador por meio de yield, em vez de return. Enquanto isso, a execução da fixture fica suspensa e os gerenciadores de contexto permanecem abertos. Quando o controle volta à fixture, o código após a instrução yield é executado. Neste caso, o gerenciador de contexto Workspace é encerrado. O pytest trata da recuperação do valor retornado por uma yield_fixture, então podemos usá-las como fixtures comuns. workspace tem escopo de classe, por isso um novo espaço de trabalho é criado para cada classe de teste. Assim, posso separar os testes de AWS e Azure por classe e garantir que seja criado um novo espaço de trabalho para cada provedor.

Em seguida, vemos que workspace depende de três fixtures: tmpdir_factory, provider e services. tmpdir_factory é uma fixture integrada que retorna um diretório temporário exclusivo. O diretório-base pode ser definido manualmente com a opção --basetemp=DIRECTORY, o que é útil para depuração. provider e services são fixtures que defini. Elas retornam o provedor usado no teste atual e a lista de serviços fornecida como entrada.

Um dos meus aspectos favoritos das fixtures é que elas incentivam abstrações claras. As entradas de um teste ou fixture são definidas por uma interface — a definição da função — enquanto as implementações ficam ocultas na definição da fixture. A modularidade das fixtures permite criar com rapidez e facilidade uma fixture separada para cada entrada de workspace, evitando a tentação de criar os valores dentro de workspace.

Por fim, observe que Workspace é uma classe gerenciadora de contexto. Isso encerra automaticamente o espaço de trabalho quando a própria fixture é finalizada. Como mencionei antes, a instrução yield não finaliza o gerenciador de contexto até que o controle volte à fixture durante sua finalização. Usei esse padrão poderoso em todo o design do autotest. A cópia das configurações do Terraform que quero usar para o espaço de trabalho é feita em initialize_workspace, tarefa deixada como exercício para o leitor.

A fixture do Terraform

@pytest.fixture(scope="class") 
def terraform(workspace): 
     """initialize Terraform in workspace""" 
     return initialize_terraform(workspace.working_dir) 

@pytest.yield_fixture(scope="class") 
def infra(request, terraform, services):
    """returns a Terraform instance associated with created resources""" 
    with build_infra(terraform, services): 
         yield terraform

@pytest.yield_fixture(scope="class") 
def terraform_resources(infra): 
    """returns the Terraform resources map""" 
    yield infra.tfstate.modules[0]['resources']

Na verdade, a fixture do Terraform é composta por três fixtures: terraform, infra e terraform_resources. O resultado dessa cadeia de fixtures é um dicionário de recursos do Terraform lidos do arquivo de estado do Terraform. terraform inicializa o Terraform em um espaço de trabalho, infra executa terraform plan e terraform apply para criar a infraestrutura, e terraform_resources extrai o mapa de recursos.

A inicialização e a extração do mapa de recursos são simples, mas vale observar que terraform_resources precisa ser uma yield_fixture, porque infra é uma yield_fixture. Se terraform_resources retornasse um valor, fecharia o contexto mantido aberto por infra e acionaria o código de desmontagem.

Em vez de usar uma classe gerenciadora de contexto, infra usa um gerenciador de contexto baseado em função, build_infra, definido a seguir:

 @contextmanager
 def build_infra(t: Terraform, services: List[str]):
    try:
       code, out, err = t.plan(out='temp.plan')
       if code == 1:
             raise TerraformPlanError(out, err)
       code, out, err = t.apply(dir_or_plan='temp.plan')
       if code == 1:
            raise TerraformApplyError(out, err)
    except TerraformApplyError:
         code, out, err = t.destroy()
         if code != 0:
             raise TerraformDestroyError(out, err)
         raise

 try:
    yield t
 finally:
    code, out, err = t.destroy()
    if code != 0:
        raise TerraformDestroyError(out, err)

build_infra usa o pacote python-terraform para chamar o Terraform e coletar sua saída. Verificamos o código de erro em todas as operações e lançamos exceções personalizadas se ocorrer algum erro. t.plan e t.apply chamam terraform plan e terraform apply, respectivamente, para planejar e criar a infraestrutura. terraform apply pode falhar depois de criar parte da infraestrutura — o terraform não destrói automaticamente os recursos que já foram criados. Por isso, capturamos TerraformApplyError e chamamos t.destroy manualmente antes de relançar a exceção original.

build_infra é um gerador: chama yield em vez de return, o que me permite usar o decorador @contextmanager para transformá-lo em um gerenciador de contexto. O bloco try...finally em torno de yield garante que o código de desmontagem seja executado mesmo quando exceções são lançadas no contexto chamador.

Por fim, observe a semelhança entre o código nos blocos except e finally. Os dois blocos fazem a mesma coisa: garantem que a infraestrutura seja destruída, aconteça o que acontecer durante a execução. Como o código em um bloco finally é executado tanto quando ocorre uma exceção quanto quando não ocorre, podemos consolidar os blocos try.

Além disso, sabemos que t.plan não cria recursos, então podemos movê-lo para fora do bloco try. Em geral, é uma boa prática manter os blocos try com o menor escopo possível. O gerenciador de contexto build_infra final fica assim:

 @contextmanager
 def build_infra(t: Terraform, services: List[str]):
    code, out, err = t.plan(out='temp.plan')
    if code == 1:
       raise TerraformPlanError(out, err)
    try:
        code, out, err = t.apply(dir_or_plan='temp.plan')
        if code == 1:
            raise TerraformApplyError(out, err)
        yield t
    finally:
         code, out, err = t.destroy()
         if code != 0:
              raise TerraformDestroyError(out, err)

Etapa 2: criando um ambiente da Fugue e fazendo a verificação

A próxima etapa do nosso design em alto nível é usar a API da Fugue para criar e verificar um ambiente na conta em que a infraestrutura foi criada na etapa 1.

Design

A API da Fugue é uma API REST definida com Swagger. Isso facilita gerar um cliente que permita acessar a API por meio de funções e tipos nativos, em vez de chamadas HTTP. Com um cliente funcional, posso usá-lo para criar um ambiente e fazer a verificação. Posso dividir o processo em três etapas:

  1. Gerar e configurar um cliente para a API da Fugue

  2. Usar o cliente para criar um ambiente

  3. Usar o cliente para iniciar uma verificação e aguardar sua conclusão

O cliente da API Swagger

Usei o swagger-codegen para gerar um cliente com base na definição da API da Fugue:

swagger-codegen generate -i swagger.yaml -l python -o .

Isso gerou um pacote Python completo para o cliente da API, que não era necessário. Então, copiei o código do cliente diretamente para autotest. Em seguida, inicializei o cliente como mostrado abaixo:

 def initialize_client(creds: dict) - ApiClient:
    c = Configuration()
    c.host = os.environ["FUGUE_API_URL"] + os.environ.get("FUGUE_API_VERSION", "v0")
    c.username = creds["username"]
    c.password = creds["password"]

    client = ApiClient(configuration=c)
    return client

Aqui, creds é apenas um parâmetro de entrada, não uma fixture, porque initialize_client não é uma fixture nem um teste. Vale lembrar que initialize_client é chamado por uma fixture que depende da fixture creds:

 @pytest.fixture(scope="session")
 def api_client(creds):
     """returns a Fugue API client"""
     return api.initialize_client(creds)
@pytest.fixture(scope="session")
def environments_api(api_client):
     """returns a Fugue Environment API instance"""
     return api.environments_api(api_client)
 @pytest.fixture(scope="session")
 def scans_api(api_client):
     """returns a Fugue Scans API instance"""
     return api.scans_api(api_client)

Definir a fixture da API de ambientes é simples, já que os clientes da API não precisam ser finalizados. O cliente gerado pelo Swagger tem um cliente de API base, que gerencia a conexão com a API, e classes separadas para cada divisão de nível superior da API. As ações da API ficam disponíveis como métodos em cada classe.

Uma observação sobre abstrações: abstraio os detalhes do cliente Swagger por meio do módulo api. Isso está de acordo com o princípio de abstrair os dois lados de uma interface e me dá flexibilidade para contornar possíveis mudanças na interface do cliente. Vou explorar isso com mais detalhes ao falar sobre a criação de um ambiente usando a API.

Criar e examinar um ambiente

Minha primeira versão de uma fixture para ambientes era mais ou menos assim:

 @pytest.yield_fixture(scope="class")
  def environment(environments_api, aws_creds,
 survey_resource_types, services):
     environment = 
 environments_api.create_environment(CreateEnvironmentInput
 (
          name="autotest-123",
          provider="aws",
          provider_options=ProviderOptions(
              aws=ProviderOptionsAws(
                  region="us-east-2",
                  role_arn=aws_creds
              )
         ),
        survey_resource_types=survey_resource_types,
        compliance_families=["FBP"]
    ))
   yield environment
   environments_api.delete_environment(environment.id)

Esta é uma aplicação simples do método create_environment da API de ambientes. Obtenho credenciais e parâmetros de entrada das fixtures aws_creds, survey_resource_types e services. A fixture tem escopo de classe, pois organizo por classe os testes para diferentes provedores de nuvem. Por fim, uso yield para disponibilizar o ambiente e excluí-lo automaticamente quando o teste terminar.

O design desta fixture é simples e atende ao que preciso. Eu poderia deixá-la como está e seguir em frente. No entanto, essa abordagem simples tem desvantagens. A entrada de create_environment é complicada porque inclui muitos campos, alguns dos quais dependem do provedor. Usar a API de ambientes diretamente na fixture limita minha flexibilidade para preparar os parâmetros de entrada de create_environment sem criar uma função enorme e difícil de ler. Tentar equilibrar legibilidade e reutilização foi tão difícil que acabei abrindo mão da reutilização, codificando o provedor diretamente e duplicando a fixture environment para ambientes Azure — o que não foi uma boa solução. Na verdade, preciso de outra camada entre o código da fixture e a API de ambientes, que encapsule a preparação inconveniente dos dados de entrada e gerencie a relação entre a fixture e a API. Felizmente, já tenho exatamente isso no módulo api.

Separei a preparação dos dados de entrada e a chamada da API em uma função no módulo api:

 # api.py
 def create_environment(
     environments_api: EnvironmentsApi,
     provider: str,
     creds,
     services: List[str],
     compliance_families: List[str] = None,
     survey_resource_types: List[str] = None,
     survey_resource_groups: List[str] = None
 ) -> Environment:
   if provider == "aws":
       provider_options = ProviderOptions(
           aws=ProviderOptionsAws(
               region="us-east-2",
               role_arn=creds
           )
       )
   elif provider == "azure":
      provider_options = ProviderOptions(
          azure=ProviderOptionsAzure(
              tenant_id=creds["ARM_TENANT_ID"],
              subscription_id=creds["ARM_SUBSCRIPTION_ID"],
              application_id=creds["ARM_CLIENT_ID"],
              client_secret=creds["ARM_CLIENT_SECRET"],
              survey_resource_groups=survey_resource_groups,
           )
     )
     survey_resource_types = []
  else:
      raise AttributeError("invalid provider")

 environments_api.create_environment(CreateEnvironmentInput( 
      name=f"autotest-{provider}", 
      provider=provider, 
      provider_options=provider_options, 
      survey_resource_types=survey_resource_types, 
      compliance_families=compliance_families 
  ))  
  return environment 

Embora seja grande, essa função dá conta tanto da AWS quanto do Azure e permite reduzir a fixture environment ao essencial:

 @pytest.yield_fixture(scope="class")
 def environment(
     environments_api,
     provider: str,
     aws_creds: str,
     azure_creds: dict,
     survey_resource_types: List[str],
     survey_resource_groups: List[str],
     services: List[str],
 ):

     """creates a Fugue Environment and deletes it automatically"""
     if provider == "aws":
         creds = aws_creds
         compliance_families = ["FBP"]
     elif provider == "azure":
         creds = azure_creds
         compliance_families = ["CISAZURE"]

     environment = api.create_environment(
         environments_api,
         provider,
         creds,
         services,
         compliance_families,
         survey_resource_types,
         survey_resource_groups
  ) 
  yield environment  
  api.delete_environment(environments_api, environment)

Posso usar a mesma abordagem para iniciar verificações em um ambiente:

 @pytest.fixture(scope="class")
 def scan
    terraform_resources: dict, #terraform resources must exist before scan
    environment: Environment,
    scans_api: ScansApi
 )-> Scan:
    """creates a Fugue Scan and waits for it to finish"""
    return api.create_scan(
         scans_api,
         environment
  ) 

Incluir a fixture terraform_resources, mesmo sem usar o resultado, garante que os recursos tenham sido criados antes de eu iniciar uma verificação. Depois, no módulo api:

  def create_scan(scans_api: ScansApi, environment: Environment) -> Scan:
      scan: Scan = scans_api.create_scan(environment.id)

      while scan.status not in ["ERROR", "SUCCESS", "CANCELLED"]:
          time.sleep(1)
          scan = scans_api.get_scan(scan.id)
      if scan.status != "SUCCESS":
          print(scan.message)
      assert scan.status == "SUCCESS"

      return scan

Agora tenho fixtures para criar recursos com Terraform, criar um ambiente Fugue e fazer uma verificação nele. Isso significa que estou quase pronto para escrever alguns testes.

Etapa 3: recuperar e verificar os resultados da verificação

Antes de escrever os testes, preciso conseguir obter os resultados da verificação pela API da Fugue. Eles estão disponíveis na API pelo endpoint custom rules test input. Criar essa fixture é uma aplicação simples dos conceitos que usei acima:

 @pytest.fixture(scope="class")
 def scan_resources(scan: Scan, custom_rules_api: CustomRulesApi) -> dict:
      """returns the results from scan Scan"""
      return api.get_scan_resources(custom_rules_api, scan.id)

get_scan_resources é apenas uma chamada repassada:

 def get_scan_resources(custom_rules_api: CustomRulesApi, scan_id: str) -> dict:
     scan_resources: ScanResources = custom_rules_api.test_custom_rule_input(scan_id)
     return scan_resources.resources

Em funções simples como esta, dá vontade de pular a camada de abstração e fazer a chamada da API diretamente na fixture. Na verdade, foi isso que fiz na primeira versão desta função. No entanto, isso cria uma abstração com vazamento — uma camada de abstração que expõe parte da funcionalidade subjacente que deveria ocultar. Abstrações com vazamento acabam aumentando, em vez de diminuir, a carga cognitiva de quem as usa.

Agora que tenho uma fixture <scan_resources>, tenho todas as entradas de que preciso para o teste. Depois de toda a preparação e das abstrações discutidas nas seções anteriores, o teste em si tem apenas algumas linhas de código:

  # test_aws.py
  class TestAWS:
     def test_verify_resource_ids(self, terraform_resources, scan_resources, check):
          for tf_id, tf_resource in terraform_resources.items():
              check.is_true(verify.verify_resource_id(tf_id, tf_resource, scan_resources))

check é uma fixture do plugin pytest que oferece asserções que não interrompem a execução em caso de falha. verify_resource_id é uma função simples de verificação que compara dois recursos e retorna true se eles tiverem o mesmo ID de recurso:

  def verify_resource_id(tf_id: str, tf_resource: dict, scan_resources: dict) -> bool:
      """verify that a resource in the terraform state file is in the scan output"""
      # skip data sources
      if tf_id.startswith('data.'):
          return True
      print(f"verifying {tf_resource['type']} {tf_resource['primary']['id']}...", end="")
      for resource in scan_resources.values():
          if tf_resource['primary']['id'] == resource['_skeleton']['primary'] ['id']:
               print("✅")
               return True
       print("❌")
       return False 

Posso acessar todos os detalhes dos recursos se quiser escrever testes mais complexos, mas, por enquanto, basta provisionar a infraestrutura e verificar se ela foi examinada corretamente. A execução do teste gera esta saída:

 ) pytest autotest --services sns
 ============================ test session starts ============================
 platform darwin -- Python 3.6.5, pytest-5.3.5, py-1.7.0, pluggy-0.13.1
 plugins: cov-2.6.1, check-0.3.7
 collected 1 item / 1 selected

 autotest/test_aws.py building infrastructure in sns
 creating environment
 environment abcdef12-abcdef12-abcdef12-abcdef12-abcdef12 created
 creating scan
 scan abcdef12-abcdef12-abcdef12-abcdef12-abcdef12 created
 scan started...
 ...
 scan 588f8ded-c219-468b-821e-9b11ee06ef17 successful!
 verifying aws_default_security_group sg-abcdef12...✅
 verifying aws_default_vpc vpc-abcdef12...✅
 verifying aws_kms_key abcdef12-abcdef12-abcdef12-abcdef12-abcdef12...✅
 verifying aws_sns_topic arn:aws:sns:us-east-2:123456789012:example-topic...✅
 .
 deleting environment
 environment deleted
 tearing down infrastructure
 infrastructure successfully torn down

 =========== 1 passed in 280.80s (0:04:40) ===========

Conclusão

Esta publicação descreveu a criação do autotest, uma ferramenta de testes automatizados de infraestrutura, usando pytest. Falei sobre o poder das fixtures modulares do pytest, que facilitam a criação de abstrações claras e eficientes. Também expliquei como usar a API da Fugue para criar ambientes e verificações, além de encapsular essas ações em fixtures. Em seguida, mostrei como usar o endpoint de entrada de testes de regras personalizadas da API da Fugue para baixar uma descrição completa dos recursos em uma verificação. Por fim, reuni tudo em um teste.

Mais uma coisa…

A Fugue oferece segurança e conformidade para a nuvem. Criada por engenheiros para engenheiros. Com a Fugue, você pode:

  1. Ter visibilidade completa da infraestrutura na nuvem e da postura de segurança do seu ambiente com visualizações dinâmicas e relatórios de conformidade.

  2. Validar a conformidade em todas as etapas do ciclo de vida de desenvolvimento de software com CIS Foundations Benchmark, HIPAA, PCI, SOC 2, NIST 800-53, ISO 27001, GDPR e suas próprias políticas.

  3. Proteger sua infraestrutura na nuvem contra configurações incorretas com a aplicação de configurações de referência, tornando a infraestrutura crítica de segurança autorreparável.

Segurança de IaC pensada para quem desenvolve

A Snyk protege sua infraestrutura como código do ciclo de vida do desenvolvimento de software até a execução na nuvem, com um mecanismo unificado de políticas como código para que todas as equipes possam desenvolver, implantar e operar com segurança.

Leia mais

feature customer snowflake
Article

Segurança desde o início: conheça a integração do Snyk Studio com o Snowflake Cortex Code

O Snyk Studio se integra ao Snowflake Cortex Code para detectar vulnerabilidades em código gerado por IA, dependências e contêineres durante o desenvolvimento.

Article

Ataque à cadeia de suprimentos Miasma: código malicioso encontrado em pacotes npm de @redhat-cloud-services

Um worm da cadeia de suprimentos chamado Miasma foi encontrado em dezenas de versões npm de @redhat-cloud-services. O hook malicioso preinstall rouba credenciais, investiga identidades na nuvem e pode republicar outros pacotes.

Blog

Vulnerabilidades de RCE no agendador de tarefas Qinglong são exploradas para mineração de criptomoedas

Duas vulnerabilidades de bypass de autenticação (CVE-2026-3965, CVE-2026-4047) no painel de agendamento de tarefas Qinglong foram exploradas para instalar malware de mineração de criptomoedas. Veja o que aconteceu, como os ataques funcionaram e o que operadores de aplicações auto-hospedadas podem aprender com esse incidente.