Skip to content

Commit a0c60c7

Browse files
authored
Add AcrPull role assignment example for AKS with user-assigned identities (#1229)
1 parent c898578 commit a0c60c7

File tree

2 files changed

+148
-0
lines changed

2 files changed

+148
-0
lines changed

docs/content/api-overview/resources/aks-cluster.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,81 @@ let myAks = aks {
128128
)
129129
}
130130
```
131+
132+
#### Granting AKS access to Azure Container Registry (ACR)
133+
134+
To allow an AKS cluster to pull container images from Azure Container Registry, you need to grant the **[AcrPull](https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#acrpull)** role to the cluster's **kubelet identity**. The kubelet identity is used by the cluster to authenticate when pulling container images. This is a common requirement when deploying containerized applications.
135+
136+
##### Recommended Approach: User-Assigned Identities
137+
138+
The recommended approach is to create user-assigned managed identities for both the cluster and the kubelet. The kubelet identity is granted AcrPull access to pull container images. This ensures identities are available immediately without waiting for Azure AD propagation:
139+
140+
```fsharp
141+
open Farmer
142+
open Farmer.Builders
143+
open Farmer.Arm.RoleAssignment
144+
145+
// Create an identity for kubelet (used to pull container images)
146+
let kubeletMsi = createUserAssignedIdentity "kubeletIdentity"
147+
// Create an identity for the AKS cluster
148+
let clusterMsi = createUserAssignedIdentity "clusterIdentity"
149+
150+
// Give the AKS cluster's identity rights to manage the kubelet MSI
151+
let assignMsiRoleNameExpr =
152+
ArmExpression.create($"guid(concat(resourceGroup().id, '{clusterMsi.ResourceId.Name.Value}', '{Roles.ManagedIdentityOperator.Id}'))")
153+
154+
let assignMsiRole = {
155+
Name = assignMsiRoleNameExpr.Eval() |> ResourceName
156+
RoleDefinitionId = Roles.ManagedIdentityOperator
157+
PrincipalId = clusterMsi.PrincipalId
158+
PrincipalType = PrincipalType.ServicePrincipal
159+
Scope = ResourceGroup
160+
Dependencies = Set [ clusterMsi.ResourceId ]
161+
}
162+
163+
// Create a container registry
164+
let myAcr = containerRegistry {
165+
name "mycontainerregistry"
166+
}
167+
let myAcrResId = (myAcr :> IBuilder).ResourceId
168+
169+
// Grant the kubelet identity AcrPull access to the container registry
170+
let acrPullRoleNameExpr =
171+
ArmExpression.create($"guid(concat(resourceGroup().id, '{kubeletMsi.ResourceId.Name.Value}', '{Roles.AcrPull.Id}'))")
172+
173+
let acrPullRole = {
174+
Name = acrPullRoleNameExpr.Eval() |> ResourceName
175+
RoleDefinitionId = Roles.AcrPull
176+
PrincipalId = kubeletMsi.PrincipalId
177+
PrincipalType = PrincipalType.ServicePrincipal
178+
Scope = AssignmentScope.SpecificResource myAcrResId
179+
Dependencies = Set [ kubeletMsi.ResourceId ]
180+
}
181+
182+
// Create an AKS cluster and assign both identities
183+
let myAks = aks {
184+
name "aks-cluster"
185+
add_identity clusterMsi
186+
service_principal_use_msi
187+
kubelet_identity kubeletMsi
188+
depends_on clusterMsi
189+
depends_on myAcr
190+
depends_on_expression assignMsiRoleNameExpr
191+
depends_on_expression acrPullRoleNameExpr
192+
}
193+
194+
let template = arm {
195+
add_resource kubeletMsi
196+
add_resource clusterMsi
197+
add_resource myAcr
198+
add_resource myAks
199+
add_resource assignMsiRole
200+
add_resource acrPullRole
201+
}
202+
```
203+
204+
This approach ensures that both identities are created first and granted the necessary permissions before the AKS cluster attempts to pull images.
205+
131206
#### Using user assigned identities and connecting to the container registry
132207
```fsharp
133208
// Create an identity for kubelet (used to connect to container registry)

src/Tests/ContainerService.fs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,4 +737,77 @@ let tests =
737737

738738
Expect.equal osSKU "AzureLinux" "Incorrect osSKU value"
739739
}
740+
test "AKS cluster with kubelet identity and ACR access" {
741+
let kubeletMsi = createUserAssignedIdentity "kubeletIdentity"
742+
let clusterMsi = createUserAssignedIdentity "clusterIdentity"
743+
744+
let assignMsiRoleNameExpr =
745+
ArmExpression.create (
746+
$"guid(concat(resourceGroup().id, '{clusterMsi.ResourceId.Name.Value}', '{Roles.ManagedIdentityOperator.Id}'))"
747+
)
748+
749+
let assignMsiRole = {
750+
Name = assignMsiRoleNameExpr.Eval() |> ResourceName
751+
RoleDefinitionId = Roles.ManagedIdentityOperator
752+
PrincipalId = clusterMsi.PrincipalId
753+
PrincipalType = PrincipalType.ServicePrincipal
754+
Scope = ResourceGroup
755+
Dependencies = Set [ clusterMsi.ResourceId ]
756+
}
757+
758+
let myAcr = containerRegistry { name "mycontainerregistry" }
759+
let myAcrResId = (myAcr :> IBuilder).ResourceId
760+
761+
let acrPullRoleNameExpr =
762+
ArmExpression.create (
763+
$"guid(concat(resourceGroup().id, '{kubeletMsi.ResourceId.Name.Value}', '{Roles.AcrPull.Id}'))"
764+
)
765+
766+
let acrPullRole = {
767+
Name = acrPullRoleNameExpr.Eval() |> ResourceName
768+
RoleDefinitionId = Roles.AcrPull
769+
PrincipalId = kubeletMsi.PrincipalId
770+
PrincipalType = PrincipalType.ServicePrincipal
771+
Scope = AssignmentScope.SpecificResource myAcrResId
772+
Dependencies = Set [ kubeletMsi.ResourceId ]
773+
}
774+
775+
let myAks = aks {
776+
name "aks-cluster"
777+
add_identity clusterMsi
778+
service_principal_use_msi
779+
kubelet_identity kubeletMsi
780+
depends_on clusterMsi
781+
depends_on myAcr
782+
depends_on_expression assignMsiRoleNameExpr
783+
depends_on_expression acrPullRoleNameExpr
784+
}
785+
786+
let template = arm {
787+
add_resource kubeletMsi
788+
add_resource clusterMsi
789+
add_resource myAcr
790+
add_resource myAks
791+
add_resource assignMsiRole
792+
add_resource acrPullRole
793+
}
794+
795+
let json = template.Template |> Writer.toJson
796+
let jobj = Newtonsoft.Json.Linq.JObject.Parse(json)
797+
798+
// Verify AKS has kubelet identity configured
799+
let kubeletIdentityClientId =
800+
jobj.SelectToken(
801+
"resources[?(@.name=='aks-cluster')].properties.identityProfile.kubeletIdentity.clientId"
802+
)
803+
804+
Expect.isNotNull kubeletIdentityClientId "AKS cluster should have kubelet identity configured"
805+
806+
// Verify role assignments exist in template (ManagedIdentityOperator + AcrPull)
807+
let roleAssignments =
808+
jobj.SelectTokens("resources[?(@.type=='Microsoft.Authorization/roleAssignments')]")
809+
|> Seq.toList
810+
811+
Expect.hasLength roleAssignments 2 "Should have two role assignments in template"
812+
}
740813
]

0 commit comments

Comments
 (0)