/ We know how

Programowanie infrastruktury z użyciem CDK. Jak to działa z Terraform CDK?

Terraform pozwala na obsługę różnorodnej architektury, uwzględniając wiele środowisk chmury obliczeniowej oraz aplikacje GIT. Powstanie Cloud Development Kit dla Terraform poszerza zakres możliwości oraz korzyści automatyzacji zarządzania infrastrukturą. Jak działa i czym różni się od pozostałych CDK? 

 

 

Wstęp    

 

Infrastruktura jako kod (ang. Infrastructure as Code, IaC) cieszy się od kilku lat ogromnym zainteresowaniem. W tym czasie powstały i osiągnęły dojrzałość zbiory dobrych praktyk oraz narzędzi, a wokół nich zgromadziły się społeczności gotowe do dzielenia się wiedzą. Terraform Cloud Development Kit jest jednym z takich zbiorów, obok wielu innych, takich jak Pulumi, AWS CDK, CloudFormation czy Heat. 

 

Wyróżnikiem Terraform jest z pewnością obsługa bardzo dużej liczby środowisk chmurowych. Obecnie Terraform posiada ponad 1000 modułów (ang. provider, czyli plug-in umożliwiający komunikację z wybranym środowiskiem chmurowym), które pozwalają na połączenia z różnymi ekosystemami chmurowymi. Moduły pozwalają ponadto także na obsługę aplikacji typu Gitlab. Ta różnorodność wynika z faktu, że każdy może zintegrować usługę z Terraform tworząc własny moduł i udostępniając w registry.terraform.io. 

 

Terraform posiada więc liczne zalety, jednak zawiera także istotne ograniczenia. Jego składnia wraz z rozwojem automatyzacji staje się nieczytelna, ponieważ jest tworzona w formacie całkowicie deklaratywnym. Składnia nie jest językiem programowania, dlatego kiedy potrzebujemy na przykład powiązać w pętli wiele obiektów, często trzeba w tym celu budować nienaturalne i złożone konstrukcje. 

 

 

 Terraform CDK vs AWS CDK 

 

Zasadnicza różnica dotyczy oczywiście przeznaczenia: AWS CDK jest tworzony do obsługi środowiska AWS, Terraform CDK, jak wspomnieliśmy, z założenia ma charakter uniwersalny. 

 

Automatyzacja w AWS CDK wykonywana jest w dość złożony sposób. Kod tłumaczony jest na platformę automatyzacji CloudFormation, następnie AWS w swojej usłudze wykonuje CloudFormation na chmurze. Dodatkowe funkcjonalności, takie jak aws-cdk8s, czyli obsługa Kubernetesa, są wykonywane wewnątrz CloudFormation przy użyciu funkcji Lambda. Rozwiązywanie problemów wewnątrz tego łańcucha jest kłopotliwe i czasochłonne. Długi czas oczekiwania wynika ze stopnia złożoności. Dochodzą także inne problemy. Na przykład zbyt duży zbiór danych na wyjściu z kubectl/helm nie pojawi się w CloudFormation. Trzeba go wówczas szukać w CloudWatch dla odpowiedniej Lambdy. 

 

Terraform CDK wykonuje bezpośrednio zapytania na API dostawcy końcowego, np. AWS, GCP czy Gitlab, dając swobodę wyboru usługi. Oznacza to prędkość działania i mniej złożony sposób wykonywania. 

 

Wadą Terraform CDK jest za to niewątpliwie sposób przechowywania stanu. Kiedy dwie osoby, lub więcej, pracują nad automatyzacją, muszą wzajemnie wiedzieć o swoich działaniach. W tym celu przechowuje się stan w zewnętrznym, współdzielonym miejscu – trzeba o to zadbać w konfiguracji. 

W przypadku AWS CDK można uruchomić deployment i po prostu wyłączyć komputer. Dzieje się tak, ponieważ w rzeczywistości deployment wykonywany jest w chmurze AWS a wynik jest dostępny w usłudze CloudFormation. Terraform CDK ma odpowiednik takiej usługi – Terraform Cloud, które posiada także darmowy pakiet. 

 

Ostateczną przewagę Terraform, czy to w “tradycyjnej” formie, czy to z użyciem CDK jest mnogość integracji z wieloma systemami, nie tylko z AWS. Możemy jednocześnie zakładać repozytoria w GitLabie, dodawać do nich webhook’i i sekrety (ang. secrets, obiekty zawierające informacje wrażliwe np. o kluczach SSH czy tokeny Oauth) dodawać od razu do Kubernetesa. 

 

 

Terraform CDK vs Pulumi 

 

Pulumi to projekt, który w ostatnim czasie stał się popularną alternatywą dla AWS CDK. Terraform CDK jest natomiast obecnie jeszcze w fazie beta i powoli dopiero awansuje na pierwszą stabilną wersję. 

 

W przeciwieństwie do Terraform CDK i AWS CDK, Pulumi całkowicie natywnie operuje na zasobach, bez potrzeby tłumaczenia kodu do deklaracji – czy to CloudFormation w AWS, czy HCL/JSON w Terraform. Ponadto, dzięki opcjonalnej, ale domyślnie włączonej obsłudze Pulumi Service, nie ma potrzeby przechowywania plików ze stanem uruchomienia automatyzacji. Są one bowiem wysyłane na nasze konto. Zmniejsza to ryzyko kolizji kiedy wiele osób pracuje przy jednym systemie. Pulumi cechuje więc w tym wypadku wyraźnie niższy próg wejścia, niż Terraform CDK czy AWS CDK. 

 

Siła rozwiązania drzemie jednak także w popularności. Pulumi nie może na razie równać się z Terraform pod względem liczebności społeczności, co wprost przekłada się na znacząco mniejszą liczbę integracji oraz przykładów. Co prawda Terraform CDK także nie dysponuje bezpośrednio wielką liczbą przykładów, ale każdy przykład z tradycyjnego Terraform można z łatwością przełożyć na Terraform CDK. To ogromna zaleta, ponieważ na fali popularności Terraform powstało bardzo dużo artykułów o transformacji do CDK opublikowanych i dostępnych w Sieci, jak również gotowych automatyzacji. W poszukiwaniu przykładów można także przeszukiwać odpowiednie bazy wiedzy na portalach typu Stackoverflow. 

 

 

Czym jest CDK i jakie problemy rozwiązuje? 

 

Sztywne ramy, które narzuca np. CloudFormation mogą być ograniczeniem dla bardziej zaawansowanych użytkowników i zespołów DevOps. Z pomocą przychodzą wówczas narzędzia zebrane w  CDK, Cloud Development Kit. Powstanie i rozwój CDK są związane właśnie z ewolucją sposobu i zakresu wykorzystania środowisk chmurowych, nowymi potrzebami, które w związku z tym się pojawiają oraz rozwojem samych środowisk chmurowych. Po powstaniu AWS CDK, przyszedł czas także na CDK for Terraform, które rozszerza zakres tej zmiany na inne środowiska chmurowe niż AWS, ale także uwzględnia coraz powszechniejsze architektury hybrydowe. 

 

Sięgnięcie po CDK pozwala uzyskać dostęp do szeregu nowych możliwości i korzystać z zalet tego podejścia. Należy podkreślić kilka kluczowych założeń, możliwości i uwarunkowań wykorzystywania CDK: 

 

  • współdzielenie kodu jest prostsze dla wprawnej osoby z umiejętnościami programistycznymi; 
  • możliwe jest warunkowanie zakresu automatyzacji, chociażby według typu środowiska (np. rozwój/testowanie/produkcja); 
  • konfiguracja poszczególnych środowisk może być pobierana z JSON, YAML czy nawet z innego repozytorium Git, w zasadzie w dowolny sposób zaimplementowany w kodzie; 
  • możliwość tworzenia dużej ilości skomplikowanych obiektów w pętlach. 

 

Sztywne ramy i forma mają swoje zalety – początkujący użytkownik ma mniejszą możliwość zrobienia rzeczy niezgodnych z powszechną praktyką. Z czasem jednak uwidaczniają się i wady tego stanu rzeczy – sztywna forma staje się ograniczeniem dla zaawansowanych użytkowników. 

 

Terraform CDK wychodzi naprzeciw potrzebom zaawansowanych developerów, pozwalając na użycie jednego z kilku wspieranych języków programowania, takich jak Golang, Python, TypeScript, Java oraz C#. Kod wytworzony w dowolnym z tych języków programowania jest tłumaczony na plik JSON, który następnie jest wykonywany przez Terraform. 

 

Wybór języka programowania ma oczywiście znaczenie. Warto wybrać język, w którym dobrze się czujemy, ponieważ to przełoży się na sposób projektowania struktury automatyzacji. 

 

Język Golang ze swej natury umożliwia łatwiejszą pracę z wieloma repozytoriami Git równocześnie, gdyż nie wymaga publikowania współdzielonego kodu w wyznaczonym specjalnie  artifactory. To wymagałoby oczywiście zestawienia pipeline’u Continous Integration, osobnego procesu i… cierpliwości. Z drugiej strony forma językowa narzuca silne typowanie, które w przypadku parsowania, np. konfiguracji w Yaml czy JSON, może czasem okazać się dodatkowym narzutem.

Dla porównania, w języku Python łatwiej jest wczytać pliki konfiguracyjne, potrzeba do tego mniej linii kodu niż w Golang. Wymaga on jednak publikacji do artifactory kodu współdzielonego pomiędzy repozytoriami Git. W przypadku monolitycznego repozytorium z wieloma automatyzacjami sprawa wygląda prościej przy wykorzystaniu modułów Pythona. Wówczas każdy katalog z automatyzacją może być modułem – możemy wydzielić tzw. współdzielone komponenty, które będą używane w wielu automatyzacjach jako ich baza.

Język TypeScript może okazać się sprawdzić na początek dla osób, które nie miały doświadczenia programistycznego. Dziś większość przykładów, czy to dla Terraform CDK, czy dla AWS CDK jest właśnie w tym języku. 

 

Use case #1 – wielosystemowa automatyzacja
 

Zakładając potrzebę utworzenia w spójny sposób środowiska produkcyjnego w AWS opartego o EKS (Kubernetes), Gitlab.com i ArgoCD możemy skorzystać z gotowych modułów w registry.terraform.io, takich jak: 

 

 

Przykładowy scenariusz może wyglądać następująco: 

 

  1. Tworzymy klaster EKS przy pomocy modułu “aws”, łącznie z zależnymi usługami. 
  1. Po utworzeniu klastra, w osobnej automatyzacji przy pomocy modułu “gitlab” tworzymy puste repozytorium lub repozytorium z szablonu dla ArgoCD; następnie podłączamy webhook pod ustalony adres, pod którym będzie się znajdować endpoint ArgoCD, generując przy tym sekret i trzymając ten obiekt w pamięci programu. 
  1. W tej samej automatyzacji, łącznie z tworzeniem zasobów w Gitlab, tworzymy zasoby w klastrze Kubernetes używając modułu “helm”, aby zainstalować ArgoCD przekazując wcześniej zachowany w pamięci sekret webhooka. 

 

Dzięki użyciu języka programowania możemy dynamicznie wygenerować np. klucze SSH do Gitlaba i umieścić je od razu w ArgoCD czy dynamicznie zakodować hasło używając htpasswd, co jest wymogiem w ArgoCD.
 

Poniżej przykład załadowania konfiguracji w formacie YAML do automatyzacji: 

with open(„env/prod.yaml”) as f:
    self.config = yaml_load(f.read()) 

 

Poniżej przykład zakładania repozytorium w Gitlab: 

project = GitlabProject(self,
                        provider=self.gitlab,
                        id=„gitlab-repo”,
                        name=self.config[’repo_name’],
                        default_branch=„main”,
                        namespace_id=self.config[’gitlab_group_id’],
                        lifecycle=TerraformResourceLifecycle(prevent_destroy=True)) 

 

Poniżej przykładowe funkcje generujące hasło w odpowiednim formacie oraz generujące potrzebny dla ArgoCD wpis “ssh_known_hosts” 

def _create_git_known_host(self, address: str) -> str:
    return subprocess.check_output([’ssh-keyscan’, address], stderr=subprocess.DEVNULL)\
        .decode(’utf-8′).strip()

@staticmethod
def _generate_password():
    plain = .join(secrets.choice(string.ascii_letters + string.digits) for i in range(20))
    encoded = subprocess.check_output(”’
                htpasswd -bnBC 10 „” „”’ + plain + ”'” | tr -d ’:\n’ | sed 's/$2y/$2a/’
            ”’, shell=True).decode(’utf-8′)

    return plain, encoded 

  

Poniżej przykładowy fragment wartości (values) instalacyjnych dla ArgoCD poprzez Helm wewnątrz kodu Terraform CDK: 

 

„configs”: {
    „knownHosts”: {
        „data”: {
            „ssh_known_hosts”: f”””
            {self._create_git_known_host(self.config[‘gitlab_domain’])}
            {self._create_git_known_host(„github.com”)}
            „””,
        }
    },
    „repositories”: {
        „cluster-root”: {
            „url”: f”git@{self.config[‘gitlab_domain’]}/{self.config[‘repo_group_name’]}/{self.config[‘repo_name’]}.git”
        }
    },
    „credentialTemplates”: {
        „ssh-creds”: {
            „url”: self._git_address,
            „sshPrivateKey”: self._get_private_key(),
        }
    },
    „secret”: {
        „argocdServerAdminPassword”: encoded,
        „gitlabSecret”: gitlab_secret_encoded,
    }
} 

 

CDK pozwala na łatwe uzupełnianie dowolnych danych dzięki możliwości wstawiania dynamicznych wartości. Bez CDK wymagałoby to tworzenia nieczytelnych skryptów bashowych lub manualnego podawania parametrów przez operatora. 

 

 

 Use case #2 – monitoring EC2 oraz zewnętrznych usług w oparciu o AWS 

 

Tworzenie wielu powtarzalnych zasobów to idealny przykład użycia języka programowania, w którym możemy skorzystać nie tylko z pętli, ale także z instrukcji warunkowych. Pozwalają one na tworzenie na dużą skalę zasobów różniących się od siebie parametrami. 

 

Jako przykład niech posłuży prosty monitoring przy użyciu usług AWS Route53, AWS SNS oraz AWS CloudWatch. Utworzone Health Check’i w Route53 tworzą metryki w CloudWatch. Następnie Alarmy, utworzone w CloudWatch, zapisują do AWS SNS informację, czy monitorowana usługa jest “healthy”. Na koniec, usługa AWS ChatBot odczytuje zapisaną informację z AWS SNS i wysyła powiadomienie na wskazany kanał w komunikatorze Slack. 

 

Zakładając więc, że w pliku konfiguracyjnym w formacie YAML mamy definicję health check’ów wykonywanych przez usługę w AWS:
 

checks:
    – name: „google”
      address: google.com
      port: 443
      endpoint: /
      failureThreshold: 2
      interval: 30
      searchString: “”
      type: HTTPS 

 

Spójrzmy na przykład implementujący tworzenie zasobów opisanych powyżej: 

 

def create_health_checks(self):
    cloudwatch_arns = []

    for check in self.config[’checks’]:
        check = Route53HealthCheck(
            self,
            f’{check.get(„name”)}-healthcheck’,
            fqdn=check.get(’address’),
            resource_path=check.get(’endpoint’),
            tags={
                ’Name’: f’{self.name}{check.get(„address”)}
            },
            port=int(check.get(’port’)),
            type=check.get(’type’),
            search_string=check.get(’searchString’) if check.get(’searchString’) else None,
            failure_threshold=int(check.get(’failureThreshold’)),
            request_interval=int(check.get(’interval’)),
            provider=self.provider
        )
        topic = SnsTopic(
            self,
            f’{check[„name”]}-topic’,
            name=f’{self.name}{check.get(„name”)},
            provider=self.provider
        )
        cloudwatch_arns.append(topic.arn)
        CloudwatchMetricAlarm(
            self,
            f’{check[„name”]}-alarm’,
            ok_actions=[topic.arn],
            dimensions={
                ’HealthCheckId’: check.id
            },
            alarm_name=f’{self.name}{check[„name”]},
            comparison_operator=’LessThanThreshold’,
            evaluation_periods=1,
            metric_name=’HealthCheckStatus’,
            namespace=’AWS/Route53′,
            period=60,
            statistic=’Minimum’,
            threshold=1,
            insufficient_data_actions=[],
            alarm_actions=[topic.arn],
            provider=self.provider
        )

    with open(’chatbot-assume-role-policy.json’, ’r’) as assume_role_policy:
        with open(’chatbot-iam-policy.json’, ’r’) as iam_policy:
            role = IamRole(
                self,
                f’{self.name}-chatbot-sns-role’,
                name=f’{self.name}-chatbot-sns-role’,
                assume_role_policy=assume_role_policy.read(),
                inline_policy=[
                    IamRoleInlinePolicy(
                        name=f’{self.name}-chatbot-sns-policy’,
                        policy=iam_policy.read()
                    )
                ],
            )

    Chatbot(
        self,
        ’chatbot’,
        configuration_name=f’{self.name}-to-slack’,
        slack_channel_id=„MYSLACKID”,
        slack_workspace_id=„MYWORKSPACEID”,
        iam_role_arn=role.arn,
        sns_topic_arns=cloudwatch_arns
    ) 

 

 

Podsumowanie 

 

Zarządzanie infrastrukturą przy pomocy kodu jest możliwe przy użyciu kilku dostępnych narzędzi.  Wykorzystują one najczęściej stosowane przez DevOps’ów języki programowania. Wykorzystanie  CDK samo w sobie przynosi wiele korzyści. Ma też swoje wady, takie jak większa złożoność czy ryzyko zaprojektowania automatyzacji niezgodnej z przyjętymi standardami. Przy wdrożeniu CDK warto mieć w zespole przynajmniej jedną osobę z kompetencjami programistycznymi, która pomoże w zaprojektowaniu struktury i właściwym stosowaniu wzorców programowania. To pozwoli na utrzymanie projektu w powszechnie znanym standardzie. Przyniesie to wiele korzyści, a jedną z nich będzie i to, że próg wejścia do zespołu dla nowych osób nie będzie zbyt wysoki.