|
| 1 | +--- |
| 2 | +title: "Manage AWS Resources from OpenFaaS Functions With IRSA" |
| 3 | +description: "We show you how to create AWS ECR repositories from a function written in Go using IAM Roles for Service Accounts." |
| 4 | +date: 2025-07-09 |
| 5 | +author_staff_member: alex |
| 6 | +categories: |
| 7 | +- aws |
| 8 | +- identity |
| 9 | +- rbac |
| 10 | +dark_background: true |
| 11 | +image: "/images/2025-07-irsa/background.png" |
| 12 | +hide_header_image: true |
| 13 | +--- |
| 14 | + |
| 15 | +In this post we'll create a function in Golang that uses AWS IAM and ambient credentials to create and manage resources in AWS. |
| 16 | + |
| 17 | +AWS Lambda is often used for this task, but how does OpenFaaS compare? |
| 18 | + |
| 19 | +OpenFaaS is a self-hosted platform that can run on any cloud or on-premises, including AWS EKS. Whilst AWS Lambda is a popular and convenient offering, it does have some tradeoffs and limitations which can cause friction for teams with more specialised requirements and workflows. |
| 20 | + |
| 21 | +If your team is developing code for Kubernetes using AWS EKS, then OpenFaaS can be a more natural fit than AWS Lambda, since it can use the same workflows, tools and processes you already have in place for your existing Kubernetes applications. That includes Helm, CRDs, Kubernetes RBAC, container builders in CI/CD and ArgoCD/Flux. |
| 22 | + |
| 23 | +Both AWS Lambda and OpenFaaS can be used to manage resources within AWS, with either shared credentials which need to be created, managed and rotated by your team, or with ambient credentials which are automatically obtained at runtime by the function. |
| 24 | + |
| 25 | +Our function will be used to create repositories in Elastic Container Registry (ECR). This is a common task for teams that run OpenFaaS in a multi-tenant environment, where each tenant or team publishes their own functions to the platform. It'll receive credentials using IAM Roles for Service Accounts (IRSA), which is the most modern way to map Kubernetes Service Accounts to native AWS IAM roles. |
| 26 | + |
| 27 | +## Create an EKS cluster with IRSA enabled |
| 28 | + |
| 29 | +You may already have an AWS EKS cluster provisioned, if so, you can enable IRSA by following these instructions: [IRSA on EKS](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html). |
| 30 | + |
| 31 | +If not, we can create a quick cluster using the [eksctl CLI tool](https://eksctl.io/): |
| 32 | + |
| 33 | +```bash |
| 34 | +eksctl create cluster \ |
| 35 | + --name of-test \ |
| 36 | + --with-oidc \ |
| 37 | + --spot \ |
| 38 | + --nodes 1 \ |
| 39 | + --nodes-max 3 \ |
| 40 | + --nodes-min 1 \ |
| 41 | + --region eu-west-1 |
| 42 | +``` |
| 43 | + |
| 44 | +Whilst eksctl looks like an imperative CLI tool, it is a client that manages declarative CloudFormation templates under the hood. You'll see the one created for your cluster by navigating to CloudFormation page of the AWS console. Provisioning can take up to 15-20 minutes depending on how many nodes and add-ons you've selected. |
| 45 | + |
| 46 | +## Install OpenFaaS Standard or For Enterprises |
| 47 | + |
| 48 | +If you don't have OpenFaaS installed, you can follow the [OpenFaaS installation guide](https://docs.openfaas.com/deployment/pro/). If you already have OpenFaaS installed, you can skip this step. |
| 49 | + |
| 50 | +For experimentation, you can use port-forwarding instead of setting up DNS and Ingress for the OpenFaaS gateway. It'll make it a bit quicker to get started. |
| 51 | + |
| 52 | +## IAM Policy for ECR Access |
| 53 | + |
| 54 | +We need to create an IAM Policy that will allow the OpenFaaS function to create and query repositories in ECR. |
| 55 | + |
| 56 | +```json |
| 57 | +{ |
| 58 | + "Version": "2012-10-17", |
| 59 | + "Statement": [ |
| 60 | + { |
| 61 | + "Effect": "Allow", |
| 62 | + "Action": [ |
| 63 | + "ecr:CreateRepository", |
| 64 | + "ecr:DeleteRepository", |
| 65 | + "ecr:DescribeRepositories" |
| 66 | + ], |
| 67 | + "Resource": "*" |
| 68 | + } |
| 69 | + ] |
| 70 | +} |
| 71 | +``` |
| 72 | + |
| 73 | +You can create this role using the AWS CLI or the AWS Management Console. If you're using the CLI, you can run the following command: |
| 74 | + |
| 75 | +```bash |
| 76 | +aws iam create-policy \ |
| 77 | + --policy-name ecr-create-query-repository \ |
| 78 | + --policy-document file://ecr-policy.json |
| 79 | +``` |
| 80 | + |
| 81 | +Note down the given ARN, i.e. |
| 82 | + |
| 83 | +``` |
| 84 | +{ |
| 85 | + "Policy": { |
| 86 | + "PolicyName": "ecr-create-query-repository", |
| 87 | + "Arn": "arn:aws:iam::ACCOUNT_NUMBER:policy/ecr-create-query-repository" |
| 88 | + } |
| 89 | +} |
| 90 | +``` |
| 91 | + |
| 92 | +## Create IAM Role and Service Account |
| 93 | + |
| 94 | +The easiest way to create the IAM Role and Service Account is to use `eksctl`: |
| 95 | + |
| 96 | +```bash |
| 97 | +export ARN=arn:aws:iam::ACCOUNT_NUMBER:policy/ecr-create-query-repository |
| 98 | + |
| 99 | +eksctl create iamserviceaccount \ |
| 100 | + --name openfaas-create-ecr-repo \ |
| 101 | + --namespace openfaas-fn \ |
| 102 | + --cluster of-test \ |
| 103 | + --role-name ecr-create-query-repository \ |
| 104 | + --attach-policy-arn $ARN \ |
| 105 | + --region eu-west-1 \ |
| 106 | + --approve |
| 107 | +``` |
| 108 | + |
| 109 | +This can also be done manually by creating the IAM Role in AWS, followed by a correctly annotated Service Account in Kubernetes using the `eks.amazonaws.com/role-arn` annotation. |
| 110 | + |
| 111 | +## Create a function that uses the IAM Role |
| 112 | + |
| 113 | +We are going to use Go to create this function. You can learn more about the Go template in the [OpenFaaS documentation](https://docs.openfaas.com/languages/go/). |
| 114 | + |
| 115 | +AWS also has [SDKs available for other languages](https://docs.aws.amazon.com/sdkref/latest/guide/overview.html) supported by OpenFaaS such as Python, Java, Node.js, C#, etc. |
| 116 | + |
| 117 | +Create a new function using the `golang-middleware` template: |
| 118 | + |
| 119 | +```bash |
| 120 | +export OPENFAAS_PREFIX=ttl.sh/openfaas |
| 121 | + |
| 122 | +faas-cli new --lang golang-middleware ecr-create-repo |
| 123 | +``` |
| 124 | + |
| 125 | + |
| 126 | +Edit the stack.yaml file to add an annotation stating which Kubernetes Service Account to use: |
| 127 | + |
| 128 | +```diff |
| 129 | +functions: |
| 130 | + ecr-create-repo: |
| 131 | ++ annotations: |
| 132 | ++ com.openfaas.serviceaccount: openfaas-create-ecr-repo |
| 133 | +``` |
| 134 | + |
| 135 | +Set the region for the function, along with the URL of the ECR registry: |
| 136 | + |
| 137 | +```diff |
| 138 | +functions: |
| 139 | + ecr-create-repo: |
| 140 | ++ environment: |
| 141 | ++ AWS_REGION: eu-west-1 |
| 142 | +``` |
| 143 | + |
| 144 | +Add the AWS SDK for Go to the function as a dependency: |
| 145 | + |
| 146 | +```bash |
| 147 | +cd ecr-create-repo |
| 148 | +go get github.com/aws/aws-sdk-go-v2/aws |
| 149 | +go get github.com/aws/aws-sdk-go-v2/config |
| 150 | +go get github.com/aws/aws-sdk-go-v2/service/ecr |
| 151 | +``` |
| 152 | + |
| 153 | +You can learn more about the AWS SDK for Go in the [AWS documentation](https://docs.aws.amazon.com/sdk-for-go/v2/developer-guide/welcome.html). |
| 154 | + |
| 155 | +Edit the functions handler to use the AWS SDK for Go: |
| 156 | + |
| 157 | +```go |
| 158 | +package function |
| 159 | + |
| 160 | +import ( |
| 161 | + "context" |
| 162 | + "encoding/json" |
| 163 | + "fmt" |
| 164 | + "io" |
| 165 | + "log" |
| 166 | + "net/http" |
| 167 | + "os" |
| 168 | + "strings" |
| 169 | + |
| 170 | + "github.com/aws/aws-sdk-go-v2/config" |
| 171 | + "github.com/aws/aws-sdk-go-v2/service/ecr" |
| 172 | + "github.com/aws/aws-sdk-go-v2/service/ecr/types" |
| 173 | +) |
| 174 | + |
| 175 | +type CreateRepoReq struct { |
| 176 | + Name string `json:"name"` |
| 177 | +} |
| 178 | + |
| 179 | +type CreateRepoRes struct { |
| 180 | + Arn string `json:"arn"` |
| 181 | +} |
| 182 | + |
| 183 | +func Handle(w http.ResponseWriter, r *http.Request) { |
| 184 | + var input []byte |
| 185 | + |
| 186 | + if r.Body != nil { |
| 187 | + defer r.Body.Close() |
| 188 | + |
| 189 | + body, _ := io.ReadAll(r.Body) |
| 190 | + |
| 191 | + input = body |
| 192 | + } |
| 193 | + |
| 194 | + var createRepoReq CreateRepoReq |
| 195 | + if len(input) > 0 { |
| 196 | + if err := json.Unmarshal(input, &createRepoReq); err != nil { |
| 197 | + http.Error(w, "Invalid request body", http.StatusBadRequest) |
| 198 | + return |
| 199 | + } |
| 200 | + } |
| 201 | + |
| 202 | + if len(createRepoReq.Name) == 0 { |
| 203 | + http.Error(w, "Missing in body: name", http.StatusBadRequest) |
| 204 | + return |
| 205 | + } |
| 206 | + |
| 207 | + cfg, err := config.LoadDefaultConfig(context.TODO(), |
| 208 | + config.WithRegion(os.Getenv("AWS_REGION"))) |
| 209 | + if err != nil { |
| 210 | + log.Fatalf("unable to load SDK config, %v", err) |
| 211 | + } |
| 212 | + |
| 213 | + // Using the Config value, create the ECR client |
| 214 | + svc := ecr.NewFromConfig(cfg) |
| 215 | + |
| 216 | + // Check if the repository already exists |
| 217 | + if _, err := svc.DescribeRepositories(context.TODO(), &ecr.DescribeRepositoriesInput{ |
| 218 | + RepositoryNames: []string{createRepoReq.Name}, |
| 219 | + }); err != nil { |
| 220 | + log.Printf("Error describing repository: %s", err.Error()) |
| 221 | + if !strings.Contains(err.Error(), "RepositoryNotFoundException") { |
| 222 | + http.Error(w, fmt.Sprintf("Failed to describe repository: %s", err.Error()), http.StatusInternalServerError) |
| 223 | + return |
| 224 | + } |
| 225 | + } |
| 226 | + |
| 227 | + // Create the repository |
| 228 | + createRes, err := svc.CreateRepository(context.TODO(), &ecr.CreateRepositoryInput{ |
| 229 | + RepositoryName: &createRepoReq.Name, |
| 230 | + ImageTagMutability: types.ImageTagMutabilityMutable, |
| 231 | + EncryptionConfiguration: &types.EncryptionConfiguration{ |
| 232 | + EncryptionType: types.EncryptionTypeAes256, |
| 233 | + }, |
| 234 | + ImageScanningConfiguration: &types.ImageScanningConfiguration{ |
| 235 | + ScanOnPush: false, |
| 236 | + }, |
| 237 | + }) |
| 238 | + if err != nil { |
| 239 | + http.Error(w, fmt.Sprintf("Failed to create repository: %s", err.Error()), http.StatusInternalServerError) |
| 240 | + return |
| 241 | + } |
| 242 | + |
| 243 | + w.WriteHeader(http.StatusCreated) |
| 244 | + |
| 245 | + createRepoRes := CreateRepoRes{ |
| 246 | + Arn: *createRes.Repository.RepositoryArn, |
| 247 | + } |
| 248 | + json.NewEncoder(w).Encode(createRepoRes) |
| 249 | +} |
| 250 | +``` |
| 251 | + |
| 252 | +## Invoke the function to create a new repository |
| 253 | + |
| 254 | +Now you can use curl to create a repository: |
| 255 | + |
| 256 | +```bash |
| 257 | +curl http://127.0.0.1:8080/function/ecr-create-repo \ |
| 258 | + -d '{"name":"tenant1/fn1"}' \ |
| 259 | + -H "Content-type: application/json" |
| 260 | +``` |
| 261 | + |
| 262 | +The response contains the ARN of the repository, ready for you to use in something like the OpenFaaS Function Builder API to push a new image. |
| 263 | + |
| 264 | +```json |
| 265 | +{ |
| 266 | + "arn": "arn:aws:ecr:eu-west-1:ACCOUNT_NUMBER:repository/tenant1/fn1" |
| 267 | +} |
| 268 | +``` |
| 269 | + |
| 270 | +You should see the repository created in AWS Console. |
| 271 | + |
| 272 | +You can also verify this from the command line: |
| 273 | + |
| 274 | +```bash |
| 275 | +aws ecr list-images --repository-name tenant1/fn1 --region eu-west-1 |
| 276 | + |
| 277 | +aws ecr describe-repositories --repository-name tenant1/fn1 --region eu-west-1 |
| 278 | +``` |
| 279 | + |
| 280 | +## Wrapping up |
| 281 | + |
| 282 | +In a very short period of time, we created a function using the `golang-middleware` template, added the AWS SDK for Go as a dependency, and used it to create a repository in ECR. |
| 283 | + |
| 284 | +This is required step to push new images to an AWS ECR registry, and could form part of a CI/CD pipeline, or a multi-tenant functions platform. |
| 285 | + |
| 286 | +With a few simple steps, you can take code in the form of a plain files, a zip file, tar file, or Git repository, and turn it into a function. |
| 287 | + |
| 288 | +1) Create a tenant namespace using the [OpenFaaS Gateway's REST API](https://docs.openfaas.com/reference/rest-api/#create-a-namespace) i.e. `tenant` |
| 289 | +2) Create a repository for the tenant's new function you want to build i.e. `tenant/fn1` |
| 290 | +3) Use the [Function Builder's API](https://docs.openfaas.com/openfaas-pro/builder/) to publish the image to the full ARN path i.e. `ACCOUNT_NUMBER.dkr.ecr.eu-west-1.amazonaws.com/tenant1/fn1:TAG` |
| 291 | +3) Post a request to the pOpenFaaS Gateway's REST API](https://docs.openfaas.com/reference/rest-api/#deploy-a-function) to deploy the function to the `tenant1` namespace |
| 292 | + |
| 293 | +Highlights of this approach: |
| 294 | + |
| 295 | +* The function operates with AWS IAM, using least privilege principles. |
| 296 | +* The function obtains ambient credentials from the Kubernetes Service Account, using IRSA instead of shared, long-lived credentials. |
| 297 | +* The function can be deployed to Kubernetes rapidly using the same workflows and tools you already use with Kubernetes. |
| 298 | + |
| 299 | +To take things further, consider authentication options for the function. |
| 300 | + |
| 301 | +1) [Built-in Function Authentication using OpenFaaS IAM](https://docs.openfaas.com/openfaas-pro/iam/function-authentication/). |
| 302 | +2) Your own code in the handler to process an Authorization header with a static key or JWT token. |
| 303 | + |
| 304 | +We wrote to the AWS API directly, however you can use the [Event Connectors for AWS SQS or SNS](https://docs.openfaas.com/openfaas-pro/sqs-events/) to receive events from other AWS services such as S3, DynamoDB, etc. |
| 305 | + |
| 306 | +The same technique can be applied for other APIs such as the Kubernetes API, for when you want a function to obtain an identity to manage resources in one or more Kubernetes clusters: [Learn how to access the Kubernetes API from a Function](https://www.openfaas.com/blog/access-kubernetes-from-a-function/). |
| 307 | + |
0 commit comments