🏗️ Terraform-Integration¶
Polycrate integriert sich nahtlos mit Terraform für Infrastructure-as-Code und Cloud-Provisioning. Mit Polycrate können Sie Terraform-Workflows in Blocks kapseln, State-Files verwalten und Terraform-Outputs als Artifacts für andere Blocks bereitstellen.
Was ist Terraform?¶
Terraform ist ein Open-Source Infrastructure-as-Code Tool von HashiCorp, das es ermöglicht, Infrastruktur mit deklarativen Konfigurationsdateien zu definieren und zu verwalten. Terraform unterstützt:
- Multi-Cloud: AWS, Azure, GCP, DigitalOcean, und viele mehr
- Deklarative Syntax: Beschreiben Sie den gewünschten Zustand, nicht die Schritte
- State-Management: Terraform verfolgt den aktuellen Zustand Ihrer Infrastruktur
- Plan-before-Apply: Vorschau der Änderungen vor Ausführung
- Resource-Graph: Automatische Abhängigkeitserkennung und parallele Ausführung
Terraform in Polycrate¶
graph TB
Block[Terraform-Block] -->|führt aus| TF[Terraform CLI]
TF -->|liest| Config[*.tf Dateien]
TF -->|verwaltet| State[State-File]
State -->|gespeichert in| Artifacts[Artifacts]
TF -->|provisioniert| Cloud[Cloud-Ressourcen]
Cloud -->|AWS| EC2[EC2, VPC, S3, ...]
Cloud -->|Azure| AzureRes[VMs, VNets, ...]
Cloud -->|GCP| GCPRes[GCE, GKE, ...]
TF -->|generiert| Output[Terraform-Output]
Output -->|konvertiert zu| Inventory[Ansible-Inventory]
Inventory -->|genutzt von| AnsibleBlock[Ansible-Block]
style Block fill:#e1f5ff
style State fill:#ffe1e1
style Cloud fill:#fff4e1 Terraform-Actions in Blocks¶
Basis-Setup¶
# blocks/infrastructure/block.poly
name: infrastructure
kind: generic
version: 1.0.0
actions:
- name: init
description: Initialize Terraform
script:
- cd terraform/
- terraform init
- name: plan
description: Plan Terraform changes
script:
- cd terraform/
- terraform plan
- name: apply
description: Apply Terraform changes
script:
- |
cd terraform/
terraform apply -auto-approve
# State als Artifact speichern
ARTIFACTS="{{ .workspace.config.artifacts_root }}/blocks/{{ .block.name }}"
cp terraform.tfstate $ARTIFACTS/terraform.tfstate
cp terraform.tfstate.backup $ARTIFACTS/terraform.tfstate.backup 2>/dev/null || true
- name: destroy
description: Destroy Terraform-managed infrastructure
prompt:
message: "Are you sure you want to destroy the infrastructure?"
script:
- cd terraform/
- terraform destroy -auto-approve
Terraform-Dateien im Block¶
blocks/infrastructure/
├── block.poly
├── terraform/
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ └── providers.tf
└── scripts/
└── generate-inventory.py
Terraform-Konfiguration¶
Beispiel: AWS EC2-Instanzen¶
terraform/main.tf:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}
resource "aws_instance" "web" {
count = var.instance_count
ami = var.ami_id
instance_type = var.instance_type
tags = {
Name = "web-${count.index + 1}"
Environment = var.environment
ManagedBy = "Polycrate"
}
key_name = var.ssh_key_name
vpc_security_group_ids = [aws_security_group.web.id]
}
resource "aws_security_group" "web" {
name = "web-sg"
description = "Security group for web servers"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
terraform/variables.tf:
variable "aws_region" {
description = "AWS region"
type = string
default = "eu-central-1"
}
variable "instance_count" {
description = "Number of EC2 instances"
type = number
default = 2
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
variable "ami_id" {
description = "AMI ID for EC2 instances"
type = string
}
variable "ssh_key_name" {
description = "SSH key pair name"
type = string
}
variable "environment" {
description = "Environment name"
type = string
default = "production"
}
terraform/outputs.tf:
output "instance_ids" {
description = "IDs of EC2 instances"
value = aws_instance.web[*].id
}
output "instance_public_ips" {
description = "Public IPs of EC2 instances"
value = aws_instance.web[*].public_ip
}
output "instance_private_ips" {
description = "Private IPs of EC2 instances"
value = aws_instance.web[*].private_ip
}
output "security_group_id" {
description = "Security group ID"
value = aws_security_group.web.id
}
# Output als Ansible-Inventory-Format
output "ansible_inventory" {
description = "Ansible inventory in JSON format"
value = {
all = {
hosts = {
for idx, instance in aws_instance.web : "web-${idx + 1}" => {
ansible_host = instance.public_ip
ansible_user = "ubuntu"
ansible_ssh_private_key_file = "{{ .workspace.config.ssh_private_key }}"
private_ip = instance.private_ip
}
}
children = {
webservers = {
hosts = {
for idx, instance in aws_instance.web : "web-${idx + 1}" => {}
}
}
}
}
}
}
Terraform-Output als Inventory¶
Output generieren und speichern¶
actions:
- name: apply
script:
- cd terraform/
- terraform apply -auto-approve
- name: generate-inventory
description: Generate Ansible inventory from Terraform output
script:
- |
cd terraform/
ARTIFACTS="{{ .workspace.config.artifacts_root }}/blocks/{{ .block.name }}"
# Terraform-Output als JSON
terraform output -json ansible_inventory | \
jq '.' > $ARTIFACTS/inventory.json
# Konvertieren zu YAML (optional)
python3 ../scripts/json-to-yaml.py $ARTIFACTS/inventory.json > $ARTIFACTS/inventory.yml
echo "Inventory generated: $ARTIFACTS/inventory.yml"
Python-Script für Konvertierung¶
scripts/json-to-yaml.py:
#!/usr/bin/env python3
import json
import yaml
import sys
with open(sys.argv[1], 'r') as f:
data = json.load(f)
# Terraform-Output hat zusätzliches "value"-Wrapper
if 'value' in data:
data = data['value']
print(yaml.dump(data, default_flow_style=False))
Inventory von anderen Blocks nutzen¶
# Block: configure-servers
name: configure-servers
kind: linuxapp
inventory:
from: infrastructure # Nutzt von Terraform generiertes Inventory
actions:
- name: install
playbook: playbooks/setup-webservers.yml
State-Management¶
State als Artifact speichern¶
Terraform-State-Files sollten persistent in Artifacts gespeichert werden:
actions:
- name: apply
script:
- |
cd terraform/
ARTIFACTS="{{ .workspace.config.artifacts_root }}/blocks/{{ .block.name }}"
# State aus Artifacts laden (falls vorhanden)
if [ -f "$ARTIFACTS/terraform.tfstate" ]; then
cp $ARTIFACTS/terraform.tfstate .
fi
# Terraform anwenden
terraform init
terraform apply -auto-approve
# State zurück in Artifacts speichern
cp terraform.tfstate $ARTIFACTS/
cp terraform.tfstate.backup $ARTIFACTS/ 2>/dev/null || true
- name: destroy
script:
- |
cd terraform/
ARTIFACTS="{{ .workspace.config.artifacts_root }}/blocks/{{ .block.name }}"
# State laden
if [ -f "$ARTIFACTS/terraform.tfstate" ]; then
cp $ARTIFACTS/terraform.tfstate .
fi
terraform destroy -auto-approve
# State aktualisieren
cp terraform.tfstate $ARTIFACTS/
Remote State (Empfohlen)¶
Für Team-Umgebungen sollten Sie Terraform Remote State verwenden:
terraform/backend.tf:
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "infrastructure/terraform.tfstate"
region = "eu-central-1"
# State-Locking mit DynamoDB
dynamodb_table = "terraform-locks"
encrypt = true
}
}
Terraform + Ansible Workflow¶
Typischer Workflow: Terraform provisioniert Infrastruktur, Ansible konfiguriert Server.
sequenceDiagram
participant User
participant TF as Terraform-Block
participant Artifacts
participant Ansible as Ansible-Block
participant Cloud
User->>TF: polycrate run infrastructure apply
TF->>Cloud: Provisioniere VMs
Cloud-->>TF: VMs erstellt (IPs)
TF->>Artifacts: Speichere Inventory
TF->>Artifacts: Speichere State
TF-->>User: Fertig
User->>Ansible: polycrate run configure-servers install
Ansible->>Artifacts: Lade Inventory
Artifacts-->>Ansible: Inventory mit IPs
Ansible->>Cloud: Konfiguriere VMs
Ansible-->>User: Fertig Workflow-Definition in workspace.poly:
name: my-workspace
blocks:
- name: infrastructure
# ...
- name: configure-servers
# ...
- name: my-app
# ...
workflows:
- name: provision-and-configure
steps:
- name: terraform-plan
block: infrastructure
action: plan
- name: terraform-apply
block: infrastructure
action: apply
prompt:
message: "Apply Terraform changes?"
- name: generate-inventory
block: infrastructure
action: generate-inventory
- name: configure-servers
block: configure-servers
action: install # Nutzt generiertes Inventory
- name: deploy-application
block: my-app
action: install
Ausführen:
Terraform-Variablen aus Block-Config¶
Nutzen Sie Snapshot-Variablen für Terraform-Variablen:
# block.poly
name: infrastructure
kind: generic
config:
aws:
region: eu-central-1
instance_type: t3.micro
instance_count: 3
environment: production
actions:
- name: apply
script:
- |
cd terraform/
# Variablen aus Block-Config als TF_VAR_* Environment-Variablen
export TF_VAR_aws_region="{{ .block.config.aws.region }}"
export TF_VAR_instance_type="{{ .block.config.aws.instance_type }}"
export TF_VAR_instance_count="{{ .block.config.aws.instance_count }}"
export TF_VAR_environment="{{ .block.config.environment }}"
terraform apply -auto-approve
Oder mit auto.tfvars:
actions:
- name: apply
script:
- |
cd terraform/
# terraform.auto.tfvars generieren
cat > terraform.auto.tfvars <<EOF
aws_region = "{{ .block.config.aws.region }}"
instance_type = "{{ .block.config.aws.instance_type }}"
instance_count = {{ .block.config.aws.instance_count }}
environment = "{{ .block.config.environment }}"
EOF
terraform apply -auto-approve
Kubernetes-Cluster mit Terraform¶
EKS-Cluster (AWS)¶
# terraform/main.tf
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "~> 19.0"
cluster_name = var.cluster_name
cluster_version = "1.28"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
eks_managed_node_groups = {
default = {
min_size = 2
max_size = 4
desired_size = 2
instance_types = ["t3.medium"]
}
}
}
output "kubeconfig" {
description = "Kubeconfig for EKS cluster"
value = module.eks.kubeconfig
sensitive = true
}
Kubeconfig generieren:
actions:
- name: create-cluster
script:
- cd terraform/
- terraform apply -auto-approve
- name: generate-kubeconfig
script:
- |
cd terraform/
ARTIFACTS="{{ .workspace.config.artifacts_root }}/blocks/{{ .block.name }}"
# Kubeconfig von Terraform-Output
terraform output -raw kubeconfig > $ARTIFACTS/kubeconfig.yml
# Oder mit AWS CLI
aws eks update-kubeconfig \
--region {{ .block.config.aws.region }} \
--name {{ .block.config.cluster_name }} \
--kubeconfig $ARTIFACTS/kubeconfig.yml
echo "Kubeconfig saved: $ARTIFACTS/kubeconfig.yml"
Apps deployen zu erstelltem Cluster:
# Block: my-k8s-app
name: my-k8s-app
kind: k8sapp
kubeconfig:
from: eks-cluster # Nutzt Kubeconfig von Terraform-Block
config:
namespace: production
actions:
- name: install
playbook: playbooks/deploy-helm.yml
Best Practices¶
1. Separate State per Environment¶
blocks/
├── infrastructure-dev/
│ ├── block.poly
│ └── terraform/
│ ├── main.tf
│ └── backend-dev.tf
└── infrastructure-prod/
├── block.poly
└── terraform/
├── main.tf
└── backend-prod.tf
2. Plan before Apply¶
Immer erst plan ausführen:
3. Terraform-Module nutzen¶
Verwenden Sie Terraform-Module für Wiederverwendbarkeit:
module "web_servers" {
source = "./modules/web-server"
count = var.instance_count
instance_type = var.instance_type
environment = var.environment
}
4. State-Locking¶
Nutzen Sie Remote State mit Locking für Team-Arbeit:
terraform {
backend "s3" {
bucket = "terraform-state"
key = "prod/terraform.tfstate"
region = "eu-central-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
5. Secrets-Management¶
Niemals Secrets im Code:
actions:
- name: apply
script:
- |
# Secrets von externen Quellen laden
export TF_VAR_db_password=$(aws secretsmanager get-secret-value \
--secret-id prod/database/password \
--query SecretString \
--output text)
cd terraform/
terraform apply -auto-approve
6. Terraform-Output für Monitoring¶
output "monitoring_endpoints" {
value = {
prometheus = "http://${aws_instance.prometheus.public_ip}:9090"
grafana = "http://${aws_instance.grafana.public_ip}:3000"
}
}
Vollständiges Beispiel¶
Multi-Cloud Infrastructure¶
# blocks/multi-cloud-infra/block.poly
name: multi-cloud-infra
kind: generic
version: 1.0.0
config:
aws:
region: eu-central-1
instance_count: 2
azure:
location: westeurope
vm_count: 1
actions:
- name: plan
description: Plan infrastructure changes
script:
- cd terraform/
- terraform init
- terraform plan
- name: apply
description: Apply infrastructure changes
prompt:
message: "Apply changes to AWS and Azure?"
script:
- |
cd terraform/
ARTIFACTS="{{ .workspace.config.artifacts_root }}/blocks/{{ .block.name }}"
# State laden
[ -f "$ARTIFACTS/terraform.tfstate" ] && cp $ARTIFACTS/terraform.tfstate .
# Variablen setzen
export TF_VAR_aws_region="{{ .block.config.aws.region }}"
export TF_VAR_aws_instance_count="{{ .block.config.aws.instance_count }}"
export TF_VAR_azure_location="{{ .block.config.azure.location }}"
export TF_VAR_azure_vm_count="{{ .block.config.azure.vm_count }}"
# Apply
terraform apply -auto-approve
# State speichern
cp terraform.tfstate $ARTIFACTS/
cp terraform.tfstate.backup $ARTIFACTS/ 2>/dev/null || true
- name: generate-inventory
description: Generate Ansible inventory
script:
- |
cd terraform/
ARTIFACTS="{{ .workspace.config.artifacts_root }}/blocks/{{ .block.name }}"
terraform output -json all_hosts | \
python3 ../scripts/tf-to-inventory.py > $ARTIFACTS/inventory.yml
echo "Inventory: $ARTIFACTS/inventory.yml"
- name: destroy
description: Destroy infrastructure
prompt:
message: "DESTROY all infrastructure in AWS and Azure?"
script:
- |
cd terraform/
ARTIFACTS="{{ .workspace.config.artifacts_root }}/blocks/{{ .block.name }}"
[ -f "$ARTIFACTS/terraform.tfstate" ] && cp $ARTIFACTS/terraform.tfstate .
terraform destroy -auto-approve
cp terraform.tfstate $ARTIFACTS/
Zusammenhang mit anderen Konzepten¶
- Artifacts: Terraform-State und Outputs als Artifacts
- Ansible: Terraform provisioniert, Ansible konfiguriert
- Workflows: Orchestrierung von Terraform-Operationen
- Snapshot: Block-Config als Terraform-Variablen
- Kubernetes: Kubernetes-Cluster mit Terraform erstellen
Terraform als Provisioner
Nutzen Sie Terraform für Infrastruktur-Provisioning und Ansible für Configuration. So bleiben Verantwortlichkeiten getrennt und beide Tools können ihre Stärken ausspielen!