BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Artigos Construindo um VPC com CloudFormation - Parte 2

Construindo um VPC com CloudFormation - Parte 2

Favoritos

Pontos Principais

  • Para uma quantidade modesta de esforço, um modelo do CloudFormation pode se tornar flexível e poderoso usando parâmetros e condições.
  • Os mapeamentos podem ser usados para selecionar condicionalmente valores de uma tabela de pesquisa
  • Saídas podem ser usadas para designar claramente recursos específicos de pilha para outras pilhas.
  • As equipes podem usar saídas exportadas como comunicações entre equipes

Na primeira parte deste artigo, foi analisado como usar a Infraestrutura como Código, e o CloudFormation em particular, para criar e manter um AWS VPC. O modelo criado do CloudFormation fornece um artefato reutilizável e simples que podemos usar sempre que precisarmos criar um VPC simples.

No entanto, esse modelo não é tão flexível quanto possa ser. Gostaríamos de ter um modelo que possa criar um VPC com um número variável de sub-redes para lidar com o desenvolvimento X teste X uso em produção. Gostaríamos de algo que pudesse criar sub-redes somente públicas se precisássemos criar rapidamente algo para fins de demonstração/POC. Ou podemos querer usar uma instância NAT em vez de um NAT Gateway.

Em vez de criar modelos separados para esses casos, podemos tornar nosso modelo existente mais flexível usando Parâmetros, Condições, Mapeamentos e Resultados. Como esse é o segundo artigo da série, provavelmente deva se familiarizar com o artigo e o modelo originais. Será apresentado nesta narrativa a perspectiva de pegar o modelo original e aprimorá-lo.

Direto ao assunto: O modelo de código-fonte do CloudFormation descrito por este artigo é encontrado aqui no GitHub. Sinta-se à vontade para fazer o download, modificar e usar este modelo da maneira que quiser (embora nós não sejamos responsabilizado por mal uso).

Número Variável de Sub-redes / AZs

Zonas de Disponibilidade: A AWS tornou fácil e barato o aproveitamento das Zonas de disponibilidade (AZs) dentro de uma determinada região. Por uma explicação excessivamente simplista, é possível pensar em uma zona de disponibilidade como um enorme datacenter independente. As AZs de uma região são conectados entre si por links privados de alta velocidade e baixa latência. Eles estão perto o suficiente um do outro para suportar comunicações síncronas, mas distantes o suficiente para mitigar o efeito de desastres naturais, quedas de energia, etc. Exatamente o quão longe não é divulgado, e não é realmente relevante.

Dois AZs são um bom número para atingir alta disponibilidade básica a um custo mínimo. Mas, às vezes, um único AZ é melhor para casos simples, como demos ou POCs. Outras vezes, três são desejados para obter uma alta disponibilidade fisicamente melhorada ou para uma melhor utilização do mercado spot. Então, vamos ajustar o modelo para tornar o número do AZ variável.

Usando o modelo do artigo 1, adicione a seguinte seção acima da seção "Resources".

Parameters:
  NumberOfAZs:
    Type: Number
    AllowedValues:
    - 1
    - 2
    - 3
    Default: 2
    Description:  How many Availability Zones do you wish to utilize?

Noções básicas do YAML: No YAML, o recuo com espaços duplos indica hierarquia (sem tabs!). O caractere traço "-" é a sintaxe YAML para definir uma 'sequência', vários valores pertencentes ao mesmo grupo. A seção de parâmetros é comumente colocada acima da seção de recursos, mas tecnicamente ela pode residir em qualquer lugar no modelo.

NumberOfAZs: Esta chamada define um parâmetro de entrada para o modelo. Ao criar uma pilha usando esse modelo no AWS Management Console, a interface do usuário solicitará que insira "NumberOfAZs" com a descrição necessária perto do campo. Como fornecemos "AllowedValues", o campo de entrada será uma caixa suspensa com as opções 1, 2 e 3. O valor 2 será usado se não fizermos outra seleção. Os tipos válidos de parâmetros são definidos aqui, poderíamos ter usado Number ou String nesse caso.

Um objetivo que deve ser observado é a capacidade de usar esse modelo único para criar uma pilha em qualquer região que desejamos. No momento da redação deste artigo, a maioria das regiões tem um mínimo de três zonas de disponibilidade, mas algumas não (Montreal, Mumbai, Pequim, Seul têm apenas duas). Selecionar três AZs nessas regiões resultará em um erro. Se vale a pena limitar a flexibilidade do modelo para eliminar um erro não desejado no menor dos casos é você quem decide.

Uso do CLI: Ao criar uma pilha por meio da interface de linha de comando (CLI) da AWS, os parâmetros de entrada ainda têm finalidade. Podemos fornecer um valor para esse parâmetro, se quisermos, ou simplesmente obter o valor padrão. Fornecer um valor de entrada fora do conjunto permitido resultará em um erro.

Agora que temos uma maneira de especificar o número desejado de AZs, precisamos alterar o restante do modelo para que o CloudFormation crie as sub-redes para corresponder ao que desejamos.

Seção de condições

Para que o CloudFormation construa uma, duas ou três sub-redes, definiremos algumas "Conditions" que podem ser usadas na seção de recursos. Adicione este código abaixo da seção Parameters e acima da seção Resources:

Conditions:
  BuildPublicB:         !Not [ !Equals [ !Ref NumberOfAZs, 1 ]] 
  BuildPublicC:         !Equals [ !Ref NumberOfAZs, 3 ] 

As condições são expressões booleanas (verdadeiro/falso) que usaremos em outro lugar no modelo. Aqui estamos criando duas, um indicando se queremos construir as sub-redes "B", e outro indicando se queremos construir as sub-redes "C". Como "1" é o número mínimo permitido por NumberOfAZs, sempre construiremos as sub-redes "A".

BuildPublicB: Esta expressão está verificando se o NumberOfAZs selecionado é diferente de um. Como não temos funções maiores que, e menos do que intrínsecas no CloudFormation, usaremos a !Equals function para fazer referência ao parâmetro de entrada e verificar sua igualdade para " 1 ". O !Not é negar esse resultado (falso se torna verdadeiro, verdadeiro se torna falso). O booleano resultante é armazenado em BuildPublicB onde ele pode ser referido em outro lugar no modelo.

BuildPublicC: Esta expressão é mais simples, o NumberOfAZs é "3" (nosso máximo permitido) ou não. Nós apenas construiremos nosso PublicSubnetC se isso for verdade.

Agora que temos condições claramente definidas sobre quais sub-redes criar, podemos usá-las para importar a criação dos recursos atuais.

O atributo de condição

No modelo original do primeiro artigo, criamos PublicSubnetB com este código:

  PublicSubnetB:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.1.20.0/24
      AvailabilityZone: !Select [ 1, !GetAZs ]    # Obtenha o segundo AZ na lista
      Tags:
      - Key: Name
        Value: !Sub ${AWS::StackName}-Public-B

Agora observe o código de substituição abaixo, especificamente o novo atributo "Condition":

PublicSubnetB:
    Type: AWS::EC2::Subnet
    Condition: BuildPublicB
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.1.20.0/24
      AvailabilityZone: !Select [ 1, !GetAZs ]    # Obtenha o segundo AZ na lista
      Tags:
      - Key: Name
        Value: !Sub ${AWS::StackName}-Public-B

O atributo de condição é uma opção disponível em qualquer recurso do CloudFormation. Essencialmente, está dizendo: "crie este recurso somente se a condição BuildPublicB for verdadeira". Quando é falso, todo o recurso é ignorado durante a criação da pilha - não haverá PublicSubnetB.

Já que estamos aqui, vamos adicionar uma terceira sub-rede pública, mas somente se a condição BuildPublicC permitir:

 PublicSubnetC:
   Type: AWS::EC2::Subnet
   Condition: BuildPublicC
   Properties:
     VpcId: !Ref VPC
     CidrBlock: 10.1.30.0/24
     AvailabilityZone: !Select [ 2, !GetAZs ]    # Obtenha o terceiro AZ na lista
     Tags:
     - Key: Name
       Value: !Sub ${AWS::StackName}-Public-C

Muitos se perguntariam se existe uma maneira de expressar a condição in-line no recurso, em vez de usar a seção "Conditions" separada. No momento da redação deste artigo, não há. Mas depois de escrever muitos modelos, passei a apreciar o simples desacoplamento do cálculo da expressão lógica de seu uso. Afinal, esses modelos podem se tornar bastante complexos com expressões sequenciais, como as que são vistas para AvailabilityZone ou Tag/Value.

Última etapa, a sub-rede para rotear associações de tabelas deve ser ajustada para o número variável de sub-redes públicas. Observe o uso dos atributos de condição no seguinte trecho:

  PublicSubnetBRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Condition: BuildPublicB
    Properties:
      SubnetId: !Ref PublicSubnetB
      RouteTableId: !Ref PublicRouteTable
  PublicSubnetCRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Condition: BuildPublicC
    Properties:
      SubnetId: !Ref PublicSubnetC
      RouteTableId: !Ref PublicRouteTable

O PublicSubnetA pode não ser exibido na associação. Como está sempre presente na pilha, não requer nenhum atributo condicional. Da mesma forma, a PublicRouteTable em si é sempre necessária. Neste ponto, nossa pilha criará uma, duas ou três sub-redes públicas com base em nosso parâmetro de entrada. Vamos ver as sub-redes privadas …

Sub-redes Privadas

Imagine que gostaríamos de usar o VPC produzido por esse modelo para algumas demonstrações rápidas voltadas ao público. Ter sub-redes privadas ou um NAT em um VPC desse tipo seria um exagero e levaria mais tempo para ser criado. Por isso, vamos adicionar um parâmetro para permitir a designação de sub-redes somente públicas. Na seção "Parameters", adicione:

  PrivateSubnets:
    Type: String
    AllowedValues:
    - True
    - False
    Default: True
    Description: Do you want to create private subnets in addition to public subnets?

Aqui estamos definindo um parâmetro de entrada para controlar quando qualquer sub-rede privada é criada. Gostaria que o CloudFormation fornecesse um tipo de entrada "Boolean" para casos de sim/não como este, mas teremos que nos contentar com uma String que aceite apenas "True" ou "False".

Vamos adicionar essas condições na seção Conditions para agir sobre o valor de entrada; isso vai ficar um pouco complicado:


  BuildPrivateSubnets: !Equals [ !Ref PrivateSubnets, True ]
  BuildPrivateA:       !Equals [ !Ref PrivateSubnets, True ]
  BuildPrivateB:       !And[!Not[!Equals[!Ref NumberOfAZs,1]],!Equals[!Ref PrivateSubnets,True]]
  BuildPrivateC:       !And[!Equals[!Ref NumberOfAZs,3],!Equals[!Ref PrivateSubnets, True]]

BuildPrivateSubnets: Esta é uma condição clara e sucinta que expressa diretamente o parâmetro de entrada. Existem alguns pontos em que vamos construir algo com base na existência de alguma sub-rede privada (ou seja, o NAT)

BuildPrivateA: Um sinônimo para "BuildPrivateSubnets". Não é estritamente necessário, mas verá como o código resultante é limpo quando terminarmos. É uma pena que não possamos referenciar uma condição de uma outra condição, o que permitiria uma boa maneira de quebrar a lógica complexa.

BuildPrivateB: A lógica aqui está dizendo: "Somente construa o PrivateSubnetB se 1) queremos usar mais de um AZ, e se 2) queremos construir sub-redes privadas"

BuildPrivateC: A lógica aqui: "Somente construa o PrivateSubnetC se 1) queremos utilizar três AZ's e se 2) queremos construir sub-redes privadas".

Agora podemos converter as definições de sub-rede privada de nosso modelo original para usar os atributos de condição, da seguinte forma:

  PrivateSubnetA:
    Type: AWS::EC2::Subnet
    Condition: BuildPrivateA
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.1.50.0/24
      AvailabilityZone: !Select [ 0, !GetAZs ]    # Obtenha o primeiro AZ na lista
      Tags:
      - Key: Name
        Value: !Sub ${AWS::StackName}-Private-A
  PrivateSubnetB:
    Type: AWS::EC2::Subnet
    Condition: BuildPrivateB
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.1.60.0/24
      AvailabilityZone: !Select [ 1, !GetAZs ]    # Obtenha o segundo AZ na lista 
      Tags:
      - Key: Name
        Value: !Sub ${AWS::StackName}-Private-B
  PrivateSubnetC:
    Type: AWS::EC2::Subnet
    Condition: BuildPrivateC
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.1.70.0/24
      AvailabilityZone: !Select [ 2, !GetAZs ]    # Obtenha o terceiro AZ na lista
      Tags:
      - Key: Name
        Value: !Sub ${AWS::StackName}-Private-C

Novamente, a única modificação do nosso modelo inicial é a adição dos atributos de Condição. Além disso, adicionamos o PrivateSubnetC, que foi relativamente fácil de clonar a partir das definições de PrivateSubnetA e PrivateSubnetB.

As associações das tabelas de rota de sub-rede exigirão modificações. Não há necessidade de uma associação de sub-rede, se não houver sub-rede:

  PrivateSubnetARouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Condition: BuildPrivateA
    Properties:
      SubnetId: !Ref PrivateSubnetA
      RouteTableId: !Ref PrivateRouteTable
  PrivateSubnetBRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Condition: BuildPrivateB
    Properties:
      SubnetId: !Ref PrivateSubnetB
      RouteTableId: !Ref PrivateRouteTable
  PrivateSubnetCRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Condition: BuildPrivateC
    Properties:
      SubnetId: !Ref PrivateSubnetC
      RouteTableId: !Ref PrivateRouteTable

O NAT Gateway

Como o nosso modelo agora cria condicionalmente as sub-redes privadas, precisamos ajustar nossas entradas de NAT Gateway e RouteTable de acordo. Primeiro, o NAT Gateway; não há mais um motivo para criá-lo ou associar seu endereço Elastic IP, se não quisermos criar sub-redes particulares:

 # Um Gateway NAT será criado e usado se o usuário selecionou sub-redes Particulares e um Gateway em vez de uma instância do EC2. 
 NATGateway:
   Type: AWS::EC2::NatGateway
   Condition: BuildPrivateSubnets
   Properties:
     AllocationId: !GetAtt ElasticIPAddress.AllocationId
     SubnetId: !Ref PublicSubnetA
     Tags:
     - Key: Name
       Value: !Sub NAT-${AWS::StackName}
 ElasticIPAddress:
   Type: AWS::EC2::EIP
   Condition: BuildPrivateSubnets
   Properties:
     Domain: VPC

A única mudança do nosso modelo original são os atributos de Condição; nós só queremos construí-los se tivermos escolhido construir sub-redes privadas.

Em seguida, a condição pode indicar que não precisamos da tabela de rotas privada ou de sua entrada:

 # Aqui está uma tabela de rotas privadas:
 PrivateRouteTable:
   Type: AWS::EC2::RouteTable
   Condition: BuildPrivateSubnets
   Properties:
     VpcId: !Ref VPC
     Tags:
     - Key: Name
       Value: Private
 PrivateRoute1:           # A tabela de rota privada pode acessar a web via NAT (criada abaixo)
   Type: AWS::EC2::Route
   Condition: BuildPrivateSubnets
   Properties:
     RouteTableId: !Ref PrivateRouteTable
     DestinationCidrBlock: 0.0.0.0/0
     # Route traffic through the NAT Gateway:
     NatGatewayId: !Ref NATGateway

Neste ponto, a pilha irá ignorar a criação de sub-redes particulares, tabelas de rota ou NATs quando o "BuildPrivateSubnets" for falso. Este modelo tem a capacidade de criar de uma a seis sub-redes com base na entrada dos parâmetros. Bastante flexível, e realmente não muito trabalhoso para conseguir isso.

Opcional: tipo NAT

Para maior flexibilidade, vamos fazer o nosso modelo fornecer uma alternativa para o NAT Gateway. O serviço gerenciado integrado é ótimo para uso em produção, mas podemos achar um pouco caro para as POCs. Nos dias antecedentes ao NAT Gateways, usamos uma instância regular do EC2 configurada para fornecer suporte NAT, e há vantagens e desvantagens de qualquer forma. Então, em nome da experimentação, vamos adicionar um parâmetro para permitir essa escolha:

  NATType:
    Type: String
    AllowedValues:
    - "EC2 NAT Instance"
    - "NAT Gateway"
    Default:  "NAT Gateway"
    

…e algumas adições à seção Conditions:

  BuildNATGateway:  !And[!Equals[!Ref PrivateSubnets,True],!Equals[!Ref NATType, "NAT Gateway"]]
  BuildNATInstance: !And[!Equals[!Ref PrivateSubnets,True],!Equals[!Ref NATType, "EC2 NAT Instance"]]

A primeira declaração é essencialmente "Construa um NAT Gateway se 1) nós escolhemos construir sub-redes privadas, e se 2) nós escolhemos construir um NAT Gateway". A segunda é "Construa uma instância do EC2 para servir como um NAT se 1) nós escolhemos construir sub-redes privadas, e se 2) nós optamos por usar uma instância do EC2 para NAT".

Nossas condições de NAT Gateway/endereço Elastic IP descritas anteriormente exigirão ajustes, agora queremos controlar sua criação com base na condição BuildNATGateway:

 # Um NAT Gateway será construído e usado se o usuário selecionou sub-redes privadas e um gateway em vez de uma instância do EC2. 
 NATGateway:
   Type: AWS::EC2::NatGateway
   Condition: BuildNATGateway
   Properties:
     AllocationId: !GetAtt ElasticIPAddress.AllocationId
     SubnetId: !Ref PublicSubnetA
     Tags:
     - Key: Name
       Value: !Sub NAT-${AWS::StackName}
 ElasticIPAddress:
   Type: AWS::EC2::EIP
   Condition: BuildNATGateway
   Properties:
     Domain: VPC

A instância NAT baseada no EC2 exigirá algumas novas construções. Primeiro, nossa instância do EC2 exigirá uma AMI, mas o valor do ID da AMI irá variar de acordo com a região em que estamos trabalhando. Para permitir que esse modelo único seja usado em qualquer região, adicione a seguinte seção Mappings ao modelo antes da seção Condition (embora tecnicamente seções possam ser localizadas em qualquer ordem, algumas pessoas gostam de colocar esta perto da parte inferior):

Mappings:
 #  Este é o Amazon Linux 2 AMI. Ajuste esses valores conforme necessário, eles podem mudar algumas vezes por ano:
 AmazonLinuxAMI:
   us-east-1:
     AMI: ami-04681a1dbd79675a5    # N Virginia
   us-east-2:
     AMI: ami-0cf31d971a3ca20d6    # Ohio     
   us-west-1:
     AMI: ami-0782017a917e973e7    # N California
   us-west-2:
     AMI: ami-6cd6f714             # Oregon
   eu-west-1:
     AMI: ami-0bdb1d6c15a40392c    # Ireland
   eu-central-1:
     AMI: ami-0f5dbc86dd9cbf7a8    # Frankfurt
   sa-east-1:
     AMI: ami-0ad7b0031d41ed4b9    # Sao Paulo
   ap-southeast-1:
     AMI: ami-01da99628f381e50a    # Singapore
   ap-southeast-2:
     AMI: ami-00e17d1165b9dd3ec    # Sydney
   ap-northeast-1:
     AMI: ami-08847abae18baa040    # Tokyo

Esta seção de mapeamento define o valor do ID da AMI para o SO Amazon Linux 2. O valor do ID varia de acordo com a região em que a pilha está sendo criada. Logo mais veremos como essa tabela de mapeamento é usada e quando definimos o recurso da instância do EC2. Mas antes de seguir em frente, há alguns pontos importantes a mencionar: 1) os comentários são seus amigos, 2) eu não forneci valores para cada região, 3) esses valores estão atualizados até a data de publicação deste artigo, de tempos em tempos o a equipe do EC2 publicará novas versões aprimoradas do AMI que deve ser usada.

Não foi difícil encontrar esses valores. Usamos o assistente de criação de instância do EC2 no console de gerenciamento da AWS. Depois que chegamos à página de seleção da AMI, foi usado a seleção da região para aparecer nos valores da coleção global. Também é importante salientar que há técnicas mais avançadas que substituem a necessidade de ter qualquer tabela de mapeamento (por exemplo, uma consulta ao Parameter Store ou um recurso personalizado do CloudFormation respaldado por uma função do Lambda), mas não queria complicar demais esse artigo.

Em seguida, a instância do EC2 precisará de um grupo de segurança:

   # Um grupo de segurança para o nosso NAT. Entrada somente dos IPs de VPC. A saída é apenas TCP e UDP:
  NATSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Condition: BuildNATInstance
    DependsOn: AttachGateway
    Properties:
      GroupName: !Sub NATSecurityGroup-${AWS::StackName}
      GroupDescription: Enable internal access to the NAT device
      VpcId: !Ref VPC
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: '0'
        ToPort: '1024'
        CidrIp: !GetAtt VPC.CidrBlock
      SecurityGroupEgress:
      - IpProtocol: tcp
        FromPort: '0'
        ToPort: '65535'
        CidrIp: 0.0.0.0/0
      - IpProtocol: udp
        FromPort: '0'
        ToPort: '65535'
        CidrIp: 0.0.0.0/0

Para manter este artigo breve, não explicaremos todas as complexidades aqui, mas o resumo é:

  • Só criamos isso se a condição para BuildNATInstance for verdadeira.
  • Não queremos tentar criá-lo até que o gateway da Internet seja anexado ao VPC (consulte o artigo anterior)
  • O nome do Grupo de segurança será baseado no nome da nossa pilha do CloudFormation (consulte o artigo anterior)
  • O Grupo de segurança permite apenas o tráfego de entrada do próprio intervalo interno de endereços do VPC. Apenas IPs privados dentro do VPC podem enviar tráfego para o nosso NAT.
  • O tráfego de saída pode ser baseado em TCP ou UDP, e pode ir basicamente para qualquer lugar.
  • Consulte o AWS::EC2::SecurityGroup para obter informações completas sobre este recurso.

Em seguida, a instância do EC2:

   # Uma instância NAT será usada se o usuário selecionou sub-redes privadas e NAT baseado em EC2.        
  NATInstance:
    Type: AWS::EC2::Instance
    Condition: BuildNATInstance
    DependsOn: PublicRoute1                           # Must have route to IGW established.
    Properties:
      ImageId: !FindInMap [ AmazonLinuxAMI, !Ref "AWS::Region", AMI]  # lookup from AMI map
      InstanceType: t2.small                          # Any instance type is fine
      NetworkInterfaces:
      - DeviceIndex: '0'
        SubnetId: !Ref PublicSubnetA                  # Any public subnet is fine
        AssociatePublicIpAddress: true                # We will need a public IP address
        GroupSet: [!Ref NATSecurityGroup]             # Plug in the security group
      SourceDestCheck: false  # NATs don't work if EC2 matches source with destinations.
      Tags:
      - Key: Name
        Value: !Sub NAT-${AWS::StackName}
      UserData:      #  This code is NAT code.  Last line signals completion:
        Fn::Base64: !Sub |
          #!/bin/bash
          yum -y update
          yum install -y aws-cfn-bootstrap
          echo 1 > /proc/sys/net/ipv4/ip_forward
          echo 0 > /proc/sys/net/ipv4/conf/eth0/send_redirects
          /sbin/iptables -t nat -A POSTROUTING -o eth0 -s 0.0.0.0/0 -j MASQUERADE
          /sbin/iptables-save > /etc/sysconfig/iptables
          mkdir -p /etc/sysctl.d/
          cat << NatConfFileMarker > /etc/sysctl.d/nat.conf
          net.ipv4.ip_forward = 1
          net.ipv4.conf.eth0.send_redirects = 0
          NatConfFileMarker
          /opt/aws/bin/cfn-signal -e 0 --resource NATInstance --stack ${AWS::StackName} --region ${AWS::Region}
     # Este NATInstance só está completo quando você recebe 1 sinal de volta dentro de 5 minutos.
    CreationPolicy:
      ResourceSignal:
        Count: 1
        Timeout: PT5M

Resumidamente:

  • Condition: Nós só construiremos esta instância se BuildNATInstance for verdadeira.
  • DependsOn: Não tentaremos criá-lo até que PublicRoute1 esteja completamente construído, ou seja, devemos estar conectados à Internet. Isso será crítico para permitir que o comando "yum" em UserData seja executado como esperado (abaixo)
  • ImageID: O AMI a ser usado vem da tabela de mapeamento estabelecida anteriormente. O "AWS::Region" é um pseudo parâmetro que sempre nos diz a região em que a pilha está sendo criada. Essencialmente, estamos pedindo ao CloudFormation para pesquisar a região na tabela de mapeamento e usar a AMI resultante.
  • SubnetId: Estamos colocando esta instância EC2 NAT em uma sub-rede pública para facilitar as comunicações de saída. É um pouco simplista usar uma única instância NAT em uma única sub-rede pública como essa, mas a principal intenção deste artigo é demonstrar flexibilidade básica e não uma cobertura exaustiva das práticas recomendadas. Sinta-se à vontade para ajustar e melhorar.
  • AssociatePublicIPAddress: O NAT precisa de um endereço IP público para conversar com as partes na Internet pública.
  • GroupSet: O NAT está associado ao grupo de segurança definido anteriormente. Esse parâmetro requer uma lista de grupos de segurança, não um valor único. Então os colchetes "[]" são adicionados para forçar o valor único em uma estrutura de lista.
  • SourceDestCheck: Isso informa ao EC2 para ignorar as verificações normais que ele faz para garantir que a instância seja a origem ou o destino de qualquer tráfego recebido, o que não é o caso de um NAT. A explicação simples é que isso é algo que devemos fazer para que o NAT funcione. Veja a verificação de origem/destino para uma explicação mais detalhada.
  • UserData: O script do Linux que é visto aqui estabelece um recurso NAT. Uma explicação completa deste script está além do escopo deste artigo. A função !Sub intrínseca procura expressões ${} e as substitui, como o ${AWS::StackName}. A linha final do script, cfn-signal, é uma função específica do CloudFormation que sinaliza a pilha do CloudFormation quando o script é concluído
  • Fn::Base64:!Sub: Um script UserData deve ser codificado em Base64, o que é fácil de fazer com a função intrínseca Fn::Base64. Normalmente, eu usaria a sintaxe de atalho Base64, mas também preciso usar a função Sub! intrínseca para substituir placeholders no script. Usando o !Sub dentro !Base64 é válido no CloudFormation, mas não é válido para o YAML, por isso temos que usar o nome da função completa para a função externa. Ugh.
  • CreationPolicy: normalmente, o CloudFormation considera um recurso totalmente criado quando o serviço subjacente diz que ele está criado. Para instâncias do EC2, isso ocorre essencialmente quando o sistema operacional começou a inicialização. No entanto, para que essa instância do EC2 NAT possa ser usada por qualquer outra pessoa nessa pilha, o script de dados do usuário precisa ser concluído. A CreationPolicy está essencialmente dizendo "este recurso não está completo até que recebamos um sinal (do comando cfn-signal) dentro de 5 minutos"

Ufa! Isso é muito! Mas a beleza do CloudFormation, ou da Infraestrutura como Código em geral, é que eu só preciso escrever isso uma vez. Veja o AWS::EC2::Instance para detalhes completos sobre todas essas configurações.

Por fim, precisaremos ajustar o PrivateRoute que criamos anteriormente. Nós precisaremos rotear o tráfego de saída para o NAT Gateway OU para a Instância NAT, dependendo de qual foi criado:

PrivateRoute1:            # A tabela de rota privada pode acessar a web via NAT (criada abaixo)
   Type: AWS::EC2::Route
   Condition: BuildPrivateSubnets
   Properties:
     RouteTableId: !Ref PrivateRouteTable
     DestinationCidrBlock: 0.0.0.0/0
     # Se estivermos usando uma instância do NAT, direcione o tráfego por meio da instância do NAT:
     InstanceId:   !If [ BuildNATInstance, !Ref NATInstance, !Ref "AWS::NoValue" ]
     # Caso contrário, se estivermos usando um gateway NAT, direcione o tráfego através do gateway NAT:
     NatGatewayId: !If [ BuildNATGateway, !Ref NATGateway, !Ref "AWS::NoValue" ]

Observe as propriedades InstanceId e NatGatewayId, de acordo com a documentação do AWS::EC2::Route, elas são mutuamente exclusivas. O InstanceId é usado nos casos em que estamos roteando o tráfego para uma instância do EC2. O !If intrinsic function é apenas para definir este valor para o NATInstance se tivermos escolhido BuildNATInstance. O AWS::NoValue é mais do que aparenta, não está apenas dizendo que não há valor para definir, mas que o CloudFormation entende que isso significa que não há necessidade de definir esse atributo. A lógica de espelhamento em NatGatewayId define o valor para NATGateway se tivermos escolhido BuildNATGateway. Como as condições são mutuamente exclusivas, apenas uma delas é definida, e nosso tráfego de saída usará a NATInstance ou a NATGateway, principalmente com base em nossa decisão de entrada original.

Opcional: modelo de metadados

Um outro ajuste que podemos fazer em nosso modelo revisado é cosmético. Gostaríamos de controlar a ordem de entrada dos parâmetros mais de perto, solicitando as escolhas mais essenciais primeiro. Para isso, adicione uma seção Metadata antes da seção Parameters (embora algumas pessoas gostem de colocar isso na parte inferior do modelo):

Metadata:
 # Controle a exibição da interface do usuário ao executar esse modelo no AWS Management Console:
 AWS::CloudFormation::Interface:
   ParameterGroups:
     - Label:
         default: "Network Configuration"
       Parameters:
         - NumberOfAZs
         - PrivateSubnets
         - NATType

Agora, ao usar esse modelo para criar uma pilha por meio do AWS Management Console, a página de parâmetros solicitará ao operador "Network Configuration" e apresentará os parâmetros na ordem desejada. Ao usar o CLI, esta seção não tem impacto.

Resultados

O modelo que foi criado é um modelo agradável de uso geral que pode ser usado como ponto de partida para outras pilhas do CloudFormation que precisam de um VPC. Queremos tornar mais fácil fornecer valores dessa pilha como entradas em outras pilhas. Isso é especialmente relevante na maioria das organizações de TI, onde as responsabilidades são segmentadas entre as equipes e as que têm permissão para gerenciar os recursos da rede não são as mesmas que as permitidas para criar recursos que usam a rede. Fornecer valores de saída de uma pilha pode ser feito criando uma seção de Outputs:

Outputs:
 VPC:
   Description: VPC of the base network
   Value: !Ref VPC
   Export:
     Name: !Sub ${AWS::StackName}-VPC
 PublicSubnetA:
   Description: First Public Subnet
   Value: !Ref PublicSubnetA
   Export:
     Name: !Sub ${AWS::StackName}-PublicSubnetA
 PublicSubnetB:
   Description: Second Public Subnet
   Condition: BuildPublicB
   Value: !Ref PublicSubnetB
   Export:
     Name: !Sub ${AWS::StackName}-PublicSubnetB
 PublicSubnetC:
   Description: Third Public Subnet
   Condition: BuildPublicC
   Value: !Ref PublicSubnetC
   Export:
     Name: !Sub ${AWS::StackName}-PublicSubnetC
 PrivateSubnetA:
   Condition: BuildPrivateSubnets
   Description: First Private Subnet
   Value: !Ref PrivateSubnetA
   Export:
     Name: !Sub ${AWS::StackName}-PrivateSubnetA
 PrivateSubnetB:
   Condition: BuildPrivateB
   Description: Second Private Subnet
   Value: !Ref PrivateSubnetB
   Export:
     Name: !Sub ${AWS::StackName}-PrivateSubnetB
 PrivateSubnetC:
   Condition: BuildPrivateC
   Description: Third Private Subnet
   Value: !Ref PrivateSubnetC
   Export:
     Name: !Sub ${AWS::StackName}-PrivateSubnetC

Basicamente, essas entradas de saída exibem os valores relevantes na saída do AWS Management Console/CLI JSON após a conclusão da pilha. Observe a inclusão de atributos condicionais para emitir apenas valores para recursos que realmente criamos.

A parte de notificação é o Export/Name. Isso está produzindo para uma região inteira um nome pelo qual esse recurso pode ser referenciado de outra pilha. Usando o PublicSubnetA como exemplo, e assumindo um nome de pilha de "my-network", o valor exportado seria "my-network-PublicSubnetA". Outra pilha poderia usar "!ImportValue my-network-PublicSubnetA" para referenciar este recurso tão facilmente quanto "!Ref" é usado dentro da pilha. Geralmente, a pilha inicial (base) é usada como um parâmetro de entrada, portanto, a parte do nome da pilha pode ser dinâmica, como em:

Fn::ImportValue: !Sub ${BaseStack}-PublicSubnetA

... onde "BaseStack" é um parâmetro de entrada de uma pilha secundária. Irritantemente, !Sub dentro de um !ImportValue é inválido no YAML, então temos que usar o "long form" do nome da função, Fn::ImportValue.

As técnicas Export/Name/!ImportValue mostradas aqui são comuns em ambientes de várias equipes. Cada equipe geralmente precisa referenciar recursos de pilhas produzidas por outras equipes e, por sua vez, produzir recursos para serem referenciados por outras pilhas. Os nomes de exportação tornam-se pontos conhecidos e confiáveis de comunicação entre equipes. Além disso, o CloudFormation rastreia essas referências entre pilhas para evitar que uma exclusão ou atualização de uma pilha invalide recursos dependentes em outra.

Resumo

Com exceção da adição da opção de instância EC2 NAT e da seção de saída volumosa, alteramos apenas um pequeno número de linhas do modelo original. Parâmetros e condições são uma maneira compacta de tornar nosso modelo muito mais capaz. Agora podemos criar VPCs com uma a seis sub-redes com uma variedade de permutações possíveis. As pilhas que criamos podem ser referenciadas por outras pilhas de maneira altamente organizada e confiável. Mas, ainda mais surpreendente, podemos usar esse modelo para modificar a pilha resultante para adicionar ou remover sub-redes, como quando uma POC inicial cresce para se tornar uma implantação de avaliação. É possível expandir as técnicas aqui para tornar esse modelo mais sofisticado, como um VPC com apenas sub-redes particulares.

Sobre o autor

A declaração de missão profissional de Ken Krueger é "orientar organizações e indivíduos para o sucesso comercial através da aplicação da tecnologia moderna". Ele tem mais de 30 anos de experiência como desenvolvedor de software, líder de projeto, gerente de projeto, scrum master e instrutor, abrangendo as épocas de mainframe, cliente-servidor e web. Ele tem grande experiência em tecnologias Java, Spring, SQL, desenvolvimento web, nuvem e tecnologias relacionadas. A experiência do setor inclui telecomunicações, finanças, imóveis, varejo, geração de energia, remessa, hospitalidade e desenvolvimento de software. Ele é formado em MIS pela University of South Florida, possui um MBA da Crummer Graduate School of Business na Rollins College, além de certificações Scrum Master, PMP, AWS e Java.

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT