Индустрия разработки программного обеспечения меняется очень быстро. После того, как термин DevOps стал очень популярным, одного написания кода уже недостаточно. Вы должны максимально автоматизировать процесс разработки программного обеспечения. В этом посте я опишу, как определить нашу инфраструктуру, просто написав код.

«Инфраструктура как код — это процесс управления компьютерными центрами обработки данных и их предоставления с помощью машиночитаемых файлов определений, а не физической конфигурации оборудования или интерактивных инструментов настройки».[1]

Зачем нам нужна инфраструктура как код?

  • После переноса контейнерного мира инфраструктуру можно чаще менять или удалять/создавать.
  • Кроме того, отслеживать настройки инфраструктуры становится очень сложно из-за множества различных настроек. Таким образом, сохранение этих настроек внутри кода помогает понять огромную сложность.

В этом посте я буду использовать CloudFormation для определения инфраструктуры.

«AWS CloudFormation предоставляет общий язык для моделирования и предоставления ресурсов AWS и сторонних приложений в вашей облачной среде. AWS CloudFormation позволяет использовать языки программирования или простой текстовый файл для автоматического и безопасного моделирования и предоставления всех ресурсов, необходимых для ваших приложений во всех регионах и учетных записях. Это дает вам единый источник достоверной информации для ваших ресурсов AWS и сторонних ресурсов».[2]

Я нарисовал архитектурную схему ниже, чтобы продемонстрировать финал.

Давайте определим компоненты, которые существуют внутри диаграммы.

Регион: это отдельные географические области, которые AWS использует для размещения своей инфраструктуры. Они распределены по всему миру, чтобы клиенты могли выбрать ближайший к ним регион, чтобы разместить там свою облачную инфраструктуру.[3]

Зона доступности: логический строительный блок, из которого состоит регион AWS. В настоящее время существует 69 AZ, которые являются изолированными местами — центрами обработки данных — в пределах региона.[3]

VPC: позволяет запускать ресурсы AWS в определенную вами виртуальную сеть. Эта виртуальная сеть очень похожа на традиционную сеть, которую вы бы использовали в собственном центре обработки данных, с преимуществами использования масштабируемой инфраструктуры AWS[4].

Подсеть: это «часть сети», другими словами, часть всей зоны доступности. Каждая подсеть должна полностью находиться в пределах одной зоны доступности и не может охватывать зоны.[5]

Для простоты я создам 1 VPC, 2 публичные подсети, 2 частные подсети. Подсети стоят разных зон доступности.

PS: развертывание команд кода

$sam package --template-file template.yaml --s3-bucket BUCKETNAME --output-template-file packaged.yaml
$sam deploy --template-file FILEPATH/packaged.yaml --stack-name test --capabilities CAPABILITY_IAM

Прежде всего, давайте определим параметры, как показано ниже, чтобы использовать их позже.

Parameters:
  EnvironmentName:
    Description: An environment name that is prefixed to resource names
    Type: String
    Default: Test

  VpcCIDR:
    Description: Please enter the IP range (CIDR notation) for this VPC
    Type: String
    Default: 10.0.0.0/16

  PublicSubnet1CIDR:
    Description: Please enter the IP range (CIDR notation) for the public subnet in the first Availability Zone
    Type: String
    Default: 10.0.1.0/24

  PublicSubnet2CIDR:
    Description: Please enter the IP range (CIDR notation) for the public subnet in the second Availability Zone
    Type: String
    Default: 10.0.2.0/24

  PrivateSubnet1CIDR:
    Description: Please enter the IP range (CIDR notation) for the private subnet in the first Availability Zone
    Type: String
    Default: 10.0.3.0/24

  PrivateSubnet2CIDR:
    Description: Please enter the IP range (CIDR notation) for the private subnet in the second Availability Zone
    Type: String
    Default: 10.0.4.0/24

PS: 10.0.0.0/16: каждое число представляет собой 8 цифр, а первые 16 цифр (первые две цифры) остаются прежними. Остальные цифры можно изменить. Таким образом, можно назначить 256 * 256 IP.

СОЗДАТЬ VPC

VPC:
  Type: AWS::EC2::VPC
  Properties:
    CidrBlock: !Ref VpcCIDR
    EnableDnsSupport: true
    EnableDnsHostnames: true
    Tags:
      - Key: Name
        Value: !Ref EnvironmentName

Давайте подключим этот VPC к интернету. Интернет-шлюз используется для доступа в Интернет из VPC.

InternetGateway:
  Type: AWS::EC2::InternetGateway
  Properties:
    Tags:
      - Key: Name
        Value: !Ref EnvironmentName

InternetGatewayAttachment:
  Type: AWS::EC2::VPCGatewayAttachment
  Properties:
    InternetGatewayId: !Ref InternetGateway
    VpcId: !Ref VPC

СОЗДАНИЕ ПОДСЕТИ

PublicSubnet1:
  Type: AWS::EC2::Subnet
  Properties:
    VpcId: !Ref VPC
    AvailabilityZone: !Select [ 0, !GetAZs '' ]
    CidrBlock: !Ref PublicSubnet1CIDR
    MapPublicIpOnLaunch: true
    Tags:
      - Key: Name
        Value: !Sub ${EnvironmentName} Public Subnet (AZ1)

PublicSubnet2:
  Type: AWS::EC2::Subnet
  Properties:
    VpcId: !Ref VPC
    AvailabilityZone: !Select [ 1, !GetAZs  '' ]
    CidrBlock: !Ref PublicSubnet2CIDR
    MapPublicIpOnLaunch: true
    Tags:
      - Key: Name
        Value: !Sub ${EnvironmentName} Public Subnet (AZ2)

PrivateSubnet1:
  Type: AWS::EC2::Subnet
  Properties:
    VpcId: !Ref VPC
    AvailabilityZone: !Select [ 0, !GetAZs  '' ]
    CidrBlock: !Ref PrivateSubnet1CIDR
    MapPublicIpOnLaunch: false
    Tags:
      - Key: Name
        Value: !Sub ${EnvironmentName} Private Subnet (AZ1)

PrivateSubnet2:
  Type: AWS::EC2::Subnet
  Properties:
    VpcId: !Ref VPC
    AvailabilityZone: !Select [ 1, !GetAZs  '' ]
    CidrBlock: !Ref PrivateSubnet2CIDR
    MapPublicIpOnLaunch: false
    Tags:
      - Key: Name
        Value: !Sub ${EnvironmentName} Private Subnet (AZ2)

PublicSubnet1 стоит в первой зоне доступности.

PublicSubnet2 стоит на втором месте.

PrivateSubnet1 стоит в первой зоне доступности.

PrivateSubnet2 стоит на втором месте.

Публичные подсети должны быть подключены к Интернету. Чтобы справиться с этим, нам нужна таблица маршрутизации.

PublicRouteTable:
  Type: AWS::EC2::RouteTable
  Properties:
    VpcId: !Ref VPC
    Tags:
      - Key: Name
        Value: !Sub ${EnvironmentName} Public Routes

DefaultPublicRoute:
  Type: AWS::EC2::Route
  DependsOn: InternetGatewayAttachment
  Properties:
    RouteTableId: !Ref PublicRouteTable
    DestinationCidrBlock: 0.0.0.0/0
    GatewayId: !Ref InternetGateway

PublicSubnet1RouteTableAssociation:
  Type: AWS::EC2::SubnetRouteTableAssociation
  Properties:
    RouteTableId: !Ref PublicRouteTable
    SubnetId: !Ref PublicSubnet1

PublicSubnet2RouteTableAssociation:
  Type: AWS::EC2::SubnetRouteTableAssociation
  Properties:
    RouteTableId: !Ref PublicRouteTable
    SubnetId: !Ref PublicSubnet2

Любой запрос со связанной общедоступной подсетью направляется на интернет-шлюз.

PS: Если вы хотите сделать то же самое для частных подсетей, вам следует использовать NAT Gateway.

СОЗДАНИЕ ЭКЗЕМПЛЯРА ELASTICACHE С КОДОМ

ServerlessSecurityGroup:
  DependsOn: VPC
  Type: AWS::EC2::SecurityGroup
  Properties:
    GroupDescription: SecurityGroup for Serverless Functions
    VpcId:
      Ref: VPC

ServerlessStorageSecurityGroup:
  DependsOn: VPC
  Type: AWS::EC2::SecurityGroup
  Properties:
    GroupDescription: Ingress for Redis Cluster
    VpcId:
      Ref: VPC
    SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: '6379'
        ToPort: '6379'
        SourceSecurityGroupId:
          Ref: ServerlessSecurityGroup

ServerlessCacheSubnetGroup:
  Type: AWS::ElastiCache::SubnetGroup
  Properties:
    Description: "Cache Subnet Group"
    SubnetIds:
      - Ref: PrivateSubnet1

ElasticCacheCluster:
  DependsOn: ServerlessStorageSecurityGroup
  Type: AWS::ElastiCache::CacheCluster
  Properties:
    AutoMinorVersionUpgrade: true
    Engine: redis
    CacheNodeType: cache.t2.micro
    NumCacheNodes: 1
    VpcSecurityGroupIds:
      - "Fn::GetAtt": ServerlessStorageSecurityGroup.GroupId
    CacheSubnetGroupName:
      Ref: ServerlessCacheSubnetGroup

Из соображений безопасности я поместил экземпляр Elasticache в частную подсеть. Поэтому нам нужно создать группу безопасности, чтобы порт 6379 мог подключаться к Redis.

ПОДКЛЮЧИТЬ REDIS ИЗ ФУНКЦИИ LAMBDA

CacheClientFunction:
  Type: AWS::Serverless::Function
  Properties:
    Tracing: Active
    CodeUri: bin/cacheClient
    Handler: cacheClient
    Runtime: go1.x
    Role: !GetAtt RootRole.Arn
    VpcConfig:
      SecurityGroupIds:
        - Ref: ServerlessSecurityGroup
      SubnetIds:
        - Ref: PublicSubnet1
    Environment:
      Variables:
        redis_url: !GetAtt ElasticCacheCluster.RedisEndpoint.Address
        redis_port: !GetAtt ElasticCacheCluster.RedisEndpoint.Port

Лямбда-функция стоит в общедоступной подсети. Мы можем определить redis_url и redis_port в соответствии с созданным ElasticCache по нашему определению. При написании кода мы можем использовать эти переменные среды для подключения Redis.

!!!Важно!!!

Лямбда-функция требует определенной роли и политики. В противном случае при создании стека будет получена ошибка. Давайте создадим роль и политику с помощью кода.

SampleManagedPolicy:
  Type: AWS::IAM::ManagedPolicy
  Properties:
    PolicyDocument:
      Version: '2012-10-17'
      Statement:
        - Sid: AllowAllUsersToListAccounts
          Effect: Allow
          Action:
            - ec2:CreateNetworkInterface
            - ec2:DescribeNetworkInterfaces
            - ec2:DeleteNetworkInterface
            - xray:PutTraceSegments
          Resource: "*"

RootRole:
  Type: 'AWS::IAM::Role'
  Properties:
    AssumeRolePolicyDocument:
      Version: '2012-10-17'
      Statement:
        - Effect: Allow
          Principal:
            Service:
              - lambda.amazonaws.com
          Action:
            - 'sts:AssumeRole'
    Path: /
    ManagedPolicyArns:
      - !Ref SampleManagedPolicy

Указанная выше политика будет создана с минимальным доступом.

Код клиента кеша:

package main

import (
   "context"
   "fmt"
   "github.com/aws/aws-lambda-go/lambda"
   "github.com/go-redis/redis"
   "os"
)

func HandleRequest(ctx context.Context) (string, error) {

   redisUrl := os.Getenv("redis_url")
   redisPort := os.Getenv("redis_port")
   client := redis.NewClient(&redis.Options{
      Addr:     fmt.Sprintf("%s:%s", redisUrl, redisPort),
      Password: "", // no password set
      DB:       0,  // use default DB
   })

   client.Set("1", "1", 0)

   return client.Get("1").String(), nil
}

func main() {
   lambda.Start(HandleRequest)
}

СОЗДАНИЕ ЭКЗЕМПЛЯРА БАЗЫ ДАННЫХ С КОДОМ

ServerlessDBSecurityGroup:
  DependsOn: VPC
  Type: AWS::EC2::SecurityGroup
  Properties:
    GroupDescription: Ingress for Redis Cluster
    VpcId:
      Ref: VPC
    SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: '5432'
        ToPort: '5432'
        SourceSecurityGroupId:
          Ref: ServerlessSecurityGroup

ServerlessDBSubnetGroup:
  DependsOn: ServerlessDBSecurityGroup
  Type: AWS::RDS::DBSubnetGroup
  Properties:
    DBSubnetGroupDescription: "DB Subnet Group"
    SubnetIds:
      - Ref: PrivateSubnet1
      - Ref: PrivateSubnet2

PostgresqlInstance:
  DependsOn: VPC
  Type: AWS::RDS::DBInstance
  Properties:
    AllocatedStorage: 30
    DBInstanceClass: db.t2.micro
    DBName: postgres
    Engine: postgres
    MasterUsername: CacheClient
    MasterUserPassword: ChangeIt2
    DBSubnetGroupName: !Ref ServerlessDBSubnetGroup
    VPCSecurityGroups:
      - "Fn::GetAtt": ServerlessDBSecurityGroup.GroupId
DbClientFunction:
  Type: AWS::Serverless::Function
  Properties:
    Tracing: Active
    CodeUri: bin/dbClient
    Handler: dbClient
    Runtime: go1.x
    Role: !GetAtt RootRole.Arn
    VpcConfig:
      SecurityGroupIds:
        - Ref: ServerlessSecurityGroup
      SubnetIds:
        - Ref: PublicSubnet1
    Environment:
      Variables:
        db_url: !GetAtt PostgresqlInstance.Endpoint.Address
        db_port: !GetAtt PostgresqlInstance.Endpoint.Port

Создание БД очень похоже на кеш. Важным моментом является то, что ваш инстанс БД должен находиться как минимум в двух зонах доступности.

Код клиента БД:

package main

import (
   "context"
   "fmt"
   "github.com/aws/aws-lambda-go/lambda"
   "github.com/jinzhu/gorm"
   _ "github.com/jinzhu/gorm/dialects/postgres"
   "os"
)

type MyEvent struct {
   Name string `json:"name"`
}

func HandleRequest(ctx context.Context, name MyEvent) (string, error) {

   dbUrl := os.Getenv("db_url")
   dbURI := fmt.Sprintf("host=%s user=CacheClient dbname=postgres sslmode=disable password=ChangeIt2", dbUrl)
   fmt.Println(dbURI)
   db, err := gorm.Open("postgres", dbURI)
   if err != nil {
      return "err", err
   }

   db.AutoMigrate(&Entity{})
   var ent = &Entity{}
   ent.Text = name.Name
   db.Save(&ent)

   return fmt.Sprint(&ent.ID), nil
}

func main() {
   lambda.Start(HandleRequest)
}
package main

import "github.com/jinzhu/gorm"

type Entity struct {
   gorm.Model
   Text string
}

Все сделано. Теперь мы создали сеть, экземпляр кэша, экземпляр БД с кодом. Мы связываем их вывод с нашими бессерверными функциями. Клиенты кеша и БД использовали созданную архитектуру по коду.

Полная версия template.yaml:

https://github.com/yunuskilicdev/infrastructureascode

Цитаты:

1-) https://stackify.com/what-is-infrastructure-as-code-how-it-works-best-practices-tutorials/

2-) https://aws.amazon.com/cloudformation/

3-) https://cloudacademy.com/blog/aws-regions-and-availability-zones-the-simplest-explanation-you-will-ever-find-around/

4-) https://docs.aws.amazon.com/vpc/latest/userguide/what-is-amazon-vpc.html

5-)https://www.infoq.com/articles/aws-vpc-explained/