Terraform İle AWS Kullanımı ve EC2 Web Server Deploy

Terraform ile AWS EC2 Instance kullanarak basit bir web server deploy etmek için neler yapmamız gerektiğini bu yazıda inceliyoruz. Amacımız, mümkün olan en basit web mimarisiyle HTTP isteklerine yanıt verebilen tek bir web sunucusu deploy etmek olacak.

Gerçek dünya use case'lerinde, web sunucusunu muhtemelen Ruby on Rails veya Django gibi bir web framework kullanarak oluşturursunuz. Ancak bu örneği basit tutmak için, her zaman “Hello, World”  döndüren basit bir Apache HTTP Server kullanalım.

#!/bin/bash
echo "Hello, World" > index.html
nohup busybox httpd -f -p 8080 &

Bu snippet, "Hello, World" metnini index.html'ye yazan ve bu dosyayı sunmak için 8080 numaralı portta bir web sunucusunu başlatmak için busybox (varsayılan olarak Ubuntu'yla yüklenir) adlı bir aracı çalıştıran Bash betiğidir. Web sunucusunun arka planda kalıcı olarak çalışması için busybox komutunu nohup ve bir ampersand işareti (&) ile çalıştırıyoruz.

💡
Bu örneğin varsayılan HTTP bağlantı noktası 80 yerine 8080 numaralı bağlantı noktasını kullanmasının nedeni, 1024'ten küçük herhangi bir bağlantı noktasında dinlemenin root kullanıcı ayrıcalıkları gerektirmesidir. Bu bir güvenlik riskidir, çünkü sunucunuzun güvenliğini aşmayı başaran herhangi bir saldırgan da root ayrıcalıklarına sahip olacaktır.

Şimdi Terraform kısmına geçeceğiz fakat bilgilerinizi tazelemek ve Terraform temellerine daha iyi hakim olmak için öncelikle şu iki yazıya göz atabilirsiniz:

Terraform Nedir? Nasıl Kurulur?
Terraform, basit ve declarative bir dil kullanarak altyapınızı kod olarak tanımlamanızı sağlayan açık kaynak kodlu bir araçtır.
Terraform İle AWS Kullanımı ve EC2 Server Deploy
Terraform ile AWS kullanarak EC2 Instance nasıl oluşturulur ve nasıl deploy edilir detaylıca gösteriyoruz.

Peki bu scripti EC2 Instance üzerinde nasıl çalıştırabilirsiniz? Normalde, "Server Templating Tools" bölümünde tartıştığımız gibi, üzerinde web sunucusunun kurulu olduğu özel bir AMI oluşturmak için Packer gibi bir araç kullanırsınız. Bu örnekteki dummy web sunucusu, busybox kullanan yalnızca tek satırlık kod olduğundan, düz bir Ubuntu 20.04 AMI kullanabilir ve EC2 Instance'ın User Data yapılandırmasının bir parçası olarak "Hello, World" komut dosyasını çalıştırabilirsiniz. Bir EC2 Instance'ı başlattığınızda, User Data içindeki shell komutları boot sırasında yürütülür. Terraform kodunuzdaki user_data değişkenini aşağıdaki gibi ayarlayarak bir shell komut dosyasını User Data'ya iletirsiniz:

resource "aws_instance" "example" {
  ami                    = "ami-0fb653ca2d3203ac1"
  instance_type          = "t2.micro"

  user_data = <<-EOF
              #!/bin/bash
              echo "Hello, World" > index.html
              nohup busybox httpd -f -p 8080 &
              EOF

  tags = {
    Name = "terraform-example"
  }
}

<<-EOF ve EOF, Terraform'un her yere \n karakteri eklemek zorunda kalmadan çok satırlı dizeler oluşturmanıza izin veren heredoc syntaxıdır.

Bu web sunucusu çalışmadan önce bir şey daha yapmanız gerekiyor. Varsayılan olarak AWS, bir EC2 Instance'ından gelen veya giden trafiğe izin vermez. EC2 Instance'ı için 8080 numaralı bağlantı noktasında trafik almasına izin vermek için bir güvenlik grubu oluşturmanız gerekir:

resource "aws_security_group" "instance" {
  name = "terraform-example-instance"

  ingress {
    from_port   = 8080
    to_port     = 8080
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Bu kod, aws_security_group adlı yeni bir resource oluşturur (AWS provider için tüm resourceların aws_ ile başladığına dikkat edin) ve bu grubun CIDR bloğu 0.0.0.0/0'dan 8080 numaralı bağlantı noktasından gelen TCP isteklerine izin verdiğini belirtir. CIDR blokları, IP adresi aralıklarını belirtmenin kısa bir yoludur. Örneğin, 10.0.0.0/24'lük bir CIDR bloğu, 10.0.0.0 ile 10.0.0.255 arasındaki tüm IP adreslerini temsil eder. CIDR bloğu 0.0.0.0/0 tüm olası IP adreslerini içeren bir IP adresi aralığıdır, bu nedenle bu güvenlik grubu herhangi bir IP adresinden 8080 numaralı bağlantı noktasından gelen isteklere izin verir.

Yalnızca bir güvenlik grubu oluşturmak yeterli değildir; ayrıca güvenlik grubunun kimliğini aws_instance kaynağının vpc_security_group_ids değişkenine geçirerek EC2 Instance'ın onu gerçekten kullanmasını söylemeniz gerekir. Bunu yapmak için önce Terraform expressions'ları öğrenmeniz gerekir.

Terraform'daki bir expression, bir değer döndüren herhangi bir şeydir. Stringler (ör., "ami-0fb653ca2d3203ac1") ve sayılar (ör. 5) gibi en basit ifade türlerini zaten gördünüz. Terraform, ilerleyen yazılar boyunca göreceğiniz diğer birçok expression türünü destekler.

Özellikle yararlı bir expression türü olan referans, kodunuzun diğer bölümlerinden değerlere erişmenizi sağlar. Güvenlik grubu resource'unun ID'sine erişmek için aşağıdaki syntaxı kullanan bir resource attribute referansı kullanmanız gerekecektir:

<PROVIDER>_<TYPE>.<NAME>.<ATTRIBUTE>
  • PROVIDER sağlayıcının adıdır (ör. aws, do),
  • TYPE resource türüdür (ör. instance, security_group),
  • NAME bu resource'un adıdır (ör. güvenlik grubunu "instance" olarak adlandırdık)
  • ATTRIBUTE ya o kaynağın değişkenlerinden biri (örneğin name) veya kaynak tarafından dışa aktarılan niteliklerden biridir (kullanılabilir niteliklerin listesini her kaynağın belgelerinde bulabilirsiniz). Güvenlik grubu, id adlı bir attribute'ü dışa aktarır

Bu bilgiler eşiğinde bu güvenlik grubu resource'unun ID bilgisine  EC2 Instance resource'u içinden şu şekilde erişebiliriz:

aws_security_group.instance.id

Bu güvenlik grubu kimliğini aws_instance'ın vpc_security_group_ids değişkeninde kullanabilirsiniz:

resource "aws_instance" "example" {
  ami                    = "ami-0fb653ca2d3203ac1"
  instance_type          = "t2.micro"
  vpc_security_group_ids = [aws_security_group.instance.id]

  user_data = <<-EOF
              #!/bin/bash
              echo "Hello, World" > index.html
              nohup busybox httpd -f -p 8080 &
              EOF

  tags = {
    Name = "terraform-example"
  }
}

Bir kaynaktan diğerine referans eklediğinizde, örtük bir bağımlılık yaratırsınız. Terraform bu bağımlılıkları ayrıştırır, onlardan bir bağımlılık grafiği oluşturur ve bunu, kaynakları hangi sırayla oluşturması gerektiğini otomatik olarak belirlemek için kullanır. Örneğin, bu kodu sıfırdan dağıtıyorsanız, Terraform güvenlik grubunu EC2 Instance'ından önce oluşturması gerektiğini bilir çünkü EC2 Instance güvenlik grubunun kimliğine başvurur. terraform graph komutunu çalıştırarak Terraform'un size bağımlılık grafiğini göstermesini bile sağlayabilirsiniz:

Şekil 1: Terraform graph komutu ile dependency listesinin çıkarılması

Çıktı, Şekil 2'de gösterilen bağımlılık grafiğine benzer şekilde, Graphviz gibi bir masaüstü uygulaması veya GraphvizOnline gibi bir web uygulaması kullanarak bir görüntüye dönüştürebileceğiniz DOT adlı bir grafik tanımlama dilindedir.

Şekil 2: GraphvizOnline ile dependecy graphı

Terraform dependecy ağacınızda gezinirken, paralel olarak olabildiğince çok kaynak yaratır, bu da değişikliklerinizi oldukça verimli bir şekilde uygulayabileceği anlamına gelir. Declarative dilin güzelliği budur: Siz sadece ne istediğinizi belirtirsiniz ve Terraform bunu gerçekleştirmenin en verimli yolunu belirler.

Apply komutunu çalıştırırsanız, Terraform'un bir güvenlik grubu oluşturmak ve EC2 Instance'ı yeni kullanıcı verilerini içeren yenisiyle değiştirmek istediğini göreceksiniz:

$ terraform apply

(...)

Terraform will perform the following actions:

  # aws_instance.example must be replaced
-/+ resource "aws_instance" "example" {
        ami                          = "ami-0fb653ca2d3203ac1"
      ~ availability_zone            = "us-east-2c" -> (known after apply)
      ~ instance_state               = "running" -> (known after apply)
        instance_type                = "t2.micro"
        (...)
      + user_data                    = "c765373..." # forces replacement
      ~ volume_tags                  = {} -> (known after apply)
      ~ vpc_security_group_ids       = [
          - "sg-871fa9ec",
        ] -> (known after apply)
        (...)
    }

  # aws_security_group.instance will be created
  + resource "aws_security_group" "instance" {
      + arn                    = (known after apply)
      + description            = "Managed by Terraform"
      + egress                 = (known after apply)
      + id                     = (known after apply)
      + ingress                = [
          + {
              + cidr_blocks      = [
                  + "0.0.0.0/0",
                ]
              + description      = ""
              + from_port        = 8080
              + ipv6_cidr_blocks = []
              + prefix_list_ids  = []
              + protocol         = "tcp"
              + security_groups  = []
              + self             = false
              + to_port          = 8080
            },
        ]
      + name                   = "terraform-example-instance"
      + owner_id               = (known after apply)
      + revoke_rules_on_delete = false
      + vpc_id                 = (known after apply)
    }

Plan: 2 to add, 0 to change, 1 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value:

Plan çıktısındaki -/+ "replace" anlamına gelir. Terraform'u değiştirme yapmaya neyin zorladığını anlamak için plan çıktısında "forces replacement" metnini arayın. aws_instance kaynağındaki argümanların çoğu, değiştirilirse yenisini zorunlu kılar. Yani bu, orijinal EC2 Instance'ın terminate edileceği ve tamamen yeni bir Instance oluşturulacağı anlamına gelir. Bu, "Server Templating Tools" da tartışılan immutable altyapı paradigmasının bir örneğidir. Web sunucusunun değiştirilmesine rağmen, o web sunucusunun herhangi bir kullanıcısının downtime yaşayacağını belirtmekte fayda var. İleride Terraform ile sıfır kesinti süreli dağıtımın nasıl yapıldığını göstereceğiz.

Plan iyi göründüğü için 'yes' yazın ve Şekil 3'te gösterildiği gibi yeni EC2 Instance'ın deploy edildiğini gözlemleyin.

Şekil 3: Yeni EC2 Instance oluşturduğumuz güvenlik grubuna sahip oldu.

Yeni Instance'a tıklarsanız, public IP adresini ekranın alt kısmındaki açıklama panelinde bulabilirsiniz. Instance'ı başlatması için bir veya iki dakika bekledikten sonra ve ardından 8080 numaralı bağlantı noktasında bu IP adresine bir HTTP isteği yapmak için bir web tarayıcısı veya curl gibi bir araç kullandığınızda metnimizi görebilirsiniz:

Şekil 4: Web sunucumuz 8080 nolu portta çalışıyor

Tebrikler! Artık AWS'de çalışan basit bir web sunucunuz var!

💡
Sürpriz faturalarla karşılaşmamak için örneklerinizi tamamladığınızda terraform destroy komutu ile tüm luşturduğunuz resource'ları temizleyebilirsiniz.