Post

The painful experience of automating a consumption based Logic App that accesses a Key Vault with a managed identity via Terraform

Intro

I needed to create an Azure Logic App that could access a Key Vault via a managed identity, and i wanted it to be 100% managed via Terraform. It needed to be a workflow/consumption based model.

There is not a lot of information on how to do what I wanted to do. So I figured I would post some of the stuff I learned.

My struggles

Deploying API Connection connecting to Key Vault via azurerm_api_connection

I started out using the built-in native “Key Vault” actions (spoiler: don’t). They require what is called an API Connection to be created, this API Connection is another resource in Azure. The API Connection then points to the correct Key Vault API endpoint and it contains the Managed Identity information for the Logic App. So the API Connection is used to define what Key Vault to connect to, and how to authenticate.

In Terraform with the provider azurerm, there is a resource type called “azurerm_api_connection” which you can use to provision an API Connection. The problem in this case is that you have to supply a “vaultName” via the alternativeParameterValues parameter, however that is not possible in azurerm_api_connection. So i ended up with the API Connection almost configured, i just needed to supply the vaultName, but i was unable to via Terraform.

So i ended up deploying the API Connection via an ARM template by using azurerm_resource_group_template_deployment. However due to another problem, i actually ended up removing the API Connection and not even needing it anyway.

Using the “Key Vault” actions

In the end, it didn’t even matter, because when i finally got the API Connection working via an ARM template, it was unusable inside the Logic App. The “Azure Key Vault” actions showed an error stating i was using the wrong Key Vault API version. I didn’t find a way of changing it, i think the api-version is hardcoded into the actions.

So i migrated to using the HTTP action, which didn’t even require the API Connection.

Freaking case sensitive

Just as i thought i was done, because i had created all actions except from the last one, i ran into another problem. For whatever reason, when i added the last action “Update apikey secret” it just broke everything. When viewing the Logic App in designer it was just completly blank. After 2 hours of troubleshooting i figured out it was because the runAfter is case sensitive.

1
2
3
4
5
6
7
8
9
10
11
"runAfter": {
    "Extract_apikey_from_Json": [
        "Succeeded"
    ]
}
 
"runAfter": {
    "Extract_apikey_from_json": [
        "Succeeded"
    ]
}

My experience

Using Terraform to deploy the Logic App took a long time, and i ran into way more problems than i had expected. Maybe i was just unlucky with the Key Vault actions, i don’t know, this is the first time i’m automating it, maybe i will update this post in the future, if i automate another Logic App using different actions. The HTTP actions i didn’t have a hard time automating.

A tip is creating the Logic App in the portal, and then using the code from the “Logic app code view” inside Terraform.

I hear that the standard Logic App is easier to automate than the consumption based Logic App, but it is not something i have tried.

Before Terraform

After Terraform

The final code

Unfortunately i was not using version-control, so i don’t have pieces of the old code, i only have the final result.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
##### Consumption based Logic App #####
### Logic App environment setup ###
resource "azurerm_logic_app_workflow" "apikey" {
  name = "${var.prefix}-apikey"
  resource_group_name = var.rg-name
  location = var.region

  identity {
    type = "SystemAssigned"
  }
}

### Configuring triggers and actions on the Logic App ###
## Triggers ##
resource "azurerm_logic_app_trigger_recurrence" "apikey" {
  name = "Scheduled trigger"
  logic_app_id = azurerm_logic_app_workflow.apikey.id
  frequency = "Hour"
  interval = "2"
  start_time = "2024-08-23T15:24:48Z"
}

## Actions ##
# Get Username from vault
resource "azurerm_logic_app_action_custom" "getusernamefromvault" {
  name = "Get_username_from_vault"
  logic_app_id = azurerm_logic_app_workflow.apikey.id
  body = <<BODY
{
    "inputs": {
        "authentication": {
            "audience": "https://vault.azure.net",
            "type": "ManagedServiceIdentity"
        },
        "method": "GET",
        "uri": "https://${azurerm_key_vault.apikey.name}.vault.azure.net/secrets/username?api-version=7.2"
    },
    "runAfter": {},
    "runtimeConfiguration": {
        "contentTransfer": {
            "transferMode": "Chunked"
        },
        "secureData": {
            "properties": [
                "inputs",
                "outputs"
            ]
        }
    },
    "type": "Http"
}
  BODY
}

# Extract username from request
resource "azurerm_logic_app_action_custom" "extractusernamefromrequest" {
  name = "Extract_username_from_request"
  logic_app_id = azurerm_logic_app_workflow.apikey.id

  depends_on = [ azurerm_logic_app_action_custom.getusernamefromvault ]

  body = <<BODY
{
    "inputs": {
        "content": "@body('Get_username_from_vault')",
        "schema": {
            "properties": {
                "value": {
                    "type": "string"
                }
            },
            "type": "object"
        }
    },
    "runAfter": {
        "Get_username_from_vault": [
            "Succeeded"
        ]
    },
    "runtimeConfiguration": {
        "secureData": {
            "properties": [
                "inputs"
            ]
        }
    },
    "type": "ParseJson"
}
  BODY
}

# Get password from vault
resource "azurerm_logic_app_action_custom" "getpasswordfromvault" {
  name = "Get_password_from_vault"
  logic_app_id = azurerm_logic_app_workflow.apikey.id

  depends_on = [ azurerm_logic_app_action_custom.extractusernamefromrequest ]

  body = <<BODY
{
    "inputs": {
        "authentication": {
            "audience": "https://vault.azure.net",
            "type": "ManagedServiceIdentity"
        },
        "method": "GET",
        "uri": "https://${azurerm_key_vault.apikey.name}.vault.azure.net/secrets/password?api-version=7.2"
    },
    "runAfter": {
        "Extract_username_from_request": [
            "Succeeded"
        ]
    },
    "runtimeConfiguration": {
        "contentTransfer": {
            "transferMode": "Chunked"
        },
        "secureData": {
            "properties": [
                "inputs",
                "outputs"
            ]
        }
    },
    "type": "Http"
}
  BODY
}

# Extract password from request
resource "azurerm_logic_app_action_custom" "extractpasswordfromrequest" {
  name = "Extract_password_from_request"
  logic_app_id = azurerm_logic_app_workflow.apikey.id

  depends_on = [ azurerm_logic_app_action_custom.getpasswordfromvault ]

  body = <<BODY
{
    "inputs": {
        "content": "@body('Get_password_from_vault')",
        "schema": {
            "properties": {
                "value": {
                    "type": "string"
                }
            },
            "type": "object"
        }
    },
    "runAfter": {
        "Get_password_from_vault": [
            "Succeeded"
        ]
    },
    "runtimeConfiguration": {
        "secureData": {
            "properties": [
                "inputs"
            ]
        }
    },
    "type": "ParseJson"
}
  BODY
}

# Acquire apikey
resource "azurerm_logic_app_action_custom" "acquireapikey" {
  name = "Acquire_apikey"
  logic_app_id = azurerm_logic_app_workflow.apikey.id

  depends_on = [ azurerm_logic_app_action_custom.extractpasswordfromrequest ]

  body = <<BODY
{
    "inputs": {
        "body": {
            "password": "@{body('Extract_password_from_request')?['value']}",
            "username": "@{body('Extract_username_from_request')?['value']}"
        },
        "headers": {
            "Content-Type": "application/json"
        },
        "method": "POST",
        "uri": "${var.apikey-endpointforlogin}"
    },
    "runAfter": {
        "Extract_password_from_request": [
            "Succeeded"
        ]
    },
    "runtimeConfiguration": {
        "contentTransfer": {
            "transferMode": "Chunked"
        },
        "secureData": {
            "properties": [
                "inputs",
                "outputs"
            ]
        },
        "staticResult": {
            "name": "HTTP0",
            "staticResultOptions": "Disabled"
        }
    },
    "type": "Http"
}
  BODY
}

# Extract apikey from json
resource "azurerm_logic_app_action_custom" "extractapikeyfromjson" {
  name = "Extract_apikey_from_json"
  logic_app_id = azurerm_logic_app_workflow.apikey.id

  depends_on = [ azurerm_logic_app_action_custom.acquireapikey ]

  body = <<BODY
{
    "inputs": {
        "content": "@body('Acquire_apikey')",
        "schema": {
            "properties": {
                "apiKey": {
                    "type": "string"
                }
            },
            "type": "object"
        }
    },
    "runAfter": {
        "Acquire_apikey": [
            "Succeeded"
        ]
    },
    "runtimeConfiguration": {
        "secureData": {
            "properties": [
                "inputs"
            ]
        }
    },
    "type": "ParseJson"
}
  BODY
}

# Update apikey secret
resource "azurerm_logic_app_action_custom" "updateapikeysecret" {
  name = "Update_apikey_secret"
  logic_app_id = azurerm_logic_app_workflow.apikey.id

  depends_on = [ azurerm_logic_app_action_custom.extractapikeyfromjson ]

  body = <<BODY
{
    "inputs": {
        "authentication": {
            "audience": "https://vault.azure.net",
            "type": "ManagedServiceIdentity"
        },
        "body": {
            "value": "@{body('Extract_apikey_from_JSON')?['apiKey']}"
        },
        "method": "PUT",
        "uri": "https://${azurerm_key_vault.apikey.name}.vault.azure.net/secrets/apikey?api-version=7.2"
    },
    "runAfter": {
        "Extract_apikey_from_json": [
            "Succeeded"
        ]
    },
    "runtimeConfiguration": {
        "contentTransfer": {
            "transferMode": "Chunked"
        },
        "secureData": {
            "properties": [
                "inputs",
                "outputs"
            ]
        }
    },
    "type": "Http"
}
  BODY
}

https://learn.microsoft.com/en-us/azure/logic-apps/authenticate-with-managed-identity?tabs=consumption#arm-template-for-api-connections-and-managed-identities

https://aztoso.com/logic-app/keyvault-connector-with-managed-identity/

https://learn.microsoft.com/en-us/rest/api/keyvault/secrets/get-secret/get-secret?view=rest-keyvault-secrets-7.4&tabs=HTTP

https://alessandromoura.azurewebsites.net/2022/04/28/creating-managed-identities-api-connections-for-logic-apps-in-bicep-arm/

This post is licensed under CC BY 4.0 by the author.