Simplify the networking with VPC Lattice
Tags: aws, network, githubactions, tutorial
Published: 2023-04-12 21:21:03

## Welcome

I started writing this on April 7th, 2023, a few weeks after the release of VPC Lattice (although it's not yet available everywhere). I was curious about what it is and what it's supposed to do, so I asked ChatGPT.


![ask-chat](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7cypf7pz0t3yx7kw0ejg.png)

As there isn't much information available yet, I'll rely on the [official documentation](https://docs.aws.amazon.com/vpc-lattice/latest/ug/what-is-vpc-service-network.html) for now.

According to the documentation, 
"Amazon VPC Lattice is a fully managed application networking service that you use to connect, secure, and monitor all of your services across multiple accounts and virtual private clouds (VPCs)." 

In other words, it allows you to use AWS as one big and complex Kubernetes cluster, while still using separate solutions as services across VPCs. It was likely created to simplify networking management and make administrators' lives easier. It allows for grouping VPCs into logical ServiceNetworks.

Let's check it now then!

## Tools used in this episode

- Python
- ECS
- CloudFormation(again!)
- EC2

## Choosing the right infrastructure-as-code tool for VPC Lattice

As an additional note, I wanted to use Terraform today, but unfortunately, when we compare the documentation for Lattice, CloudFormation seems more prepared:

-   [CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_VpcLattice.html)
-   [Terraform](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpclattice_service)

Note that this is a relatively new service, so the situation may change in the coming weeks.

## Key concepts

Here are some key concepts related to VPC Lattice:

**Service** - A set of objects (such as EC2 instances or ECS tasks) that act as one logical object. Importantly, it's an independent solution, so it can be handled by a dedicated team and has logical boundaries.

**Target group** - A logical representation of our compute resources.

**Listener** - A process that checks for the possibility to connect with our resources.

**Rule** - A rule of routing attached to a listener.

**Service network** - In my understanding, this is an abstraction on top of VPCs, as it allows us to associate resources in different VPCs.

**Service directory** - A service discovery solution for all VPC Lattice resources.

**Auth policies** - Authorization policies for services inside VPC Lattice.


![personal-view](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fbowkpf2m7a2twc9id1a.png)

## Project architecture 

As you probably know, I like whole solution, or just working examples. So here as always I will try to build something usable. The plan is to have:
- EC2 with Nginx in VPC 1, private subnet with NAT gateway
- ECS sample app in VPC 2, without public LB(internal)
- VPC Lattice Service

Additionally:
- static credentials should be replaced - OICD config for GitHub Action


![planned-implementation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aqa15re0da29wqbl5f3a.png)

## Implementation 

In the beginning, let's focus on location. I started with `eu-central-1`, however after the first deployment I realized that the service it's unavailable in Europe yet, so I switched to `us-east-1`.
After that, I just implemented a sample EC2-based template, next in the row was the ECS-based template. As CloudFormation commands were getting longer and longer I added a `Makefile`.

```Makefile
default: validate

REGION := "us-east-1"

validate:
	@echo "Validating CloudFormation templates"
	cfn-lint ec2-formation.yaml
	cfn-lint ecs-formation.yaml
	

clean-ec2:
	aws cloudformation delete-stack --stack-name lattice-ec2 --region $(REGION)

clean-ecs:
	aws cloudformation delete-stack --stack-name lattice-ecs --region $(REGION)

clean-lattice:
	aws cloudformation delete-stack --stack-name lattice-itself --region $(REGION)

ec2: 
	cfn-lint ec2-formation.yaml
	aws cloudformation deploy --stack-name lattice-ec2 --template-file ec2-formation.yaml --region $(REGION) --capabilities CAPABILITY_NAMED_IAM

ecs:
	cfn-lint ecs-formation.yaml
	aws cloudformation deploy --stack-name lattice-ecs --template-file ecs-formation.yaml --region $(REGION) --capabilities CAPABILITY_NAMED_IAM

ecs-fastapi:
	cfn-lint ecs-formation.yaml 
	aws cloudformation deploy --stack-name lattice-ecs --template-file ecs-formation.yaml --region $(REGION) --capabilities CAPABILITY_NAMED_IAM --parameter-overrides ContainerImage=123441.dkr.ecr.us-east-1.amazonaws.com/fastapi-repository:latest HCPath=/healtz

lattice:
	aws cloudformation deploy --stack-name lattice-itself --template-file lattice-formation.yaml --region $(REGION) --capabilities CAPABILITY_NAMED_IAM

build: validate ec2 ecs lattice

clean: clean-ec2 clean-ecs clean-lattice
```

As you can see there is no linting on the Lattice template. The reason was simple, support it's not ready yet. Some types are broken, also the lack of general availability is painful.

Then I added a small FastAPI code, for building container images with REST API. That leads us to CI/CD and GitHub Action. Obviously, the next step was OICD support implementation. If you are interested in  topic, I can recommend some documentation:
- [about-security-hardening-with-openid-connect](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
- [configuring-openid-connect-in-amazon-web-services](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services)
- [id_roles_providers_create_oidc](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html)
- [d_roles_create_for-idp_oidc](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html)

If you're lazy, here is my code:
```yaml
  GitHubOIDC:
    Type: AWS::IAM::OIDCProvider
    Properties:
      Url: https://token.actions.githubusercontent.com
      ThumbprintList:
        - f879abce0008e4eb126e0097e46620f5aaae26ad # valid until 2023-11-07 23:59:59
      ClientIdList:
        - sts.amazonaws.com
      Tags:
        - Key: Env
          Value: !Ref EnvironmentName

  OIDCRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${AWS::StackName}-GitHub-to-${ServiceName}-role"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Federated: !Ref GitHubOIDC
            Action: sts:AssumeRoleWithWebIdentity
            Condition:
              ForAnyValue:StringEquals:
                token.actions.githubusercontent.com:sub:
                  - !Sub "repo:${OrgName}/${RepoName}:ref:refs/heads/main"
                  - !Sub "repo:${OrgName}/${RepoName}:ref:refs/heads/dev"
                token.actions.githubusercontent.com:aud: sts.amazonaws.com
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser"
      Tags:
        - Key: Env
          Value: !Ref EnvironmentName
```

At the beginning take a look at `ThumbprintList`. It's a bit dynamic variable, based on the sha1 of the GitHub certificate. You can calculate it relatively easily according to [this](https://binx.io/2022/09/05/how-to-update-the-thumbprint-for-an-openid-connect-identity-provider-in-cloudformation/) article. The next important thing is the policy's condition. Especially `token.actions.githubusercontent.com:sub`, which should be pointed to the right repository and branch - you can use `ForAnyValue` here!
Seems easy right? Great. Now you need to obtain, OIDCRole's ARN and paste it into GitHub Action secret tab. The simple action workflow could look like that:

```yaml
name: Building my awesome docker image
run-name: ${{ github.actor }} on GitHub Actions 🚀
on: push

env:
  AWS_REGION: "us-east-1"

permissions:
  id-token: write
  contents: read

jobs:
  Build-FastAPI-docker:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: configure aws credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      - name: Build, tag, and push docker image to Amazon ECR
        env:
          REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          REPOSITORY: fastapi-repository
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $REGISTRY/$REPOSITORY:$IMAGE_TAG ./ecs-api/
          docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
          docker tag $REGISTRY/$REPOSITORY:$IMAGE_TAG $REGISTRY/$REPOSITORY:latest
          docker push $REGISTRY/$REPOSITORY:latest
```

After that, I started the main point of the article - VPC Lattice landscape. Implementation was funny, as It's not a part of the Dash doc set yet, I was forced to use web docs! And that was the place were the fun began. For example: 
```console
Resource handler returned message: "ALB Target Group does not support health check config (Service:
```
Or information about usable `AWS::VpcLattice::TargetGroup`, `Types` was under `Target`, as part of `ID` description. Pure joy! Ah and only Internal ALB is supported, you can't connect internal-facing, but you can connect IP of any solution in the whole universe. So the final implementation was long and rather not exciting:
```yaml
AWSTemplateFormatVersion: 2010-09-09
Description: Run CFn Lattice itself

Resources:
  EC2TargetGroup:
    Type: AWS::VpcLattice::TargetGroup
    Properties:
      Name: ec2-lattice-tg
      Type: INSTANCE
      Config:
        HealthCheck:
          Enabled: true
          Path: "/"
          Port: 80
          Protocol: HTTP
          Matcher:
            HttpCode: "200"
        Port: 80
        Protocol: HTTP
        ProtocolVersion: HTTP1
        VpcIdentifier: !ImportValue ec2-vpc
      Targets:
        - Id: !ImportValue ec2-instanceid
          Port: 80
      # INSTANCE | IP | LAMBDA | ALB
      Tags:
        - Key: Owner
          Value: kuba
        - Key: Project
          Value: blogpost

  ECSTargetGroup:
    Type: AWS::VpcLattice::TargetGroup
    Properties:
      Name: ecs-lattice-tg
      # INSTANCE | IP | LAMBDA | ALB
      Type: ALB
      Config:
        # HC not supported for ALB
        #HealthCheck:
        #  Enabled: true
        #  Path: "/"
        #  Port: 80
        #  Protocol: HTTP
        #  Matcher:
        #    HttpCode: "200"
        Port: 80
        Protocol: HTTP
        ProtocolVersion: HTTP1
        VpcIdentifier: !ImportValue ecs-vpc
      Targets:
        - Id: !ImportValue ecs-alb-arn
          Port: 80

      Tags:
        - Key: Owner
          Value: kuba
        - Key: Project
          Value: blogpost

  GeneralListener:
    Type: AWS::VpcLattice::Listener
    Properties:
      Name: ec2-80
      Port: 80
      Protocol: HTTP
      ServiceIdentifier: !Ref Service
      DefaultAction:
        Forward:
          TargetGroups:
            - TargetGroupIdentifier: !Ref EC2TargetGroup
              Weight: 10
            - TargetGroupIdentifier: !Ref ECSTargetGroup
              Weight: 10
      Tags:
        - Key: Owner
          Value: kuba
        - Key: Project
          Value: blogpost

  ECSGeneralSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: lattice-ecs-too-open-security-group
      VpcId: !ImportValue ecs-vpc
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !ImportValue ecs-vpc-cidr
      Tags:
        - Key: Owner
          Value: kuba
        - Key: Project
          Value: blogpost

  EC2GeneralSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: lattice-ecs-too-open-security-group
      VpcId: !ImportValue ec2-vpc
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !ImportValue ec2-vpc-cidr
      Tags:
        - Key: Owner
          Value: kuba
        - Key: Project
          Value: blogpost

  ECSVpcAssocination:
    Type: AWS::VpcLattice::ServiceNetworkVpcAssociation
    Properties:
      SecurityGroupIds: [!Ref ECSGeneralSecurityGroup]
      ServiceNetworkIdentifier: !Ref LatticeServiceNetwork
      VpcIdentifier: !ImportValue ecs-vpc
      Tags:
        - Key: Owner
          Value: kuba
        - Key: Project
          Value: blogpost

  EC2VpcAssocination:
    Type: AWS::VpcLattice::ServiceNetworkVpcAssociation
    Properties:
      SecurityGroupIds: [!Ref EC2GeneralSecurityGroup]
      ServiceNetworkIdentifier: !Ref LatticeServiceNetwork
      VpcIdentifier: !ImportValue ec2-vpc
      Tags:
        - Key: Owner
          Value: kuba
        - Key: Project
          Value: blogpost

  LatticeServiceNetwork:
    Type: AWS::VpcLattice::ServiceNetwork
    Properties:
      AuthType: NONE
      Name: awesome-service-network
      Tags:
        - Key: Owner
          Value: kuba
        - Key: Project
          Value: blogpost

  Service:
    Type: AWS::VpcLattice::Service
    Properties:
      AuthType: NONE
      Name: awesome-service
      Tags:
        - Key: Owner
          Value: kuba
        - Key: Project
          Value: blogpost

  ServiceNetworkAssocination:
    Type: AWS::VpcLattice::ServiceNetworkServiceAssociation
    Properties:
      ServiceIdentifier: !Ref Service
      ServiceNetworkIdentifier: !Ref LatticeServiceNetwork
      Tags:
        - Key: Owner
          Value: kuba
        - Key: Project
          Value: blogpost
```

As you can see, we need `TargetGroup` for `Listener`, which needs to be linked with `Service`. `Service` needs to be connected with `ServiceNetwork`. `ServiceNetwork` needs an association
with `VPC`, and `SecurityGroup` in some way.

## Testing 

After implementation, I was able to execute the flow:

```bash
make build
git push -f # image building 
make ecs-fastapi
```

Next, I just get the service DNS entry, and with that, in the clipboard, I started the SSM session with my standalone instance. The result was as expected:

```console
root@ip-10-192-20-222 bin]# curl -si awesome-service-03ce89237939acfce.7d67968.vpc-lattice-svcs.us-east-1.on.aws | grep content-type
content-type: text/html
[root@ip-10-192-20-222 bin]# curl -si awesome-service-03ce89237939acfce.7d67968.vpc-lattice-svcs.us-east-1.on.aws | grep content-type
content-type: application/json
[root@ip-10-192-20-222 bin]# curl -si awesome-service-03ce89237939acfce.7d67968.vpc-lattice-svcs.us-east-1.on.aws | grep content-type
content-type: application/json
[root@ip-10-192-20-222 bin]# curl -si awesome-service-03ce89237939acfce.7d67968.vpc-lattice-svcs.us-east-1.on.aws | grep content-type
content-type: text/html
```

I was able to hit the service endpoint and received two independent responses from two different sources(FastAPI responded with JSON, and Nginx with HTML). Yay!

## Summary

AWS Lattice presents some exciting possibilities for programmers in the networking area. As I dived into the implementation of services, I gained confidence that its MESH-alike solution, heavily inspired Kubernetes' approach to services.   

Additionally, Lattice's networking capabilities offer an excellent opportunity for developers to explore distributed systems and service-oriented architecture. With VPC Lattice, developers can build complex networking applications that enable real-time communication between different services, which is critical in building scalable and robust applications. Especially with distributed teams. 

I'm waiting for wide service availability and documentation quality improvement. However, even with the current state, I was able to implement a working solution, so it's not such bad.
Also, use OIDC with your pipeline always when it's possible! It's easy, fast, and secure. Reduce credentials leak is always a good thing.

### Code

As always you can find my code here:
- [GitHub](https://github.com/3sky/special-couscous)