AWS Fargate ve Github Actions ile FastAPI CI/CD Pipeline

10 minute read

Published:

Giriş

Bu yazıda bir FastAPI uygulamasını AWS Fargate üzerinde çalıştırmak için GitHub Actions kullanarak CI/CD pipeline oluşturacağız. AWS Fargate Docker konteynerlerini çalıştırmak için kullanılan serverless bir servistir. GitHub Actions ise GitHub üzerindeki repository’lerdeki işlemleri otomatize etmek için kullanılan bir CI/CD uygulamasıdır.

Github Actions’ın detayları için daha önce yazdığım MLOps Nedir? Github Actions ile CML yazısına göz atabilirsiniz.

Github Actions kullandığım diğer örnekler 👇🏻

Peki nedir bu CI/CD akışı (pipeline)? CI/CD, Continuous Integration ve Continuous Deployment (bazen de continous development) kelimelerinin baş harflerinden oluşur. CI/CD pipeline, yazılım geliştirme döngüsünde (software development cycle) yazılımın test edilmesi, paketlenmesi ve dağıtılmasını otomatize eden bir süreçtir. Bu şekilde uygulamanın hızlı, hatasız, test edilebilir, ve tekrarlanabilir bir şekilde canlıya alınması sağlanır.


Görsel Kaynağı: A Crash Course in CI/CD

Kodun derlenmesi, test edilmesi, paketlenmesi ve dağıtılması gibi farklı aşama ve işlemler olduğu için bu işleri yapan bir hayli farklı araçlar ve servisler de bulunmaktadır.


Görsel Kaynağı: A Crash Course in CI/CD

CI ve CD Akışlarında Kullanılan Araçlar

Aşağıda CI ve CD süreçlerinde kullanılan araçları farklı aşamalardaki kullanım amaçlarına göre gruplanmış olarak bulabilirsiniz 👇🏻

Versiyon Kontrol Sistemleri

Bu araçlar, kodun merkezi bir yerde tutulmasını ve sürüm yönetimini sağlar.

  • Git: Sürüm kontrol sistemi (version control).
  • GitHub, GitLab, Bitbucket: Git tabanlı kod depolama platformları.

CI Araçları

Kodun entegrasyonu, derlenmesi ve test edilmesi için kullanılır:

  • CodePipeline, Jenkins, GitLab CI/CD, GitHub Actions, Travis CI, CircleCI, Azure DevOps.

Kod Derleme Araçları

Kodun çalıştırılabilir bir forma dönüştürülmesi için kullanılır:

  • Maven, Gradle, Ant, Make.

Test Otomasyon Araçları

Kodun doğruluğunu kontrol etmek için otomatik testler yapılır:

  • Selenium, JUnit, Postman, pytest.

CD Araçları

Kodun staging veya canlı ortama aktarılması için kullanılır:

  • AWS CodePipeline, Azure DevOps Pipelines, Argo CD.

Konteyner ve Orkestrasyon Araçları

Uygulamaları konteynerlere paketlemek ve yönetmek için kullanılır:

  • Docker, Kubernetes, Helm, OpenShift

İzleme Araçları

Canlıdaki uygulamaların performansını izlemek ve hata takibi yapmak için:

  • CloudWatch, Prometheus, Grafana, New Relic, Datadog, Sentry.

Konfigürasyon Yönetim Araçları

Ortam ve uygulama yapılandırmasını yönetmek için:

  • Ansible, Puppet, Chef, Terraform

Uygulama

Uygulamanın mermaid akışı (flowchart) 👇🏻

Akış 🔓
graph TD

    subgraph Developer
        A[Developer - Local Env.] -->|Push Code| B[GitHub Repository]
    end

    subgraph GitHub Actions
        B -->|Trigger| C[Build Docker Image]
        C -->|Push| D[AWS ECR]
    end

    subgraph AWS Infra
        D -->|Pull Image| E[ECS Fargate]
        E -->|Deploy| F[Task Definition]
        F -->|Run in| G[VPC]
        G -->|Route through| H[Internet Gateway]
        H -->|Connect to| I[FastAPI App]
        E -->|Log to| J[CloudWatch]
    end

    subgraph Users
        N[End Users] -->| | I
    end

FastAPI Uygulaması

İlk olarak en basit FastAPI uygulamasını oluşturalım.

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"message": "Hello World"}

Uygulama tek bir endpoint’e sahip ve bu endpoint’e yapılan GET isteğine “Hello World” mesajı döndürüyor.

Şimdi bu uygulamayı dockerize edelim.

WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt

COPY app.py .

CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "80"]
# requirements.txt
fastapi
uvicorn

Uygulamamız dockerize edildi ve çalıştırmaya hazır. Şimdi kodun CI ve CD süreçlerini otomatize etmek için GitHub Actions kullanacağız. Bu işlem için bir repo oluşturuyoruz ve bu repo’ya gerekli workflow dosyamızı yml formatında ekliyoruz.

GitHub Actions Workflow

name: Deploy to AWS

# tetiklenme koşulu
# master branch'ine yapılan her push işlemi sonrasında çalışacak
on:
  push:
    branches: master

# AWS için environment değişkenleri
# bu değişkenlerin Fargate'i ayağa kaldırırken aynı olması gerekiyor
env:
  AWS_REGION: eu-central-1
  ECR_REPOSITORY: fastapi-app
  ECS_SERVICE: fastapi-service
  ECS_CLUSTER: fastapi-cluster
  CONTAINER_NAME: fastapi-container

# deployment işlemleri
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2

    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: $
        aws-secret-access-key: $
        aws-region: $

    - name: Login to Amazon ECR
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v1

    - name: Build and push Docker image
      env:
        ECR_REGISTRY: $
        IMAGE_TAG: latest
      run: |
        docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
        docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG

    - name: Update ECS service
      run: |
        aws ecs update-service --cluster $ECS_CLUSTER \
                              --service $ECS_SERVICE \
                              --force-new-deployment

Bu workflow master branch’ine yapılan her push işlemi sonrasında çalışacak ve aşağıdaki işlemleri gerçekleştirecek:

  • AWS kimlik bilgilerini tanımlayacak (Configure AWS credentials).
  • Amazon ECR’a giriş yapacak (Login to Amazon ECR).
  • Docker imajını oluşturacak ve Amazon ECR’a push’layacak (Build and push Docker image).
  • ECS servisini güncelleyecek ve yeni bir versiyon ile canlıya alacak (Update ECS service).

Bu işlem ile Github Actions aşaması da tamamlanmış oldu. Şimdi bu uygulamayı AWS Fargate üzerinde çalıştırmak için gerekli adımları gerçekleştireceğiz.

AWS Fargate

AWS Fargate, Amazon Elastic Container Service (ECS) ve Amazon Elastic Kubernetes Service (EKS) ile birlikte çalışan konteynerleri yönetmeye yarayan serverless bir çalışma ortamıdır. Kullanıcıların altyapıyı yönetme gereksinimini ortadan kaldırır ve sadece konteynerlerin çalıştırılmasına odaklanır. Temel olarak 👇🏻

  • Sunucusuz: Altındaki altyapıyı AWS yönetir, kullanıcı yalnızca konteynerleri tanımlar.
  • Esnek: İhtiyaç duyulan kaynakları (CPU, bellek) esnek bir şekilde ayarlama.
  • Güvenli: AWS’in yerleşik güvenlik mekanizmalarından yararlanır.
  • Ölçeklenebilir: Talebe göre otomatik ölçeklendirme sağlar.

Ben geliştirmeyi her zamankinden farklı olarak PowerShell üzerinden yaptığm için bazı aws-cli komutları farklılık gösterebilir.

İlk olarak ecsTaskExecutionRole adında bir IAM rolü oluşturuyoruz. Bu rol ECS servislerinin çalıştırılması için gerekli izinleri sağlıyor.

Daha önce ECS servislerini çalıştırmak için bir IAM rolü oluşturduysanız bu adımı atlayabilirsiniz.

$trustPolicy = @"
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs-tasks.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
"@
$trustPolicy | Out-File -FilePath "trust-policy.json"

aws iam create-role --role-name ecsTaskExecutionRole --assume-role-policy-document file://trust-policy.json

aws iam attach-role-policy --role-name ecsTaskExecutionRole --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

Daha sonra Fargate’in çalışacağı bir VPC oluşturuyoruz. Eğer hali hazırda bir VPC’niz varsa bu adımı atlayabilirsiniz.

Aşağıdaki kod parçacığını basitçe özetlemek gerekirse; ilk olarak bir VPC (10.0.0.0/16) oluşturuyoruz. Daha sonra bu VPC içinde iki subnet (10.0.1.0/24 & 10.0.2.0/24) oluşturuyoruz. Bu subnet’ler farklı availability zone’larında olacak şekilde oluşturuluyor. Daha sonra bu subnet’leri public hale getiriyoruz ve internete çıkış yapabilmeleri için bir internet gateway oluşturuyoruz. Son olarak bu internet gateway‘i VPC’ye ekliyoruz (attach) ve bir route table oluşturup bu route table’ı subnet’lerle ilişkilendiriyoruz. Böylece subnet’ler internete çıkış yapabilecek (public subnet) hale geliyor. Bununla birlikte dışarıdan tüm trafiğin (HTTP/80) gelmesine izin veren bir security group oluşturuyoruz.

$VPC_ID = aws ec2 create-vpc --cidr-block 10.0.0.0/16 --query 'Vpc.VpcId' --output text

$SUBNET1_ID = aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.1.0/24 --availability-zone eu-central-1a --query 'Subnet.SubnetId' --output text
$SUBNET2_ID = aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.2.0/24 --availability-zone eu-central-1b --query 'Subnet.SubnetId' --output text

$SG_ID = aws ec2 create-security-group --group-name fastapi-sg --description "Security group for FastAPI" --vpc-id $VPC_ID --query 'GroupId' --output text

aws ec2 authorize-security-group-ingress --group-id $SG_ID --protocol tcp --port 80 --cidr 0.0.0.0/0

$IGW_ID = aws ec2 create-internet-gateway --query 'InternetGateway.InternetGatewayId' --output text
aws ec2 attach-internet-gateway --vpc-id $VPC_ID --internet-gateway-id $IGW_ID

$RT_ID = aws ec2 create-route-table --vpc-id $VPC_ID --query 'RouteTable.RouteTableId' --output text
aws ec2 create-route --route-table-id $RT_ID --destination-cidr-block 0.0.0.0/0 --gateway-id $IGW_ID

$SUBNET_IDS = @("subnet-id1", "subnet-id2")
foreach ($SUBNET_ID in $SUBNET_IDS) {
    aws ec2 associate-route-table --subnet-id $SUBNET_ID --route-table-id $RT_ID
    aws ec2 modify-subnet-attribute --subnet-id $SUBNET_ID --map-public-ip-on-launch
}

VPC, subnet’ler, security group ve route table oluşturulduktan sonra AWS servis kullanıcısını oluşturup gerekli izinleri tanımlıyoruz.

Servis Kullanıcısı ve İzinler

İzin vereceğimiz servisler;

  • ECR (Container Registry)
  • ECS (Container Service)
  • IAM (Role operations)
  • CloudWatch Logs
  • VPC
// github-actions-policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ecr:GetAuthorizationToken",
                "ecr:BatchCheckLayerAvailability",
                "ecr:GetDownloadUrlForLayer",
                "ecr:GetRepositoryPolicy",
                "ecr:DescribeRepositories",
                "ecr:ListImages",
                "ecr:DescribeImages",
                "ecr:BatchGetImage",
                "ecr:InitiateLayerUpload",
                "ecr:UploadLayerPart",
                "ecr:CompleteLayerUpload",
                "ecr:PutImage"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ecs:ListClusters",
                "ecs:ListTaskDefinitions",
                "ecs:ListServices",
                "ecs:DescribeServices",
                "ecs:UpdateService",
                "ecs:RegisterTaskDefinition"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "iam:PassRole"
            ],
            "Resource": [
                "arn:aws:iam::*:role/ecsTaskExecutionRole"
            ]
        }
    ]
}
aws iam create-user --user-name github-actions

aws iam create-policy --policy-name github-actions-policy --policy-document file://github-actions-policy.json
aws iam attach-user-policy --user-name github-actions --policy-arn arn:aws:iam::<AWS-ID>:policy/github-actions-policy

aws iam create-access-key --user-name github-actions

Bu işlemler sonucunda oluşturduğumuz servis kullanıcısına ait kimlik bilgilerini (secret key ve access key) GitHub Actions’ta kullanmak üzere kaydediyoruz.

CloudWatch Log’larının Oluşturulması

Bu aşama aşağıdaki hatanın alınmasına karşın. Eğer Fargate deployment’ı sırasında böyle bir hata almıyorsanız bu adımı atlayabilirsiniz.

ResourceInitializationError: failed to validate logger args: create stream has been retried 1 times: failed to create Cloudwatch log stream: ResourceNotFoundException: The specified log group does not exist. : exit status 1.

aws logs create-log-group --log-group-name /ecs/fastapi-task

$logPolicy = @"
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:eu-central-1:<AWS-ID>:log-group:/ecs/fastapi-task:*"
        }
    ]
}
"@
$logPolicy | Out-File -FilePath "log-policy.json"

aws iam put-role-policy --role-name ecsTaskExecutionRole --policy-name cloudwatch-logs-policy --policy-document file://log-policy.json

ECR Repository Oluşturma

Sırasıyla ECR repository ve ECS cluster oluşturuyoruz.

aws ecr create-repository --repository-name fastapi-app
aws ecs create-cluster --cluster-name fastapi-cluster

Task definition Oluşturulması:

//  task-definition.json
{
    "family": "fastapi-task",
    "networkMode": "awsvpc",
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "cpu": "256",
    "memory": "512",
    "executionRoleArn": "arn:aws:iam::<AWS-ID>:role/ecsTaskExecutionRole",
    "containerDefinitions": [
        {
            "name": "fastapi-container",
            "image": "<ECR_URI>:latest",
            "portMappings": [
                {
                    "containerPort": 80,
                    "protocol": "tcp"
                }
            ],
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "/ecs/fastapi-task",
                    "awslogs-region": "eu-central-1",
                    "awslogs-stream-prefix": "ecs"
                }
            }
        }
    ]
}

Yukarıda oluşturduğumuz task definition’ı task-definition.json adıyla kaydettikten sonra aşağıdaki komut ile task definition’ı ECS’e kaydediyoruz.

aws ecs register-task-definition --cli-input-json file://task-definition.json

Son olarak oluşturduğumuz task definition’ı kullanarak bir ECS servisi oluşturuyoruz.

aws ecs create-service \ 
--cluster fastapi-cluster \ 
--service-name fastapi-service \ 
--task-definition fastapi-task \ 
--desired-count 1 \ 
--launch-type FARGATE \ 
--network-configuration "awsvpcConfiguration={subnets=[subnet-id1,subnet-id2],securityGroups=[sg-id],assignPublicIp=ENABLED}"

Tüm bu işlemlerden sonra FastAPI uygulamamız AWS Fargate üzerinde çalışmaya başlamış olacak. Fargate servisini başlatmadan önce uygulamamızın Docker imajını ECR’a push’lamayı unutmayın.

Tüm bu aşamalardan sonra GitHub Actions ile CI/CD pipeline’ımızı kullanarak uygulamamızı güncelleyebilir ve canlıya alabiliriz. Buna ek olarak uygulamamızın testi için testler ekleyebilir ve bu testleri CI pipeline’ımıza entegre edebiliriz. Bu şekilde testler başarılı bir şekilde geçtiğinde uygulamamızın canlıya alınmasını sağlayabiliriz.

Bir dipnot: uygulama her push sonrasında bir sunucuyu kapatıp yenisini açacak. Bu durumda uygulama sürekli ayakta kalmasına karşın her defasında yeni bir IP ile canlıya alınacak. Bunu önlemek için Application Load Balancer (ALB) kullanabiliriz.

Actions çalışması sonucunda log’lar içeresinde AWS kimlik bilgileri, region, ECR adresi gibi bilgiler yer aldığı için diğer yazılarımdan farklı olarak bu yazıda Github repo’su paylaşmayacağım. Komutların tamamına ulaşmak isterseniz burada bulunan Gist’e göz atabilirsiniz.

Bu yazıda bugüne kadar olan yazılarımdan farklı olarak neredeyse hiç ML/DS konusuna değinmedim (%100 DevOps oldu 😅). Bu demek değil ki bu yazıdaki uygulama ML akışları için kullanılamaz. Tam tersine servis aşamasına gelmiş herhangi bir ML modeli deploy edebilir ve bu modeli CI/CD pipeline’ınıza entegre edebilirsiniz. Örneğin her yeniden eğitim (retraining) sonrasında modeli bu akış ile canlıya alıp modeli güncelleyebilrsiniz.

Ekran Görüntüleri 🚀

  • Github Actions Deploy Job’ı

  • Github Actions Deploy Süresi

  • Fargate Tasks
    • Deployment sonrasında eskisi kapatılıp yeni bir task başlatılıyor.

  • Son olarak da canlıda çalışan sunucuya Postman ile yapılan bir istek 👇🏻